Something went wrong. Try again.
My website. cameron.stream
Something went wrong. Try again.
4.9 kB · 140 lines
TSX
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141// Bluesky feed component — server-rendered, no client JS.
import { raw } from "hono/html";import { getAuthorFeed, type FeedPost } from "../data.ts";
function escapeHtml(str: string): string { return str .replace(/&/g, "&") .replace(/</g, "<") .replace(/>/g, ">") .replace(/"/g, """);}
function timeAgo(dateStr: string): string { const now = Date.now(); const then = new Date(dateStr).getTime(); const diff = now - then; const minutes = Math.floor(diff / 60000); if (minutes < 1) return "just now"; if (minutes < 60) return `${minutes}m`; const hours = Math.floor(minutes / 60); if (hours < 24) return `${hours}h`; const days = Math.floor(hours / 24); if (days < 30) return `${days}d`; return new Date(dateStr).toLocaleDateString("en-US", { month: "short", day: "numeric", });}
function postUrl(uri: string): string { // at://did:plc:.../app.bsky.feed.post/rkey -> bsky.app link const parts = uri.split("/"); const did = parts[2]; const rkey = parts[parts.length - 1]; return `https://bsky.app/profile/${did}/post/${rkey}`;}
// Linkify URLs, @mentions, and facets in post textfunction truncateUrl(url: string, max = 40): string { try { const u = new URL(url); const path = u.pathname + u.search; const display = u.hostname + (path.length > 1 ? path : ""); return display.length > max ? display.slice(0, max) + "…" : display; } catch { return url.length > max ? url.slice(0, max) + "…" : url; }}
function linkifyText(text: string): string { const escaped = escapeHtml(text); return escaped // URLs — truncate display text .replace( /(https?:\/\/[^\s<]+)/g, (_match, url) => `<a href="${url}" target="_blank" rel="noopener" class="feed-link">${truncateUrl(url)}</a>` ) // @mentions .replace( /@([\w.-]+\.[\w.-]+)/g, '<a href="https://bsky.app/profile/$1" target="_blank" rel="noopener" class="feed-mention">@$1</a>' );}
function renderImages(images: NonNullable<FeedPost["images"]>): string { if (images.length === 1) { return `<div class="feed-images feed-images-1"> <img src="${images[0].thumb}" alt="${escapeHtml(images[0].alt)}" loading="lazy" /> </div>`; } const grid = images .map( (img) => `<img src="${img.thumb}" alt="${escapeHtml(img.alt)}" loading="lazy" />` ) .join(""); return `<div class="feed-images feed-images-grid">${grid}</div>`;}
function renderExternal(ext: NonNullable<FeedPost["external"]>): string { return `<a href="${escapeHtml(ext.uri)}" target="_blank" rel="noopener" class="feed-card"> ${ext.thumb ? `<img src="${ext.thumb}" alt="" class="feed-card-thumb" loading="lazy" />` : ""} <div class="feed-card-body"> <div class="feed-card-title">${escapeHtml(ext.title)}</div> <div class="feed-card-desc">${escapeHtml(ext.description).slice(0, 120)}</div> <div class="feed-card-url">${escapeHtml(new URL(ext.uri).hostname)}</div> </div> </a>`;}
function renderQuote(qp: NonNullable<FeedPost["quotedPost"]>): string { return `<div class="feed-quote"> <div class="feed-quote-author"> ${qp.author.avatar ? `<img src="${qp.author.avatar}" alt="" class="feed-avatar-sm" />` : ""} <span class="feed-display-name">${escapeHtml(qp.author.displayName ?? qp.author.handle)}</span> <span class="feed-handle">@${escapeHtml(qp.author.handle)}</span> </div> <div class="feed-quote-text">${linkifyText(qp.text).slice(0, 300)}</div> </div>`;}
function renderPost(post: FeedPost): string { const url = postUrl(post.uri); return `<article class="feed-post"> <div class="feed-post-header"> ${post.author.avatar ? `<img src="${post.author.avatar}" alt="" class="feed-avatar" />` : ""} <div> <span class="feed-display-name">${escapeHtml(post.author.displayName ?? post.author.handle)}</span> <a href="${url}" target="_blank" rel="noopener" class="feed-time">${timeAgo(post.createdAt)}</a> </div> </div> <div class="feed-text">${linkifyText(post.text)}</div> ${post.images ? renderImages(post.images) : ""} ${post.external ? renderExternal(post.external) : ""} ${post.quotedPost ? renderQuote(post.quotedPost) : ""} <div class="feed-stats"> <span title="Replies">${post.replyCount ? `💬 ${post.replyCount}` : ""}</span> <span title="Reposts">${post.repostCount ? `🔁 ${post.repostCount}` : ""}</span> <span title="Likes">${post.likeCount ? `♥ ${post.likeCount}` : ""}</span> </div> </article>`;}
export async function Feed({ limit = 10 }: { limit?: number }) { const posts = await getAuthorFeed(limit);
if (posts.length === 0) { return <div class="feed-empty">No posts yet.</div>; }
const html = posts.map(renderPost).join("");
return ( <div class="feed"> {raw(html)} </div> );}