"use client"; import { useState, useEffect, useCallback } from "react"; import Link from "next/link"; import { ArrowLeft, Heart, Send } from "lucide-react"; 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 { Navbar } from "@/components/public/Navbar"; import { Footer } from "@/components/public/Footer"; import type { Components } from "react-markdown"; interface Post { id: string; slug: string; title: string; content: string; excerpt?: string; authorName?: string; authorPubkey?: string; publishedAt?: string; createdAt?: string; nostrEventId?: string; categories?: { category: { id: string; name: string; slug: string } }[]; } interface NostrReply { id: string; pubkey: string; content: string; created_at: number; } const markdownComponents: Components = { h1: ({ children }) => (

{children}

), h2: ({ children }) => (

{children}

), h3: ({ children }) => (

{children}

), h4: ({ children }) => (

{children}

), p: ({ children }) => (

{children}

), a: ({ href, children }) => ( {children} ), ul: ({ children }) => ( ), ol: ({ children }) => (
    {children}
), li: ({ children }) => (
  • {children}
  • ), blockquote: ({ children }) => (
    {children}
    ), code: ({ className, children }) => { const isBlock = className?.includes("language-"); if (isBlock) { return ( {children} ); } return ( {children} ); }, pre: ({ children }) => (
          {children}
        
    ), img: ({ src, alt }) => ( {alt ), hr: () =>
    , table: ({ children }) => (
    {children}
    ), th: ({ children }) => ( {children} ), td: ({ children }) => ( {children} ), }; function ArticleSkeleton() { const widths = [85, 92, 78, 95, 88, 72, 90, 83]; return (
    {widths.map((w, i) => (
    ))}
    ); } export default function BlogPostClient({ slug }: { slug: string }) { const [post, setPost] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [liked, setLiked] = useState(false); const [likeCount, setLikeCount] = useState(0); const [comment, setComment] = useState(""); const [replies, setReplies] = useState([]); const [hasNostr, setHasNostr] = useState(false); const [submitting, setSubmitting] = useState(false); const [authorProfile, setAuthorProfile] = useState(null); useEffect(() => { setHasNostr(hasNostrExtension()); }, []); useEffect(() => { if (!slug) return; setLoading(true); setError(null); api .getPost(slug) .then((data) => { setPost(data); if (data?.authorPubkey) { fetchNostrProfile(data.authorPubkey) .then((profile) => setAuthorProfile(profile)) .catch(() => {}); } }) .catch((err) => setError(err.message)) .finally(() => setLoading(false)); }, [slug]); useEffect(() => { if (!slug) return; api.getPostReactions(slug) .then((data) => setLikeCount(data.count)) .catch(() => {}); api.getPostReplies(slug) .then((data) => setReplies(data.replies || [])) .catch(() => {}); }, [slug]); const handleLike = useCallback(async () => { if (liked || !post?.nostrEventId || !hasNostr) return; try { const pubkey = await getPublicKey(); const reactionEvent = { kind: 7, created_at: Math.floor(Date.now() / 1000), tags: [["e", post.nostrEventId], ["p", post.authorPubkey || ""]], content: "+", pubkey, }; const signedReaction = await signEvent(reactionEvent); await publishEvent(signedReaction); setLiked(true); setLikeCount((c) => c + 1); } catch { // User rejected or extension unavailable } }, [liked, post, hasNostr]); const handleComment = useCallback(async () => { if (!comment.trim() || !post?.nostrEventId || !hasNostr) return; setSubmitting(true); try { const pubkey = await getPublicKey(); const replyEvent = { kind: 1, created_at: Math.floor(Date.now() / 1000), tags: [["e", post.nostrEventId, "", "reply"], ["p", post.authorPubkey || ""]], content: comment.trim(), pubkey, }; const signed = await signEvent(replyEvent); await publishEvent(signed); setReplies((prev) => [ ...prev, { id: signed.id || Date.now().toString(), pubkey, content: comment.trim(), created_at: Math.floor(Date.now() / 1000), }, ]); setComment(""); } catch { // User rejected or extension unavailable } finally { setSubmitting(false); } }, [comment, post, hasNostr]); const categories = post?.categories?.map((c) => c.category) || []; return ( <>
    Back to Blog {loading && } {error && (
    Failed to load post: {error}
    )} {!loading && !error && post && ( <>
    {categories.length > 0 && (
    {categories.map((cat) => ( {cat.name} ))}
    )}

    {post.title}

    {(authorProfile || post.authorName || post.authorPubkey) && (
    {authorProfile?.picture && ( {authorProfile.name { (e.target as HTMLImageElement).style.display = "none"; }} /> )} {authorProfile?.name || post.authorName || shortenPubkey(post.authorPubkey!)}
    )} {(post.publishedAt || post.createdAt) && ( <> {(authorProfile || post.authorName || post.authorPubkey) && ( · )} {formatDate(post.publishedAt || post.createdAt!)} )}
    {post.content}
    {!hasNostr && ( Install a Nostr extension to like and comment )}

    Comments {replies.length > 0 && `(${replies.length})`}

    {hasNostr && (