"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import Image from "next/image"; import { Send, FileText, Clock, CheckCircle, XCircle, Plus, User, Loader2, AtSign, Radio, Trash2, Download, Eye, Pencil } from "lucide-react"; import { useAuth } from "@/hooks/useAuth"; import { api } from "@/lib/api"; import { shortenPubkey, fetchEventFromRelays, fetchLongformFromRelays } from "@/lib/nostr"; import { formatDate } from "@/lib/utils"; import { Button } from "@/components/ui/Button"; interface Submission { id: string; eventId?: string; naddr?: string; title: string; status: string; reviewNote?: string; createdAt: string; } const STATUS_CONFIG: Record = { PENDING: { label: "Pending Review", icon: Clock, className: "text-primary bg-primary/10", }, APPROVED: { label: "Approved", icon: CheckCircle, className: "text-green-400 bg-green-400/10", }, REJECTED: { label: "Rejected", icon: XCircle, className: "text-error bg-error/10", }, }; interface UserRelay { id: string; url: string; read: boolean; write: boolean; createdAt: string; } type Tab = "submissions" | "profile" | "relays"; type UsernameStatus = | { state: "idle" } | { state: "checking" } | { state: "available" } | { state: "unavailable"; reason: string }; export default function DashboardPage() { const { user, login } = useAuth(); const [activeTab, setActiveTab] = useState("submissions"); // Submissions state const [submissions, setSubmissions] = useState([]); const [loadingSubs, setLoadingSubs] = useState(true); const [showForm, setShowForm] = useState(false); const [noteInput, setNoteInput] = useState(""); const [submitting, setSubmitting] = useState(false); const [formError, setFormError] = useState(""); const [formSuccess, setFormSuccess] = useState(""); // Profile state const [username, setUsername] = useState(""); const [usernameStatus, setUsernameStatus] = useState({ state: "idle" }); const [saving, setSaving] = useState(false); const [saveError, setSaveError] = useState(""); const [saveSuccess, setSaveSuccess] = useState(""); const [hostname, setHostname] = useState(""); const debounceRef = useRef | null>(null); // Relay state const [userRelays, setUserRelays] = useState([]); const [loadingRelays, setLoadingRelays] = useState(true); const [newRelayUrl, setNewRelayUrl] = useState(""); const [addingRelay, setAddingRelay] = useState(false); const [importingNip65, setImportingNip65] = useState(false); const [relayError, setRelayError] = useState(""); const [relaySuccess, setRelaySuccess] = useState(""); const displayName = user?.name || user?.displayName || shortenPubkey(user?.pubkey || ""); useEffect(() => { setHostname(window.location.hostname); }, []); useEffect(() => { if (user?.username) { setUsername(user.username); } }, [user?.username]); const loadSubmissions = useCallback(async () => { try { const data = await api.getMySubmissions(); setSubmissions(data); } catch { // Silently handle } finally { setLoadingSubs(false); } }, []); const loadRelays = useCallback(async () => { try { const data = await api.getUserRelays(); setUserRelays(data); } catch { // Silently handle } finally { setLoadingRelays(false); } }, []); useEffect(() => { loadSubmissions(); loadRelays(); }, [loadSubmissions, loadRelays]); const extractTitle = (event: any): string => { const titleTag = event?.tags?.find((t: string[]) => t[0] === "title"); return titleTag?.[1]?.trim() || "Untitled"; }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setFormError(""); setFormSuccess(""); const input = noteInput.trim(); if (!input) { setFormError("A Note ID or naddr is required"); return; } setSubmitting(true); try { let payload: { title: string; eventId?: string; naddr?: string }; if (input.startsWith("naddr1")) { const event = await fetchLongformFromRelays(input); if (!event) { throw new Error("Could not find that article on the relays"); } payload = { title: extractTitle(event), naddr: input }; } else { let hexId = input; if (input.startsWith("note1")) { try { const { nip19 } = await import("nostr-tools"); const decoded = nip19.decode(input); if (decoded.type !== "note") throw new Error(); hexId = decoded.data as string; } catch { throw new Error("Invalid note id"); } } else if (!/^[0-9a-f]{64}$/i.test(input)) { throw new Error("Enter a valid note id, naddr, or hex event id"); } const event = await fetchEventFromRelays(hexId); if (!event) { throw new Error("Could not find that note on the relays"); } payload = { title: extractTitle(event), eventId: hexId }; } await api.createSubmission(payload); setFormSuccess(`Submission "${payload.title}" sent for review!`); setNoteInput(""); setShowForm(false); await loadSubmissions(); } catch (err: any) { setFormError(err.message || "Failed to submit"); } finally { setSubmitting(false); } }; const handleAddRelay = async (e: React.FormEvent) => { e.preventDefault(); setRelayError(""); setRelaySuccess(""); const url = newRelayUrl.trim(); if (!url) { setRelayError("Relay URL is required"); return; } if (!url.startsWith("wss://") && !url.startsWith("ws://")) { setRelayError("URL must start with wss:// or ws://"); return; } setAddingRelay(true); try { await api.addUserRelay({ url }); setNewRelayUrl(""); setRelaySuccess("Relay added"); await loadRelays(); } catch (err: any) { setRelayError(err.message || "Failed to add relay"); } finally { setAddingRelay(false); } }; const handleRemoveRelay = async (id: string) => { setRelayError(""); try { await api.removeUserRelay(id); setUserRelays((prev) => prev.filter((r) => r.id !== id)); } catch (err: any) { setRelayError(err.message || "Failed to remove relay"); } }; const handleImportNip65 = async () => { setRelayError(""); setRelaySuccess(""); setImportingNip65(true); try { const result = await api.importNip65Relays(); if (result.imported > 0) { setRelaySuccess(`Imported ${result.imported} relay(s) from your NIP-65 list`); await loadRelays(); } else { setRelayError(result.message || "No NIP-65 relay list found for your pubkey"); } } catch (err: any) { setRelayError(err.message || "Failed to import NIP-65 relays"); } finally { setImportingNip65(false); } }; const handleUsernameChange = (value: string) => { setUsername(value); setSaveError(""); setSaveSuccess(""); if (debounceRef.current) clearTimeout(debounceRef.current); const trimmed = value.trim().toLowerCase(); if (!trimmed || trimmed === (user?.username ?? "")) { setUsernameStatus({ state: "idle" }); return; } setUsernameStatus({ state: "checking" }); debounceRef.current = setTimeout(async () => { try { const result = await api.checkUsername(trimmed); if (result.available) { setUsernameStatus({ state: "available" }); } else { setUsernameStatus({ state: "unavailable", reason: result.reason || "Username is not available" }); } } catch { setUsernameStatus({ state: "unavailable", reason: "Could not check availability" }); } }, 500); }; const handleSaveProfile = async (e: React.FormEvent) => { e.preventDefault(); setSaveError(""); setSaveSuccess(""); const trimmed = username.trim().toLowerCase(); if (!trimmed) { setSaveError("Username is required"); return; } setSaving(true); try { const updated = await api.updateProfile({ username: trimmed }); setSaveSuccess(`Username saved! Your NIP-05 address is ${updated.username}@${hostname}`); setUsernameStatus({ state: "idle" }); // Persist updated username into stored user const stored = localStorage.getItem("bbe_user"); if (stored) { try { const parsed = JSON.parse(stored); localStorage.setItem("bbe_user", JSON.stringify({ ...parsed, username: updated.username })); } catch { // ignore } } } catch (err: any) { setSaveError(err.message || "Failed to save username"); } finally { setSaving(false); } }; const isSaveDisabled = saving || usernameStatus.state === "checking" || usernameStatus.state === "unavailable" || !username.trim(); return (
{user?.picture ? ( {displayName} ) : (
{(displayName)[0]?.toUpperCase() || "?"}
)}

{displayName}

Your Dashboard

{/* Tabs */}
{/* Submissions tab */} {activeTab === "submissions" && ( <>

Submit a Post

{!showForm && ( )}
{formSuccess && (
{formSuccess}
)} {showForm && (

Submit a Nostr longform post for moderator review. Paste the note ID or naddr of the article you'd like published on the blog. The title is pulled automatically from the note.

setNoteInput(e.target.value)} placeholder="naddr1... or note1... or hex event id" className="w-full bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 font-mono text-sm placeholder:text-on-surface-variant/40 focus:outline-none focus:ring-1 focus:ring-primary/40" />
{formError && (

{formError}

)}
)}

My Submissions

{loadingSubs ? (
{[1, 2, 3].map((i) => (
))}
) : submissions.length === 0 ? (

No submissions yet. Submit a Nostr longform post for review.

) : (
{submissions.map((sub) => { const statusCfg = STATUS_CONFIG[sub.status] || STATUS_CONFIG.PENDING; const StatusIcon = statusCfg.icon; return (

{sub.title}

{formatDate(sub.createdAt)} {sub.eventId && ( {sub.eventId.slice(0, 16)}... )} {sub.naddr && ( {sub.naddr.slice(0, 20)}... )}

{statusCfg.label}
{sub.reviewNote && (

{sub.reviewNote}

)}
); })}
)}
)} {/* Profile tab */} {activeTab === "profile" && (

NIP-05 Username

Claim a NIP-05 verified Nostr address hosted on this site. Other Nostr clients will display your identity as{" "} username@{hostname || "…"}.

handleUsernameChange(e.target.value)} placeholder="yourname" maxLength={50} className="w-full bg-surface-container-highest text-on-surface rounded-lg pl-10 pr-10 py-3 font-mono text-sm placeholder:text-on-surface-variant/40 focus:outline-none focus:ring-1 focus:ring-primary/40" />
{usernameStatus.state === "checking" && ( )} {usernameStatus.state === "available" && ( )} {usernameStatus.state === "unavailable" && ( )}
{/* Status message */}
{usernameStatus.state === "checking" && (

Checking availability…

)} {usernameStatus.state === "available" && (

Available

)} {usernameStatus.state === "unavailable" && (

{usernameStatus.reason}

)}
{/* NIP-05 preview */} {username.trim() && (

NIP-05 Address

{username.trim().toLowerCase()}@{hostname || "…"}

)} {saveError && (

{saveError}

)} {saveSuccess && (
{saveSuccess}
)}
)} {/* Relays tab */} {activeTab === "relays" && (

Your Relays

Manage the Nostr relays used for your interactions on this site. These relays are used when publishing events and fetching your content. You can also import your relay list from your Nostr profile (NIP-65).

{relaySuccess && (
{relaySuccess}
)} {relayError && (
{relayError}
)}
setNewRelayUrl(e.target.value)} placeholder="wss://relay.example.com" className="flex-1 bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 font-mono text-sm placeholder:text-on-surface-variant/40 focus:outline-none focus:ring-1 focus:ring-primary/40" />
{loadingRelays ? (
{[1, 2, 3].map((i) => (
))}
) : userRelays.length === 0 ? (

No relays configured. Add a relay manually or import from your NIP-65 profile.

) : (
{userRelays.map((relay) => (

{relay.url}

{relay.read && ( Read )} {relay.write && ( Write )}
))}
)}
)}
); }