From 8b5b1379d72f570e1f97654719cb2bd52e37aeb3 Mon Sep 17 00:00:00 2001 From: Ewan Croft Date: Mon, 15 Jun 2026 01:27:18 +0100 Subject: [PATCH] feat: implement dynamic verifier display with profile cards --- src/lib/components/VerifierCard.svelte | 52 ++++++++++++++++++++++++++ src/lib/utils/avatar.ts | 44 ++++++++++++++++++++++ src/routes/+page.server.ts | 32 ++++++++++++++++ src/routes/+page.svelte | 4 +- src/routes/site/design/+page.server.ts | 10 +++++ src/routes/site/design/+page.svelte | 13 +++++++ 6 files changed, 153 insertions(+), 2 deletions(-) create mode 100644 src/lib/components/VerifierCard.svelte create mode 100644 src/lib/utils/avatar.ts create mode 100644 src/routes/site/design/+page.server.ts diff --git a/src/lib/components/VerifierCard.svelte b/src/lib/components/VerifierCard.svelte new file mode 100644 index 0000000..551c86e --- /dev/null +++ b/src/lib/components/VerifierCard.svelte @@ -0,0 +1,52 @@ + + +
+
+ {name} +
+
+ {name} + {#if handle} + @{handle} + {/if} +
+
+ + diff --git a/src/lib/utils/avatar.ts b/src/lib/utils/avatar.ts new file mode 100644 index 0000000..f758e75 --- /dev/null +++ b/src/lib/utils/avatar.ts @@ -0,0 +1,44 @@ +/** + * Generates a deterministic 8x8 blocky SVG avatar based on a seed (DID). + */ +export function getPlaceholderAvatar(seed: string): string { + // Simple deterministic hash function + let hash = 0; + for (let i = 0; i < seed.length; i++) { + hash = seed.charCodeAt(i) + ((hash << 5) - hash); + } + + // Generate color + const hue = Math.abs(hash) % 360; + const color = `hsl(${hue}, 60%, 50%)`; + + // Generate a deterministic 8x8 grid of blocks (only need 4x8 for symmetry) + const grid = []; + for (let i = 0; i < 32; i++) { + // Use bits from hash to decide if block is active + grid.push((hash >> (i % 31)) & 1); + } + + // Generate SVG blocks + const blocks = []; + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + // Create symmetry: 8x8 grid mirroring 4x8 + const active = j < 4 ? grid[i * 4 + j] : grid[i * 4 + (7 - j)]; + if (active) { + blocks.push( + ``, + ); + } + } + } + + const svg = ` + + + ${blocks.join("")} + + `.trim(); + + return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; +} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index 2776cf5..00bd672 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -12,7 +12,39 @@ export const load: PageServerLoad = async ({ fetch, setHeaders }) => { const profile = await fetchProfile(PUBLIC_ATPROTO_DID, fetch); + // Fetch verifications from Constellation + const subject = PUBLIC_ATPROTO_DID; + const constellationUrl = `https://constellation.microcosm.blue/xrpc/blue.microcosm.links.getBacklinks?subject=${encodeURIComponent(subject)}&source=app.bsky.graph.verification:subject`; + + let verifications = []; + try { + const response = await fetch(constellationUrl, { + headers: { Accept: "application/json" }, + }); + if (response.ok) { + const data = await response.json(); + if (data && Array.isArray(data.records)) { + // Resolve profile details for each verifier DID + verifications = await Promise.all( + data.records.map(async (record: any) => { + const verifierProfile = await fetchProfile(record.did, fetch); + return { + did: record.did, + name: verifierProfile.displayName || verifierProfile.handle, + avatarUrl: verifierProfile.avatar, + handle: verifierProfile.handle, + date: "", // Date is not easily available in the verification record + }; + }), + ); + } + } + } catch (e) { + console.error("Failed to fetch verifications:", e); + } + return { profile, + verifications, }; }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 354283e..d9a0155 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -3,7 +3,7 @@ import SiteHead from '$lib/components/SiteHead.svelte'; import EmptyState from '$lib/components/EmptyState.svelte'; import LoadingSkeleton from '$lib/components/LoadingSkeleton.svelte'; - import VerificationBadge from '../components/VerificationBadge.svelte'; + import VerificationBadge from '$lib/components/VerificationBadge.svelte'; import { ArrowRight, ExternalLink, Music } from '@lucide/svelte'; import { normalizeSlug } from '$lib/utils/slugify'; import type { ProfileData } from '@ewanc26/atproto'; @@ -57,7 +57,7 @@

{profile.displayName ?? profile.handle} - +

{profile.description}

diff --git a/src/routes/site/design/+page.server.ts b/src/routes/site/design/+page.server.ts new file mode 100644 index 0000000..7479029 --- /dev/null +++ b/src/routes/site/design/+page.server.ts @@ -0,0 +1,10 @@ +import type { PageServerLoad } from "./$types"; +import { fetchProfile } from "@ewanc26/atproto"; +import { PUBLIC_ATPROTO_DID } from "$env/static/public"; + +export const load: PageServerLoad = async ({ fetch }) => { + const profile = await fetchProfile(PUBLIC_ATPROTO_DID, fetch); + return { + profile, + }; +}; diff --git a/src/routes/site/design/+page.svelte b/src/routes/site/design/+page.svelte index 936dd93..f67301e 100644 --- a/src/routes/site/design/+page.svelte +++ b/src/routes/site/design/+page.svelte @@ -18,7 +18,10 @@ import favicon from '$lib/assets/favicon.svg'; import { Copy, Check } from '@lucide/svelte'; import ColourDemonstrator from '$lib/components/ColourDemonstrator.svelte'; + import VerifierCard from '$lib/components/VerifierCard.svelte'; + import { PUBLIC_ATPROTO_DID } from '$env/static/public'; + let { data } = $props(); let copiedIndex = $state(null); function copyCode(code: string, id: string) { @@ -510,6 +513,16 @@
+
+

Verifier Card (Live Profile Data and image fallback example)

+ +
+

Panel Primitive

-- 2.51.2