mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 21:00:09 +00:00
Apple-style audiobook card & modal redesign
Add Apple-style UI refinements and UX improvements across audiobooks. - Add app icons and update manifest/layout to reference new 180x180 and 1024x1024 app icons. - Add global CSS animations (shimmer and toast slide-in) for improved skeletons/toasts. - Refactor AudiobookCard: new "cover-first" design, accessibility improvements, hover actions, request handling, inline toast/error UI, status helper (status -> badge/state mapping), simplified metadata layout, and polished animations/visuals. Removed some legacy imports and adjusted request UX (shorter toasts, error timing). - Enhance AudiobookDetailsModal: mobile-first sticky header, desktop modal polish, improved status logic, toast/notification helper, ASIN copy behavior, ebook request flows, uses preferences (squareCovers) and various layout/formatting tweaks. - Propagate hideRequestActions from bookdate page (bookdate/page.tsx). These changes focus on visual polish, accessibility, and smoother request/notification UX. Files changed: public manifest & icons, globals.css, app/layout.tsx, bookdate page, AudiobookCard, AudiobookDetailsModal, and PreferencesContext (context usage updated).
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
/**
|
||||
* Component: Audiobook Grid
|
||||
* Documentation: documentation/frontend/components.md
|
||||
*
|
||||
* Premium grid layout with generous spacing and elegant skeletons
|
||||
*/
|
||||
|
||||
'use client';
|
||||
@@ -18,21 +20,20 @@ interface AudiobookGridProps {
|
||||
squareCovers?: boolean; // true = square (1:1), false = rectangle (2:3)
|
||||
}
|
||||
|
||||
// Helper function to get grid classes based on card size
|
||||
// IMPORTANT: Classes must be explicit strings (not template literals) for Tailwind purging
|
||||
// Grid classes with generous spacing for premium feel
|
||||
// IMPORTANT: Classes must be explicit strings for Tailwind purging
|
||||
function getGridClasses(size: number): string {
|
||||
const sizeMap: Record<number, string> = {
|
||||
1: 'grid-cols-4 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10', // Smallest
|
||||
1: 'grid-cols-4 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10',
|
||||
2: 'grid-cols-3 md:grid-cols-5 lg:grid-cols-7 xl:grid-cols-9',
|
||||
3: 'grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8',
|
||||
4: 'grid-cols-2 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-7',
|
||||
5: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5', // Default
|
||||
5: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5',
|
||||
6: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
|
||||
7: 'grid-cols-2 md:grid-cols-3',
|
||||
8: 'grid-cols-2',
|
||||
9: 'grid-cols-1', // Largest
|
||||
9: 'grid-cols-1',
|
||||
};
|
||||
|
||||
return sizeMap[size] || sizeMap[5];
|
||||
}
|
||||
|
||||
@@ -48,9 +49,9 @@ export function AudiobookGrid({
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className={`grid ${gridClasses} gap-3 sm:gap-4 md:gap-6`}>
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<SkeletonCard key={i} squareCovers={squareCovers} />
|
||||
<div className={`grid ${gridClasses} gap-5 sm:gap-6 lg:gap-8`}>
|
||||
{Array.from({ length: 10 }).map((_, i) => (
|
||||
<SkeletonCard key={i} squareCovers={squareCovers} index={i} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
@@ -58,27 +59,19 @@ export function AudiobookGrid({
|
||||
|
||||
if (audiobooks.length === 0) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<svg
|
||||
className="w-16 h-16 text-gray-400 mb-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3"
|
||||
/>
|
||||
</svg>
|
||||
<p className="text-gray-600 dark:text-gray-400">{emptyMessage}</p>
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<div className="w-20 h-20 rounded-full bg-gray-100 dark:bg-gray-800 flex items-center justify-center mb-6">
|
||||
<svg className="w-10 h-10 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-gray-500 dark:text-gray-400 text-lg">{emptyMessage}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`grid ${gridClasses} gap-3 sm:gap-4 md:gap-6 transition-all duration-300`}>
|
||||
<div className={`grid ${gridClasses} gap-5 sm:gap-6 lg:gap-8`}>
|
||||
{audiobooks.map((audiobook) => (
|
||||
<AudiobookCard
|
||||
key={audiobook.asin}
|
||||
@@ -91,28 +84,29 @@ export function AudiobookGrid({
|
||||
);
|
||||
}
|
||||
|
||||
function SkeletonCard({ squareCovers = false }: { squareCovers?: boolean }) {
|
||||
// Premium skeleton with shimmer effect
|
||||
function SkeletonCard({ squareCovers = false, index = 0 }: { squareCovers?: boolean; index?: number }) {
|
||||
return (
|
||||
<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={`bg-gray-200 dark:bg-gray-700 ${
|
||||
squareCovers ? 'aspect-square' : 'aspect-[2/3]'
|
||||
}`} />
|
||||
<div
|
||||
className="animate-pulse"
|
||||
style={{ animationDelay: `${index * 50}ms` }}
|
||||
>
|
||||
{/* Cover Skeleton */}
|
||||
<div
|
||||
className={`
|
||||
relative overflow-hidden rounded-2xl
|
||||
bg-gradient-to-br from-gray-200 to-gray-300 dark:from-gray-700 dark:to-gray-800
|
||||
${squareCovers ? 'aspect-square' : 'aspect-[2/3]'}
|
||||
`}
|
||||
>
|
||||
{/* Shimmer overlay */}
|
||||
<div className="absolute inset-0 -translate-x-full animate-[shimmer_2s_infinite] bg-gradient-to-r from-transparent via-white/20 to-transparent" />
|
||||
</div>
|
||||
|
||||
{/* Content Skeleton */}
|
||||
<div className="p-4 space-y-3">
|
||||
{/* Title */}
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-3/4" />
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2" />
|
||||
|
||||
{/* Author */}
|
||||
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-2/3" />
|
||||
|
||||
{/* Metadata */}
|
||||
<div className="h-3 bg-gray-200 dark:bg-gray-700 rounded w-1/3" />
|
||||
|
||||
{/* Button */}
|
||||
<div className="h-10 bg-gray-200 dark:bg-gray-700 rounded" />
|
||||
{/* Text Skeleton */}
|
||||
<div className="mt-3 px-1 space-y-2">
|
||||
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded-lg w-4/5" />
|
||||
<div className="h-3.5 bg-gray-200 dark:bg-gray-700 rounded-lg w-3/5" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user