your personal website on atproto - mirror blento.app

add event cards

+540 -1
+4
src/lib/cards/index.ts
··· 27 import { StandardSiteDocumentListCardDefinition } from './content/StandardSiteDocumentListCard'; 28 import { StatusphereCardDefinition } from './media/StatusphereCard'; 29 import { EventCardDefinition } from './social/EventCard'; 30 import { VCardCardDefinition } from './social/VCardCard'; 31 import { DrawCardDefinition } from './visual/DrawCard'; 32 import { TimerCardDefinition } from './utilities/TimerCard'; ··· 83 StandardSiteDocumentListCardDefinition, 84 StatusphereCardDefinition, 85 EventCardDefinition, 86 VCardCardDefinition, 87 DrawCardDefinition, 88 TimerCardDefinition,
··· 27 import { StandardSiteDocumentListCardDefinition } from './content/StandardSiteDocumentListCard'; 28 import { StatusphereCardDefinition } from './media/StatusphereCard'; 29 import { EventCardDefinition } from './social/EventCard'; 30 + import { UpcomingEventsCardDefinition } from './social/UpcomingEventsCard'; 31 + import { UpcomingRsvpsCardDefinition } from './social/UpcomingRsvpsCard'; 32 import { VCardCardDefinition } from './social/VCardCard'; 33 import { DrawCardDefinition } from './visual/DrawCard'; 34 import { TimerCardDefinition } from './utilities/TimerCard'; ··· 85 StandardSiteDocumentListCardDefinition, 86 StatusphereCardDefinition, 87 EventCardDefinition, 88 + UpcomingEventsCardDefinition, 89 + UpcomingRsvpsCardDefinition, 90 VCardCardDefinition, 91 DrawCardDefinition, 92 TimerCardDefinition,
+265
src/lib/cards/social/UpcomingEventsCard/UpcomingEventsCard.svelte
···
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { Badge } from '@foxui/core'; 4 + import { 5 + getAdditionalUserData, 6 + getDidContext, 7 + getHandleContext, 8 + getIsMobile 9 + } from '$lib/website/context'; 10 + import type { ContentComponentProps } from '../../types'; 11 + import { CardDefinitionsByType } from '../..'; 12 + import type { EventData } from '../EventCard'; 13 + import { user } from '$lib/atproto'; 14 + import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 15 + import * as TID from '@atcute/tid'; 16 + 17 + let { item }: ContentComponentProps = $props(); 18 + 19 + let isMobile = getIsMobile(); 20 + let isLoaded = $state(false); 21 + const data = getAdditionalUserData(); 22 + const did = getDidContext(); 23 + const handle = getHandleContext(); 24 + 25 + type EventWithRkey = EventData & { rkey: string }; 26 + 27 + // svelte-ignore state_referenced_locally 28 + let events = $state<EventWithRkey[]>( 29 + ((data['upcomingEvents'] as { events?: EventWithRkey[] })?.events ?? []) as EventWithRkey[] 30 + ); 31 + 32 + onMount(async () => { 33 + try { 34 + const loaded = await CardDefinitionsByType[item.cardType]?.loadData?.([item], { 35 + did, 36 + handle 37 + }); 38 + const result = loaded as { events?: EventWithRkey[] } | undefined; 39 + const freshEvents = result?.events ?? []; 40 + 41 + if (freshEvents.length > 0) { 42 + events = freshEvents; 43 + } 44 + 45 + data['upcomingEvents'] = { events }; 46 + } catch (e) { 47 + console.error('Failed to load upcoming events', e); 48 + } 49 + 50 + isLoaded = true; 51 + }); 52 + 53 + function formatDate(dateStr: string): string { 54 + const date = new Date(dateStr); 55 + return date.toLocaleDateString('en-US', { 56 + weekday: 'short', 57 + month: 'short', 58 + day: 'numeric' 59 + }); 60 + } 61 + 62 + function formatTime(dateStr: string): string { 63 + const date = new Date(dateStr); 64 + return date.toLocaleTimeString('en-US', { 65 + hour: 'numeric', 66 + minute: '2-digit' 67 + }); 68 + } 69 + 70 + function getModeLabel(mode: string): string { 71 + if (mode.includes('virtual')) return 'Virtual'; 72 + if (mode.includes('hybrid')) return 'Hybrid'; 73 + if (mode.includes('inperson')) return 'In-Person'; 74 + return 'Event'; 75 + } 76 + 77 + function getModeColor(mode: string): string { 78 + if (mode.includes('virtual')) return 'blue'; 79 + if (mode.includes('hybrid')) return 'purple'; 80 + if (mode.includes('inperson')) return 'green'; 81 + return 'gray'; 82 + } 83 + 84 + let isOwner = $derived(user.isLoggedIn && user.did === did); 85 + let isRefreshing = $state(false); 86 + 87 + async function refreshEvents() { 88 + isRefreshing = true; 89 + try { 90 + const loaded = await CardDefinitionsByType[item.cardType]?.loadData?.([item], { 91 + did, 92 + handle 93 + }); 94 + const result = loaded as { events?: EventWithRkey[] } | undefined; 95 + const freshEvents = result?.events ?? []; 96 + events = freshEvents; 97 + data['upcomingEvents'] = { events }; 98 + } catch (e) { 99 + console.error('Failed to refresh events', e); 100 + } 101 + isRefreshing = false; 102 + } 103 + </script> 104 + 105 + <div class="flex h-full flex-col overflow-hidden p-4"> 106 + <!-- Header row --> 107 + <div class="mb-3 flex items-center justify-between"> 108 + <div class="flex items-center gap-2"> 109 + <div 110 + class="bg-base-100 border-base-300 accent:bg-accent-100/50 accent:border-accent-200 dark:border-base-800 dark:bg-base-900 flex size-8 shrink-0 items-center justify-center rounded-xl border" 111 + > 112 + <svg 113 + xmlns="http://www.w3.org/2000/svg" 114 + fill="none" 115 + viewBox="0 0 24 24" 116 + stroke-width="1.5" 117 + stroke="currentColor" 118 + class="size-4" 119 + > 120 + <path 121 + stroke-linecap="round" 122 + stroke-linejoin="round" 123 + d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" 124 + /> 125 + </svg> 126 + </div> 127 + <span class="text-base-900 dark:text-base-50 text-sm font-semibold">Events</span> 128 + </div> 129 + {#if isOwner} 130 + <div class="flex items-center gap-1"> 131 + <button 132 + onclick={refreshEvents} 133 + disabled={isRefreshing} 134 + title="Refresh events" 135 + class="bg-base-100 hover:bg-base-200 dark:bg-base-800 dark:hover:bg-base-700 accent:bg-accent-400/30 accent:hover:bg-accent-400/50 text-base-700 dark:text-base-300 z-50 flex size-7 items-center justify-center rounded-lg transition-colors disabled:opacity-50" 136 + > 137 + <svg 138 + xmlns="http://www.w3.org/2000/svg" 139 + fill="none" 140 + viewBox="0 0 24 24" 141 + stroke-width="2" 142 + stroke="currentColor" 143 + class="size-4" 144 + class:animate-spin={isRefreshing} 145 + > 146 + <path 147 + stroke-linecap="round" 148 + stroke-linejoin="round" 149 + d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.992 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182M20.016 4.657v4.992" 150 + /> 151 + </svg> 152 + </button> 153 + <a 154 + href="/{handle}/events/{TID.now()}/edit" 155 + target="_blank" 156 + title="Create new event" 157 + class="bg-base-100 hover:bg-base-200 dark:bg-base-800 dark:hover:bg-base-700 accent:bg-accent-400/30 accent:hover:bg-accent-400/50 text-base-700 dark:text-base-300 z-50 flex size-7 items-center justify-center rounded-lg transition-colors" 158 + > 159 + <svg 160 + xmlns="http://www.w3.org/2000/svg" 161 + fill="none" 162 + viewBox="0 0 24 24" 163 + stroke-width="2" 164 + stroke="currentColor" 165 + class="size-4" 166 + > 167 + <path 168 + stroke-linecap="round" 169 + stroke-linejoin="round" 170 + d="M12 4.5v15m7.5-7.5h-15" 171 + /> 172 + </svg> 173 + </a> 174 + </div> 175 + {/if} 176 + </div> 177 + 178 + <!-- Scrollable list --> 179 + <div class="flex-1 overflow-y-auto"> 180 + {#if events.length > 0} 181 + <div class="flex flex-col gap-2"> 182 + {#each events as event (event.rkey)} 183 + <a 184 + href="https://blento.app/{did}/events/{event.rkey}" 185 + target="_blank" 186 + class="hover:bg-base-100 dark:hover:bg-base-800 accent:hover:bg-accent-400/20 flex flex-col gap-1 rounded-lg p-2 transition-colors" 187 + use:qrOverlay={{ context: { title: event.name } }} 188 + > 189 + <div class="flex items-center gap-2"> 190 + <span class="text-base-900 dark:text-base-50 line-clamp-1 flex-1 text-sm font-medium" 191 + >{event.name}</span 192 + > 193 + <Badge size="sm" color={getModeColor(event.mode)}> 194 + <span class="accent:text-base-900">{getModeLabel(event.mode)}</span> 195 + </Badge> 196 + </div> 197 + <div 198 + class="text-base-500 dark:text-base-400 accent:text-base-800 flex items-center gap-1 text-xs" 199 + > 200 + <svg 201 + xmlns="http://www.w3.org/2000/svg" 202 + fill="none" 203 + viewBox="0 0 24 24" 204 + stroke-width="1.5" 205 + stroke="currentColor" 206 + class="size-3" 207 + > 208 + <path 209 + stroke-linecap="round" 210 + stroke-linejoin="round" 211 + d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" 212 + /> 213 + </svg> 214 + <span>{formatDate(event.startsAt)} at {formatTime(event.startsAt)}</span> 215 + </div> 216 + {#if event.locations && event.locations.length > 0} 217 + {@const loc = event.locations[0]?.address} 218 + {#if loc} 219 + {@const parts = [loc.locality, loc.region, loc.country].filter(Boolean)} 220 + {#if parts.length > 0} 221 + <div 222 + class="text-base-500 dark:text-base-400 accent:text-base-800 flex items-center gap-1 text-xs" 223 + > 224 + <svg 225 + xmlns="http://www.w3.org/2000/svg" 226 + fill="none" 227 + viewBox="0 0 24 24" 228 + stroke-width="1.5" 229 + stroke="currentColor" 230 + class="size-3 shrink-0" 231 + > 232 + <path 233 + stroke-linecap="round" 234 + stroke-linejoin="round" 235 + d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 236 + /> 237 + <path 238 + stroke-linecap="round" 239 + stroke-linejoin="round" 240 + d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z" 241 + /> 242 + </svg> 243 + <span class="truncate">{parts.join(', ')}</span> 244 + </div> 245 + {/if} 246 + {/if} 247 + {/if} 248 + </a> 249 + {/each} 250 + </div> 251 + {:else if isLoaded} 252 + <div 253 + class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm" 254 + > 255 + No upcoming events 256 + </div> 257 + {:else} 258 + <div 259 + class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm" 260 + > 261 + Loading events... 262 + </div> 263 + {/if} 264 + </div> 265 + </div>
+52
src/lib/cards/social/UpcomingEventsCard/index.ts
···
··· 1 + import { listRecords } from '$lib/atproto'; 2 + import type { CardDefinition } from '../../types'; 3 + import UpcomingEventsCard from './UpcomingEventsCard.svelte'; 4 + import type { Did } from '@atcute/lexicons'; 5 + import type { EventData } from '../EventCard'; 6 + 7 + const EVENT_COLLECTION = 'community.lexicon.calendar.event'; 8 + 9 + export const UpcomingEventsCardDefinition = { 10 + type: 'upcomingEvents', 11 + contentComponent: UpcomingEventsCard, 12 + createNew: (card) => { 13 + card.w = 4; 14 + card.h = 4; 15 + card.mobileW = 8; 16 + card.mobileH = 6; 17 + }, 18 + minW: 2, 19 + minH: 3, 20 + 21 + loadData: async (_items, { did }) => { 22 + const records = await listRecords({ 23 + did: did as Did, 24 + collection: EVENT_COLLECTION, 25 + limit: 100 26 + }); 27 + 28 + const now = new Date(); 29 + const events: Array<EventData & { rkey: string }> = []; 30 + 31 + for (const record of records) { 32 + const event = record.value as EventData; 33 + const endsAt = event.endsAt ? new Date(event.endsAt) : null; 34 + const startsAt = new Date(event.startsAt); 35 + 36 + if ((endsAt && endsAt >= now) || (!endsAt && startsAt >= now)) { 37 + const uri = record.uri as string; 38 + const rkey = uri.split('/').pop() || ''; 39 + events.push({ ...event, rkey }); 40 + } 41 + } 42 + 43 + events.sort((a, b) => new Date(a.startsAt).getTime() - new Date(b.startsAt).getTime()); 44 + 45 + return { events }; 46 + }, 47 + 48 + name: 'Upcoming Events', 49 + keywords: ['events', 'hosting', 'calendar', 'upcoming'], 50 + groups: ['Social'], 51 + icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5m-9-6h.008v.008H12v-.008ZM12 15h.008v.008H12V15Zm0 2.25h.008v.008H12v-.008ZM9.75 15h.008v.008H9.75V15Zm0 2.25h.008v.008H9.75v-.008ZM7.5 15h.008v.008H7.5V15Zm0 2.25h.008v.008H7.5v-.008Zm6.75-4.5h.008v.008h-.008v-.008Zm0 2.25h.008v.008h-.008V15Zm0 2.25h.008v.008h-.008v-.008Zm2.25-4.5h.008v.008H16.5v-.008Zm0 2.25h.008v.008H16.5V15Z" /></svg>` 52 + } as CardDefinition & { type: 'upcomingEvents' };
+177
src/lib/cards/social/UpcomingRsvpsCard/UpcomingRsvpsCard.svelte
···
··· 1 + <script lang="ts"> 2 + import { onMount } from 'svelte'; 3 + import { Badge } from '@foxui/core'; 4 + import { getAdditionalUserData, getDidContext, getHandleContext } from '$lib/website/context'; 5 + import type { ContentComponentProps } from '../../types'; 6 + import { CardDefinitionsByType } from '../..'; 7 + import type { ResolvedRsvp } from '$lib/events/fetch-attendees'; 8 + import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 9 + 10 + let { item }: ContentComponentProps = $props(); 11 + 12 + let isLoaded = $state(false); 13 + const data = getAdditionalUserData(); 14 + const did = getDidContext(); 15 + const handle = getHandleContext(); 16 + 17 + // svelte-ignore state_referenced_locally 18 + let rsvps = $state<ResolvedRsvp[]>( 19 + ((data['upcomingRsvps'] as { rsvps?: ResolvedRsvp[] })?.rsvps ?? []) as ResolvedRsvp[] 20 + ); 21 + 22 + onMount(async () => { 23 + try { 24 + const loaded = await CardDefinitionsByType[item.cardType]?.loadData?.([item], { 25 + did, 26 + handle 27 + }); 28 + const result = loaded as { rsvps?: ResolvedRsvp[] } | undefined; 29 + const freshRsvps = result?.rsvps ?? []; 30 + 31 + if (freshRsvps.length > 0) { 32 + rsvps = freshRsvps; 33 + } 34 + 35 + data['upcomingRsvps'] = { rsvps }; 36 + } catch (e) { 37 + console.error('Failed to load RSVPs', e); 38 + } 39 + 40 + isLoaded = true; 41 + }); 42 + 43 + function formatDate(dateStr: string): string { 44 + const date = new Date(dateStr); 45 + return date.toLocaleDateString('en-US', { 46 + weekday: 'short', 47 + month: 'short', 48 + day: 'numeric' 49 + }); 50 + } 51 + 52 + function formatTime(dateStr: string): string { 53 + const date = new Date(dateStr); 54 + return date.toLocaleTimeString('en-US', { 55 + hour: 'numeric', 56 + minute: '2-digit' 57 + }); 58 + } 59 + 60 + function getModeLabel(mode: string): string { 61 + if (mode.includes('virtual')) return 'Virtual'; 62 + if (mode.includes('hybrid')) return 'Hybrid'; 63 + if (mode.includes('inperson')) return 'In-Person'; 64 + return 'Event'; 65 + } 66 + 67 + function getModeColor(mode: string): string { 68 + if (mode.includes('virtual')) return 'blue'; 69 + if (mode.includes('hybrid')) return 'purple'; 70 + if (mode.includes('inperson')) return 'green'; 71 + return 'gray'; 72 + } 73 + </script> 74 + 75 + <div class="flex h-full flex-col overflow-hidden p-4"> 76 + <div class="mb-3 flex items-center gap-2"> 77 + <div 78 + class="bg-base-100 border-base-300 accent:bg-accent-100/50 accent:border-accent-200 dark:border-base-800 dark:bg-base-900 flex size-8 shrink-0 items-center justify-center rounded-xl border" 79 + > 80 + <svg 81 + xmlns="http://www.w3.org/2000/svg" 82 + fill="none" 83 + viewBox="0 0 24 24" 84 + stroke-width="1.5" 85 + stroke="currentColor" 86 + class="size-4" 87 + > 88 + <path 89 + stroke-linecap="round" 90 + stroke-linejoin="round" 91 + d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" 92 + /> 93 + </svg> 94 + </div> 95 + <span class="text-base-900 dark:text-base-50 text-sm font-semibold">RSVPs</span> 96 + </div> 97 + 98 + <div class="flex-1 overflow-y-auto"> 99 + {#if rsvps.length > 0} 100 + <div class="flex flex-col gap-2"> 101 + {#each rsvps as rsvp (rsvp.eventUri)} 102 + <a 103 + href="https://blento.app/{rsvp.hostDid}/events/{rsvp.rkey}" 104 + target="_blank" 105 + class="hover:bg-base-100 dark:hover:bg-base-800 accent:hover:bg-accent-400/20 flex flex-col gap-1 rounded-lg p-2 transition-colors" 106 + use:qrOverlay={{ context: { title: rsvp.event.name } }} 107 + > 108 + <div class="flex items-center gap-2"> 109 + <span class="text-base-900 dark:text-base-50 line-clamp-1 flex-1 text-sm font-medium" 110 + >{rsvp.event.name}</span 111 + > 112 + <Badge size="sm" color={rsvp.status === 'going' ? 'green' : 'amber'}> 113 + <span class="accent:text-base-900" 114 + >{rsvp.status === 'going' ? 'Going' : 'Interested'}</span 115 + > 116 + </Badge> 117 + </div> 118 + <div 119 + class="text-base-500 dark:text-base-400 accent:text-base-800 flex items-center gap-1 text-xs" 120 + > 121 + <svg 122 + xmlns="http://www.w3.org/2000/svg" 123 + fill="none" 124 + viewBox="0 0 24 24" 125 + stroke-width="1.5" 126 + stroke="currentColor" 127 + class="size-3 shrink-0" 128 + > 129 + <path 130 + stroke-linecap="round" 131 + stroke-linejoin="round" 132 + d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" 133 + /> 134 + </svg> 135 + <span>{formatDate(rsvp.event.startsAt)} at {formatTime(rsvp.event.startsAt)}</span> 136 + </div> 137 + {#if rsvp.hostProfile} 138 + <div 139 + class="text-base-500 dark:text-base-400 accent:text-base-800 flex items-center gap-1 text-xs" 140 + > 141 + <svg 142 + xmlns="http://www.w3.org/2000/svg" 143 + fill="none" 144 + viewBox="0 0 24 24" 145 + stroke-width="1.5" 146 + stroke="currentColor" 147 + class="size-3 shrink-0" 148 + > 149 + <path 150 + stroke-linecap="round" 151 + stroke-linejoin="round" 152 + d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" 153 + /> 154 + </svg> 155 + <span class="truncate" 156 + >{rsvp.hostProfile.displayName || rsvp.hostProfile.handle}</span 157 + > 158 + </div> 159 + {/if} 160 + </a> 161 + {/each} 162 + </div> 163 + {:else if isLoaded} 164 + <div 165 + class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm" 166 + > 167 + No upcoming RSVPs 168 + </div> 169 + {:else} 170 + <div 171 + class="text-base-500 dark:text-base-400 accent:text-white/60 flex h-full items-center justify-center text-center text-sm" 172 + > 173 + Loading RSVPs... 174 + </div> 175 + {/if} 176 + </div> 177 + </div>
+41
src/lib/cards/social/UpcomingRsvpsCard/index.ts
···
··· 1 + import { fetchUserRsvps } from '$lib/events/fetch-attendees'; 2 + import type { CardDefinition } from '../../types'; 3 + import UpcomingRsvpsCard from './UpcomingRsvpsCard.svelte'; 4 + import type { ResolvedRsvp } from '$lib/events/fetch-attendees'; 5 + 6 + export type { ResolvedRsvp }; 7 + 8 + export const UpcomingRsvpsCardDefinition = { 9 + type: 'upcomingRsvps', 10 + contentComponent: UpcomingRsvpsCard, 11 + createNew: (card) => { 12 + card.w = 4; 13 + card.h = 4; 14 + card.mobileW = 8; 15 + card.mobileH = 6; 16 + }, 17 + minW: 2, 18 + minH: 3, 19 + 20 + loadData: async (_items, { did, cache }) => { 21 + const rsvps = await fetchUserRsvps(did, cache); 22 + 23 + const now = new Date(); 24 + const upcoming = rsvps.filter((r) => { 25 + const endsAt = r.event.endsAt ? new Date(r.event.endsAt) : null; 26 + const startsAt = new Date(r.event.startsAt); 27 + return (endsAt && endsAt >= now) || (!endsAt && startsAt >= now); 28 + }); 29 + 30 + upcoming.sort( 31 + (a, b) => new Date(a.event.startsAt).getTime() - new Date(b.event.startsAt).getTime() 32 + ); 33 + 34 + return { rsvps: upcoming }; 35 + }, 36 + 37 + name: 'Upcoming RSVPs', 38 + keywords: ['rsvp', 'attending', 'going', 'interested', 'events'], 39 + groups: ['Social'], 40 + icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="size-4"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /></svg>` 41 + } as CardDefinition & { type: 'upcomingRsvps' };
+1 -1
src/lib/website/Account.svelte
··· 20 <Popover sideOffset={8} bind:open={settingsPopoverOpen} class="bg-base-100 dark:bg-base-900"> 21 {#snippet child({ props })} 22 <button {...props}> 23 - <Avatar src={user.profile?.avatar} alt="" class="size-15 rounded-full" /> 24 </button> 25 {/snippet} 26
··· 20 <Popover sideOffset={8} bind:open={settingsPopoverOpen} class="bg-base-100 dark:bg-base-900"> 21 {#snippet child({ props })} 22 <button {...props}> 23 + <Avatar src={user.profile?.avatar} alt="" class="size-15 rounded-full cursor-pointer" /> 24 </button> 25 {/snippet} 26