Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 110 additions & 36 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* @license
* SPDX-License-Identifier: Apache-2.0
*/
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useMemo, useRef } from 'react';
import { Play, Pause, SkipForward, SkipBack, Menu, Search, BookOpen, Disc, Bookmark, Star, Headphones, Rocket, ArrowLeft, Grid, Download, AlertCircle, LoaderCircle } from 'lucide-react';

// --- Types & Mock Data ---
Expand Down Expand Up @@ -85,6 +85,9 @@ const MOCK_BOOKS: Book[] = [
export default function App() {
const [currentView, setCurrentView] = useState<ViewState>('library');
const [selectedBook, setSelectedBook] = useState<Book | null>(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState('');

// Audio State
const audioRef = useRef<HTMLAudioElement>(null);
Expand All @@ -98,6 +101,16 @@ export default function App() {
const [isStandalone, setIsStandalone] = useState(false);
const [isIos, setIsIos] = useState(false);

const filteredBooks = useMemo(() => {
const normalizedQuery = searchQuery.trim().toLowerCase();
if (!normalizedQuery) return MOCK_BOOKS;
return MOCK_BOOKS.filter((book) => {
const bookFields = [book.title, book.author, book.year, book.tag, book.description].join(' ').toLowerCase();
const trackFields = book.tracks.map((track) => track.title).join(' ').toLowerCase();
return bookFields.includes(normalizedQuery) || trackFields.includes(normalizedQuery);
});
}, [searchQuery]);

// Fetch real data from IA (Optional enhancement, using mock for stability as requested)
useEffect(() => {
// In a full implementation, we'd fetch from https://archive.org/metadata/IsaacAsimovAudioBookCollection
Expand Down Expand Up @@ -169,6 +182,16 @@ export default function App() {
};
}, []);

useEffect(() => {
const handleEscape = (event: KeyboardEvent) => {
if (event.key !== 'Escape') return;
setIsMenuOpen(false);
setIsSearchOpen(false);

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Escape handler closes the search UI (setIsSearchOpen(false)) but leaves searchQuery intact, so the library/collection stay filtered while the query is hidden. Consider clearing searchQuery when closing search (Escape and/or toggle-off), or keeping the search bar visible/indicating an active filter when a query is present to avoid confusing “missing items” states.

Suggested change
setIsSearchOpen(false);
setIsSearchOpen(false);
setSearchQuery('');

Copilot uses AI. Check for mistakes.
};
window.addEventListener('keydown', handleEscape);
return () => window.removeEventListener('keydown', handleEscape);
}, []);

const handlePlayPause = async () => {
if (!audioRef.current || !currentTrack) return;
if (isPlaying) {
Expand Down Expand Up @@ -231,6 +254,11 @@ export default function App() {
setDeferredInstallPrompt(null);
};

const navigateTo = (view: ViewState) => {
setCurrentView(view);
setIsMenuOpen(false);
};

const formatTime = (time: number) => {
if (isNaN(time)) return '00:00';
const m = Math.floor(time / 60);
Expand All @@ -251,7 +279,7 @@ export default function App() {
<ArrowLeft size={24} />
</button>
) : (
<button className="text-primary hover:text-white transition-colors">
<button onClick={() => setIsMenuOpen((open) => !open)} className="text-primary hover:text-white transition-colors" aria-label="Open navigation menu">

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The menu icon button is a toggle, but the aria-label is always “Open navigation menu” and it doesn’t expose expanded state. Update the label to reflect open/closed state and add aria-expanded (and ideally aria-controls pointing at the menu) for better accessibility.

Suggested change
<button onClick={() => setIsMenuOpen((open) => !open)} className="text-primary hover:text-white transition-colors" aria-label="Open navigation menu">
<button
onClick={() => setIsMenuOpen((open) => !open)}
className="text-primary hover:text-white transition-colors"
aria-label={isMenuOpen ? 'Close navigation menu' : 'Open navigation menu'}
aria-expanded={isMenuOpen}
>

Copilot uses AI. Check for mistakes.
<Menu size={24} />
</button>
)}
Expand All @@ -261,7 +289,7 @@ export default function App() {
<span className="font-headline tracking-[0.1em] uppercase text-sm text-primary hidden md:block">
{currentView === 'library' ? 'Archive Sector 7G' : currentView === 'collection' ? 'Vault Status: Encrypted' : ''}
</span>
<button className="text-primary hover:text-white transition-colors">
<button onClick={() => setIsSearchOpen((open) => !open)} className="text-primary hover:text-white transition-colors" aria-label="Toggle search">
<Search size={24} />
</button>
{!isStandalone && deferredInstallPrompt && (
Expand All @@ -276,10 +304,44 @@ export default function App() {
</div>
</header>

{isSearchOpen && (
<div className="fixed top-16 left-0 right-0 z-40 border-b border-outline-variant/20 bg-slate-950/95 px-6 py-4 backdrop-blur-xl">
<div className="mx-auto max-w-5xl">
<input
value={searchQuery}
onChange={(event) => setSearchQuery(event.target.value)}

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The search input relies on placeholder text only; it doesn’t have an accessible label. Add an explicit label (visible or screen-reader-only) or an aria-label/aria-labelledby so screen readers can announce what the field is for.

Suggested change
onChange={(event) => setSearchQuery(event.target.value)}
onChange={(event) => setSearchQuery(event.target.value)}
aria-label="Search titles, authors, tags, and descriptions"

Copilot uses AI. Check for mistakes.
placeholder="Search titles, authors, tags, descriptions..."
className="w-full rounded-md border border-primary/30 bg-slate-900 px-4 py-3 text-sm text-on-surface-variant outline-none transition-colors focus:border-primary"
/>
</div>
</div>
)}

{isMenuOpen && currentView !== 'details' && (
<>
<button
onClick={() => setIsMenuOpen(false)}
className="fixed inset-0 z-40 cursor-pointer bg-black/40"
aria-label="Close navigation menu"
/>
<div className="fixed left-4 top-20 z-50 w-60 rounded-xl border border-outline-variant/20 bg-slate-950/95 p-3 shadow-2xl backdrop-blur-xl">
Comment on lines +324 to +327

Copilot AI Apr 12, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The backdrop for the open menu uses z-40 while the header/bottom nav use z-50, so users can still interact with those controls while the menu is open (e.g., navigate via bottom nav and leave the menu open). Consider increasing the backdrop z-index above the header/nav (and raising the menu panel above the backdrop), or otherwise ensuring navigation actions also close the menu.

Suggested change
className="fixed inset-0 z-40 cursor-pointer bg-black/40"
aria-label="Close navigation menu"
/>
<div className="fixed left-4 top-20 z-50 w-60 rounded-xl border border-outline-variant/20 bg-slate-950/95 p-3 shadow-2xl backdrop-blur-xl">
className="fixed inset-0 z-60 cursor-pointer bg-black/40"
aria-label="Close navigation menu"
/>
<div className="fixed left-4 top-20 z-70 w-60 rounded-xl border border-outline-variant/20 bg-slate-950/95 p-3 shadow-2xl backdrop-blur-xl">

Copilot uses AI. Check for mistakes.
<button onClick={() => navigateTo('library')} className="mb-1 flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-on-surface-variant transition-colors hover:bg-slate-800/70 hover:text-white">
<BookOpen size={16} /> Library
</button>
<button onClick={() => navigateTo('player')} className="mb-1 flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-on-surface-variant transition-colors hover:bg-slate-800/70 hover:text-white">
<Disc size={16} /> Player
</button>
<button onClick={() => navigateTo('collection')} className="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm text-on-surface-variant transition-colors hover:bg-slate-800/70 hover:text-white">
<Bookmark size={16} /> Collection
</button>
</div>
</>
)}

{/* Main Content Area */}
<main className="flex-1 pt-24 pb-32 w-full relative overflow-hidden">
<main className={`flex-1 ${isSearchOpen ? 'pt-40' : 'pt-24'} pb-32 w-full relative overflow-hidden`}>
{currentView === 'library' && (
<LibraryView books={MOCK_BOOKS} onSelectBook={(b) => { setSelectedBook(b); setCurrentView('details'); }} />
<LibraryView books={filteredBooks} onSelectBook={(b) => { setSelectedBook(b); setCurrentView('details'); setIsMenuOpen(false); }} />
)}
{currentView === 'details' && selectedBook && (
<DetailsView book={selectedBook} onPlayTrack={(t) => playTrack(selectedBook, t)} />
Expand All @@ -299,7 +361,7 @@ export default function App() {
/>
)}
{currentView === 'collection' && (
<CollectionView books={MOCK_BOOKS} onSelectBook={(b) => { setSelectedBook(b); setCurrentView('details'); }} />
<CollectionView books={filteredBooks} onSelectBook={(b) => { setSelectedBook(b); setCurrentView('details'); setIsMenuOpen(false); }} />
)}
</main>

Expand Down Expand Up @@ -352,24 +414,30 @@ function LibraryView({ books, onSelectBook }: { books: Book[], onSelectBook: (b:
</section>

<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-x-8 gap-y-12">
{books.map((book, idx) => (
<div key={book.id} onClick={() => onSelectBook(book)} className={`group cursor-pointer ${idx % 2 !== 0 ? 'md:translate-y-16' : ''}`}>
<div className="relative mb-6 aspect-[2/3] bg-surface-container-low overflow-hidden rounded-md transition-transform duration-500 group-hover:-translate-y-2">
<img src={book.cover} alt={book.title} className="w-full h-full object-cover opacity-80 group-hover:opacity-100 transition-opacity" />
<div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-transparent opacity-60"></div>
<div className="absolute bottom-4 right-4 bg-primary-container text-on-primary-container p-3 rounded-full flex items-center justify-center shadow-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<Play size={20} fill="currentColor" />
{books.length === 0 ? (
<div className="col-span-full rounded-md border border-outline-variant/20 bg-surface-container-low px-6 py-8 text-center text-on-surface-variant">
No archive entries matched your search.
</div>
) : (
books.map((book, idx) => (
<div key={book.id} onClick={() => onSelectBook(book)} className={`group cursor-pointer ${idx % 2 !== 0 ? 'md:translate-y-16' : ''}`}>
<div className="relative mb-6 aspect-[2/3] bg-surface-container-low overflow-hidden rounded-md transition-transform duration-500 group-hover:-translate-y-2">
<img src={book.cover} alt={book.title} className="w-full h-full object-cover opacity-80 group-hover:opacity-100 transition-opacity" />
<div className="absolute inset-0 bg-gradient-to-t from-background via-transparent to-transparent opacity-60"></div>
<div className="absolute bottom-4 right-4 bg-primary-container text-on-primary-container p-3 rounded-full flex items-center justify-center shadow-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<Play size={20} fill="currentColor" />
</div>
</div>
</div>
<div className="flex justify-between items-start">
<div>
<h3 className="font-headline text-xl font-bold tracking-tight text-primary leading-tight group-hover:text-white transition-colors">{book.title}</h3>
<p className="font-body text-on-surface-variant text-sm mt-1">{book.year} • {book.author}</p>
<div className="flex justify-between items-start">
<div>
<h3 className="font-headline text-xl font-bold tracking-tight text-primary leading-tight group-hover:text-white transition-colors">{book.title}</h3>
<p className="font-body text-on-surface-variant text-sm mt-1">{book.year} • {book.author}</p>
</div>
<span className="font-label text-[10px] text-secondary border border-secondary/30 px-2 py-0.5 rounded tracking-tighter">{book.tag}</span>
</div>
<span className="font-label text-[10px] text-secondary border border-secondary/30 px-2 py-0.5 rounded tracking-tighter">{book.tag}</span>
</div>
</div>
))}
))
)}
</div>
</div>
);
Expand Down Expand Up @@ -562,23 +630,29 @@ function CollectionView({ books, onSelectBook }: { books: Book[], onSelectBook:
</button>
</div>
<div className="grid grid-cols-2 md:grid-cols-3 gap-y-10 gap-x-6">
{books.map(book => (
<div key={book.id} onClick={() => onSelectBook(book)} className="flex flex-col gap-3 cursor-pointer">
<div className="aspect-[2/3] bg-surface-container rounded shadow-2xl relative group">
<img src={book.cover} alt={book.title} className="w-full h-full object-cover rounded" />
<div className="absolute top-2 right-2"><Star size={16} className="text-secondary" fill="currentColor" /></div>
<div className="absolute inset-0 bg-primary/10 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<button className="p-3 bg-white/10 backdrop-blur-md rounded-full text-white border border-white/20">
<Headphones size={24} />
</button>
{books.length === 0 ? (
<div className="col-span-full rounded-md border border-outline-variant/20 bg-surface-container-low px-6 py-8 text-center text-on-surface-variant">
No collection items matched your search.
</div>
) : (
books.map(book => (
<div key={book.id} onClick={() => onSelectBook(book)} className="flex flex-col gap-3 cursor-pointer">
<div className="aspect-[2/3] bg-surface-container rounded shadow-2xl relative group">
<img src={book.cover} alt={book.title} className="w-full h-full object-cover rounded" />
<div className="absolute top-2 right-2"><Star size={16} className="text-secondary" fill="currentColor" /></div>
<div className="absolute inset-0 bg-primary/10 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
<button className="p-3 bg-white/10 backdrop-blur-md rounded-full text-white border border-white/20">
<Headphones size={24} />
</button>
</div>
</div>
<div>
<h5 className="font-headline text-sm font-bold text-on-surface uppercase tracking-tight">{book.title}</h5>
<p className="font-label text-[10px] text-outline-variant uppercase tracking-widest">{book.author}</p>
</div>
</div>
<div>
<h5 className="font-headline text-sm font-bold text-on-surface uppercase tracking-tight">{book.title}</h5>
<p className="font-label text-[10px] text-outline-variant uppercase tracking-widest">{book.author}</p>
</div>
</div>
))}
))
)}
</div>
</section>
</div>
Expand Down