diff --git a/frontend/app/blog/[slug]/BlogPostClient.tsx b/frontend/app/blog/[slug]/BlogPostClient.tsx index 85921e8..ccf00b5 100644 --- a/frontend/app/blog/[slug]/BlogPostClient.tsx +++ b/frontend/app/blog/[slug]/BlogPostClient.tsx @@ -12,7 +12,7 @@ import { getPublicKey, signEvent, publishEvent, - shortenPubkey, + shortenNpub, fetchNostrProfile, fetchLongformFromRelays, fetchEventFromRelays, @@ -23,6 +23,7 @@ import { Navbar } from "@/components/public/Navbar"; import { Footer } from "@/components/public/Footer"; import { markdownComponents } from "./markdownComponents"; import { remarkNostr } from "./remarkNostr"; +import { NostrAuthor } from "./NostrEmbeds"; interface Post { id: string; @@ -30,6 +31,7 @@ interface Post { title: string; content: string; excerpt?: string; + image?: string; authorName?: string; authorPubkey?: string; publishedAt?: string; @@ -59,6 +61,7 @@ function postFromEvent(event: any, slug: string): Post { title: tag("title") || "Untitled", content: event.content || "", excerpt: tag("summary"), + image: tag("image"), authorPubkey: event.pubkey, publishedAt: new Date(publishedAtSec * 1000).toISOString(), nostrEventId: event.id, @@ -117,6 +120,7 @@ export default function BlogPostClient({ slug }: { slug: string }) { const [submitting, setSubmitting] = useState(false); const [authorProfile, setAuthorProfile] = useState(null); const [liveContent, setLiveContent] = useState(null); + const [liveImage, setLiveImage] = useState(null); const [loadingContent, setLoadingContent] = useState(false); const [resolvingFromRelays, setResolvingFromRelays] = useState(false); const [contentError, setContentError] = useState(false); @@ -134,6 +138,7 @@ export default function BlogPostClient({ slug }: { slug: string }) { setLoading(true); setError(null); setLiveContent(null); + setLiveImage(null); setPost(null); setAuthorProfile(null); setContentError(false); @@ -159,8 +164,14 @@ export default function BlogPostClient({ slug }: { slug: string }) { fetchPromise .then((event) => { if (cancelled) return; - if (event?.content) setLiveContent(event.content); - else setContentError(true); // not found / timed out + if (event?.content) { + setLiveContent(event.content); + // Pull the longform header image (`image` tag) for display. + const img = event.tags?.find((t: string[]) => t[0] === "image")?.[1]; + if (img) setLiveImage(img); + } else { + setContentError(true); // not found / timed out + } }) .catch(() => !cancelled && setContentError(true)) .finally(() => !cancelled && setLoadingContent(false)); @@ -258,6 +269,7 @@ export default function BlogPostClient({ slug }: { slug: string }) { }, [comment, post, hasNostr]); const categories = post?.categories?.map((c) => c.category) || []; + const headerImage = post?.image || liveImage; return ( <> @@ -321,7 +333,7 @@ export default function BlogPostClient({ slug }: { slug: string }) { /> )} - {authorProfile?.name || post.authorName || shortenPubkey(post.authorPubkey!)} + {authorProfile?.name || post.authorName || shortenNpub(post.authorPubkey!)} )} @@ -338,6 +350,17 @@ export default function BlogPostClient({ slug }: { slug: string }) { + {headerImage && ( + {post.title} { + (e.target as HTMLImageElement).style.display = "none"; + }} + /> + )} +
{loadingContent ? ( @@ -424,9 +447,7 @@ export default function BlogPostClient({ slug }: { slug: string }) { className="bg-surface-container-high rounded-lg p-4" >
- - {shortenPubkey(r.pubkey)} - + · {formatDate(new Date(r.created_at * 1000))} diff --git a/frontend/app/blog/[slug]/NostrEmbeds.tsx b/frontend/app/blog/[slug]/NostrEmbeds.tsx index 18fc676..02bed66 100644 --- a/frontend/app/blog/[slug]/NostrEmbeds.tsx +++ b/frontend/app/blog/[slug]/NostrEmbeds.tsx @@ -5,7 +5,7 @@ import { nip19 } from "nostr-tools"; import { loadNostrProfile, resolveEventFromRelays, - shortenPubkey, + shortenNpub, type NostrProfile, } from "@/lib/nostr"; @@ -39,11 +39,8 @@ export function NostrMention({ bech32 }: { bech32: string }) { }, [pubkey]); // Show the username; fall back to a shortened npub only when no profile. - const npub = useMemo(() => { - if (bech32.startsWith("npub")) return bech32; - return pubkey ? nip19.npubEncode(pubkey) : bech32; - }, [bech32, pubkey]); - const name = profile?.name || profile?.displayName || shortenPubkey(npub); + const name = + profile?.name || profile?.displayName || shortenNpub(pubkey || bech32); return ( (null); useEffect(() => { let cancelled = false; @@ -79,21 +78,14 @@ function NoteAuthor({ pubkey }: { pubkey: string }) { cancelled = true; }; }, [pubkey]); - const fallback = useMemo(() => { - try { - return shortenPubkey(nip19.npubEncode(pubkey)); - } catch { - return shortenPubkey(pubkey); - } - }, [pubkey]); - const name = profile?.name || profile?.displayName || fallback; + const name = profile?.name || profile?.displayName || shortenNpub(pubkey); return ( - + {profile?.picture && ( { (e.target as HTMLImageElement).style.display = "none"; }} @@ -151,7 +143,7 @@ export function NostrNote({ bech32 }: { bech32: string }) { {state === "done" && event && ( - + = mobileVisible, so "mobile-only" never occurs). + const cardVisibilityClass = (i: number) => { + if (i < mobileVisible) return "flex"; + if (i < desktopVisible) return "hidden md:flex"; + return "hidden"; + }; + + // Show the button only when content is still hidden at the given breakpoint. + let loadMoreClass = "hidden"; + if (meetups.length > desktopVisible) loadMoreClass = "flex"; + else if (meetups.length > mobileVisible) loadMoreClass = "flex md:hidden"; + return (
@@ -59,7 +87,7 @@ export function MeetupsSection({ meetups }: MeetupsSectionProps) {
@@ -113,6 +141,16 @@ export function MeetupsSection({ meetups }: MeetupsSectionProps) {
)} +
+ +
+