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