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