mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-19 12:40:09 +00:00
Add per-user home sections & unified Audible cache
Introduce per-user configurable home page sections and a unified Audible cache/category model. Adds Prisma models (UserHomeSection, AudibleCacheCategory) and migrations to create tables and remove legacy popular/new_release flags; updates schema.prisma accordingly. Add API routes for user home sections, live Audible categories, and category-based audiobook listing, and refactor popular/new-releases/covers routes to read from AudibleCacheCategory. Frontend: new HomeSection component, HomeSectionConfigModal, useHomeSections hook, and homepage changes to render dynamic sections plus image fallback to a placeholder SVG. Also add placeholder_cover.svg and tests for home sections and the audible refresh processor.
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { SeriesSummary } from '@/lib/hooks/useSeries';
|
||||
@@ -20,6 +20,7 @@ interface SeriesCardProps {
|
||||
}
|
||||
|
||||
export function SeriesCard({ series, squareCovers = false }: SeriesCardProps) {
|
||||
const [coverError, setCoverError] = useState(false);
|
||||
const visibleTags = series.tags.slice(0, 2);
|
||||
const hasTags = visibleTags.length > 0;
|
||||
const hasRating = series.rating != null && series.rating > 0;
|
||||
@@ -42,30 +43,23 @@ export function SeriesCard({ series, squareCovers = false }: SeriesCardProps) {
|
||||
`}
|
||||
>
|
||||
{/* Cover Art or Fallback */}
|
||||
{series.coverArtUrl ? (
|
||||
{series.coverArtUrl && !coverError ? (
|
||||
<Image
|
||||
src={series.coverArtUrl}
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 20vw"
|
||||
onError={() => setCoverError(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-emerald-600 to-teal-800 dark:from-emerald-700 dark:to-teal-900 flex items-center justify-center">
|
||||
<svg
|
||||
className="w-1/3 h-1/3 text-white/40"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={1.2}
|
||||
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<Image
|
||||
src="/placeholder_cover.svg"
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 20vw"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Top-row badges — Rating (left) + Book count (right) */}
|
||||
|
||||
@@ -8,11 +8,13 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { SeriesDetail } from '@/lib/hooks/useSeries';
|
||||
import { WatchSeriesButton } from '@/components/ui/WatchButton';
|
||||
|
||||
const PLACEHOLDER_COVER = '/placeholder_cover.svg';
|
||||
|
||||
interface SeriesDetailCardProps {
|
||||
series: SeriesDetail;
|
||||
squareCovers?: boolean;
|
||||
@@ -20,6 +22,7 @@ interface SeriesDetailCardProps {
|
||||
|
||||
export function SeriesDetailCard({ series, squareCovers = false }: SeriesDetailCardProps) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [coverError, setCoverError] = useState(false);
|
||||
const hasLongDescription = (series.description?.length || 0) > 300;
|
||||
|
||||
return (
|
||||
@@ -27,7 +30,7 @@ export function SeriesDetailCard({ series, squareCovers = false }: SeriesDetailC
|
||||
{/* Rectangular Cover */}
|
||||
<div className="flex-shrink-0">
|
||||
<div className={`relative w-36 sm:w-44 lg:w-52 ${squareCovers ? 'aspect-square' : 'aspect-[2/3]'} rounded-xl overflow-hidden shadow-xl shadow-black/20 dark:shadow-black/40`}>
|
||||
{series.books[0]?.coverArtUrl ? (
|
||||
{series.books[0]?.coverArtUrl && !coverError ? (
|
||||
<Image
|
||||
src={series.books[0].coverArtUrl}
|
||||
alt={series.title}
|
||||
@@ -35,13 +38,16 @@ export function SeriesDetailCard({ series, squareCovers = false }: SeriesDetailC
|
||||
className="object-cover"
|
||||
sizes="(max-width: 640px) 144px, (max-width: 1024px) 176px, 208px"
|
||||
priority
|
||||
onError={() => setCoverError(true)}
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-emerald-100 to-teal-200 dark:from-emerald-900 dark:to-teal-900 flex items-center justify-center">
|
||||
<svg className="w-1/3 h-1/3 text-emerald-400 dark:text-emerald-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
|
||||
</svg>
|
||||
</div>
|
||||
<Image
|
||||
src={PLACEHOLDER_COVER}
|
||||
alt={series.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="(max-width: 640px) 144px, (max-width: 1024px) 176px, 208px"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -97,21 +97,14 @@ export function SimilarSeriesRow({ series, currentSeriesTitle, squareCovers = fa
|
||||
>
|
||||
{/* Cover */}
|
||||
<div className={`relative w-20 ${squareCovers ? 'h-20 sm:w-24 sm:h-24' : 'h-[120px] sm:w-24 sm:h-36'} rounded-lg overflow-hidden shadow-md shadow-black/15 dark:shadow-black/30 group-hover/card:shadow-lg group-hover/card:scale-[1.04] group-hover/card:-translate-y-0.5 transition-all duration-300`}>
|
||||
{s.coverArtUrl ? (
|
||||
<Image
|
||||
src={s.coverArtUrl}
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="96px"
|
||||
/>
|
||||
) : (
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-emerald-100 to-teal-200 dark:from-emerald-900 dark:to-teal-900 flex items-center justify-center">
|
||||
<span className="text-lg font-bold text-emerald-400 dark:text-emerald-300">
|
||||
{s.title.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Image
|
||||
src={s.coverArtUrl || '/placeholder_cover.svg'}
|
||||
alt=""
|
||||
fill
|
||||
className="object-cover"
|
||||
sizes="96px"
|
||||
onError={(e) => { (e.target as HTMLImageElement).src = '/placeholder_cover.svg'; }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Title */}
|
||||
|
||||
Reference in New Issue
Block a user