mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +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:
@@ -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 (
|
||||
<>
|
||||
<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 */}
|
||||
<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)}
|
||||
>
|
||||
{audiobook.coverArtUrl ? (
|
||||
|
||||
@@ -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 (
|
||||
<div className={`grid ${gridClasses} gap-3 sm:gap-4 md:gap-6`}>
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<SkeletonCard key={i} />
|
||||
<SkeletonCard key={i} squareCovers={squareCovers} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -76,23 +78,26 @@ export function AudiobookGrid({
|
||||
}
|
||||
|
||||
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) => (
|
||||
<AudiobookCard
|
||||
key={audiobook.asin}
|
||||
audiobook={audiobook}
|
||||
onRequestSuccess={onRequestSuccess}
|
||||
squareCovers={squareCovers}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SkeletonCard() {
|
||||
function SkeletonCard({ squareCovers = false }: { squareCovers?: boolean }) {
|
||||
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 */}
|
||||
<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 */}
|
||||
<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="flex items-center justify-between">
|
||||
{/* Logo and Version Badge */}
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/" className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 md:gap-3 min-w-0">
|
||||
<Link href="/" className="flex items-center gap-2 min-w-0">
|
||||
<img
|
||||
src="/rmab_32x32.png"
|
||||
src="/RMAB_1024x1024_ICON.png"
|
||||
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
|
||||
</span>
|
||||
</Link>
|
||||
<VersionBadge />
|
||||
{/* Hide version badge on mobile to prevent overlap */}
|
||||
<div className="hidden sm:block flex-shrink-0">
|
||||
<VersionBadge />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
@@ -203,7 +206,7 @@ export function Header() {
|
||||
</button>
|
||||
|
||||
{user ? (
|
||||
<div className="relative" ref={containerRef}>
|
||||
<div className="relative flex-shrink-0" ref={containerRef}>
|
||||
<button
|
||||
onClick={() => setShowUserMenu(!showUserMenu)}
|
||||
className="flex items-center gap-2 hover:opacity-80 transition-opacity"
|
||||
@@ -212,10 +215,10 @@ export function Header() {
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
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()}
|
||||
</div>
|
||||
)}
|
||||
@@ -278,6 +281,10 @@ export function Header() {
|
||||
</Link>
|
||||
)}
|
||||
</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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user