mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-02 20:30:10 +00:00
Add square cover toggle and UI support
Introduce a SquareCoversToggle component and wire cover-aspect switching throughout the app. PreferencesContext now stores and persists a new squareCovers flag (with cross-tab sync), and pages (Home, Search) expose the toggle and pass the squareCovers prop to AudiobookGrid/AudiobookCard. AudiobookCard/Grid and skeletons were updated to respect square vs 2:3 aspect ratios and include smoother transitions. Also update app icons/manifest references to RMAB_1024x1024_ICON.png and make header/branding responsive (truncate titles, adjust version badge placement and logo usage). Minor UI/UX tweaks added for accessibility and visual polish.
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
@@ -8,7 +8,7 @@
|
|||||||
"theme_color": "#1e3a5f",
|
"theme_color": "#1e3a5f",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/RMAB_1024x1024.png",
|
"src": "/RMAB_1024x1024_ICON.png",
|
||||||
"sizes": "1024x1024",
|
"sizes": "1024x1024",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
|
|||||||
+1
-1
@@ -30,7 +30,7 @@ export const metadata: Metadata = {
|
|||||||
],
|
],
|
||||||
shortcut: "/rmab_icon.ico",
|
shortcut: "/rmab_icon.ico",
|
||||||
apple: [
|
apple: [
|
||||||
{ url: "/RMAB_1024x1024.png", sizes: "1024x1024", type: "image/png" },
|
{ url: "/RMAB_1024x1024_ICON.png", sizes: "1024x1024", type: "image/png" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
appleWebApp: {
|
appleWebApp: {
|
||||||
|
|||||||
+10
-5
@@ -12,12 +12,13 @@ import { useAudiobooks } from '@/lib/hooks/useAudiobooks';
|
|||||||
import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
|
import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
|
||||||
import { StickyPagination } from '@/components/ui/StickyPagination';
|
import { StickyPagination } from '@/components/ui/StickyPagination';
|
||||||
import { CardSizeControls } from '@/components/ui/CardSizeControls';
|
import { CardSizeControls } from '@/components/ui/CardSizeControls';
|
||||||
|
import { SquareCoversToggle } from '@/components/ui/SquareCoversToggle';
|
||||||
import { usePreferences } from '@/contexts/PreferencesContext';
|
import { usePreferences } from '@/contexts/PreferencesContext';
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
const [popularPage, setPopularPage] = useState(1);
|
const [popularPage, setPopularPage] = useState(1);
|
||||||
const [newReleasesPage, setNewReleasesPage] = useState(1);
|
const [newReleasesPage, setNewReleasesPage] = useState(1);
|
||||||
const { cardSize, setCardSize } = usePreferences();
|
const { cardSize, setCardSize, squareCovers, setSquareCovers } = usePreferences();
|
||||||
|
|
||||||
// Refs for auto-scrolling to section tops
|
// Refs for auto-scrolling to section tops
|
||||||
const popularSectionRef = useRef<HTMLElement>(null);
|
const popularSectionRef = useRef<HTMLElement>(null);
|
||||||
@@ -62,10 +63,11 @@ export default function HomePage() {
|
|||||||
<div className="bg-white/90 dark:bg-gray-800/90 backdrop-blur-md rounded-2xl px-4 sm:px-6 py-3 border border-gray-200/50 dark:border-gray-700/50 shadow-sm">
|
<div className="bg-white/90 dark:bg-gray-800/90 backdrop-blur-md rounded-2xl px-4 sm:px-6 py-3 border border-gray-200/50 dark:border-gray-700/50 shadow-sm">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-1 h-6 bg-gradient-to-b from-blue-500 to-purple-500 rounded-full" />
|
<div className="w-1 h-6 bg-gradient-to-b from-blue-500 to-purple-500 rounded-full" />
|
||||||
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-900 dark:text-gray-100">
|
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-900 dark:text-gray-100 truncate">
|
||||||
Popular Audiobooks
|
Popular Audiobooks
|
||||||
</h2>
|
</h2>
|
||||||
<div className="ml-auto">
|
<div className="ml-auto flex items-center gap-1">
|
||||||
|
<SquareCoversToggle enabled={squareCovers} onToggle={setSquareCovers} />
|
||||||
<CardSizeControls size={cardSize} onSizeChange={setCardSize} />
|
<CardSizeControls size={cardSize} onSizeChange={setCardSize} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,6 +91,7 @@ export default function HomePage() {
|
|||||||
isLoading={loadingPopular}
|
isLoading={loadingPopular}
|
||||||
emptyMessage="No popular audiobooks available"
|
emptyMessage="No popular audiobooks available"
|
||||||
cardSize={cardSize}
|
cardSize={cardSize}
|
||||||
|
squareCovers={squareCovers}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -101,10 +104,11 @@ export default function HomePage() {
|
|||||||
<div className="bg-white/90 dark:bg-gray-800/90 backdrop-blur-md rounded-2xl px-4 sm:px-6 py-3 border border-gray-200/50 dark:border-gray-700/50 shadow-sm">
|
<div className="bg-white/90 dark:bg-gray-800/90 backdrop-blur-md rounded-2xl px-4 sm:px-6 py-3 border border-gray-200/50 dark:border-gray-700/50 shadow-sm">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-1 h-6 bg-gradient-to-b from-emerald-500 to-teal-500 rounded-full" />
|
<div className="w-1 h-6 bg-gradient-to-b from-emerald-500 to-teal-500 rounded-full" />
|
||||||
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-900 dark:text-gray-100">
|
<h2 className="text-xl sm:text-2xl md:text-3xl font-bold text-gray-900 dark:text-gray-100 truncate">
|
||||||
New Releases
|
New Releases
|
||||||
</h2>
|
</h2>
|
||||||
<div className="ml-auto">
|
<div className="ml-auto flex items-center gap-1">
|
||||||
|
<SquareCoversToggle enabled={squareCovers} onToggle={setSquareCovers} />
|
||||||
<CardSizeControls size={cardSize} onSizeChange={setCardSize} />
|
<CardSizeControls size={cardSize} onSizeChange={setCardSize} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -128,6 +132,7 @@ export default function HomePage() {
|
|||||||
isLoading={loadingNewReleases}
|
isLoading={loadingNewReleases}
|
||||||
emptyMessage="No new releases available"
|
emptyMessage="No new releases available"
|
||||||
cardSize={cardSize}
|
cardSize={cardSize}
|
||||||
|
squareCovers={squareCovers}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,13 +11,14 @@ import { AudiobookGrid } from '@/components/audiobooks/AudiobookGrid';
|
|||||||
import { useSearch } from '@/lib/hooks/useAudiobooks';
|
import { useSearch } from '@/lib/hooks/useAudiobooks';
|
||||||
import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
|
import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
|
||||||
import { CardSizeControls } from '@/components/ui/CardSizeControls';
|
import { CardSizeControls } from '@/components/ui/CardSizeControls';
|
||||||
|
import { SquareCoversToggle } from '@/components/ui/SquareCoversToggle';
|
||||||
import { usePreferences } from '@/contexts/PreferencesContext';
|
import { usePreferences } from '@/contexts/PreferencesContext';
|
||||||
|
|
||||||
export default function SearchPage() {
|
export default function SearchPage() {
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [debouncedQuery, setDebouncedQuery] = useState('');
|
const [debouncedQuery, setDebouncedQuery] = useState('');
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const { cardSize, setCardSize } = usePreferences();
|
const { cardSize, setCardSize, squareCovers, setSquareCovers } = usePreferences();
|
||||||
|
|
||||||
// Debounce search query
|
// Debounce search query
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -109,15 +110,16 @@ export default function SearchPage() {
|
|||||||
<div className="bg-white/90 dark:bg-gray-800/90 backdrop-blur-md rounded-2xl px-4 sm:px-6 py-3 border border-gray-200/50 dark:border-gray-700/50 shadow-sm">
|
<div className="bg-white/90 dark:bg-gray-800/90 backdrop-blur-md rounded-2xl px-4 sm:px-6 py-3 border border-gray-200/50 dark:border-gray-700/50 shadow-sm">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="w-1 h-6 bg-gradient-to-b from-blue-500 to-purple-500 rounded-full" />
|
<div className="w-1 h-6 bg-gradient-to-b from-blue-500 to-purple-500 rounded-full" />
|
||||||
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-gray-100">
|
<h2 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-gray-100 truncate">
|
||||||
Search Results
|
Search Results
|
||||||
</h2>
|
</h2>
|
||||||
{!isLoading && totalResults > 0 && (
|
{!isLoading && totalResults > 0 && (
|
||||||
<span className="text-sm text-gray-600 dark:text-gray-400">
|
<span className="text-sm text-gray-600 dark:text-gray-400 hidden sm:inline whitespace-nowrap">
|
||||||
({totalResults.toLocaleString()} result{totalResults !== 1 ? 's' : ''})
|
({totalResults.toLocaleString()} result{totalResults !== 1 ? 's' : ''})
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<div className="ml-auto">
|
<div className="ml-auto flex items-center gap-1">
|
||||||
|
<SquareCoversToggle enabled={squareCovers} onToggle={setSquareCovers} />
|
||||||
<CardSizeControls size={cardSize} onSizeChange={setCardSize} />
|
<CardSizeControls size={cardSize} onSizeChange={setCardSize} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -130,6 +132,7 @@ export default function SearchPage() {
|
|||||||
isLoading={!!(isLoading && page === 1)}
|
isLoading={!!(isLoading && page === 1)}
|
||||||
emptyMessage={`No results found for "${debouncedQuery}"`}
|
emptyMessage={`No results found for "${debouncedQuery}"`}
|
||||||
cardSize={cardSize}
|
cardSize={cardSize}
|
||||||
|
squareCovers={squareCovers}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Load More */}
|
{/* Load More */}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface AudiobookCardProps {
|
|||||||
isRequested?: boolean;
|
isRequested?: boolean;
|
||||||
requestStatus?: string;
|
requestStatus?: string;
|
||||||
onRequestSuccess?: () => void;
|
onRequestSuccess?: () => void;
|
||||||
|
squareCovers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AudiobookCard({
|
export function AudiobookCard({
|
||||||
@@ -26,6 +27,7 @@ export function AudiobookCard({
|
|||||||
isRequested = false,
|
isRequested = false,
|
||||||
requestStatus,
|
requestStatus,
|
||||||
onRequestSuccess,
|
onRequestSuccess,
|
||||||
|
squareCovers = false,
|
||||||
}: AudiobookCardProps) {
|
}: AudiobookCardProps) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { createRequest, isLoading } = useCreateRequest();
|
const { createRequest, isLoading } = useCreateRequest();
|
||||||
@@ -59,10 +61,12 @@ export function AudiobookCard({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-shadow">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden hover:shadow-lg transition-all duration-300">
|
||||||
{/* Cover Art - Clickable */}
|
{/* Cover Art - Clickable */}
|
||||||
<div
|
<div
|
||||||
className="relative aspect-[2/3] bg-gray-200 dark:bg-gray-700 cursor-pointer group"
|
className={`relative bg-gray-200 dark:bg-gray-700 cursor-pointer group overflow-hidden ${
|
||||||
|
squareCovers ? 'aspect-square' : 'aspect-[2/3]'
|
||||||
|
}`}
|
||||||
onClick={() => setShowModal(true)}
|
onClick={() => setShowModal(true)}
|
||||||
>
|
>
|
||||||
{audiobook.coverArtUrl ? (
|
{audiobook.coverArtUrl ? (
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ interface AudiobookGridProps {
|
|||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
onRequestSuccess?: () => void;
|
onRequestSuccess?: () => void;
|
||||||
cardSize?: number; // 1-9, default 5
|
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
|
// Helper function to get grid classes based on card size
|
||||||
@@ -41,6 +42,7 @@ export function AudiobookGrid({
|
|||||||
emptyMessage = 'No audiobooks found',
|
emptyMessage = 'No audiobooks found',
|
||||||
onRequestSuccess,
|
onRequestSuccess,
|
||||||
cardSize = 5,
|
cardSize = 5,
|
||||||
|
squareCovers = false,
|
||||||
}: AudiobookGridProps) {
|
}: AudiobookGridProps) {
|
||||||
const gridClasses = getGridClasses(cardSize);
|
const gridClasses = getGridClasses(cardSize);
|
||||||
|
|
||||||
@@ -48,7 +50,7 @@ export function AudiobookGrid({
|
|||||||
return (
|
return (
|
||||||
<div className={`grid ${gridClasses} gap-3 sm:gap-4 md:gap-6`}>
|
<div className={`grid ${gridClasses} gap-3 sm:gap-4 md:gap-6`}>
|
||||||
{Array.from({ length: 8 }).map((_, i) => (
|
{Array.from({ length: 8 }).map((_, i) => (
|
||||||
<SkeletonCard key={i} />
|
<SkeletonCard key={i} squareCovers={squareCovers} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -76,23 +78,26 @@ export function AudiobookGrid({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`grid ${gridClasses} gap-3 sm:gap-4 md:gap-6`}>
|
<div className={`grid ${gridClasses} gap-3 sm:gap-4 md:gap-6 transition-all duration-300`}>
|
||||||
{audiobooks.map((audiobook) => (
|
{audiobooks.map((audiobook) => (
|
||||||
<AudiobookCard
|
<AudiobookCard
|
||||||
key={audiobook.asin}
|
key={audiobook.asin}
|
||||||
audiobook={audiobook}
|
audiobook={audiobook}
|
||||||
onRequestSuccess={onRequestSuccess}
|
onRequestSuccess={onRequestSuccess}
|
||||||
|
squareCovers={squareCovers}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SkeletonCard() {
|
function SkeletonCard({ squareCovers = false }: { squareCovers?: boolean }) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden animate-pulse">
|
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-md overflow-hidden animate-pulse transition-all duration-300">
|
||||||
{/* Cover Art Skeleton */}
|
{/* Cover Art Skeleton */}
|
||||||
<div className="aspect-[2/3] bg-gray-200 dark:bg-gray-700" />
|
<div className={`bg-gray-200 dark:bg-gray-700 ${
|
||||||
|
squareCovers ? 'aspect-square' : 'aspect-[2/3]'
|
||||||
|
}`} />
|
||||||
|
|
||||||
{/* Content Skeleton */}
|
{/* Content Skeleton */}
|
||||||
<div className="p-4 space-y-3">
|
<div className="p-4 space-y-3">
|
||||||
|
|||||||
@@ -118,18 +118,21 @@ export function Header() {
|
|||||||
<div className="container mx-auto px-4 py-3 md:py-4 max-w-7xl">
|
<div className="container mx-auto px-4 py-3 md:py-4 max-w-7xl">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Logo and Version Badge */}
|
{/* Logo and Version Badge */}
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2 md:gap-3 min-w-0">
|
||||||
<Link href="/" className="flex items-center gap-2">
|
<Link href="/" className="flex items-center gap-2 min-w-0">
|
||||||
<img
|
<img
|
||||||
src="/rmab_32x32.png"
|
src="/RMAB_1024x1024_ICON.png"
|
||||||
alt="ReadMeABook Logo"
|
alt="ReadMeABook Logo"
|
||||||
className="w-8 h-8"
|
className="w-8 h-8 flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
<span className="text-lg md:text-xl font-bold text-gray-900 dark:text-gray-100">
|
<span className="text-lg md:text-xl font-bold text-gray-900 dark:text-gray-100 truncate">
|
||||||
ReadMeABook
|
ReadMeABook
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<VersionBadge />
|
{/* Hide version badge on mobile to prevent overlap */}
|
||||||
|
<div className="hidden sm:block flex-shrink-0">
|
||||||
|
<VersionBadge />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
{/* Desktop Navigation */}
|
||||||
@@ -203,7 +206,7 @@ export function Header() {
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{user ? (
|
{user ? (
|
||||||
<div className="relative" ref={containerRef}>
|
<div className="relative flex-shrink-0" ref={containerRef}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowUserMenu(!showUserMenu)}
|
onClick={() => setShowUserMenu(!showUserMenu)}
|
||||||
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
||||||
@@ -212,10 +215,10 @@ export function Header() {
|
|||||||
<img
|
<img
|
||||||
src={user.avatarUrl}
|
src={user.avatarUrl}
|
||||||
alt={user.username}
|
alt={user.username}
|
||||||
className="w-8 h-8 rounded-full"
|
className="w-8 h-8 rounded-full flex-shrink-0"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white font-medium">
|
<div className="w-8 h-8 rounded-full bg-blue-600 flex items-center justify-center text-white font-medium flex-shrink-0">
|
||||||
{user.username.charAt(0).toUpperCase()}
|
{user.username.charAt(0).toUpperCase()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -278,6 +281,10 @@ export function Header() {
|
|||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
{/* Version badge in mobile menu */}
|
||||||
|
<div className="sm:hidden mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 px-3">
|
||||||
|
<VersionBadge />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<button
|
||||||
|
onClick={() => onToggle(!enabled)}
|
||||||
|
aria-label={enabled ? 'Switch to rectangular covers' : 'Switch to square covers'}
|
||||||
|
aria-pressed={enabled}
|
||||||
|
title={enabled ? 'Square covers (on)' : 'Square covers (off)'}
|
||||||
|
className={`
|
||||||
|
p-1.5 rounded-md transition-all duration-200
|
||||||
|
${enabled
|
||||||
|
? 'bg-blue-500/20 dark:bg-blue-400/20 text-blue-600 dark:text-blue-400 ring-1 ring-blue-500/30 dark:ring-blue-400/30 shadow-inner'
|
||||||
|
: 'text-gray-600 dark:text-gray-400 hover:bg-white/20 dark:hover:bg-gray-700/50'
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{/* Crop/aspect ratio icon */}
|
||||||
|
<svg
|
||||||
|
className="w-5 h-5"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
{/* Square frame representing crop to 1:1 */}
|
||||||
|
<rect
|
||||||
|
x="3"
|
||||||
|
y="3"
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
rx="2"
|
||||||
|
strokeWidth={2}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
{/* Inner crop marks suggesting aspect ratio change */}
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth={2}
|
||||||
|
d="M3 9h4M3 15h4M21 9h-4M21 15h-4"
|
||||||
|
opacity={enabled ? 1 : 0.4}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -9,23 +9,28 @@ import React, { createContext, useContext, useState, useEffect, ReactNode } from
|
|||||||
|
|
||||||
interface Preferences {
|
interface Preferences {
|
||||||
cardSize: number; // 1-9, default 5
|
cardSize: number; // 1-9, default 5
|
||||||
|
squareCovers: boolean; // true = square (1:1), false = rectangle (2:3)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PreferencesContextType {
|
interface PreferencesContextType {
|
||||||
cardSize: number;
|
cardSize: number;
|
||||||
setCardSize: (size: number) => void;
|
setCardSize: (size: number) => void;
|
||||||
|
squareCovers: boolean;
|
||||||
|
setSquareCovers: (enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PreferencesContext = createContext<PreferencesContextType | undefined>(undefined);
|
const PreferencesContext = createContext<PreferencesContextType | undefined>(undefined);
|
||||||
|
|
||||||
const DEFAULT_PREFERENCES: Preferences = {
|
const DEFAULT_PREFERENCES: Preferences = {
|
||||||
cardSize: 5,
|
cardSize: 5,
|
||||||
|
squareCovers: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const STORAGE_KEY = 'preferences';
|
const STORAGE_KEY = 'preferences';
|
||||||
|
|
||||||
export function PreferencesProvider({ children }: { children: ReactNode }) {
|
export function PreferencesProvider({ children }: { children: ReactNode }) {
|
||||||
const [cardSize, setCardSizeState] = useState<number>(DEFAULT_PREFERENCES.cardSize);
|
const [cardSize, setCardSizeState] = useState<number>(DEFAULT_PREFERENCES.cardSize);
|
||||||
|
const [squareCovers, setSquareCoversState] = useState<boolean>(DEFAULT_PREFERENCES.squareCovers);
|
||||||
|
|
||||||
// Load preferences from localStorage on mount
|
// Load preferences from localStorage on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -42,10 +47,13 @@ export function PreferencesProvider({ children }: { children: ReactNode }) {
|
|||||||
// Invalid size, reset to default
|
// Invalid size, reset to default
|
||||||
setCardSizeState(DEFAULT_PREFERENCES.cardSize);
|
setCardSizeState(DEFAULT_PREFERENCES.cardSize);
|
||||||
}
|
}
|
||||||
|
// Load squareCovers preference (defaults to false if not set)
|
||||||
|
setSquareCoversState(preferences.squareCovers ?? DEFAULT_PREFERENCES.squareCovers);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load preferences from localStorage:', error);
|
console.error('Failed to load preferences from localStorage:', error);
|
||||||
setCardSizeState(DEFAULT_PREFERENCES.cardSize);
|
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)
|
// Listen for storage changes in other tabs (cross-tab sync)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
@@ -80,6 +104,8 @@ export function PreferencesProvider({ children }: { children: ReactNode }) {
|
|||||||
if (preferences.cardSize >= 1 && preferences.cardSize <= 9) {
|
if (preferences.cardSize >= 1 && preferences.cardSize <= 9) {
|
||||||
setCardSizeState(preferences.cardSize);
|
setCardSizeState(preferences.cardSize);
|
||||||
}
|
}
|
||||||
|
// Sync squareCovers preference
|
||||||
|
setSquareCoversState(preferences.squareCovers ?? DEFAULT_PREFERENCES.squareCovers);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to parse preferences from storage event:', error);
|
console.error('Failed to parse preferences from storage event:', error);
|
||||||
}
|
}
|
||||||
@@ -93,7 +119,7 @@ export function PreferencesProvider({ children }: { children: ReactNode }) {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PreferencesContext.Provider value={{ cardSize, setCardSize }}>
|
<PreferencesContext.Provider value={{ cardSize, setCardSize, squareCovers, setSquareCovers }}>
|
||||||
{children}
|
{children}
|
||||||
</PreferencesContext.Provider>
|
</PreferencesContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user