feat: user relay management, blog naddr sync, and OG image update
Add UserRelay API and dashboard relays tab with NIP-65 import, store post naddr for Nostr articles, and ship Prisma migrations for board tables, user relays, and post metadata. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,7 +7,7 @@ import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { api } from "@/lib/api";
|
||||
import { formatDate } from "@/lib/utils";
|
||||
import { hasNostrExtension, getPublicKey, signEvent, publishEvent, shortenPubkey, fetchNostrProfile, type NostrProfile } from "@/lib/nostr";
|
||||
import { hasNostrExtension, getPublicKey, signEvent, publishEvent, shortenPubkey, fetchNostrProfile, fetchLongformFromRelays, fetchEventFromRelays, type NostrProfile } from "@/lib/nostr";
|
||||
import { Navbar } from "@/components/public/Navbar";
|
||||
import { Footer } from "@/components/public/Footer";
|
||||
import type { Components } from "react-markdown";
|
||||
@@ -23,6 +23,7 @@ interface Post {
|
||||
publishedAt?: string;
|
||||
createdAt?: string;
|
||||
nostrEventId?: string;
|
||||
naddr?: string;
|
||||
categories?: { category: { id: string; name: string; slug: string } }[];
|
||||
}
|
||||
|
||||
@@ -147,6 +148,8 @@ export default function BlogPostClient({ slug }: { slug: string }) {
|
||||
const [hasNostr, setHasNostr] = useState(false);
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [authorProfile, setAuthorProfile] = useState<NostrProfile | null>(null);
|
||||
const [liveContent, setLiveContent] = useState<string | null>(null);
|
||||
const [loadingContent, setLoadingContent] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setHasNostr(hasNostrExtension());
|
||||
@@ -156,18 +159,36 @@ export default function BlogPostClient({ slug }: { slug: string }) {
|
||||
if (!slug) return;
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setLiveContent(null);
|
||||
api
|
||||
.getPost(slug)
|
||||
.then((data) => {
|
||||
setPost(data);
|
||||
setLoading(false);
|
||||
if (data?.authorPubkey) {
|
||||
fetchNostrProfile(data.authorPubkey)
|
||||
.then((profile) => setAuthorProfile(profile))
|
||||
.catch(() => {});
|
||||
}
|
||||
if (!data?.content && (data?.naddr || data?.nostrEventId)) {
|
||||
setLoadingContent(true);
|
||||
const fetchPromise = data.naddr
|
||||
? fetchLongformFromRelays(data.naddr)
|
||||
: fetchEventFromRelays(data.nostrEventId!);
|
||||
fetchPromise
|
||||
.then((event) => {
|
||||
if (event?.content) {
|
||||
setLiveContent(event.content);
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoadingContent(false));
|
||||
}
|
||||
})
|
||||
.catch((err) => setError(err.message))
|
||||
.finally(() => setLoading(false));
|
||||
.catch((err) => {
|
||||
setError(err.message);
|
||||
setLoading(false);
|
||||
});
|
||||
}, [slug]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -306,12 +327,24 @@ export default function BlogPostClient({ slug }: { slug: string }) {
|
||||
</header>
|
||||
|
||||
<article className="mb-16">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={markdownComponents}
|
||||
>
|
||||
{post.content}
|
||||
</ReactMarkdown>
|
||||
{loadingContent ? (
|
||||
<div className="animate-pulse space-y-4">
|
||||
{[85, 92, 78, 95, 88, 72, 90, 83].map((w, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="h-4 bg-surface-container-high rounded"
|
||||
style={{ width: `${w}%` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={[remarkGfm]}
|
||||
components={markdownComponents}
|
||||
>
|
||||
{post.content || liveContent || ""}
|
||||
</ReactMarkdown>
|
||||
)}
|
||||
</article>
|
||||
|
||||
<section className="bg-surface-container-low rounded-xl p-8 mb-16">
|
||||
|
||||
Reference in New Issue
Block a user