/** * Component: Home Section — renders a single audiobook discovery section * Documentation: documentation/features/home-sections.md * * Handles popular, new_releases, and category section types with unified rendering. */ 'use client'; import React, { useEffect } from 'react'; import { AudiobookGrid } from '@/components/audiobooks/AudiobookGrid'; import { SectionToolbar } from '@/components/ui/SectionToolbar'; import { useAudiobooks } from '@/lib/hooks/useAudiobooks'; import { useCategoryAudiobooks } from '@/lib/hooks/useHomeSections'; import { Cog6ToothIcon, ClockIcon } from '@heroicons/react/24/outline'; const SECTION_COLORS = [ 'from-blue-500 to-indigo-500', 'from-emerald-500 to-teal-500', 'from-violet-500 to-purple-500', 'from-amber-500 to-orange-500', 'from-rose-500 to-pink-500', 'from-cyan-500 to-sky-500', 'from-fuchsia-500 to-pink-500', 'from-lime-500 to-green-500', 'from-orange-500 to-red-500', 'from-teal-500 to-emerald-500', ]; export const SECTION_DOT_COLORS = [ 'bg-blue-500', 'bg-emerald-500', 'bg-violet-500', 'bg-amber-500', 'bg-rose-500', 'bg-cyan-500', 'bg-fuchsia-500', 'bg-lime-500', 'bg-orange-500', 'bg-teal-500', ]; function getSectionTitle(sectionType: string, categoryName?: string | null): string { if (sectionType === 'popular') return 'Popular Audiobooks'; if (sectionType === 'new_releases') return 'New Releases'; return categoryName || 'Category'; } /** * Formats a nextRefresh ISO timestamp into a friendly, readable string. * Examples: "today at 6:00 PM", "tomorrow at 2:00 AM", "Saturday at 9:00 AM" */ function formatNextRefresh(isoString: string): string { const refreshDate = new Date(isoString); const now = new Date(); const refreshMidnight = new Date(refreshDate); refreshMidnight.setHours(0, 0, 0, 0); const todayMidnight = new Date(now); todayMidnight.setHours(0, 0, 0, 0); const tomorrowMidnight = new Date(todayMidnight); tomorrowMidnight.setDate(tomorrowMidnight.getDate() + 1); const dayAfterMidnight = new Date(tomorrowMidnight); dayAfterMidnight.setDate(dayAfterMidnight.getDate() + 1); const timeStr = refreshDate.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit', hour12: true, }); if (refreshMidnight.getTime() === todayMidnight.getTime()) { return `today at ${timeStr}`; } if (refreshMidnight.getTime() === tomorrowMidnight.getTime()) { return `tomorrow at ${timeStr}`; } if (refreshMidnight.getTime() < dayAfterMidnight.getTime()) { const dayName = refreshDate.toLocaleDateString(undefined, { weekday: 'long' }); return `${dayName} at ${timeStr}`; } const dateStr = refreshDate.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric', }); return `${dateStr} at ${timeStr}`; } interface HomeSectionProps { sectionType: 'popular' | 'new_releases' | 'category'; categoryId: string | null; categoryName: string | null; colorIndex: number; page: number; sectionRef: React.RefObject; cardSize: number; squareCovers: boolean; hideAvailable: boolean; onToggleHideAvailable: (v: boolean) => void; onToggleSquareCovers: (v: boolean) => void; onCardSizeChange: (v: number) => void; onConfigOpen?: () => void; onTotalPagesChange?: (totalPages: number) => void; nextRefresh: string | null; } function PopularOrNewSection({ type, page, hideAvailable, onTotalPagesChange, ...renderProps }: { type: 'popular' | 'new-releases'; page: number; hideAvailable: boolean; onTotalPagesChange?: (totalPages: number) => void; } & RenderSectionProps) { const { audiobooks, isLoading, totalPages, message } = useAudiobooks(type, 20, page, hideAvailable); useEffect(() => { onTotalPagesChange?.(totalPages); }, [totalPages, onTotalPagesChange]); return ( ); } function CategorySection({ categoryId, page, hideAvailable, onTotalPagesChange, ...renderProps }: { categoryId: string; page: number; hideAvailable: boolean; onTotalPagesChange?: (totalPages: number) => void; } & RenderSectionProps) { const { audiobooks, isLoading, totalPages, message } = useCategoryAudiobooks( categoryId, 20, page, hideAvailable ); useEffect(() => { onTotalPagesChange?.(totalPages); }, [totalPages, onTotalPagesChange]); return ( ); } interface RenderSectionProps { cardSize: number; squareCovers: boolean; nextRefresh?: string | null; } function CategoryEmptyState({ nextRefresh }: { nextRefresh?: string | null }) { const refreshLabel = nextRefresh ? formatNextRefresh(nextRefresh) : null; return (

No audiobooks yet

{refreshLabel ? <>This section will fill in after the next data refresh, scheduled for {refreshLabel}. : 'This section will fill in after the next scheduled data refresh.'}

); } function RenderSection({ audiobooks, isLoading, totalPages, message, cardSize, squareCovers, nextRefresh, }: RenderSectionProps & { audiobooks: any[]; isLoading: boolean; totalPages: number; message: string | null; }) { if (message && !isLoading && audiobooks.length === 0) { return ; } return ( ); } export function HomeSection({ sectionType, categoryId, categoryName, colorIndex, page, sectionRef, cardSize, squareCovers, hideAvailable, onToggleHideAvailable, onToggleSquareCovers, onCardSizeChange, onConfigOpen, onTotalPagesChange, nextRefresh, }: HomeSectionProps) { const gradient = SECTION_COLORS[colorIndex % SECTION_COLORS.length]; const title = getSectionTitle(sectionType, categoryName); const renderProps: RenderSectionProps = { cardSize, squareCovers, nextRefresh }; return (
{/* Sticky Section Header */}

{title}

{onConfigOpen && ( )}
{/* Section Content */}
{sectionType === 'popular' && ( )} {sectionType === 'new_releases' && ( )} {sectionType === 'category' && categoryId && ( )}
); }