diff --git a/docs/Autofill.md b/docs/Autofill.md --- a/docs/Autofill.md +++ b/docs/Autofill.md @@ -11,7 +11,7 @@ - blue.flashes.actor.portfolio - social.grain.gallery - xyz.statusphere.status - - site.standard.publication + - site.standard.publication - fm.teal.alpha.feed.play - dev.npmx.feed.like - add bluesky profile card diff --git a/src/lib/atproto/methods.ts b/src/lib/atproto/methods.ts --- a/src/lib/atproto/methods.ts +++ b/src/lib/atproto/methods.ts @@ -395,7 +395,8 @@ */ export function getCDNImageBlobUrl({ did, - blob + blob, + type = 'webp' }: { did?: string; blob: { @@ -404,11 +405,12 @@ $link: string; }; }; + type?: 'webp' | 'jpeg'; }) { if (!blob || !did) return; did ??= user.did; - return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${blob.ref.$link}@webp`; + return `https://cdn.bsky.app/img/feed_thumbnail/plain/${did}/${blob.ref.$link}@${type}`; } /** diff --git a/src/routes/[[actor=actor]]/e/+page.server.ts b/src/routes/[[actor=actor]]/e/+page.server.ts new file mode 100644 --- /dev/null +++ b/src/routes/[[actor=actor]]/e/+page.server.ts @@ -0,0 +1,77 @@ +import { error } from '@sveltejs/kit'; +import type { EventData } from '$lib/cards/social/EventCard'; +import { getBlentoOrBskyProfile, resolveHandle } from '$lib/atproto/methods.js'; +import { isHandle } from '@atcute/lexicons/syntax'; +import { createCache, type CachedProfile } from '$lib/cache'; +import type { ActorIdentifier, Did } from '@atcute/lexicons'; +import { env as publicEnv } from '$env/dynamic/public'; + +export async function load({ params, platform, request }) { + const cache = createCache(platform); + + const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); + + let actor: ActorIdentifier | undefined = params.actor; + + if (!actor) { + const kv = platform?.env?.CUSTOM_DOMAINS; + + if (kv && customDomain) { + try { + const did = await kv.get(customDomain); + + if (did) actor = did as ActorIdentifier; + } catch (error) { + console.error('failed to get custom domain kv', error); + } + } else { + actor = publicEnv.PUBLIC_HANDLE as ActorIdentifier; + } + } else if (customDomain && params.actor) { + actor = undefined; + } + + const did = isHandle(actor) ? await resolveHandle({ handle: actor }) : actor; + + if (!did) { + throw error(404, 'Events not found'); + } + + try { + const [eventsResponse, hostProfile] = await Promise.all([ + fetch( + `https://smokesignal.events/xrpc/community.lexicon.calendar.searchEvents?repository=${encodeURIComponent(did)}&query=upcoming` + ), + cache + ? cache.getProfile(did as Did).catch(() => null) + : getBlentoOrBskyProfile({ did: did as Did }) + .then( + (p): CachedProfile => ({ + did: p.did as string, + handle: p.handle as string, + displayName: p.displayName as string | undefined, + avatar: p.avatar as string | undefined, + hasBlento: p.hasBlento, + url: p.url + }) + ) + .catch(() => null) + ]); + + if (!eventsResponse.ok) { + throw error(404, 'Events not found'); + } + + const data: { results: EventData[] } = await eventsResponse.json(); + const events = data.results; + + return { + events, + did, + hostProfile: hostProfile ?? null + }; + } catch (e) { + if (e && typeof e === 'object' && 'status' in e) throw e; + throw error(404, 'Events not found'); + } +} diff --git a/src/routes/[[actor=actor]]/e/+page.svelte b/src/routes/[[actor=actor]]/e/+page.svelte new file mode 100644 --- /dev/null +++ b/src/routes/[[actor=actor]]/e/+page.svelte @@ -0,0 +1,170 @@ + + + + {hostName} - Events + + + + + + + + +
+
+ +
+ +
+

Events

+

Hosted by {hostName}

+
+
+ + {#if events.length === 0} +

No events found.

+ {:else} + + {/if} +
+
diff --git a/src/lib/cards/social/EventCard/index.ts b/src/lib/cards/social/EventCard/index.ts --- a/src/lib/cards/social/EventCard/index.ts +++ b/src/lib/cards/social/EventCard/index.ts @@ -24,7 +24,8 @@ alt?: string; role?: string; content?: { - ref?: { + $type: 'blob'; + ref: { $link: string; }; mimeType?: string; diff --git a/src/routes/[[actor=actor]]/e/[rkey]/+page.svelte b/src/routes/[[actor=actor]]/e/[rkey]/+page.svelte --- a/src/routes/[[actor=actor]]/e/[rkey]/+page.svelte +++ b/src/routes/[[actor=actor]]/e/[rkey]/+page.svelte @@ -1,8 +1,10 @@ {eventData.name} + + + + + + +
diff --git a/src/routes/[[actor=actor]]/e/[rkey]/og.png/+server.ts b/src/routes/[[actor=actor]]/e/[rkey]/og.png/+server.ts new file mode 100644 --- /dev/null +++ b/src/routes/[[actor=actor]]/e/[rkey]/og.png/+server.ts @@ -0,0 +1,84 @@ +import { getCDNImageBlobUrl, resolveHandle } from '$lib/atproto/methods.js'; +import { env as publicEnv } from '$env/dynamic/public'; + +import type { ActorIdentifier } from '@atcute/lexicons'; +import { isHandle } from '@atcute/lexicons/syntax'; +import type { EventData } from '$lib/cards/social/EventCard'; +import { ImageResponse } from '@ethercorps/sveltekit-og'; +import { error } from '@sveltejs/kit'; +import EventOgImage from './EventOgImage.svelte'; + +function formatDate(dateStr: string): string { + const date = new Date(dateStr); + const weekday = date.toLocaleDateString('en-US', { weekday: 'long' }); + const month = date.toLocaleDateString('en-US', { month: 'long' }); + const day = date.getDate(); + return `${weekday}, ${month} ${day}`; +} + +export async function GET({ params, platform, request }) { + const { rkey } = params; + + const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); + + let actor: ActorIdentifier | undefined = params.actor; + + if (!actor) { + const kv = platform?.env?.CUSTOM_DOMAINS; + + if (kv && customDomain) { + try { + const did = await kv.get(customDomain); + if (did) actor = did as ActorIdentifier; + } catch (error) { + console.error('failed to get custom domain kv', error); + } + } else { + actor = publicEnv.PUBLIC_HANDLE as ActorIdentifier; + } + } + + const did = isHandle(actor) ? await resolveHandle({ handle: actor }) : actor; + + if (!did || !rkey) { + throw error(404, 'Event not found'); + } + + let eventData: EventData; + + try { + const eventResponse = await fetch( + `https://smokesignal.events/xrpc/community.lexicon.calendar.GetEvent?repository=${encodeURIComponent(did)}&record_key=${encodeURIComponent(rkey)}` + ); + + if (!eventResponse.ok) { + throw error(404, 'Event not found'); + } + + eventData = await eventResponse.json(); + } catch (e) { + if (e && typeof e === 'object' && 'status' in e) throw e; + throw error(404, 'Event not found'); + } + + const dateStr = formatDate(eventData.startsAt); + + let thumbnailUrl: string | null = null; + if (eventData.media && eventData.media.length > 0) { + const media = eventData.media.find((m) => m.role === 'thumbnail'); + if (media?.content) { + thumbnailUrl = getCDNImageBlobUrl({ did, blob: media.content, type: 'jpeg' }) ?? null; + } + } + + return new ImageResponse( + EventOgImage, + { width: 1200, height: 630, debug: false }, + { + name: eventData.name, + dateStr, + thumbnailUrl, + rkey + } + ); +} diff --git a/src/routes/[[actor=actor]]/e/[rkey]/og.png/EventOgImage.svelte b/src/routes/[[actor=actor]]/e/[rkey]/og.png/EventOgImage.svelte new file mode 100644 --- /dev/null +++ b/src/routes/[[actor=actor]]/e/[rkey]/og.png/EventOgImage.svelte @@ -0,0 +1,63 @@ + + +
+
+
+ {#if thumbnailUrl} + {name} + {:else} + + {/if} +
+
+ +
+

+ {name} +

+ +
+ + + + {dateStr} +
+ +
+