Something went wrong. Try again.
your personal website on atproto - mirror blento.app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291<script lang="ts"> import { onMount } from 'svelte'; import { Badge, Button } from '@foxui/core'; import { getAdditionalUserData, getIsMobile } from '$lib/website/context'; import type { ContentComponentProps } from '../../types'; import { CardDefinitionsByType } from '../..'; import type { EventData } from '.'; import { parseUri } from '$lib/atproto'; import { browser } from '$app/environment'; import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; import type { Did } from '@atcute/lexicons';
let { item }: ContentComponentProps = $props();
let isMobile = getIsMobile(); let isLoaded = $state(false); let fetchedEventData = $state<EventData | undefined>(undefined);
const data = getAdditionalUserData();
let eventData = $derived( fetchedEventData || ((data[item.cardType] as Record<string, EventData> | undefined)?.[item.id] as | EventData | undefined) );
let parsedUri = $derived(item.cardData?.uri ? parseUri(item.cardData.uri) : null);
onMount(async () => { if (!eventData && item.cardData?.uri && parsedUri?.repo) { const loadedData = (await CardDefinitionsByType[item.cardType]?.loadData?.([item], { did: parsedUri.repo as Did, handle: '' })) as Record<string, EventData> | undefined;
if (loadedData?.[item.id]) { fetchedEventData = loadedData[item.id]; if (!data[item.cardType]) { data[item.cardType] = {}; } (data[item.cardType] as Record<string, EventData>)[item.id] = fetchedEventData; } } isLoaded = true; });
function formatDate(dateStr: string): string { const date = new Date(dateStr); return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' }); }
function formatTime(dateStr: string): string { const date = new Date(dateStr); return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' }); }
function getModeLabel(mode: string): string { if (mode.includes('virtual')) return 'Virtual'; if (mode.includes('hybrid')) return 'Hybrid'; if (mode.includes('inperson')) return 'In-Person'; return 'Event'; }
function getModeColor(mode: string): string { if (mode.includes('virtual')) return 'blue'; if (mode.includes('hybrid')) return 'purple'; if (mode.includes('inperson')) return 'green'; return 'gray'; }
function getLocationString( locations: | Array<{ address?: { locality?: string; region?: string; country?: string } }> | undefined ): string | undefined { if (!locations || locations.length === 0) return undefined; const loc = locations[0]?.address; if (!loc) return undefined;
const parts = [loc.locality, loc.region, loc.country].filter(Boolean); return parts.length > 0 ? parts.join(', ') : undefined; }
let eventUrl = $derived(() => { if (eventData?.url) return eventData.url; if (parsedUri) { return `https://smokesignal.events/${parsedUri.repo}/${parsedUri.rkey}`; } return '#'; });
let location = $derived(getLocationString(eventData?.locations));
let headerImage = $derived(() => { if (!eventData?.media || !parsedUri) return null; const header = eventData.media.find((m) => m.role === 'header'); if (!header?.content?.ref?.$link) return null; return { url: `https://cdn.bsky.app/img/feed_thumbnail/plain/${parsedUri.repo}/${header.content.ref.$link}@jpeg`, alt: header.alt || eventData.name }; });
let showImage = $derived( browser && headerImage() && ((isMobile() && item.mobileH >= 8) || (!isMobile() && item.h >= 4)) );</script>
<div class="flex h-full flex-col justify-between overflow-hidden p-4"> {#if eventData} <div class="min-w-0 flex-1 overflow-hidden"> <div class="mb-2 flex items-center justify-between gap-2"> <div class="flex items-center gap-2"> <div 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" > <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" 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.5" /> </svg> </div> <Badge size="sm" color={getModeColor(eventData.mode)}> <span class="accent:text-base-900">{getModeLabel(eventData.mode)}</span> </Badge> </div>
{#if isMobile() ? item.mobileW > 4 : item.w > 2} <Button href={eventUrl()} target="_blank" rel="noopener noreferrer" class="z-50" >View event</Button > {/if} </div>
<h3 class="text-base-900 dark:text-base-50 mb-2 line-clamp-2 text-lg leading-tight font-bold"> {eventData.name} </h3>
<div class="text-base-600 dark:text-base-400 accent:text-base-800 mb-2 text-sm"> <div class="flex items-center gap-1"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 shrink-0" > <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" /> </svg> <span class="truncate"> {formatDate(eventData.startsAt)} at {formatTime(eventData.startsAt)} {#if eventData.endsAt} - {formatDate(eventData.endsAt)} {/if} </span> </div> </div>
{#if location} <div class="text-base-600 dark:text-base-400 accent:text-base-800 mb-2 flex items-center gap-1 text-sm" > <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4 shrink-0" > <path stroke-linecap="round" stroke-linejoin="round" d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" /> <path stroke-linecap="round" stroke-linejoin="round" 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" /> </svg> <span class="truncate">{location}</span> </div> {/if}
{#if eventData.description && ((isMobile() && item.mobileH >= 5) || (!isMobile() && item.h >= 3))} <p class="text-base-500 dark:text-base-400 accent:text-base-900 mb-3 line-clamp-3 text-sm"> {eventData.description} </p> {/if}
{#if (eventData.countGoing !== undefined || eventData.countInterested !== undefined) && ((isMobile() && item.mobileH >= 4) || (!isMobile() && item.h >= 3))} <div class="text-base-600 dark:text-base-400 accent:text-base-800 flex flex-wrap gap-3 text-xs" > {#if eventData.countGoing !== undefined} <div class="flex items-center gap-1"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" 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> <span>{eventData.countGoing} going</span> </div> {/if} {#if eventData.countInterested !== undefined} <div class="flex items-center gap-1"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-4" > <path stroke-linecap="round" stroke-linejoin="round" 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" /> </svg> <span>{eventData.countInterested} interested</span> </div> {/if} </div> {/if} </div>
{#if showImage} {@const img = headerImage()} {#if img} <img src={img.url} alt={img.alt} class="mt-3 aspect-3/1 w-full rounded-xl object-cover" /> {/if} {/if}
<a href={eventUrl()} class="absolute inset-0 h-full w-full" target="_blank" rel="noopener noreferrer" use:qrOverlay={{ context: { title: eventData?.name ?? '' } }} > <span class="sr-only">View event on smokesignal.events</span> </a> {:else if isLoaded} <div class="flex h-full w-full items-center justify-center"> <span class="text-base-500 dark:text-base-400">Event not found</span> </div> {:else} <div class="flex h-full w-full items-center justify-center"> <span class="text-base-500 dark:text-base-400">Loading event...</span> </div> {/if}</div>