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:
kikootwo
2026-03-05 11:30:39 -05:00
parent 248bd5359c
commit cc8e106a2b
40 changed files with 2582 additions and 655 deletions
+119
View File
@@ -0,0 +1,119 @@
/**
* Component: Home Sections Hook
* Documentation: documentation/features/home-sections.md
*
* Manages user home section configuration (CRUD) and category fetching.
*/
'use client';
import useSWR, { mutate as globalMutate } from 'swr';
import { authenticatedFetcher } from '@/lib/utils/api';
import { useCallback, useRef } from 'react';
export interface HomeSection {
id: string;
sectionType: 'popular' | 'new_releases' | 'category';
categoryId: string | null;
categoryName: string | null;
sortOrder: number;
}
export interface HomeSectionsResponse {
success: boolean;
sections: HomeSection[];
nextRefresh: string | null;
}
export interface AudibleCategory {
id: string;
name: string;
}
const HOME_SECTIONS_KEY = '/api/user/home-sections';
/**
* Hook to fetch and manage user home sections.
*/
export function useHomeSections() {
const { data, error, isLoading, mutate } = useSWR<HomeSectionsResponse>(
HOME_SECTIONS_KEY,
authenticatedFetcher,
{
revalidateOnFocus: false,
dedupingInterval: 30000,
}
);
const saveSections = useCallback(
async (sections: Omit<HomeSection, 'id'>[]) => {
const { fetchJSON } = await import('@/lib/utils/api');
const result = await fetchJSON<HomeSectionsResponse>(HOME_SECTIONS_KEY, {
method: 'PUT',
body: JSON.stringify({ sections }),
});
// Update local cache
mutate(result, false);
return result;
},
[mutate]
);
return {
sections: data?.sections || [],
nextRefresh: data?.nextRefresh || null,
isLoading,
error,
saveSections,
mutate,
};
}
/**
* Hook to fetch Audible categories (live scrape, for config modal).
*/
export function useAudibleCategories() {
const { data, error, isLoading } = useSWR<{ success: boolean; categories: AudibleCategory[] }>(
null, // Don't fetch automatically — use fetchCategories
authenticatedFetcher,
{ revalidateOnFocus: false }
);
return {
categories: data?.categories || [],
isLoading,
error,
};
}
/**
* Hook to fetch category audiobooks (same pattern as useAudiobooks).
*/
export function useCategoryAudiobooks(
categoryId: string | null,
limit: number = 20,
page: number = 1,
hideAvailable: boolean = false
) {
const hideParam = hideAvailable ? '&hideAvailable=true' : '';
const endpoint = categoryId
? `/api/audiobooks/category/${categoryId}?page=${page}&limit=${limit}${hideParam}`
: null;
const { data, error, isLoading } = useSWR(endpoint, authenticatedFetcher, {
revalidateOnFocus: false,
revalidateOnReconnect: false,
dedupingInterval: 60000,
});
return {
audiobooks: data?.audiobooks || [],
totalCount: data?.totalCount || 0,
totalPages: data?.totalPages || 0,
currentPage: data?.page || page,
hasMore: data?.hasMore || false,
message: data?.message || null,
isLoading,
error,
};
}