Write on the margins of the internet. Powered by the AT Protocol.
margin.at
extension
web
atproto
comments
1import React, { useState, useEffect } from "react";
2import { X, Loader2, History } from "lucide-react";
3import { formatDistanceToNow } from "date-fns";
4import type { AnnotationItem, EditHistoryItem } from "../../types";
5
6interface EditHistoryModalProps {
7 isOpen: boolean;
8 onClose: () => void;
9 item: AnnotationItem;
10}
11
12export default function EditHistoryModal({
13 isOpen,
14 onClose,
15 item,
16}: EditHistoryModalProps) {
17 const [history, setHistory] = useState<EditHistoryItem[]>([]);
18 const [loading, setLoading] = useState(false);
19 const [error, setError] = useState<string | null>(null);
20
21 useEffect(() => {
22 const fetchHistory = async () => {
23 if (!item.uri) return;
24
25 try {
26 setLoading(true);
27 setError(null);
28 const res = await fetch(
29 `/api/annotations/history?uri=${encodeURIComponent(item.uri)}`,
30 );
31 if (!res.ok) throw new Error("Failed to fetch history");
32 const data = await res.json();
33 setHistory(data);
34 } catch (err) {
35 console.error(err);
36 setError("Failed to load edit history");
37 } finally {
38 setLoading(false);
39 }
40 };
41
42 if (isOpen && item.uri) {
43 fetchHistory();
44 document.body.style.overflow = "hidden";
45 }
46 return () => {
47 document.body.style.overflow = "unset";
48 };
49 }, [isOpen, item.uri]);
50
51 if (!isOpen) return null;
52
53 return (
54 <div
55 className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm animate-fade-in"
56 onClick={onClose}
57 >
58 <div
59 className="w-full max-w-lg bg-white dark:bg-surface-900 rounded-3xl shadow-2xl overflow-hidden flex flex-col max-h-[80vh]"
60 onClick={(e) => e.stopPropagation()}
61 >
62 <div className="p-4 flex justify-between items-center border-b border-surface-100 dark:border-surface-800 shrink-0">
63 <div className="flex items-center gap-2">
64 <History className="text-surface-500" size={20} />
65 <h2 className="text-xl font-display font-bold text-surface-900 dark:text-white">
66 Edit History
67 </h2>
68 </div>
69 <button
70 onClick={onClose}
71 className="p-2 text-surface-400 hover:text-surface-900 dark:hover:text-white hover:bg-surface-50 dark:hover:bg-surface-800 rounded-full transition-colors"
72 >
73 <X size={20} />
74 </button>
75 </div>
76
77 <div className="p-0 overflow-y-auto flex-1 custom-scrollbar">
78 {loading ? (
79 <div className="flex justify-center p-8">
80 <Loader2 className="animate-spin text-primary-500" size={32} />
81 </div>
82 ) : error ? (
83 <div className="p-8 text-center text-red-500">{error}</div>
84 ) : history.length === 0 ? (
85 <div className="p-8 text-center text-surface-500">
86 No edit history found.
87 </div>
88 ) : (
89 <div className="divide-y divide-surface-100 dark:divide-surface-800">
90 <div className="p-4 bg-primary-50/50 dark:bg-primary-900/10">
91 <div className="flex justify-between items-start mb-2">
92 <span className="text-xs font-bold uppercase tracking-wider text-primary-600 dark:text-primary-400">
93 Current Version
94 </span>
95 <span className="text-xs text-surface-400">
96 {item.editedAt
97 ? `Edited ${formatDistanceToNow(new Date(item.editedAt))} ago`
98 : `Posted ${formatDistanceToNow(new Date(item.createdAt))} ago`}
99 </span>
100 </div>
101 <div className="text-surface-900 dark:text-white whitespace-pre-wrap text-sm">
102 {item.text || item.body?.value}
103 </div>
104 </div>
105
106 {history.map((edit, index) => (
107 <div
108 key={edit.cid || index}
109 className="p-4 hover:bg-surface-50 dark:hover:bg-surface-800/50 transition-colors"
110 >
111 <div className="flex justify-between items-start mb-2">
112 <span className="text-xs font-medium text-surface-500">
113 Previous Version
114 </span>
115 <span
116 className="text-xs text-surface-400"
117 title={new Date(edit.editedAt).toLocaleString()}
118 >
119 {formatDistanceToNow(new Date(edit.editedAt))} ago
120 </span>
121 </div>
122 <div className="text-surface-600 dark:text-surface-300 whitespace-pre-wrap text-sm">
123 {edit.previousContent}
124 </div>
125 </div>
126 ))}
127 </div>
128 )}
129 </div>
130
131 <div className="p-4 border-t border-surface-100 dark:border-surface-800 bg-surface-50 dark:bg-surface-800/50 shrink-0">
132 <button
133 onClick={onClose}
134 className="w-full py-2.5 bg-white dark:bg-surface-800 border border-surface-200 dark:border-surface-700 text-surface-700 dark:text-surface-200 font-medium rounded-xl hover:bg-surface-50 dark:hover:bg-surface-700 transition-colors"
135 >
136 Close
137 </button>
138 </div>
139 </div>
140 </div>
141 );
142}