feat: blog header images, npub display, and meetups load more
Show longform image tags on posts, use shortened npubs for author names, and paginate the homepage meetups section with responsive load more. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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<NostrProfile | null>(null);
|
||||
const [liveContent, setLiveContent] = useState<string | null>(null);
|
||||
const [liveImage, setLiveImage] = useState<string | null>(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 }) {
|
||||
/>
|
||||
)}
|
||||
<span className="font-medium text-on-surface-variant">
|
||||
{authorProfile?.name || post.authorName || shortenPubkey(post.authorPubkey!)}
|
||||
{authorProfile?.name || post.authorName || shortenNpub(post.authorPubkey!)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -338,6 +350,17 @@ export default function BlogPostClient({ slug }: { slug: string }) {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{headerImage && (
|
||||
<img
|
||||
src={headerImage}
|
||||
alt={post.title}
|
||||
className="w-full rounded-xl mb-12 object-cover max-h-[28rem]"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<article className="mb-16">
|
||||
{loadingContent ? (
|
||||
<RelayLoading />
|
||||
@@ -424,9 +447,7 @@ export default function BlogPostClient({ slug }: { slug: string }) {
|
||||
className="bg-surface-container-high rounded-lg p-4"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 mb-2">
|
||||
<span className="font-semibold text-xs font-mono text-on-surface-variant/70">
|
||||
{shortenPubkey(r.pubkey)}
|
||||
</span>
|
||||
<NostrAuthor pubkey={r.pubkey} />
|
||||
<span className="text-on-surface-variant/30">·</span>
|
||||
<span className="text-xs text-on-surface-variant/50">
|
||||
{formatDate(new Date(r.created_at * 1000))}
|
||||
|
||||
Reference in New Issue
Block a user