Files
recall/src/components/version-history.tsx
Daniel Arroyo 8c80a12b81 feat: MVP-5 Sprint 1 - Backup/Restore system
- Add backup types and RecallBackup format
- Create backup snapshot engine (createBackupSnapshot)
- Add IndexedDB storage for local backups
- Implement retention policy (max 10, 30-day cleanup)
- Add backup validation and restore logic (merge/replace modes)
- Add backup restore UI dialog with preview and confirmation
- Add unsaved changes guard hook
- Integrate backups section in Settings
- Add backup endpoint to export-import API
2026-03-22 18:16:36 -03:00

159 lines
5.1 KiB
TypeScript

'use client'
import { useState } from 'react'
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogDescription, DialogFooter } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { History, RotateCcw } from 'lucide-react'
import { toast } from 'sonner'
interface Version {
id: string
title: string
content: string
createdAt: string
}
interface VersionHistoryProps {
noteId: string
}
export function VersionHistory({ noteId }: VersionHistoryProps) {
const [open, setOpen] = useState(false)
const [versions, setVersions] = useState<Version[]>([])
const [loading, setLoading] = useState(false)
const [restoring, setRestoring] = useState<string | null>(null)
const [confirmRestore, setConfirmRestore] = useState<string | null>(null)
const fetchVersions = async () => {
setLoading(true)
try {
const res = await fetch(`/api/notes/${noteId}/versions`)
const data = await res.json()
if (data.success) {
setVersions(data.data)
}
} catch {
toast.error('Error al cargar las versiones')
} finally {
setLoading(false)
}
}
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen)
if (newOpen && versions.length === 0) {
fetchVersions()
}
}
const handleRestore = async (versionId: string) => {
setRestoring(versionId)
try {
const res = await fetch(`/api/notes/${noteId}/versions/${versionId}`, {
method: 'PUT',
})
const data = await res.json()
if (data.success) {
toast.success('Versión restaurada correctamente')
setOpen(false)
window.location.reload()
} else {
toast.error(data.error || 'Error al restaurar la versión')
}
} catch {
toast.error('Error al restaurar la versión')
} finally {
setRestoring(null)
setConfirmRestore(null)
}
}
const formatDate = (dateString: string) => {
const date = new Date(dateString)
return date.toLocaleString('es-ES', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
}
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<div onClick={() => handleOpenChange(true)}>
<Button variant="outline" size="sm" className="cursor-pointer">
<History className="h-4 w-4 mr-1" /> Historial
</Button>
</div>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle>Historial de versiones</DialogTitle>
<DialogDescription>
Revisa y restaura versiones anteriores de esta nota
</DialogDescription>
</DialogHeader>
<div className="flex-1 overflow-y-auto py-4">
{loading ? (
<div className="text-center py-8 text-gray-500">Cargando versiones...</div>
) : versions.length === 0 ? (
<div className="text-center py-8 text-gray-500">No hay versiones guardadas</div>
) : (
<div className="space-y-3">
{versions.map((version) => (
<div
key={version.id}
className="flex items-start justify-between gap-4 p-3 border rounded-lg hover:bg-gray-50"
>
<div className="flex-1 min-w-0">
<div className="font-medium text-sm mb-1">
{formatDate(version.createdAt)}
</div>
<div className="text-gray-600 text-sm truncate">
{version.title}
</div>
<div className="text-gray-400 text-xs mt-1 truncate">
{version.content.substring(0, 50)}...
</div>
</div>
{confirmRestore === version.id ? (
<div className="flex gap-2 items-center">
<span className="text-xs text-gray-500">¿Restaurar?</span>
<Button
size="sm"
variant="destructive"
onClick={() => handleRestore(version.id)}
disabled={restoring === version.id}
>
{restoring === version.id ? '...' : 'Sí'}
</Button>
<Button
size="sm"
variant="outline"
onClick={() => setConfirmRestore(null)}
>
No
</Button>
</div>
) : (
<Button
size="sm"
variant="ghost"
onClick={() => setConfirmRestore(version.id)}
title="Restaurar esta versión"
>
<RotateCcw className="h-4 w-4" />
</Button>
)}
</div>
))}
</div>
)}
</div>
</DialogContent>
</Dialog>
)
}