atmo.rsvp
3
fork

Configure Feed

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

at main 203 lines 7.2 kB view raw
1<script lang="ts"> 2 import type { AttendeeInfo } from '$lib/contrail'; 3 import { Avatar as FoxAvatar } from '@foxui/core'; 4 import { scale } from 'svelte/transition'; 5 import { flip } from 'svelte/animate'; 6 import { Modal } from '@foxui/core'; 7 8 let { 9 going = [], 10 interested = [], 11 goingCount: initialGoingCount = going.length, 12 interestedCount: initialInterestedCount = interested.length 13 }: { 14 going?: AttendeeInfo[]; 15 interested?: AttendeeInfo[]; 16 goingCount?: number; 17 interestedCount?: number; 18 } = $props(); 19 20 let goingCountOverride: number | null = $state(null); 21 let interestedCountOverride: number | null = $state(null); 22 let goingAttendeesOverride: AttendeeInfo[] | null = $state(null); 23 let interestedAttendeesOverride: AttendeeInfo[] | null = $state(null); 24 25 let modalOpen = $state(false); 26 let modalGroup: 'going' | 'interested' = $state('going'); 27 28 const MAX_AVATARS = 18; 29 30 let goingCount = $derived(goingCountOverride ?? initialGoingCount); 31 let interestedCount = $derived(interestedCountOverride ?? initialInterestedCount); 32 let goingAttendees = $derived(goingAttendeesOverride ?? going); 33 let interestedAttendees = $derived(interestedAttendeesOverride ?? interested); 34 35 let totalCount = $derived(goingCount + interestedCount); 36 37 let goingDisplay = $derived(goingAttendees.slice(0, MAX_AVATARS)); 38 let goingOverflow = $derived(goingCount - goingDisplay.length); 39 40 let interestedDisplay = $derived(interestedAttendees.slice(0, MAX_AVATARS)); 41 let interestedOverflow = $derived(interestedCount - interestedDisplay.length); 42 43 let modalAttendees = $derived(modalGroup === 'going' ? goingAttendees : interestedAttendees); 44 let modalTitle = $derived(modalGroup === 'going' ? 'Going' : 'Interested'); 45 46 function openModal(group: 'going' | 'interested') { 47 modalGroup = group; 48 modalOpen = true; 49 } 50 51 export function addAttendee(attendee: AttendeeInfo) { 52 const nextGoing = goingAttendees.filter((a) => a.did !== attendee.did); 53 const nextInterested = interestedAttendees.filter((a) => a.did !== attendee.did); 54 55 // Remove from both lists first (in case of status change) 56 if (attendee.status === 'going') { 57 goingAttendeesOverride = [attendee, ...nextGoing]; 58 interestedAttendeesOverride = nextInterested; 59 goingCountOverride = goingAttendeesOverride.length; 60 interestedCountOverride = interestedAttendeesOverride.length; 61 } else if (attendee.status === 'interested') { 62 goingAttendeesOverride = nextGoing; 63 interestedAttendeesOverride = [attendee, ...nextInterested]; 64 goingCountOverride = goingAttendeesOverride.length; 65 interestedCountOverride = interestedAttendeesOverride.length; 66 } 67 } 68 69 function thumbnail(url: string | undefined) { 70 return url?.replace('/avatar/', '/avatar_thumbnail/'); 71 } 72 73 export function removeAttendee(did: string) { 74 const wasGoing = goingAttendees.some((a) => a.did === did); 75 const wasInterested = interestedAttendees.some((a) => a.did === did); 76 goingAttendeesOverride = goingAttendees.filter((a) => a.did !== did); 77 interestedAttendeesOverride = interestedAttendees.filter((a) => a.did !== did); 78 if (wasGoing) goingCountOverride = goingAttendeesOverride.length; 79 if (wasInterested) interestedCountOverride = interestedAttendeesOverride.length; 80 } 81</script> 82 83{#if totalCount > 0} 84 <div class="mb-2"> 85 {#if goingCount > 0} 86 <button 87 type="button" 88 class="hover:bg-base-100 dark:hover:bg-base-800/50 -mx-2 block w-full cursor-pointer rounded-xl px-2 py-2 text-left transition-colors" 89 onclick={() => openModal('going')} 90 > 91 <p class="text-base-900 dark:text-base-50 mb-2 text-sm"> 92 <span class="font-bold">{goingCount}</span> 93 <span 94 class="text-base-500 dark:text-base-400 text-xs font-semibold tracking-wider uppercase" 95 >Going</span 96 > 97 </p> 98 <div class="flex items-center"> 99 <div class="flex flex-wrap -space-y-2 -space-x-4 pr-4"> 100 {#each goingDisplay as person (person.did)} 101 <div 102 animate:flip={{ duration: 300 }} 103 in:scale={{ duration: 300, start: 0.5 }} 104 out:scale={{ duration: 200, start: 0.5 }} 105 > 106 <FoxAvatar 107 src={thumbnail(person.avatar)} 108 alt={person.name} 109 class="border-base-100 dark:border-base-900 size-12 border-2" 110 /> 111 </div> 112 {/each} 113 {#if goingOverflow > 0} 114 <span 115 class="bg-base-200 dark:bg-base-800 text-base-950 dark:text-base-100 border-base-100 dark:border-base-900 z-10 inline-flex size-12 items-center justify-center rounded-full border-2 text-sm font-semibold" 116 > 117 +{goingOverflow} 118 </span> 119 {/if} 120 </div> 121 </div> 122 </button> 123 {/if} 124 125 {#if interestedCount > 0} 126 <button 127 type="button" 128 class="hover:bg-base-100 dark:hover:bg-base-800/50 -mx-2 mt-4 block w-full cursor-pointer rounded-xl px-2 py-2 text-left transition-colors" 129 onclick={() => openModal('interested')} 130 > 131 <p class="text-base-900 dark:text-base-50 mb-2 text-sm"> 132 <span class="font-bold">{interestedCount}</span> 133 <span 134 class="text-base-500 dark:text-base-400 text-xs font-semibold tracking-wider uppercase" 135 >Interested</span 136 > 137 </p> 138 <div class="flex items-center"> 139 <div class="flex flex-wrap -space-y-2 -space-x-4 pr-4"> 140 {#each interestedDisplay as person (person.did)} 141 <div 142 animate:flip={{ duration: 300 }} 143 in:scale={{ duration: 300, start: 0.5 }} 144 out:scale={{ duration: 200, start: 0.5 }} 145 > 146 <FoxAvatar 147 src={thumbnail(person.avatar)} 148 alt={person.name} 149 class="border-base-100 dark:border-base-900 size-12 border-2" 150 /> 151 </div> 152 {/each} 153 {#if interestedOverflow > 0} 154 <span 155 class="bg-base-200 dark:bg-base-800 text-base-950 dark:text-base-100 border-base-100 dark:border-base-900 z-10 inline-flex size-12 items-center justify-center rounded-full border-2 text-sm font-semibold" 156 > 157 +{interestedOverflow} 158 </span> 159 {/if} 160 </div> 161 </div> 162 </button> 163 {/if} 164 </div> 165{/if} 166 167<Modal 168 bind:open={modalOpen} 169 closeButton 170 onOpenAutoFocus={(e: Event) => e.preventDefault()} 171 class="p-0" 172> 173 <p class="text-base-900 dark:text-base-50 px-4 pt-4 text-lg font-semibold"> 174 {modalTitle} 175 <span class="text-base-500 dark:text-base-400 text-sm font-normal"> 176 ({modalAttendees.length}) 177 </span> 178 </p> 179 <div 180 class="dark:bg-base-900/50 bg-base-200/30 mx-4 mb-4 max-h-80 space-y-1 overflow-y-auto rounded-xl p-2" 181 > 182 {#each modalAttendees as person (person.did)} 183 <a 184 href={person.url} 185 target={person.url?.startsWith('/') ? undefined : '_blank'} 186 rel={person.url?.startsWith('/') ? undefined : 'noopener noreferrer'} 187 class="hover:bg-base-200 dark:hover:bg-base-900 flex items-center gap-3 rounded-xl px-2 py-2 transition-colors" 188 > 189 <FoxAvatar src={thumbnail(person.avatar)} alt={person.name} class="size-10 shrink-0" /> 190 <div class="min-w-0"> 191 <p class="text-base-900 dark:text-base-50 truncate text-sm font-medium"> 192 {person.name} 193 </p> 194 {#if person.handle} 195 <p class="text-base-500 dark:text-base-400 truncate text-xs"> 196 @{person.handle} 197 </p> 198 {/if} 199 </div> 200 </a> 201 {/each} 202 </div> 203</Modal>