import { useState } from 'react'; import { useQueryClient } from '@tanstack/react-query'; import { Archive, Loader2, Check, AlertTriangle } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useNostrPublish } from '@/hooks/useNostrPublish'; import { useNewsletterIssues } from '@/hooks/useNewsletterIssues'; import { useToast } from '@/hooks/useToast'; import { isAdminPubkey } from '@/lib/admins'; import { buildIssueArticle, issueSlug, type StaticIssueLike } from '@/lib/newsletterArticle'; /** * Admin-only one-time backfill: publishes the static archive issues * (NEWSLETTER_ISSUES, passed in as a prop to avoid an import cycle with the * page) to Nostr as NIP-23 articles. Skips any issue whose slug already exists * on-relay, so it's safe to run repeatedly. The rich static page rendering is * untouched — this just mirrors the archive onto Nostr for subscribers/readers. */ export function NewsletterBackfillPanel({ issues }: { issues?: StaticIssueLike[] }) { const { user } = useCurrentUser(); const { mutateAsync: publish } = useNostrPublish(); const { data: liveIssues } = useNewsletterIssues(); const { toast } = useToast(); const queryClient = useQueryClient(); const [running, setRunning] = useState(false); const [done, setDone] = useState(false); if (!isAdminPubkey(user?.pubkey) || !issues || issues.length === 0) return null; // Which static issues aren't on Nostr yet? (match by slug) const liveSlugs = new Set((liveIssues ?? []).map((i) => i.identifier)); const missing = issues.filter((i) => !liveSlugs.has(issueSlug(i.number))); const handleBackfill = async () => { setRunning(true); let published = 0; const failures: number[] = []; // Publish oldest → newest so numbering reads naturally on relays. const ordered = [...missing].sort((a, b) => a.number - b.number); for (const issue of ordered) { try { await publish(buildIssueArticle(issue)); published++; } catch { failures.push(issue.number); } } setRunning(false); setDone(true); queryClient.invalidateQueries({ queryKey: ['newsletter-issues'] }); if (failures.length === 0) { toast({ title: `Backfilled ${published} issue${published === 1 ? '' : 's'} 🛹`, description: 'The archive now lives on Nostr for subscribers and readers.', }); } else { toast({ title: `Published ${published}, ${failures.length} failed`, description: `Retry to finish: #${failures.map((n) => String(n).padStart(3, '0')).join(', #')}.`, variant: 'destructive', }); setDone(false); } }; if (missing.length === 0) { return (

Archive synced to Nostr

); } return (

{missing.length} archive issue{missing.length === 1 ? '' : 's'} not on Nostr yet

Publish {missing.map((i) => `#${String(i.number).padStart(3, '0')}`).join(', ')} as NIP-23 articles so subscribers and long-form readers can see the full archive. The page keeps its rich layout regardless.

); }