atmo.rsvp
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

improve atmosphereconf page

Florian 83c5c9de c4d15df7

+120 -25
+1 -1
src/routes/p/[actor]/e/[rkey]/+page.svelte
··· 6 6 import Map from '$lib/components/Map.svelte'; 7 7 import ShareModal from '$lib/components/ShareModal.svelte'; 8 8 import Avatar from 'svelte-boring-avatars'; 9 - import EventRsvp from './EventRsvp.svelte'; 9 + import EventRsvp from '$lib/components/EventRsvp.svelte'; 10 10 import EventCard from '$lib/components/EventCard.svelte'; 11 11 import EventAttendees from './EventAttendees.svelte'; 12 12 import { page } from '$app/state';
+6 -4
src/routes/p/[actor]/e/[rkey]/EventRsvp.svelte src/lib/components/EventRsvp.svelte
··· 12 12 initialRsvpStatus = null, 13 13 initialRsvpRkey = null, 14 14 onrsvp, 15 - oncancel 15 + oncancel, 16 + onlogin 16 17 }: { 17 18 eventUri: string; 18 19 eventCid: string | null; 19 20 initialRsvpStatus?: 'going' | 'interested' | 'notgoing' | null; 20 21 initialRsvpRkey?: string | null; 21 - onrsvp?: (status: 'going' | 'interested') => void; 22 + onrsvp?: (status: 'going' | 'interested', rkey: string) => void; 22 23 oncancel?: () => void; 24 + onlogin?: () => void; 23 25 } = $props(); 24 26 25 27 let rsvpStatusOverride: 'going' | 'interested' | 'notgoing' | null | undefined = $state( ··· 58 60 rsvpStatusOverride = status; 59 61 rsvpRkeyOverride = key; 60 62 launchConfetti(); 61 - onrsvp?.(status); 63 + onrsvp?.(status, key); 62 64 } 63 65 } catch (e) { 64 66 console.error('Failed to submit RSVP:', e); ··· 95 97 <div class="flex items-center justify-between gap-4"> 96 98 <p class="text-base-600 dark:text-base-400 text-sm">Log in to RSVP to this event</p> 97 99 98 - <Button onclick={() => atProtoLoginModalState.show()}>Log in to RSVP</Button> 100 + <Button onclick={() => { onlogin?.(); atProtoLoginModalState.show(); }}>Log in to RSVP</Button> 99 101 </div> 100 102 {:else if rsvpStatus === 'going'} 101 103 <div class="flex items-center justify-between">
+10 -6
src/routes/p/atmosphereconf.org/+page.server.ts
··· 25 25 26 26 const events = response ? flattenEventRecords(response.records) : []; 27 27 28 - // Build a set of event URIs the user has RSVP'd to (going or interested) 29 - const rsvpEventUris = new Set<string>(); 28 + // Build maps of event URI → rsvp status and rkey 29 + const rsvpStatuses: Record<string, string> = {}; 30 + const rsvpRkeys: Record<string, string> = {}; 30 31 if (rsvpResponse?.ok) { 31 32 for (const r of rsvpResponse.data.records ?? []) { 32 33 const status = r.record?.status; 33 - if (status?.endsWith('#going') || status?.endsWith('#interested')) { 34 - const uri = r.record?.subject?.uri; 35 - if (uri) rsvpEventUris.add(uri); 34 + const uri = r.record?.subject?.uri; 35 + if (status && uri) { 36 + const shortStatus = status.split('#').pop()!; 37 + rsvpStatuses[uri] = shortStatus; 38 + if (r.rkey) rsvpRkeys[uri] = r.rkey; 36 39 } 37 40 } 38 41 } ··· 41 44 hostProfile: profile, 42 45 events, 43 46 actor, 44 - rsvpEventUris: [...rsvpEventUris], 47 + rsvpStatuses, 48 + rsvpRkeys, 45 49 loggedIn: !!locals.did 46 50 }; 47 51 }
+18 -2
src/routes/p/atmosphereconf.org/+page.svelte
··· 23 23 ); 24 24 25 25 let scheduleEvents = $derived(getScheduleEvents(data.events)); 26 - let rsvpUris = $derived(new Set(data.rsvpEventUris ?? [])); 26 + let rsvpStatuses: Record<string, string> = $state(data.rsvpStatuses ?? {}); 27 + let rsvpRkeys: Record<string, string> = $state(data.rsvpRkeys ?? {}); 27 28 let filterMode: string = $state('all'); 28 29 let selectedDay: string = $state('all'); 29 30 ··· 73 74 const lastEnd = last.end || last.start; 74 75 return now >= new Date(first) && now <= new Date(lastEnd); 75 76 }); 77 + 78 + function handleRsvpChange(uri: string, status: string | null, rkey?: string) { 79 + if (status) { 80 + rsvpStatuses = { ...rsvpStatuses, [uri]: status }; 81 + if (rkey) rsvpRkeys = { ...rsvpRkeys, [uri]: rkey }; 82 + } else { 83 + const { [uri]: _, ...rest } = rsvpStatuses; 84 + rsvpStatuses = rest; 85 + const { [uri]: __, ...restKeys } = rsvpRkeys; 86 + rsvpRkeys = restKeys; 87 + } 88 + } 76 89 77 90 function scrollToNow() { 78 91 const els = document.querySelectorAll('[data-now-line]'); ··· 193 206 bind:activeRoom={() => activeRooms[dayIndex] ?? 0, (v) => (activeRooms[dayIndex] = v)} 194 207 {nowVancouverKey} 195 208 {nowVancouverMinutes} 196 - {rsvpUris} 209 + {rsvpStatuses} 210 + {rsvpRkeys} 197 211 dimUnattended={filterMode === 'attending'} 212 + loggedIn={data.loggedIn} 213 + onrsvpchange={handleRsvpChange} 198 214 /> 199 215 </section> 200 216 {/each}
+13 -6
src/routes/p/atmosphereconf.org/DaySchedule.svelte
··· 18 18 activeRoom = $bindable(0), 19 19 nowVancouverKey, 20 20 nowVancouverMinutes, 21 - rsvpUris = new Set(), 22 - dimUnattended = false 21 + rsvpStatuses = {}, 22 + rsvpRkeys = {}, 23 + dimUnattended = false, 24 + loggedIn = false, 25 + onrsvpchange 23 26 }: { 24 27 grid: GridData; 25 28 rooms: string[]; ··· 28 31 activeRoom: number; 29 32 nowVancouverKey: string; 30 33 nowVancouverMinutes: number; 31 - rsvpUris?: Set<string>; 34 + rsvpStatuses?: Record<string, string>; 35 + rsvpRkeys?: Record<string, string>; 32 36 dimUnattended?: boolean; 37 + loggedIn?: boolean; 38 + onrsvpchange?: (uri: string, status: string | null, rkey?: string) => void; 33 39 } = $props(); 34 40 35 41 let dayKey = $derived(dayEvents[0]?.start ? getDayKey(dayEvents[0].start) : ''); ··· 39 45 if (!dimUnattended) return false; 40 46 if (event.type === 'info') return true; 41 47 const uri = `at://${event.did}/community.lexicon.calendar.event/${event.rkey}`; 42 - return !rsvpUris.has(uri); 48 + const status = rsvpStatuses[uri]; 49 + return !status || status === 'notgoing'; 43 50 } 44 51 </script> 45 52 ··· 97 104 class="relative flex min-h-5 p-0.5 transition-opacity {isDimmed(event) ? (linkableTypes.has(event.type) && event.rkey ? 'opacity-30 hover:opacity-80' : 'opacity-30') : ''}" 98 105 style="grid-row: {event.startRow} / span {event.spanRows}; grid-column: 1; z-index: {event.zIndex}" 99 106 > 100 - <ScheduleEventCell {event} /> 107 + <ScheduleEventCell {event} {rsvpStatuses} {rsvpRkeys} {loggedIn} {onrsvpchange} /> 101 108 </li> 102 109 {/each} 103 110 {#if nowRow} ··· 179 186 class="relative flex min-h-5 p-0.5 transition-opacity {isDimmed(event) ? (linkableTypes.has(event.type) && event.rkey ? 'opacity-30 hover:opacity-80' : 'opacity-30') : ''}" 180 187 style="grid-row: {event.startRow} / span {event.spanRows}; grid-column: {event.colStart} / span {event.colSpan}; z-index: {event.zIndex}" 181 188 > 182 - <ScheduleEventCell {event} /> 189 + <ScheduleEventCell {event} {rsvpStatuses} {rsvpRkeys} {loggedIn} {onrsvpchange} /> 183 190 </li> 184 191 {/each} 185 192 {#if nowRow}
+67 -5
src/routes/p/atmosphereconf.org/ScheduleEventCell.svelte
··· 7 7 durationMinutes, 8 8 formatTime 9 9 } from './schedule-utils'; 10 + import { Modal, Button } from '@foxui/core'; 11 + import EventRsvp from '$lib/components/EventRsvp.svelte'; 10 12 11 - let { event }: { event: GridEvent } = $props(); 13 + let { 14 + event, 15 + rsvpStatuses = {}, 16 + rsvpRkeys = {}, 17 + loggedIn = false, 18 + onrsvpchange 19 + }: { 20 + event: GridEvent; 21 + rsvpStatuses?: Record<string, string>; 22 + rsvpRkeys?: Record<string, string>; 23 + loggedIn?: boolean; 24 + onrsvpchange?: (uri: string, status: string | null, rkey?: string) => void; 25 + } = $props(); 26 + 27 + let modalOpen = $state(false); 28 + 29 + let initialRsvpStatus = $derived((rsvpStatuses[event.uri] as 'going' | 'interested' | 'notgoing' | undefined) ?? null); 30 + let initialRsvpRkey = $derived(rsvpRkeys[event.uri] ?? null); 12 31 </script> 13 32 14 33 {#if linkableTypes.has(event.type) && event.rkey} 15 - <a 16 - href="/p/atmosphereconf.org/e/{event.rkey}" 17 - class="flex-1 overflow-hidden rounded-md leading-tight transition-[filter] hover:brightness-95 {getEventColor( 34 + <button 35 + onclick={() => (modalOpen = true)} 36 + class="relative cursor-pointer flex-1 overflow-hidden rounded-md leading-tight transition-[filter] hover:brightness-95 {getEventColor( 18 37 event.type 19 38 )} {event.type === 'info' 20 39 ? 'flex flex-col items-center justify-center px-2 py-1.5 text-center text-xs' ··· 26 45 {#if event.speakers?.length && !isLightning(event.type)} 27 46 <p class="mt-0.5 opacity-75">{event.speakers.map((s) => s.name).join(', ')}</p> 28 47 {/if} 29 - </a> 48 + {#if initialRsvpStatus === 'going'} 49 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="absolute right-1 bottom-1 size-3 opacity-60"> 50 + <path fill-rule="evenodd" d="M16.704 4.153a.75.75 0 0 1 .143 1.052l-8 10.5a.75.75 0 0 1-1.127.075l-4.5-4.5a.75.75 0 0 1 1.06-1.06l3.894 3.893 7.48-9.817a.75.75 0 0 1 1.05-.143Z" clip-rule="evenodd" /> 51 + </svg> 52 + {:else if initialRsvpStatus === 'interested'} 53 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="absolute right-1 bottom-1 size-3 opacity-60"> 54 + <path fill-rule="evenodd" d="M10.868 2.884c-.321-.772-1.415-.772-1.736 0l-1.83 4.401-4.753.381c-.833.067-1.171 1.107-.536 1.651l3.62 3.102-1.106 4.637c-.194.813.691 1.456 1.405 1.02L10 15.591l4.069 2.485c.713.436 1.598-.207 1.404-1.02l-1.106-4.637 3.62-3.102c.635-.544.297-1.584-.536-1.65l-4.752-.382-1.831-4.401Z" clip-rule="evenodd" /> 55 + </svg> 56 + {/if} 57 + </button> 58 + 59 + <Modal bind:open={modalOpen}> 60 + <div> 61 + <h2 class="text-base-900 dark:text-base-50 text-lg font-semibold">{event.title}</h2> 62 + 63 + {#if event.start} 64 + <p class="text-base-500 dark:text-base-400 mt-1 text-sm"> 65 + {formatTime(event.start)}{event.end ? ` – ${formatTime(event.end)}` : ''} 66 + </p> 67 + {/if} 68 + 69 + {#if event.speakers?.length} 70 + <p class="text-base-600 dark:text-base-300 mt-1 text-sm"> 71 + {event.speakers.map((s) => s.name).join(', ')} 72 + </p> 73 + {/if} 74 + 75 + {#if event.description} 76 + <p class="text-base-500 dark:text-base-400 mt-3 text-sm">{event.description}</p> 77 + {/if} 78 + 79 + <EventRsvp 80 + eventUri={event.uri} 81 + eventCid={event.cid ?? null} 82 + {initialRsvpStatus} 83 + {initialRsvpRkey} 84 + onlogin={() => (modalOpen = false)} 85 + onrsvp={(status, key) => { onrsvpchange?.(event.uri, status, key); modalOpen = false; }} 86 + oncancel={() => { onrsvpchange?.(event.uri, null); }} 87 + /> 88 + 89 + <Button href="/p/atmosphereconf.org/e/{event.rkey}" variant="secondary" class="mt-2 w-full">Go to event</Button> 90 + </div> 91 + </Modal> 30 92 {:else} 31 93 <div 32 94 class="flex-1 overflow-hidden rounded-md leading-tight {getEventColor(
+5 -1
src/routes/p/atmosphereconf.org/schedule-utils.ts
··· 10 10 room?: string; 11 11 description?: string; 12 12 did: string; 13 + uri: string; 14 + cid?: string | null; 13 15 } 14 16 15 17 export interface GridEvent extends ScheduleEvent { ··· 131 133 end: e.endsAt, 132 134 room: ad.room as string | undefined, 133 135 description: e.description, 134 - did: e.did 136 + did: e.did, 137 + uri: e.uri, 138 + cid: e.cid 135 139 }; 136 140 }) 137 141 .sort((a, b) => a.start.localeCompare(b.start));