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