7.5 KB222 lines
Blame
1"use client";
2
3import { useState, useRef } from "react";
4import { useCollab } from "./collab-provider";
5import type { CollabNote } from "../hooks/use-collab-socket";
6
7interface NotesSidebarProps {
8 diagramId: string;
9 diagramTitle: string;
10}
11
12function timeAgo(ts: string): string {
13 const secs = Math.floor((Date.now() - new Date(ts).getTime()) / 1000);
14 if (secs < 60) return "just now";
15 if (secs < 3600) return Math.floor(secs / 60) + "m ago";
16 if (secs < 86400) return Math.floor(secs / 3600) + "h ago";
17 return Math.floor(secs / 86400) + "d ago";
18}
19
20function NoteCard({ note, onEdit, onDelete }: { note: CollabNote; onEdit: (text: string) => void; onDelete: () => void }) {
21 const [editing, setEditing] = useState(false);
22 const [editText, setEditText] = useState(note.text);
23
24 const handleSave = () => {
25 if (editText.trim() && editText !== note.text) {
26 onEdit(editText.trim());
27 }
28 setEditing(false);
29 };
30
31 return (
32 <div
33 style={{
34 padding: "8px 10px",
35 borderBottom: "1px solid var(--border-subtle, var(--divide))",
36 fontSize: "13px",
37 position: "relative",
38 transition: "background-color 0.12s",
39 }}
40 onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = "var(--bg-hover)")}
41 onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = "transparent")}
42 >
43 <div className="flex items-center gap-1.5" style={{ marginBottom: "3px", fontSize: "10px", color: "var(--text-muted)", fontFamily: "'JetBrains Mono', Menlo, monospace" }}>
44 <span style={{ fontWeight: 500, color: "var(--accent)" }}>{note.author}</span>
45 <span>&middot;</span>
46 <span>{timeAgo(note.timestamp)}</span>
47 </div>
48 {editing ? (
49 <div>
50 <textarea
51 value={editText}
52 onChange={(e) => setEditText(e.target.value)}
53 onKeyDown={(e) => {
54 if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSave(); }
55 if (e.key === "Escape") setEditing(false);
56 }}
57 className="w-full text-sm px-2 py-1"
58 style={{
59 backgroundColor: "var(--bg-input)",
60 border: "1px solid var(--border)",
61 color: "var(--text-primary)",
62 resize: "vertical",
63 minHeight: "2.5rem",
64 outline: "none",
65 font: "inherit",
66 }}
67 rows={2}
68 autoFocus
69 />
70 <div className="flex gap-1 mt-1">
71 <button
72 onClick={handleSave}
73 className="text-xs px-2 py-0.5"
74 style={{ backgroundColor: "var(--accent)", color: "var(--accent-text)", border: "none", cursor: "pointer", font: "inherit" }}
75 >
76 Save
77 </button>
78 <button
79 onClick={() => setEditing(false)}
80 className="text-xs px-2 py-0.5"
81 style={{ border: "1px solid var(--border)", background: "none", color: "var(--text-muted)", cursor: "pointer", font: "inherit" }}
82 >
83 Cancel
84 </button>
85 </div>
86 </div>
87 ) : (
88 <div>
89 <p style={{ color: "var(--text-primary)", whiteSpace: "pre-wrap", wordBreak: "break-word", fontSize: "12px", lineHeight: 1.5 }}>
90 {note.text}
91 </p>
92 <div className="note-actions flex gap-2 mt-1">
93 <button
94 onClick={() => { setEditText(note.text); setEditing(true); }}
95 className="text-xs"
96 style={{ color: "var(--text-faint)", background: "none", border: "none", cursor: "pointer", font: "inherit" }}
97 >
98 Edit
99 </button>
100 <button
101 onClick={onDelete}
102 className="text-xs"
103 style={{ color: "var(--text-faint)", background: "none", border: "none", cursor: "pointer", font: "inherit" }}
104 >
105 Delete
106 </button>
107 </div>
108 </div>
109 )}
110 </div>
111 );
112}
113
114export function NotesSidebar({ diagramId, diagramTitle }: NotesSidebarProps) {
115 const { notes, emit } = useCollab();
116 const [text, setText] = useState("");
117 const diagramNotes = notes[diagramId] ?? [];
118
119 const handleAdd = () => {
120 if (!text.trim()) return;
121 emit("add-note", { text: text.trim(), diagramId, diagramTitle });
122 setText("");
123 };
124
125 const handleEdit = (noteId: string, newText: string) => {
126 emit("edit-note", { noteId, diagramId, text: newText });
127 };
128
129 const handleDelete = (noteId: string) => {
130 emit("delete-note", { noteId, diagramId });
131 };
132
133 return (
134 <div
135 style={{
136 width: "300px",
137 minWidth: "180px",
138 borderLeft: "1px solid var(--border)",
139 background: "var(--bg-card)",
140 display: "flex",
141 flexDirection: "column",
142 flexShrink: 0,
143 overflow: "hidden",
144 }}
145 >
146 <div
147 style={{
148 padding: "8px 12px",
149 fontSize: "11px",
150 fontWeight: 500,
151 color: "var(--text-muted)",
152 borderBottom: "1px solid var(--border-subtle, var(--divide))",
153 fontFamily: "'JetBrains Mono', Menlo, monospace",
154 textTransform: "uppercase",
155 letterSpacing: "0.5px",
156 }}
157 >
158 Notes
159 </div>
160 <div style={{ flex: 1, overflowY: "auto", padding: "6px" }}>
161 {diagramNotes.length === 0 && (
162 <p style={{ color: "var(--text-faint)", fontSize: "12px", fontStyle: "italic", textAlign: "center", padding: "32px 16px" }}>
163 No notes yet for this diagram.
164 Click on the diagram or type below to add one.
165 </p>
166 )}
167 {diagramNotes.map((note) => (
168 <NoteCard
169 key={note.id}
170 note={note}
171 onEdit={(t) => handleEdit(note.id, t)}
172 onDelete={() => handleDelete(note.id)}
173 />
174 ))}
175 </div>
176 <div style={{ borderTop: "1px solid var(--border-subtle, var(--divide))", padding: "8px", display: "flex", gap: "6px", alignItems: "flex-end" }}>
177 <textarea
178 value={text}
179 onChange={(e) => setText(e.target.value)}
180 onKeyDown={(e) => {
181 if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleAdd(); }
182 }}
183 placeholder="Add a note..."
184 style={{
185 flex: 1,
186 backgroundColor: "var(--bg-input, var(--bg-inset))",
187 border: "1px solid var(--border)",
188 color: "var(--text-primary)",
189 resize: "none",
190 outline: "none",
191 font: "inherit",
192 fontSize: "13px",
193 padding: "6px 8px",
194 lineHeight: 1.4,
195 }}
196 rows={1}
197 />
198 <button
199 onClick={handleAdd}
200 disabled={!text.trim()}
201 title="Send note (Enter)"
202 style={{
203 width: "32px",
204 height: "32px",
205 display: "flex",
206 alignItems: "center",
207 justifyContent: "center",
208 background: text.trim() ? "var(--accent)" : "var(--bg-hover)",
209 color: text.trim() ? "var(--accent-text)" : "var(--text-faint)",
210 border: "none",
211 borderRadius: "4px",
212 cursor: text.trim() ? "pointer" : "default",
213 flexShrink: 0,
214 }}
215 >
216 <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>
217 </button>
218 </div>
219 </div>
220 );
221}
222