Something went wrong. Try again.
My website. cameron.stream
Something went wrong. Try again.
9.9 kB · 276 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277// Library page — combines saved links and annotations from Cameron's PDS.
import { getProfile, getSembleData, type SembleCard,} from "../data.ts";import { AnnotationList, listAnnotations } from "./library-annotations.tsx";import { PageNav } from "./page-nav.tsx";
function displayDomain(url: string): string { try { return new URL(url).hostname.replace(/^www\./, ""); } catch { return url; }}
function cardTitle(card: SembleCard): string { const metadataTitle = card.cardContent?.title; if (metadataTitle && metadataTitle !== "Bluesky") return metadataTitle;
try { const url = new URL(card.url); if (url.hostname === "bsky.app" && url.pathname.includes("/profile/")) { const profile = url.pathname.split("/").filter(Boolean).at(-1); if (profile) return `@${profile}`; } } catch { // Fall through to the stored title or domain. }
return metadataTitle || displayDomain(card.url);}
function formattedDate(date: string): string { return new Date(date).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", });}
function recordUrl(uri?: string): string | undefined { return uri ? `https://pdsls.dev/${uri}` : undefined;}
function cleanDescription(description?: string): string | undefined { return description?.replace(/\s+/g, " ").trim();}
export async function Library({ embedded = false }: { embedded?: boolean } = {}) { const [{ collections, cards, available }, annotations, profile] = await Promise.all([ getSembleData(100), listAnnotations(), getProfile(), ]);
const urlCards = cards.filter((card) => card.type === "URL"); const looseCount = urlCards.filter((card) => card.collections.length === 0).length;
return ( <div class="semble-shell"> {!embedded && <PageNav profile={profile} active="knowledge" />}
<main class="semble-page" id={embedded ? "links" : undefined}> {!embedded && <header class="semble-hero"> <div class="semble-hero-copy"> <h1>Library</h1> <p class="semble-intro"> Links and passages I wanted to keep. Collections gather related material; annotations stay attached to the thing that provoked them. </p> </div>
<dl class="semble-totals" aria-label="Catalogue totals"> <div> <dt><a href="#saved-links">Saved links</a></dt> <dd>{String(urlCards.length).padStart(2, "0")}</dd> </div> <div> <dt><a href="#saved-links">Collections</a></dt> <dd>{String(collections.length).padStart(2, "0")}</dd> </div> <div> <dt><a href="#annotations">Annotations</a></dt> <dd>{String(annotations.length).padStart(2, "0")}</dd> </div> </dl> </header>}
{!available ? ( <section class="semble-unavailable" aria-live="polite"> <p class="semble-kicker">Catalogue unavailable</p> <h2>The records are still there.</h2> <p> This page could not reach the PDS just now. It is an upstream read failure, not an empty collection. </p> </section> ) : ( <section class="semble-section semble-cards-section" id="saved-links" aria-labelledby="semble-cards-title" > <div class="semble-section-heading"> <div> <h2 id="semble-cards-title">Saved links</h2> </div> <p id="semble-filter-status" aria-live="polite"> {urlCards.length} links saved with{" "} <a href="https://semble.so/profile/cameron.stream" target="_blank" rel="noopener" > Semble </a> . </p> </div>
<div class="semble-filter-bar" aria-label="Filter cards"> <button type="button" data-collection-filter="all" aria-pressed="true" > All <span>{urlCards.length}</span> </button> {collections.map((collection) => ( <button type="button" data-collection-filter={collection.id} aria-pressed="false" key={collection.id} > {collection.name} <span>{collection.cardCount}</span> </button> ))} {looseCount > 0 && ( <button type="button" data-collection-filter="loose" aria-pressed="false" > Loose <span>{looseCount}</span> </button> )} </div>
{urlCards.length === 0 ? ( <p class="semble-empty">No cards yet.</p> ) : ( <div class="semble-card-grid" id="semble-card-grid"> {urlCards.map((card) => { const title = cardTitle(card); const domain = displayDomain(card.url); const description = cleanDescription(card.cardContent?.description); const cardCollections = card.collections.map((item) => item.id); const pdsRecordUrl = recordUrl(card.uri);
return ( <article class="semble-card" key={card.id} data-card-collections={cardCollections.join(" ")} data-card-loose={cardCollections.length === 0 ? "true" : "false"} > <div class="semble-card-body"> <div class="semble-card-source"> <span>{card.cardContent?.siteName || domain}</span> <span>{formattedDate(card.createdAt)}</span> </div> <h3> <a href={card.url} target="_blank" rel="noopener"> {title} <span class="semble-external" aria-hidden="true">↗</span> </a> </h3> {description && <p class="semble-card-description">{description}</p>}
{card.note?.text && ( <aside class="semble-card-note"> <span>Note</span> <p>{card.note.text}</p> </aside> )}
<div class="semble-card-footer"> <div class="semble-card-membership"> {card.collections.length > 0 ? ( card.collections.map((collection) => ( <button type="button" data-collection-filter={collection.id} aria-pressed="false" key={collection.id} > {collection.name} </button> )) ) : ( <span>Loose</span> )} </div> {pdsRecordUrl && ( <a href={pdsRecordUrl} target="_blank" rel="noopener" class="semble-record-link" > record </a> )} </div> </div> </article> ); })} </div> )} </section> )}
<section class="semble-section library-annotations-section" id="annotations" aria-labelledby="library-annotations-title" > <div class="semble-section-heading"> <div> <h2 id="library-annotations-title">Annotations</h2> </div> <p> {annotations.length} passages and notes made with{" "} <a href="https://margin.at" target="_blank" rel="noopener"> Margin </a> . </p> </div> {annotations.length === 0 ? ( <p class="semble-empty">No annotations yet.</p> ) : ( <AnnotationList annotations={annotations} /> )} </section>
<footer class="semble-footer"> <p>Public, portable records on my PDS.</p> <div> <a href="https://semble.so/profile/cameron.stream" target="_blank" rel="noopener" > Semble profile <span aria-hidden="true">↗</span> </a> <a href="https://margin.at" target="_blank" rel="noopener" > Margin <span aria-hidden="true">↗</span> </a> </div> </footer> </main>
<script src="/public/semble.js"></script> </div> );}