Skip to content

Commit 5c9b2fa

Browse files
committed
feat: add modal for import/export functionality and enhance accessibility
1 parent bb47505 commit 5c9b2fa

File tree

2 files changed

+195
-149
lines changed

2 files changed

+195
-149
lines changed

src/components/EventOptions.tsx

Lines changed: 150 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -94,160 +94,164 @@ export function EventOptions({
9494
};
9595

9696
return (
97-
<div className="bg-sub-alt rounded-lg p-4 border border-sub/20 space-y-4">
98-
{/* Header with controls */}
99-
<div className="flex items-center justify-between">
100-
<h2 className="text-lg font-semibold text-text">Display Options</h2>
101-
<div className="flex items-center space-x-2">
102-
<span className="text-sm text-sub">
103-
Showing {eventCount} of {totalEvents} events
104-
</span>
105-
<button
106-
onClick={handleShowAll}
107-
className="text-xs px-2 py-1 bg-main/20 hover:bg-main/30 text-main rounded transition-colors duration-200"
108-
>
109-
Show All
110-
</button>
111-
<button
112-
onClick={handleHideAll}
113-
className="text-xs px-2 py-1 bg-error/20 hover:bg-error/30 text-error rounded transition-colors duration-200"
114-
>
115-
Hide All
116-
</button>
117-
<label className="flex items-center cursor-pointer text-xs text-text">
118-
<input
119-
type="checkbox"
120-
checked={showDelays}
121-
onChange={(e) => onShowDelaysChange(e.target.checked)}
122-
className="sr-only"
123-
/>
124-
<div
125-
className={`w-4 h-4 rounded border-2 flex items-center justify-center transition-all duration-200 mr-2 ${
126-
showDelays
127-
? "border-main bg-main shadow-sm"
128-
: "border-sub hover:border-main/70 bg-bg"
129-
}`}
97+
<>
98+
<ExportImportModal
99+
isOpen={modalOpen}
100+
onClose={() => setModalOpen(false)}
101+
events={events}
102+
onImportEvents={onImportEvents}
103+
mode={modalMode}
104+
/>
105+
<div className="bg-sub-alt rounded-lg p-4 border border-sub/20 space-y-4">
106+
{/* Header with controls */}
107+
<div className="flex items-center justify-between">
108+
<h2 className="text-lg font-semibold text-text">Display Options</h2>
109+
<div className="flex items-center space-x-2">
110+
<span className="text-sm text-sub">
111+
Showing {eventCount} of {totalEvents} events
112+
</span>
113+
<button
114+
onClick={handleShowAll}
115+
className="text-xs px-2 py-1 bg-main/20 hover:bg-main/30 text-main rounded transition-colors duration-200"
130116
>
131-
{showDelays && (
132-
<svg
133-
className="w-2.5 h-2.5 text-bg"
134-
fill="currentColor"
135-
viewBox="0 0 20 20"
136-
>
137-
<path
138-
fillRule="evenodd"
139-
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
140-
clipRule="evenodd"
141-
/>
142-
</svg>
143-
)}
144-
</div>
145-
Show Delays
146-
</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>
117+
Show All
118+
</button>
119+
<button
120+
onClick={handleHideAll}
121+
className="text-xs px-2 py-1 bg-error/20 hover:bg-error/30 text-error rounded transition-colors duration-200"
122+
>
123+
Hide All
124+
</button>
125+
<label className="flex items-center cursor-pointer text-xs text-text">
126+
<input
127+
type="checkbox"
128+
checked={showDelays}
129+
onChange={(e) => onShowDelaysChange(e.target.checked)}
130+
className="sr-only"
131+
/>
132+
<div
133+
className={`w-4 h-4 rounded border-2 flex items-center justify-center transition-all duration-200 mr-2 ${
134+
showDelays
135+
? "border-main bg-main shadow-sm"
136+
: "border-sub hover:border-main/70 bg-bg"
137+
}`}
138+
>
139+
{showDelays && (
140+
<svg
141+
className="w-2.5 h-2.5 text-bg"
142+
fill="currentColor"
143+
viewBox="0 0 20 20"
144+
>
145+
<path
146+
fillRule="evenodd"
147+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
148+
clipRule="evenodd"
149+
/>
150+
</svg>
151+
)}
152+
</div>
153+
Show Delays
154+
</label>
155+
<button
156+
onClick={handleExport}
157+
className="text-xs px-2 py-1 bg-sub/20 hover:bg-sub/30 text-text rounded transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-sub/20"
158+
disabled={events.length === 0}
159+
>
160+
Export JSON
161+
</button>
162+
<button
163+
onClick={handleImport}
164+
className="text-xs px-2 py-1 bg-sub/20 hover:bg-sub/30 text-text rounded transition-colors duration-200"
165+
>
166+
Import JSON
167+
</button>
168+
</div>
160169
</div>
161-
</div>
162170

163-
{/* Event type toggles */}
164-
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
165-
{eventTypes.map(({ key, label, description }) => (
166-
<label
167-
key={key}
168-
className="flex items-center cursor-pointer group p-2 rounded-lg hover:bg-sub-alt/30 transition-all duration-200"
169-
title={description}
170-
>
171-
<input
172-
type="checkbox"
173-
checked={filters[key]}
174-
onChange={() => handleToggle(key)}
175-
className="sr-only"
176-
/>
177-
{/* Custom checkbox */}
178-
<div
179-
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-all duration-200 ${
180-
filters[key]
181-
? "border-main bg-main shadow-sm"
182-
: "border-sub hover:border-main/70 bg-bg"
183-
}`}
171+
{/* Event type toggles */}
172+
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3">
173+
{eventTypes.map(({ key, label, description }) => (
174+
<label
175+
key={key}
176+
className="flex items-center cursor-pointer group p-2 rounded-lg hover:bg-sub-alt/30 transition-all duration-200"
177+
title={description}
184178
>
185-
{filters[key] && (
186-
<svg
187-
className="w-3 h-3 text-bg"
188-
fill="currentColor"
189-
viewBox="0 0 20 20"
179+
<input
180+
type="checkbox"
181+
checked={filters[key]}
182+
onChange={() => handleToggle(key)}
183+
className="sr-only"
184+
/>
185+
{/* Custom checkbox */}
186+
<div
187+
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-all duration-200 ${
188+
filters[key]
189+
? "border-main bg-main shadow-sm"
190+
: "border-sub hover:border-main/70 bg-bg"
191+
}`}
192+
>
193+
{filters[key] && (
194+
<svg
195+
className="w-3 h-3 text-bg"
196+
fill="currentColor"
197+
viewBox="0 0 20 20"
198+
>
199+
<path
200+
fillRule="evenodd"
201+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
202+
clipRule="evenodd"
203+
/>
204+
</svg>
205+
)}
206+
</div>
207+
<div
208+
className="w-3 h-3 rounded-full flex-shrink-0 ml-4 transition-opacity duration-200"
209+
style={{
210+
backgroundColor:
211+
EVENT_COLORS[key as keyof typeof EVENT_COLORS],
212+
opacity: filters[key] ? 1 : 0.4,
213+
}}
214+
/>
215+
<span
216+
className={`text-sm font-mono ml-1.5 transition-colors duration-200 ${
217+
filters[key]
218+
? "text-text group-hover:text-main"
219+
: "text-sub group-hover:text-text"
220+
}`}
221+
>
222+
{label}
223+
</span>
224+
</label>
225+
))}
226+
</div>
227+
228+
{/* Event statistics */}
229+
<div className="pt-3 border-t border-sub/20">
230+
<div className="flex flex-wrap gap-4 text-xs text-sub">
231+
{eventTypes.map(({ key, label }) => {
232+
const count = eventCounts[key] || 0;
233+
if (!filters[key] || count === 0) return null;
234+
return (
235+
<span
236+
key={key}
237+
className="font-mono flex items-center space-x-1"
190238
>
191-
<path
192-
fillRule="evenodd"
193-
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
194-
clipRule="evenodd"
239+
<div
240+
className="w-1.5 h-1.5 rounded-full flex-shrink-0"
241+
style={{
242+
backgroundColor:
243+
EVENT_COLORS[key as keyof typeof EVENT_COLORS],
244+
}}
195245
/>
196-
</svg>
197-
)}
198-
</div>
199-
<div
200-
className="w-3 h-3 rounded-full flex-shrink-0 ml-4 transition-opacity duration-200"
201-
style={{
202-
backgroundColor:
203-
EVENT_COLORS[key as keyof typeof EVENT_COLORS],
204-
opacity: filters[key] ? 1 : 0.4,
205-
}}
206-
/>
207-
<span
208-
className={`text-sm font-mono ml-1.5 transition-colors duration-200 ${
209-
filters[key]
210-
? "text-text group-hover:text-main"
211-
: "text-sub group-hover:text-text"
212-
}`}
213-
>
214-
{label}
215-
</span>
216-
</label>
217-
))}
218-
</div>
219-
220-
{/* Event statistics */}
221-
<div className="pt-3 border-t border-sub/20">
222-
<div className="flex flex-wrap gap-4 text-xs text-sub">
223-
{eventTypes.map(({ key, label }) => {
224-
const count = eventCounts[key] || 0;
225-
if (!filters[key] || count === 0) return null;
226-
return (
227-
<span key={key} className="font-mono flex items-center space-x-1">
228-
<div
229-
className="w-1.5 h-1.5 rounded-full flex-shrink-0"
230-
style={{
231-
backgroundColor:
232-
EVENT_COLORS[key as keyof typeof EVENT_COLORS],
233-
}}
234-
/>
235-
<span>
236-
{label}: {count}
246+
<span>
247+
{label}: {count}
248+
</span>
237249
</span>
238-
</span>
239-
);
240-
})}
250+
);
251+
})}
252+
</div>
241253
</div>
242254
</div>
243-
244-
<ExportImportModal
245-
isOpen={modalOpen}
246-
onClose={() => setModalOpen(false)}
247-
events={events}
248-
onImportEvents={onImportEvents}
249-
mode={modalMode}
250-
/>
251-
</div>
255+
</>
252256
);
253257
}

src/components/ExportImportModal.tsx

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useState } from "react";
1+
import { useState, useEffect } from "react";
22
import type { EventData } from "../types/events";
33

44
interface ExportImportModalProps {
@@ -21,6 +21,42 @@ export function ExportImportModal({
2121

2222
const exportJson = JSON.stringify(events, null, 2);
2323

24+
// Handle ESC key to close modal
25+
useEffect(() => {
26+
const handleEscKey = (event: KeyboardEvent) => {
27+
if (event.key === 'Escape' && isOpen) {
28+
event.preventDefault();
29+
event.stopPropagation();
30+
onClose();
31+
}
32+
};
33+
34+
if (isOpen) {
35+
document.addEventListener('keydown', handleEscKey);
36+
return () => {
37+
document.removeEventListener('keydown', handleEscKey);
38+
};
39+
}
40+
}, [isOpen, onClose]);
41+
42+
// Prevent body scroll when modal is open and preserve scrollbar space
43+
useEffect(() => {
44+
if (isOpen) {
45+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
46+
const body = document.body;
47+
const originalOverflow = body.style.overflow;
48+
const originalPaddingRight = body.style.paddingRight;
49+
50+
body.style.overflow = 'hidden';
51+
body.style.paddingRight = `${(parseInt(originalPaddingRight) || 0) + scrollbarWidth}px`;
52+
53+
return () => {
54+
body.style.overflow = originalOverflow;
55+
body.style.paddingRight = originalPaddingRight;
56+
};
57+
}
58+
}, [isOpen]);
59+
2460
const handleCopy = async () => {
2561
try {
2662
await navigator.clipboard.writeText(exportJson);
@@ -49,8 +85,14 @@ export function ExportImportModal({
4985
if (!isOpen) return null;
5086

5187
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">
88+
<div
89+
className="fixed inset-0 bg-black/50 flex items-center justify-center z-50"
90+
onClick={onClose}
91+
>
92+
<div
93+
className="bg-bg border border-sub rounded-lg p-6 max-w-2xl w-full mx-4 max-h-[80vh] flex flex-col"
94+
onClick={(e) => e.stopPropagation()}
95+
>
5496
<div className="flex justify-between items-center mb-4">
5597
<h2 className="text-xl font-bold text-main">
5698
{mode === "export" ? "Export Events" : "Import Events"}

0 commit comments

Comments
 (0)