+23
-19
src/App.tsx
+23
-19
src/App.tsx
···
9
import Firefly from "./components/Firefly";
10
import NotificationContainer from "./components/common/NotificationContainer";
11
import ErrorBoundary from "./components/common/ErrorBoundary";
12
import { SearchResultSkeleton } from "./components/common/LoadingSkeleton";
13
import { DEFAULT_SETTINGS } from "./types/settings";
14
import type { UserSettings, SearchResult } from "./types";
···
48
logout,
49
} = useAuth();
50
51
-
// Notifications hook (replaces alerts)
52
-
const { notifications, removeNotification, success, error, info } =
53
-
useNotifications();
54
55
// Theme hook
56
const { isDark, reducedMotion, toggleTheme, toggleMotion } = useTheme();
···
169
setSearchResults([]);
170
setCurrentPlatform("tiktok");
171
setCurrentStep("home");
172
-
info("No previous results found.");
173
return;
174
}
175
···
194
195
setSearchResults(loadedResults);
196
setCurrentStep("results");
197
-
success(`Loaded ${loadedResults.length} results from previous upload`);
198
} catch (err) {
199
console.error("Failed to load upload:", err);
200
error("Failed to load previous upload. Please try again.");
201
setCurrentStep("home");
202
}
203
},
204
-
[setStatusMessage, setCurrentStep, setSearchResults, info, error, success],
205
);
206
207
// Login handler
···
231
setSearchResults([]);
232
setCurrentPlatform("tiktok");
233
setSavedUploads(new Set());
234
-
success("Logged out successfully");
235
} catch (err) {
236
error("Failed to logout. Please try again.");
237
}
238
-
}, [logout, setSearchResults, success, error]);
239
240
return (
241
<ErrorBoundary>
242
<div className="min-h-screen relative overflow-hidden">
243
-
{/* Notification Container */}
244
<NotificationContainer
245
notifications={notifications}
246
onRemove={removeNotification}
247
/>
248
249
{/* Firefly particles - only render if motion not reduced */}
250
{!reducedMotion && (
···
258
))}
259
</div>
260
)}
261
-
262
-
{/* Status message for screen readers */}
263
-
<div
264
-
role="status"
265
-
aria-live="polite"
266
-
aria-atomic="true"
267
-
className="sr-only"
268
-
>
269
-
{statusMessage}
270
-
</div>
271
272
{/* Skip to main content link */}
273
<a
···
9
import Firefly from "./components/Firefly";
10
import NotificationContainer from "./components/common/NotificationContainer";
11
import ErrorBoundary from "./components/common/ErrorBoundary";
12
+
import AriaLiveAnnouncer from "./components/common/AriaLiveAnnouncer";
13
import { SearchResultSkeleton } from "./components/common/LoadingSkeleton";
14
import { DEFAULT_SETTINGS } from "./types/settings";
15
import type { UserSettings, SearchResult } from "./types";
···
49
logout,
50
} = useAuth();
51
52
+
// Notifications hook (only for errors now)
53
+
const { notifications, removeNotification, error } = useNotifications();
54
+
55
+
// Aria-live announcements for non-error feedback (invisible, screen-reader only)
56
+
const [ariaAnnouncement, setAriaAnnouncement] = useState("");
57
58
// Theme hook
59
const { isDark, reducedMotion, toggleTheme, toggleMotion } = useTheme();
···
172
setSearchResults([]);
173
setCurrentPlatform("tiktok");
174
setCurrentStep("home");
175
+
// No visual feedback needed - empty state will show in UI
176
+
setAriaAnnouncement("No previous results found.");
177
return;
178
}
179
···
198
199
setSearchResults(loadedResults);
200
setCurrentStep("results");
201
+
// Announce to screen readers only - visual feedback is navigation to results page
202
+
setAriaAnnouncement(
203
+
`Loaded ${loadedResults.length} results from previous upload`,
204
+
);
205
} catch (err) {
206
console.error("Failed to load upload:", err);
207
error("Failed to load previous upload. Please try again.");
208
setCurrentStep("home");
209
}
210
},
211
+
[setStatusMessage, setCurrentStep, setSearchResults, setAriaAnnouncement, error],
212
);
213
214
// Login handler
···
238
setSearchResults([]);
239
setCurrentPlatform("tiktok");
240
setSavedUploads(new Set());
241
+
// No visual feedback needed - user sees login page
242
+
setAriaAnnouncement("Logged out successfully");
243
} catch (err) {
244
error("Failed to logout. Please try again.");
245
}
246
+
}, [logout, setSearchResults, setAriaAnnouncement, error]);
247
248
return (
249
<ErrorBoundary>
250
<div className="min-h-screen relative overflow-hidden">
251
+
{/* Notification Container - errors only */}
252
<NotificationContainer
253
notifications={notifications}
254
onRemove={removeNotification}
255
/>
256
+
257
+
{/* Invisible announcer for screen readers - non-error feedback */}
258
+
<AriaLiveAnnouncer message={ariaAnnouncement} politeness="polite" />
259
+
260
+
{/* Status message for screen readers - loading/progress updates */}
261
+
<AriaLiveAnnouncer message={statusMessage} politeness="polite" />
262
263
{/* Firefly particles - only render if motion not reduced */}
264
{!reducedMotion && (
···
272
))}
273
</div>
274
)}
275
276
{/* Skip to main content link */}
277
<a
+45
src/components/common/AriaLiveAnnouncer.tsx
+45
src/components/common/AriaLiveAnnouncer.tsx
···
···
1
+
import React, { useEffect, useState } from "react";
2
+
3
+
interface AriaLiveAnnouncerProps {
4
+
message: string;
5
+
politeness?: "polite" | "assertive";
6
+
clearAfter?: number;
7
+
}
8
+
9
+
/**
10
+
* Invisible component that announces messages to screen readers
11
+
* without displaying visual toasts
12
+
*/
13
+
const AriaLiveAnnouncer: React.FC<AriaLiveAnnouncerProps> = ({
14
+
message,
15
+
politeness = "polite",
16
+
clearAfter = 5000,
17
+
}) => {
18
+
const [announcement, setAnnouncement] = useState("");
19
+
20
+
useEffect(() => {
21
+
if (message) {
22
+
// Set the message to trigger screen reader announcement
23
+
setAnnouncement(message);
24
+
25
+
// Clear after specified time to allow new announcements
26
+
if (clearAfter > 0) {
27
+
const timer = setTimeout(() => setAnnouncement(""), clearAfter);
28
+
return () => clearTimeout(timer);
29
+
}
30
+
}
31
+
}, [message, clearAfter]);
32
+
33
+
return (
34
+
<div
35
+
role="status"
36
+
aria-live={politeness}
37
+
aria-atomic="true"
38
+
className="sr-only"
39
+
>
40
+
{announcement}
41
+
</div>
42
+
);
43
+
};
44
+
45
+
export default AriaLiveAnnouncer;