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:
@@ -3,6 +3,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { api } from "@/lib/api";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { fetchEventFromRelays, fetchLongformFromRelays } from "@/lib/nostr";
|
||||
import {
|
||||
Pencil,
|
||||
Trash2,
|
||||
@@ -56,13 +57,24 @@ export default function BlogPage() {
|
||||
setFetching(true);
|
||||
setError("");
|
||||
try {
|
||||
const isNaddr = importInput.startsWith("naddr");
|
||||
const data = await api.fetchNostrEvent(
|
||||
isNaddr ? { naddr: importInput } : { eventId: importInput }
|
||||
);
|
||||
setImportPreview(data);
|
||||
const isNaddr = importInput.trim().startsWith("naddr");
|
||||
const event = isNaddr
|
||||
? await fetchLongformFromRelays(importInput.trim())
|
||||
: await fetchEventFromRelays(importInput.trim());
|
||||
|
||||
if (!event) {
|
||||
setError("Event not found on relays");
|
||||
setImportPreview(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const titleTag = event.tags?.find((t: string[]) => t[0] === "title");
|
||||
setImportPreview({
|
||||
...event,
|
||||
title: titleTag?.[1] || "Untitled",
|
||||
});
|
||||
} catch (err: any) {
|
||||
setError(err.message);
|
||||
setError(err.message || "Failed to fetch event");
|
||||
setImportPreview(null);
|
||||
} finally {
|
||||
setFetching(false);
|
||||
@@ -70,14 +82,29 @@ export default function BlogPage() {
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (!importInput.trim()) return;
|
||||
if (!importPreview) return;
|
||||
setImporting(true);
|
||||
setError("");
|
||||
try {
|
||||
const isNaddr = importInput.startsWith("naddr");
|
||||
await api.importPost(
|
||||
isNaddr ? { naddr: importInput } : { eventId: importInput }
|
||||
);
|
||||
const isNaddr = importInput.trim().startsWith("naddr");
|
||||
const excerpt = (importPreview.content || "")
|
||||
.slice(0, 200)
|
||||
.replace(/[#*_\n]/g, "")
|
||||
.trim();
|
||||
|
||||
const tags: string[] = (importPreview.tags || [])
|
||||
.filter((t: string[]) => t[0] === "t" && t[1])
|
||||
.map((t: string[]) => t[1].toLowerCase());
|
||||
|
||||
await api.importPost({
|
||||
nostrEventId: importPreview.id,
|
||||
naddr: isNaddr ? importInput.trim() : undefined,
|
||||
title: importPreview.title || "Untitled",
|
||||
excerpt: excerpt || undefined,
|
||||
authorPubkey: importPreview.pubkey,
|
||||
publishedAt: importPreview.created_at,
|
||||
tags: tags.length > 0 ? tags : undefined,
|
||||
});
|
||||
setImportInput("");
|
||||
setImportPreview(null);
|
||||
setImportOpen(false);
|
||||
@@ -95,7 +122,7 @@ export default function BlogPage() {
|
||||
title: post.title || "",
|
||||
slug: post.slug || "",
|
||||
excerpt: post.excerpt || "",
|
||||
categories: post.categories?.map((c: any) => c.id || c) || [],
|
||||
categories: post.categories?.map((c: any) => c.categoryId || c.category?.id || c) || [],
|
||||
featured: post.featured || false,
|
||||
visible: post.visible !== false,
|
||||
});
|
||||
@@ -184,6 +211,20 @@ export default function BlogPage() {
|
||||
<p className="text-on-surface/60 text-sm mt-1 line-clamp-3">
|
||||
{importPreview.content?.slice(0, 300)}...
|
||||
</p>
|
||||
{(() => {
|
||||
const previewTags = (importPreview.tags || [])
|
||||
.filter((t: string[]) => t[0] === "t" && t[1])
|
||||
.map((t: string[]) => t[1]);
|
||||
return previewTags.length > 0 ? (
|
||||
<div className="flex flex-wrap gap-1.5 mt-2">
|
||||
{previewTags.map((tag: string) => (
|
||||
<span key={tag} className="rounded-full px-2 py-0.5 text-xs bg-primary/10 text-primary font-medium">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
) : null;
|
||||
})()}
|
||||
<button
|
||||
onClick={handleImport}
|
||||
disabled={importing}
|
||||
@@ -303,12 +344,12 @@ export default function BlogPage() {
|
||||
<p className="text-on-surface/50 text-sm truncate">/{post.slug}</p>
|
||||
{post.categories?.length > 0 && (
|
||||
<div className="flex gap-2 mt-2">
|
||||
{post.categories.map((cat: any) => (
|
||||
{post.categories.map((pc: any) => (
|
||||
<span
|
||||
key={cat.id || cat}
|
||||
key={pc.categoryId || pc.category?.id}
|
||||
className="rounded-full px-2 py-0.5 text-xs bg-surface-container-highest text-on-surface/60"
|
||||
>
|
||||
{cat.name || cat}
|
||||
{pc.category?.name || pc.categoryId}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user