'use client'; import { useState, useEffect } from 'react'; import type { BackupItem, BackupTarget } from '@/app/api/admin/backups/route'; import * as Icons from './Icons'; import { useScrollLock } from './useScrollLock'; import { useModalDialog } from '@/hooks/useModalDialog'; import { reportContentRestored } from './contentRestored'; import { useAdminFetch } from './useAdminFetch'; interface Props { isOpen: boolean; onClose: () => void; onRestoreSuccess: () => void; } const TABS: { target: BackupTarget; label: string; file: string }[] = [ { target: 'gallery', label: 'Gallery', file: 'gallery.yaml' }, { target: 'settings', label: 'Settings', file: 'settings.yaml' }, { target: 'about', label: 'About', file: 'about.md' }, { target: 'privacy', label: 'Privacy', file: 'privacy.md' }, { target: 'journal', label: 'Journal', file: 'journal entries' }, { target: 'pages', label: 'Content pages', file: 'content pages' }, ]; type BackupLists = Record; const EMPTY_LISTS: BackupLists = { gallery: [], settings: [], about: [], privacy: [], journal: [], pages: [], }; export default function BackupManagerModal({ isOpen, onClose, onRestoreSuccess }: Props) { const [activeTab, setActiveTab] = useState('gallery'); const [showAllBackups, setShowAllBackups] = useState(false); const list = useAdminFetch<{ backups: Partial }>( isOpen ? '/api/admin/backups' : null, ); const backups: BackupLists = { ...EMPTY_LISTS, ...list.data?.backups }; /** A failed restore; a failed load is list.error. */ const [restoreError, setRestoreError] = useState(null); const error = list.error ?? restoreError; const loading = list.loading; const reloadList = list.reload; const [restoringFilename, setRestoringFilename] = useState(null); const [confirmItem, setConfirmItem] = useState(null); const [successMsg, setSuccessMsg] = useState(null); useEffect(() => { if (isOpen) { // Each opening shows the backups as they are now, not as they were // the last time the modal was open. reloadList(); setRestoreError(null); setConfirmItem(null); setSuccessMsg(null); setShowAllBackups(false); } }, [isOpen, reloadList]); useScrollLock(isOpen); /* Before the early `return null`: hooks must not run conditionally. */ const cardRef = useModalDialog(onClose, isOpen); if (!isOpen) return null; async function handleRestore(item: BackupItem) { const filename = item.filename; setRestoringFilename(filename); setRestoreError(null); setSuccessMsg(null); try { const res = await fetch('/api/admin/backups', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backupFilename: filename, target: item.target }), }); const data = await res.json(); if (!res.ok) { throw new Error(data.error || 'Failed to restore backup'); } setSuccessMsg( item.slug ? `${item.target === 'pages' ? 'Page' : 'Journal entry'} "${item.slug}" restored.` : `Backup restored successfully! (${filename})`, ); setConfirmItem(null); // Open editors still hold the pre-restore state; they reload on this. reportContentRestored({ target: item.target, slug: item.slug }); reloadList(); onRestoreSuccess(); } catch (err) { setRestoreError(err instanceof Error ? err.message : 'Error restoring backup'); } finally { setRestoringFilename(null); } } const currentList = backups[activeTab] || []; const INITIAL_VISIBLE_COUNT = 3; const visibleList = showAllBackups ? currentList : currentList.slice(0, INITIAL_VISIBLE_COUNT); const hiddenCount = currentList.length - INITIAL_VISIBLE_COUNT; return (
e.stopPropagation()} >

Backup History & Restoration

Restore an earlier version of your pages, settings, about page, journal or content pages — including deleted journal entries and pages.

{/* Tab Selection */}
{TABS.map((tab) => ( ))}
{/* Status Messages */} {error &&
{error}
} {successMsg &&
{successMsg}
} {/* Confirmation Modal Section */} {confirmItem && (
Confirm Restoration

Are you sure you want to restore {confirmItem.filename}? A safety snapshot of your current state will be created automatically before reverting.

)} {/* Backup List Content */}
{loading ? (
Loading backups...
) : currentList.length === 0 ? (
No backups available yet for {TABS.find((t) => t.target === activeTab)?.file}
) : (
{visibleList.map((item) => { const dateObj = item.timestamp ? new Date(item.timestamp) : null; const formattedDate = dateObj && !isNaN(dateObj.getTime()) ? dateObj.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'medium', }) : item.filename; return (
{item.slug && {item.slug}} {formattedDate} {item.isDeleted && ( Deleted entry )} {item.isPreRestore && ( Pre-Restore Snapshot )}
{item.filename}
); })} {currentList.length > INITIAL_VISIBLE_COUNT && ( )}
)}
); }