atmo.rsvp
3
fork

Configure Feed

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

more commiats

Florian cc8d2130 32142f1d

+163 -8
+1 -1
src/lib/components/EventEditor.svelte
··· 574 574 user.profile?.handle && user.profile.handle !== 'handle.invalid' 575 575 ? user.profile.handle 576 576 : user.did; 577 - goto(`/p/${handle}/e/${rkey}`); 577 + goto(`/p/${handle}/e/${rkey}${isNew ? '?created=true' : ''}`); 578 578 } else { 579 579 error = `Failed to ${isNew ? 'create' : 'save'} event. Please try again.`; 580 580 }
+103
src/lib/components/ShareModal.svelte
··· 1 + <script lang="ts"> 2 + import { Modal, Button, Avatar } from '@foxui/core'; 3 + import { LinkCard } from '@foxui/social'; 4 + import { user } from '$lib/atproto/auth.svelte'; 5 + 6 + let { 7 + open = $bindable(false), 8 + url, 9 + title = 'Event created!', 10 + shareText, 11 + eventName, 12 + ogImageUrl 13 + }: { 14 + open: boolean; 15 + url: string; 16 + title?: string; 17 + shareText?: string; 18 + eventName?: string; 19 + ogImageUrl?: string; 20 + } = $props(); 21 + 22 + let copiedUrl = $state(false); 23 + let copiedText = $state(false); 24 + 25 + // Split share text into the part before the URL 26 + let textBeforeUrl = $derived(shareText ? shareText.replace(url, '').trim() : undefined); 27 + 28 + async function copyUrl() { 29 + try { 30 + await navigator.clipboard.writeText(url); 31 + copiedUrl = true; 32 + setTimeout(() => (copiedUrl = false), 2000); 33 + } catch {} 34 + } 35 + 36 + async function copyText() { 37 + if (!shareText) return; 38 + try { 39 + await navigator.clipboard.writeText(shareText); 40 + copiedText = true; 41 + setTimeout(() => (copiedText = false), 2000); 42 + } catch {} 43 + } 44 + 45 + let blueskyButton: HTMLAnchorElement | undefined = $state(); 46 + 47 + let blueskyUrl = $derived( 48 + `https://bsky.app/intent/compose?text=${encodeURIComponent(shareText || url)}` 49 + ); 50 + </script> 51 + 52 + <Modal 53 + bind:open 54 + onOpenAutoFocus={(e) => { 55 + e.preventDefault(); 56 + blueskyButton?.focus(); 57 + }} 58 + > 59 + <div> 60 + <h2 class="text-base-900 dark:text-base-50 mb-2 text-xl font-bold">{title}</h2> 61 + <p class="text-base-500 dark:text-base-400 mb-4 text-sm">Share it with others!</p> 62 + 63 + <div 64 + class="bg-base-200 dark:bg-base-950/30 border-base-400/40 dark:border-base-700 mb-6 overflow-hidden rounded-xl border px-4 py-3 text-left" 65 + > 66 + {#if user.profile} 67 + <div class="flex items-center gap-2 pb-4"> 68 + <Avatar src={user.profile.avatar} alt="" class="size-6" /> 69 + <span class="text-base-700 dark:text-base-200 text-sm font-medium" 70 + >{user.profile.handle}</span 71 + > 72 + </div> 73 + {/if} 74 + {#if textBeforeUrl} 75 + <p class="text-base-700 dark:text-base-200 text-md font-semibold">{textBeforeUrl}</p> 76 + {/if} 77 + {#if eventName} 78 + <LinkCard 79 + href={url} 80 + meta={{ 81 + title: eventName, 82 + image: ogImageUrl 83 + }} 84 + class="mb-1" 85 + /> 86 + {/if} 87 + </div> 88 + 89 + <div class="flex flex-col gap-2 sm:flex-row"> 90 + <Button class="flex-1" variant="secondary" onclick={copyUrl}> 91 + {copiedUrl ? 'Copied!' : 'Copy link'} 92 + </Button> 93 + {#if shareText} 94 + <Button class="flex-1" variant="secondary" onclick={copyText}> 95 + {copiedText ? 'Copied!' : 'Copy text'} 96 + </Button> 97 + {/if} 98 + <a bind:this={blueskyButton} class="flex-1" href={blueskyUrl} target="_blank"> 99 + <Button class="w-full">Share to Bluesky</Button> 100 + </a> 101 + </div> 102 + </div> 103 + </Modal>
+22 -5
src/lib/contrail.ts
··· 300 300 const goingProfiles = goingResponse.ok ? (goingResponse.data.profiles ?? []) : []; 301 301 const interestedProfiles = interestedResponse.ok ? (interestedResponse.data.profiles ?? []) : []; 302 302 303 + // Deduplicate by DID (keep first occurrence) 304 + const seenGoing = new Set<string>(); 305 + const uniqueGoing = goingRecords.filter((r) => { 306 + if (seenGoing.has(r.did)) return false; 307 + seenGoing.add(r.did); 308 + return true; 309 + }); 310 + const seenInterested = new Set<string>(); 311 + const uniqueInterested = interestedRecords.filter((r) => { 312 + if (seenInterested.has(r.did)) return false; 313 + seenInterested.add(r.did); 314 + return true; 315 + }); 316 + 303 317 return { 304 - going: goingRecords.map((record) => buildAttendee(record.did, 'going', goingProfiles)), 305 - interested: interestedRecords.map((record) => 318 + going: uniqueGoing.map((record) => buildAttendee(record.did, 'going', goingProfiles)), 319 + interested: uniqueInterested.map((record) => 306 320 buildAttendee(record.did, 'interested', interestedProfiles) 307 321 ), 308 - goingCount: goingRecords.length, 309 - interestedCount: interestedRecords.length 322 + goingCount: uniqueGoing.length, 323 + interestedCount: uniqueInterested.length 310 324 }; 311 325 } 312 326 ··· 321 335 322 336 if (!response.ok) return []; 323 337 338 + const seen = new Set<string>(); 324 339 return (response.data.records ?? []) 325 340 .filter((record) => { 326 341 const status = record.record?.status; ··· 329 344 .flatMap((record) => { 330 345 if (!record.event) return []; 331 346 const flatEvent = flattenEventRecord(record.event); 332 - return flatEvent ? [flatEvent] : []; 347 + if (!flatEvent || seen.has(flatEvent.uri)) return []; 348 + seen.add(flatEvent.uri); 349 + return [flatEvent]; 333 350 }); 334 351 }
+35 -2
src/routes/p/[actor]/e/[rkey]/+page.svelte
··· 4 4 import { user } from '$lib/atproto/auth.svelte'; 5 5 import { Avatar as FoxAvatar, Badge, Button } from '@foxui/core'; 6 6 import Map from '$lib/components/Map.svelte'; 7 + import ShareModal from '$lib/components/ShareModal.svelte'; 7 8 import Avatar from 'svelte-boring-avatars'; 8 9 import EventRsvp from './EventRsvp.svelte'; 9 10 import EventAttendees from './EventAttendees.svelte'; 10 11 import { page } from '$app/state'; 12 + import { goto } from '$app/navigation'; 11 13 import { marked } from 'marked'; 12 14 import { sanitize } from '$lib/cal/sanitize'; 13 15 import { generateICalEvent } from '$lib/cal/ical'; 16 + import { launchConfetti } from '@foxui/visual'; 14 17 15 18 let { data } = $props(); 16 19 ··· 21 24 let attendees = $derived(data.attendees); 22 25 23 26 let hostUrl = $derived(`/p/${hostProfile?.handle || did}`); 27 + let eventPath = $derived(`/p/${hostProfile?.handle || did}/e/${data.rkey}`); 28 + let shareUrl = $derived(typeof window !== 'undefined' ? `${window.location.origin}${eventPath}` : eventPath); 24 29 25 30 let startDate = $derived(new Date(eventData.startsAt)); 26 31 let endDate = $derived(eventData.endsAt ? new Date(eventData.endsAt) : null); ··· 121 126 } 122 127 } 123 128 129 + let showShareModal = $state(false); 130 + let shareModalTitle = $state('Event created!'); 131 + let shareModalText: string | undefined = $state(undefined); 132 + 124 133 import { onMount } from 'svelte'; 125 - onMount(initGeoLocation); 134 + onMount(() => { 135 + initGeoLocation(); 136 + 137 + const url = new URL(window.location.href); 138 + if (url.searchParams.has('created')) { 139 + url.searchParams.delete('created'); 140 + history.replaceState({}, '', url.pathname); 141 + launchConfetti(); 142 + shareModalTitle = 'Event created!'; 143 + shareModalText = `I'm hosting "${eventData.name}"!\n\n${shareUrl}`; 144 + showShareModal = true; 145 + } 146 + }); 126 147 127 148 let thumbnailImage = $derived.by(() => { 128 149 if (!eventData.media || eventData.media.length === 0) return null; ··· 225 246 : null 226 247 ); 227 248 228 - // let smokesignalUrl = $derived(`https://smokesignal.events/${did}/${rkey}`); 229 249 let eventUri = $derived(`at://${did}/community.lexicon.calendar.event/${rkey}`); 230 250 231 251 let ogImageUrl = $derived(`${page.url.origin}${page.url.pathname}/og.png`); ··· 244 264 handle: user.profile?.handle, 245 265 url: `/${user.profile?.handle || user.did}` 246 266 }); 267 + if(status === 'interested') return; 268 + shareModalTitle = "You're going!"; 269 + shareModalText = `I'm going to "${eventData.name}"!\n\n${shareUrl}`; 270 + showShareModal = true; 247 271 } 248 272 249 273 function handleRsvpCancel() { ··· 548 572 </div> 549 573 </div> 550 574 </div> 575 + 576 + <ShareModal 577 + bind:open={showShareModal} 578 + url={shareUrl} 579 + title={shareModalTitle} 580 + shareText={shareModalText} 581 + eventName={eventData.name} 582 + ogImageUrl={ogImageUrl} 583 + />
+2
src/routes/p/[actor]/e/[rkey]/EventRsvp.svelte
··· 4 4 import { notifyContrailOfUpdate } from '$lib/contrail'; 5 5 import { atProtoLoginModalState } from '@foxui/social'; 6 6 import { Avatar, Button } from '@foxui/core'; 7 + import { launchConfetti } from '@foxui/visual'; 7 8 8 9 let { 9 10 eventUri, ··· 56 57 notifyContrailOfUpdate(rsvpUri); 57 58 rsvpStatusOverride = status; 58 59 rsvpRkeyOverride = key; 60 + launchConfetti(); 59 61 onrsvp?.(status); 60 62 } 61 63 } catch (e) {