your personal website on atproto - mirror blento.app
at fix-500-on-first-login 270 lines 7.5 kB view raw
1<script lang="ts"> 2 import { onMount } from 'svelte'; 3 import { Badge, Button } from '@foxui/core'; 4 import { getAdditionalUserData } from '$lib/website/context'; 5 import type { ContentComponentProps } from '../../types'; 6 import { CardDefinitionsByType } from '../..'; 7 import type { EventData } from '.'; 8 import { parseUri } from '$lib/atproto'; 9 import { browser } from '$app/environment'; 10 import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; 11 import type { Did } from '@atcute/lexicons'; 12 13 let { item }: ContentComponentProps = $props(); 14 15 let isLoaded = $state(false); 16 let fetchedEventData = $state<EventData | undefined>(undefined); 17 18 const data = getAdditionalUserData(); 19 20 let eventData = $derived( 21 fetchedEventData || 22 ((data[item.cardType] as Record<string, EventData> | undefined)?.[item.id] as 23 | EventData 24 | undefined) 25 ); 26 27 let parsedUri = $derived(item.cardData?.uri ? parseUri(item.cardData.uri) : null); 28 29 onMount(async () => { 30 if (!eventData && item.cardData?.uri && parsedUri?.repo) { 31 const loadedData = (await CardDefinitionsByType[item.cardType]?.loadData?.([item], { 32 did: parsedUri.repo as Did, 33 handle: '' 34 })) as Record<string, EventData> | undefined; 35 36 if (loadedData?.[item.id]) { 37 fetchedEventData = loadedData[item.id]; 38 if (!data[item.cardType]) { 39 data[item.cardType] = {}; 40 } 41 (data[item.cardType] as Record<string, EventData>)[item.id] = fetchedEventData; 42 } 43 } 44 isLoaded = true; 45 }); 46 47 function formatDate(dateStr: string): string { 48 const date = new Date(dateStr); 49 return date.toLocaleDateString('en-US', { 50 weekday: 'short', 51 month: 'short', 52 day: 'numeric', 53 year: 'numeric' 54 }); 55 } 56 57 function formatTime(dateStr: string): string { 58 const date = new Date(dateStr); 59 return date.toLocaleTimeString('en-US', { 60 hour: 'numeric', 61 minute: '2-digit' 62 }); 63 } 64 65 function getModeLabel(mode: string): string { 66 if (mode.includes('virtual')) return 'Virtual'; 67 if (mode.includes('hybrid')) return 'Hybrid'; 68 if (mode.includes('inperson')) return 'In-Person'; 69 return 'Event'; 70 } 71 72 function getModeColor(mode: string): string { 73 if (mode.includes('virtual')) return 'blue'; 74 if (mode.includes('hybrid')) return 'purple'; 75 if (mode.includes('inperson')) return 'green'; 76 return 'gray'; 77 } 78 79 function getLocationString( 80 locations: 81 | Array<{ address?: { locality?: string; region?: string; country?: string } }> 82 | undefined 83 ): string | undefined { 84 if (!locations || locations.length === 0) return undefined; 85 const loc = locations[0]?.address; 86 if (!loc) return undefined; 87 88 const parts = [loc.locality, loc.region, loc.country].filter(Boolean); 89 return parts.length > 0 ? parts.join(', ') : undefined; 90 } 91 92 let eventUrl = $derived(() => { 93 if (parsedUri) { 94 return `https://blento.app/${parsedUri.repo}/events/${parsedUri.rkey}`; 95 } 96 return '#'; 97 }); 98 99 let location = $derived(getLocationString(eventData?.locations)); 100 101 let headerImage = $derived(() => { 102 if (!eventData?.media || !parsedUri) return null; 103 const header = eventData.media.find((m) => m.role === 'header'); 104 if (!header?.content?.ref?.$link) return null; 105 return { 106 url: `https://cdn.bsky.app/img/feed_thumbnail/plain/${parsedUri.repo}/${header.content.ref.$link}@jpeg`, 107 alt: header.alt || eventData.name 108 }; 109 }); 110 111 let showImage = $derived(browser && headerImage()); 112</script> 113 114<div class="event-card flex h-full flex-col justify-between overflow-hidden p-4"> 115 {#if eventData} 116 <div class="min-w-0 flex-1 overflow-hidden"> 117 <div class="mb-2 flex items-center justify-between gap-2"> 118 <div class="flex items-center gap-2"> 119 <div 120 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" 121 > 122 <svg 123 xmlns="http://www.w3.org/2000/svg" 124 fill="none" 125 viewBox="0 0 24 24" 126 stroke-width="1.5" 127 stroke="currentColor" 128 class="size-4" 129 > 130 <path 131 stroke-linecap="round" 132 stroke-linejoin="round" 133 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" 134 /> 135 </svg> 136 </div> 137 <Badge size="sm" color={getModeColor(eventData.mode)}> 138 <span class="accent:text-base-900">{getModeLabel(eventData.mode)}</span> 139 </Badge> 140 </div> 141 142 <div class="event-action z-50"> 143 <Button href={eventUrl()} target="_blank">View event</Button> 144 </div> 145 </div> 146 147 <h3 class="text-base-900 dark:text-base-50 mb-2 line-clamp-2 text-lg leading-tight font-bold"> 148 {eventData.name} 149 </h3> 150 151 <div class="text-base-600 dark:text-base-400 accent:text-base-800 mb-2 text-sm"> 152 <div class="flex items-center gap-1"> 153 <svg 154 xmlns="http://www.w3.org/2000/svg" 155 fill="none" 156 viewBox="0 0 24 24" 157 stroke-width="1.5" 158 stroke="currentColor" 159 class="size-4 shrink-0" 160 > 161 <path 162 stroke-linecap="round" 163 stroke-linejoin="round" 164 d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" 165 /> 166 </svg> 167 <span class="truncate"> 168 {formatDate(eventData.startsAt)} at {formatTime(eventData.startsAt)} 169 {#if eventData.endsAt} 170 - {formatDate(eventData.endsAt)} 171 {/if} 172 </span> 173 </div> 174 </div> 175 176 {#if location} 177 <div 178 class="text-base-600 dark:text-base-400 accent:text-base-800 mb-2 flex items-center gap-1 text-sm" 179 > 180 <svg 181 xmlns="http://www.w3.org/2000/svg" 182 fill="none" 183 viewBox="0 0 24 24" 184 stroke-width="1.5" 185 stroke="currentColor" 186 class="size-4 shrink-0" 187 > 188 <path 189 stroke-linecap="round" 190 stroke-linejoin="round" 191 d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" 192 /> 193 <path 194 stroke-linecap="round" 195 stroke-linejoin="round" 196 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" 197 /> 198 </svg> 199 <span class="truncate">{location}</span> 200 </div> 201 {/if} 202 203 {#if eventData.description} 204 <p 205 class="event-description text-base-500 dark:text-base-400 accent:text-base-900 mb-3 line-clamp-3 text-sm" 206 > 207 {eventData.description} 208 </p> 209 {/if} 210 </div> 211 212 {#if showImage} 213 {@const img = headerImage()} 214 {#if img} 215 <img 216 src={img.url} 217 alt={img.alt} 218 class="event-image mt-3 aspect-3/1 w-full rounded-xl object-cover" 219 /> 220 {/if} 221 {/if} 222 223 <a 224 href={eventUrl()} 225 target="_blank" 226 class="absolute inset-0 h-full w-full" 227 use:qrOverlay={{ 228 context: { 229 title: eventData?.name ?? '' 230 } 231 }} 232 > 233 <span class="sr-only">View event</span> 234 </a> 235 {:else if isLoaded} 236 <div class="flex h-full w-full items-center justify-center"> 237 <span class="text-base-500 dark:text-base-400">Event not found</span> 238 </div> 239 {:else} 240 <div class="flex h-full w-full items-center justify-center"> 241 <span class="text-base-500 dark:text-base-400">Loading event...</span> 242 </div> 243 {/if} 244</div> 245 246<style> 247 .event-action, 248 .event-description, 249 .event-image { 250 display: none; 251 } 252 253 @container card (width >= 18rem) { 254 .event-action { 255 display: inline-flex; 256 } 257 } 258 259 @container card (height >= 12rem) { 260 .event-description { 261 display: block; 262 } 263 } 264 265 @container card (height >= 15rem) { 266 .event-image { 267 display: block; 268 } 269 } 270</style>