"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 } 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 [title, setTitle] = useState(""); const [eventId, setEventId] = useState(""); const [naddr, setNaddr] = 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 handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setFormError(""); setFormSuccess(""); if (!title.trim()) { setFormError("Title is required"); return; } if (!eventId.trim() && !naddr.trim()) { setFormError("Either an Event ID or naddr is required"); return; } setSubmitting(true); try { await api.createSubmission({ title: title.trim(), eventId: eventId.trim() || undefined, naddr: naddr.trim() || undefined, }); setFormSuccess("Submission sent for review!"); setTitle(""); setEventId(""); setNaddr(""); 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. Provide the event ID or naddr of the article you'd like published on the blog.

setTitle(e.target.value)} placeholder="My Bitcoin Article" className="w-full bg-surface-container-highest text-on-surface rounded-lg px-4 py-3 placeholder:text-on-surface-variant/40 focus:outline-none focus:ring-1 focus:ring-primary/40" />
setEventId(e.target.value)} placeholder="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" />
setNaddr(e.target.value)} placeholder="naddr1..." 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 )}
))}
)}
)}
); }