diff --git a/public/RMAB_1024x1024_ICON.png b/public/RMAB_1024x1024_ICON.png new file mode 100644 index 0000000..73c5dd7 Binary files /dev/null and b/public/RMAB_1024x1024_ICON.png differ diff --git a/public/manifest.json b/public/manifest.json index f9dd91c..efd2c66 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -8,7 +8,7 @@ "theme_color": "#1e3a5f", "icons": [ { - "src": "/RMAB_1024x1024.png", + "src": "/RMAB_1024x1024_ICON.png", "sizes": "1024x1024", "type": "image/png", "purpose": "any maskable" diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 468ac29..cdfcfe4 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -30,7 +30,7 @@ export const metadata: Metadata = { ], shortcut: "/rmab_icon.ico", apple: [ - { url: "/RMAB_1024x1024.png", sizes: "1024x1024", type: "image/png" }, + { url: "/RMAB_1024x1024_ICON.png", sizes: "1024x1024", type: "image/png" }, ], }, appleWebApp: { diff --git a/src/app/page.tsx b/src/app/page.tsx index d1abd9f..b760dc1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -12,12 +12,13 @@ import { useAudiobooks } from '@/lib/hooks/useAudiobooks'; import { ProtectedRoute } from '@/components/auth/ProtectedRoute'; import { StickyPagination } from '@/components/ui/StickyPagination'; import { CardSizeControls } from '@/components/ui/CardSizeControls'; +import { SquareCoversToggle } from '@/components/ui/SquareCoversToggle'; import { usePreferences } from '@/contexts/PreferencesContext'; export default function HomePage() { const [popularPage, setPopularPage] = useState(1); const [newReleasesPage, setNewReleasesPage] = useState(1); - const { cardSize, setCardSize } = usePreferences(); + const { cardSize, setCardSize, squareCovers, setSquareCovers } = usePreferences(); // Refs for auto-scrolling to section tops const popularSectionRef = useRef(null); @@ -62,10 +63,11 @@ export default function HomePage() {
-

+

Popular Audiobooks

-
+
+
@@ -89,6 +91,7 @@ export default function HomePage() { isLoading={loadingPopular} emptyMessage="No popular audiobooks available" cardSize={cardSize} + squareCovers={squareCovers} /> )}
@@ -101,10 +104,11 @@ export default function HomePage() {
-

+

New Releases

-
+
+
@@ -128,6 +132,7 @@ export default function HomePage() { isLoading={loadingNewReleases} emptyMessage="No new releases available" cardSize={cardSize} + squareCovers={squareCovers} /> )}
diff --git a/src/app/search/page.tsx b/src/app/search/page.tsx index 8852f5c..d904291 100644 --- a/src/app/search/page.tsx +++ b/src/app/search/page.tsx @@ -11,13 +11,14 @@ import { AudiobookGrid } from '@/components/audiobooks/AudiobookGrid'; import { useSearch } from '@/lib/hooks/useAudiobooks'; import { ProtectedRoute } from '@/components/auth/ProtectedRoute'; import { CardSizeControls } from '@/components/ui/CardSizeControls'; +import { SquareCoversToggle } from '@/components/ui/SquareCoversToggle'; import { usePreferences } from '@/contexts/PreferencesContext'; export default function SearchPage() { const [query, setQuery] = useState(''); const [debouncedQuery, setDebouncedQuery] = useState(''); const [page, setPage] = useState(1); - const { cardSize, setCardSize } = usePreferences(); + const { cardSize, setCardSize, squareCovers, setSquareCovers } = usePreferences(); // Debounce search query useEffect(() => { @@ -109,15 +110,16 @@ export default function SearchPage() {
-

+

Search Results

{!isLoading && totalResults > 0 && ( - + ({totalResults.toLocaleString()} result{totalResults !== 1 ? 's' : ''}) )} -
+
+
@@ -130,6 +132,7 @@ export default function SearchPage() { isLoading={!!(isLoading && page === 1)} emptyMessage={`No results found for "${debouncedQuery}"`} cardSize={cardSize} + squareCovers={squareCovers} /> {/* Load More */} diff --git a/src/components/audiobooks/AudiobookCard.tsx b/src/components/audiobooks/AudiobookCard.tsx index 8d38a16..5d8675e 100644 --- a/src/components/audiobooks/AudiobookCard.tsx +++ b/src/components/audiobooks/AudiobookCard.tsx @@ -19,6 +19,7 @@ interface AudiobookCardProps { isRequested?: boolean; requestStatus?: string; onRequestSuccess?: () => void; + squareCovers?: boolean; } export function AudiobookCard({ @@ -26,6 +27,7 @@ export function AudiobookCard({ isRequested = false, requestStatus, onRequestSuccess, + squareCovers = false, }: AudiobookCardProps) { const { user } = useAuth(); const { createRequest, isLoading } = useCreateRequest(); @@ -59,10 +61,12 @@ export function AudiobookCard({ return ( <> -
+
{/* Cover Art - Clickable */}
setShowModal(true)} > {audiobook.coverArtUrl ? ( diff --git a/src/components/audiobooks/AudiobookGrid.tsx b/src/components/audiobooks/AudiobookGrid.tsx index 2599ebb..4c671b3 100644 --- a/src/components/audiobooks/AudiobookGrid.tsx +++ b/src/components/audiobooks/AudiobookGrid.tsx @@ -15,6 +15,7 @@ interface AudiobookGridProps { emptyMessage?: string; onRequestSuccess?: () => void; cardSize?: number; // 1-9, default 5 + squareCovers?: boolean; // true = square (1:1), false = rectangle (2:3) } // Helper function to get grid classes based on card size @@ -41,6 +42,7 @@ export function AudiobookGrid({ emptyMessage = 'No audiobooks found', onRequestSuccess, cardSize = 5, + squareCovers = false, }: AudiobookGridProps) { const gridClasses = getGridClasses(cardSize); @@ -48,7 +50,7 @@ export function AudiobookGrid({ return (
{Array.from({ length: 8 }).map((_, i) => ( - + ))}
); @@ -76,23 +78,26 @@ export function AudiobookGrid({ } return ( -
+
{audiobooks.map((audiobook) => ( ))}
); } -function SkeletonCard() { +function SkeletonCard({ squareCovers = false }: { squareCovers?: boolean }) { return ( -
+
{/* Cover Art Skeleton */} -
+
{/* Content Skeleton */}
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 1210e9f..b3f172b 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -118,18 +118,21 @@ export function Header() {
{/* Logo and Version Badge */} -
- +
+ ReadMeABook Logo - + ReadMeABook - + {/* Hide version badge on mobile to prevent overlap */} +
+ +
{/* Desktop Navigation */} @@ -203,7 +206,7 @@ export function Header() { {user ? ( -
+
diff --git a/src/components/ui/SquareCoversToggle.tsx b/src/components/ui/SquareCoversToggle.tsx new file mode 100644 index 0000000..05244b8 --- /dev/null +++ b/src/components/ui/SquareCoversToggle.tsx @@ -0,0 +1,60 @@ +/** + * Component: Square Covers Toggle + * Documentation: UI toggle for switching between square (1:1) and rectangle (2:3) cover aspect ratios + */ + +'use client'; + +import React from 'react'; + +interface SquareCoversToggleProps { + enabled: boolean; + onToggle: (enabled: boolean) => void; +} + +export function SquareCoversToggle({ enabled, onToggle }: SquareCoversToggleProps) { + return ( + + ); +} diff --git a/src/contexts/PreferencesContext.tsx b/src/contexts/PreferencesContext.tsx index 540068c..e7325bd 100644 --- a/src/contexts/PreferencesContext.tsx +++ b/src/contexts/PreferencesContext.tsx @@ -9,23 +9,28 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from interface Preferences { cardSize: number; // 1-9, default 5 + squareCovers: boolean; // true = square (1:1), false = rectangle (2:3) } interface PreferencesContextType { cardSize: number; setCardSize: (size: number) => void; + squareCovers: boolean; + setSquareCovers: (enabled: boolean) => void; } const PreferencesContext = createContext(undefined); const DEFAULT_PREFERENCES: Preferences = { cardSize: 5, + squareCovers: false, }; const STORAGE_KEY = 'preferences'; export function PreferencesProvider({ children }: { children: ReactNode }) { const [cardSize, setCardSizeState] = useState(DEFAULT_PREFERENCES.cardSize); + const [squareCovers, setSquareCoversState] = useState(DEFAULT_PREFERENCES.squareCovers); // Load preferences from localStorage on mount useEffect(() => { @@ -42,10 +47,13 @@ export function PreferencesProvider({ children }: { children: ReactNode }) { // Invalid size, reset to default setCardSizeState(DEFAULT_PREFERENCES.cardSize); } + // Load squareCovers preference (defaults to false if not set) + setSquareCoversState(preferences.squareCovers ?? DEFAULT_PREFERENCES.squareCovers); } } catch (error) { console.error('Failed to load preferences from localStorage:', error); setCardSizeState(DEFAULT_PREFERENCES.cardSize); + setSquareCoversState(DEFAULT_PREFERENCES.squareCovers); } }, []); @@ -68,6 +76,22 @@ export function PreferencesProvider({ children }: { children: ReactNode }) { } }; + // Update square covers preference in state and localStorage + const setSquareCovers = (enabled: boolean) => { + if (typeof window === 'undefined') return; + + setSquareCoversState(enabled); + + try { + const stored = localStorage.getItem(STORAGE_KEY); + const preferences: Preferences = stored ? JSON.parse(stored) : { ...DEFAULT_PREFERENCES }; + preferences.squareCovers = enabled; + localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences)); + } catch (error) { + console.error('Failed to save preferences to localStorage:', error); + } + }; + // Listen for storage changes in other tabs (cross-tab sync) useEffect(() => { if (typeof window === 'undefined') return; @@ -80,6 +104,8 @@ export function PreferencesProvider({ children }: { children: ReactNode }) { if (preferences.cardSize >= 1 && preferences.cardSize <= 9) { setCardSizeState(preferences.cardSize); } + // Sync squareCovers preference + setSquareCoversState(preferences.squareCovers ?? DEFAULT_PREFERENCES.squareCovers); } catch (error) { console.error('Failed to parse preferences from storage event:', error); } @@ -93,7 +119,7 @@ export function PreferencesProvider({ children }: { children: ReactNode }) { }, []); return ( - + {children} );