Skip to content

Commit bb47505

Browse files
committed
feat: add import/export functionality for events with modal support
1 parent 2b8302a commit bb47505

File tree

4 files changed

+168
-0
lines changed

4 files changed

+168
-0
lines changed

src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ function App() {
1414
filters,
1515
attachEventListeners,
1616
clearEvents,
17+
importEvents,
1718
updateFilters,
1819
} = useKeyboardEvents();
1920

@@ -69,6 +70,7 @@ function App() {
6970
events={allEvents}
7071
showDelays={showDelays}
7172
onShowDelaysChange={setShowDelays}
73+
onImportEvents={importEvents}
7274
/>
7375

7476
{/* Timeline */}

src/components/EventOptions.tsx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { useState } from "react";
12
import type { EventFilters, EventData } from "../types/events";
23
import { EVENT_COLORS } from "../utils/eventHelpers";
4+
import { ExportImportModal } from "./ExportImportModal";
35

46
interface EventOptionsProps {
57
filters: EventFilters;
@@ -9,6 +11,7 @@ interface EventOptionsProps {
911
events: EventData[];
1012
showDelays: boolean;
1113
onShowDelaysChange: (showDelays: boolean) => void;
14+
onImportEvents: (events: EventData[]) => void;
1215
}
1316

1417
export function EventOptions({
@@ -19,7 +22,11 @@ export function EventOptions({
1922
events,
2023
showDelays,
2124
onShowDelaysChange,
25+
onImportEvents,
2226
}: EventOptionsProps) {
27+
const [modalOpen, setModalOpen] = useState(false);
28+
const [modalMode, setModalMode] = useState<"export" | "import">("export");
29+
2330
// Calculate event counts by type
2431
const eventCounts = events.reduce((counts, event) => {
2532
counts[event.eventType] = (counts[event.eventType] || 0) + 1;
@@ -76,6 +83,16 @@ export function EventOptions({
7683
onFiltersChange(allDisabled);
7784
};
7885

86+
const handleExport = () => {
87+
setModalMode("export");
88+
setModalOpen(true);
89+
};
90+
91+
const handleImport = () => {
92+
setModalMode("import");
93+
setModalOpen(true);
94+
};
95+
7996
return (
8097
<div className="bg-sub-alt rounded-lg p-4 border border-sub/20 space-y-4">
8198
{/* Header with controls */}
@@ -127,6 +144,19 @@ export function EventOptions({
127144
</div>
128145
Show Delays
129146
</label>
147+
<button
148+
onClick={handleExport}
149+
className="text-xs px-2 py-1 bg-sub/20 hover:bg-sub/30 text-text rounded transition-colors duration-200"
150+
disabled={events.length === 0}
151+
>
152+
Export JSON
153+
</button>
154+
<button
155+
onClick={handleImport}
156+
className="text-xs px-2 py-1 bg-sub/20 hover:bg-sub/30 text-text rounded transition-colors duration-200"
157+
>
158+
Import JSON
159+
</button>
130160
</div>
131161
</div>
132162

@@ -210,6 +240,14 @@ export function EventOptions({
210240
})}
211241
</div>
212242
</div>
243+
244+
<ExportImportModal
245+
isOpen={modalOpen}
246+
onClose={() => setModalOpen(false)}
247+
events={events}
248+
onImportEvents={onImportEvents}
249+
mode={modalMode}
250+
/>
213251
</div>
214252
);
215253
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { useState } from "react";
2+
import type { EventData } from "../types/events";
3+
4+
interface ExportImportModalProps {
5+
isOpen: boolean;
6+
onClose: () => void;
7+
events: EventData[];
8+
onImportEvents: (events: EventData[]) => void;
9+
mode: "export" | "import";
10+
}
11+
12+
export function ExportImportModal({
13+
isOpen,
14+
onClose,
15+
events,
16+
onImportEvents,
17+
mode,
18+
}: ExportImportModalProps) {
19+
const [importText, setImportText] = useState("");
20+
const [copySuccess, setCopySuccess] = useState(false);
21+
22+
const exportJson = JSON.stringify(events, null, 2);
23+
24+
const handleCopy = async () => {
25+
try {
26+
await navigator.clipboard.writeText(exportJson);
27+
setCopySuccess(true);
28+
setTimeout(() => setCopySuccess(false), 2000);
29+
} catch (err) {
30+
console.error("Failed to copy text: ", err);
31+
}
32+
};
33+
34+
const handleImport = () => {
35+
try {
36+
const importedEvents = JSON.parse(importText);
37+
if (Array.isArray(importedEvents)) {
38+
onImportEvents(importedEvents);
39+
onClose();
40+
setImportText("");
41+
} else {
42+
alert("Invalid format: Expected an array of events");
43+
}
44+
} catch {
45+
alert("Error parsing JSON data");
46+
}
47+
};
48+
49+
if (!isOpen) return null;
50+
51+
return (
52+
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
53+
<div className="bg-bg border border-sub rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[80vh] flex flex-col">
54+
<div className="flex justify-between items-center mb-4">
55+
<h2 className="text-xl font-bold text-main">
56+
{mode === "export" ? "Export Events" : "Import Events"}
57+
</h2>
58+
<button
59+
onClick={onClose}
60+
className="text-sub hover:text-text transition-colors"
61+
>
62+
63+
</button>
64+
</div>
65+
66+
{mode === "export" ? (
67+
<div className="flex flex-col flex-1">
68+
<div className="flex justify-between items-center mb-2">
69+
<p className="text-text">
70+
Copy the JSON data below ({events.length} events):
71+
</p>
72+
<button
73+
onClick={handleCopy}
74+
className="px-3 py-1 bg-main text-bg rounded hover:bg-main/80 transition-colors"
75+
>
76+
{copySuccess ? "Copied!" : "Copy"}
77+
</button>
78+
</div>
79+
<textarea
80+
value={exportJson}
81+
readOnly
82+
className="flex-1 bg-bg border border-sub rounded p-3 text-text font-mono text-sm resize-none"
83+
style={{ minHeight: "300px" }}
84+
/>
85+
</div>
86+
) : (
87+
<div className="flex flex-col flex-1">
88+
<p className="text-text mb-2">Paste the JSON data below:</p>
89+
<textarea
90+
value={importText}
91+
onChange={(e) => setImportText(e.target.value)}
92+
placeholder="Paste exported JSON data here..."
93+
className="flex-1 bg-bg border border-sub rounded p-3 text-text font-mono text-sm resize-none"
94+
style={{ minHeight: "300px" }}
95+
/>
96+
<div className="flex justify-end gap-2 mt-4">
97+
<button
98+
onClick={onClose}
99+
className="px-4 py-2 bg-sub/20 hover:bg-sub/30 text-text rounded transition-colors"
100+
>
101+
Cancel
102+
</button>
103+
<button
104+
onClick={handleImport}
105+
disabled={!importText.trim()}
106+
className="px-4 py-2 bg-main text-bg rounded hover:bg-main/80 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
107+
>
108+
Import
109+
</button>
110+
</div>
111+
</div>
112+
)}
113+
</div>
114+
</div>
115+
);
116+
}

src/hooks/useKeyboardEvents.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,17 @@ export function useKeyboardEvents() {
194194
keydownTimestamps.current = {}; // Clear stored keydown timestamps
195195
}, []);
196196

197+
const importEvents = useCallback((importedEvents: EventData[]) => {
198+
setEvents(importedEvents);
199+
// Reset event ID to the highest ID in imported events + 1
200+
const maxId = importedEvents.reduce((max, event) => Math.max(max, event.eventId), 0);
201+
eventIdRef.current = maxId + 1;
202+
// Reset timestamps - they'll be relative to the imported events
203+
const firstEvent = importedEvents[0];
204+
startTimeRef.current = firstEvent ? firstEvent.timestamp - firstEvent.relativeTime : 0;
205+
keydownTimestamps.current = {};
206+
}, []);
207+
197208
const updateFilters = useCallback((newFilters: Partial<EventFilters>) => {
198209
setFilters((prev) => ({ ...prev, ...newFilters }));
199210
}, []);
@@ -208,6 +219,7 @@ export function useKeyboardEvents() {
208219
filters,
209220
attachEventListeners,
210221
clearEvents,
222+
importEvents,
211223
updateFilters,
212224
};
213225
}

0 commit comments

Comments
 (0)