mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 12:50:09 +00:00
Add hideAvailable filter and unified pagination
Add support for hiding audiobooks that are already available by introducing a hideAvailable query flag and excluding matching ASINs at the DB level. Implemented getAvailableAsins() in audiobook-matcher to gather ASINs from the library and completed requests, and wired it into the popular and new-releases API routes to apply a notIn filter. Propagated the hideAvailable flag through useAudiobooks so client requests include the parameter, and adjusted the homepage to reset pagination when the flag changes. Replaced two StickyPagination instances with a new UnifiedPagination component (new file) that provides a single context-aware floating paginator which tracks the dominant section and allows switching between Popular and New Releases. Also removed client-side filtering in favor of server-side exclusion and made small imports/cleanup in page.tsx.
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher';
|
||||
import { enrichAudiobooksWithMatches, getAvailableAsins } from '@/lib/utils/audiobook-matcher';
|
||||
import { getCurrentUser } from '@/lib/middleware/auth';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
||||
const limit = parseInt(searchParams.get('limit') || '20', 10);
|
||||
const hideAvailable = searchParams.get('hideAvailable') === 'true';
|
||||
|
||||
// Validate pagination parameters
|
||||
if (page < 1 || limit < 1 || limit > 100) {
|
||||
@@ -38,12 +39,22 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// When hideAvailable is enabled, exclude ASINs that are in the library or have completed requests
|
||||
let excludedAsins: string[] = [];
|
||||
if (hideAvailable) {
|
||||
const availableSet = await getAvailableAsins();
|
||||
excludedAsins = [...availableSet];
|
||||
}
|
||||
|
||||
const whereClause = {
|
||||
isNewRelease: true,
|
||||
...(excludedAsins.length > 0 ? { asin: { notIn: excludedAsins } } : {}),
|
||||
};
|
||||
|
||||
// Query audible_cache for new release audiobooks
|
||||
const [audiobooks, totalCount] = await Promise.all([
|
||||
prisma.audibleCache.findMany({
|
||||
where: {
|
||||
isNewRelease: true,
|
||||
},
|
||||
where: whereClause,
|
||||
orderBy: {
|
||||
newReleaseRank: 'asc',
|
||||
},
|
||||
@@ -66,9 +77,7 @@ export async function GET(request: NextRequest) {
|
||||
},
|
||||
}),
|
||||
prisma.audibleCache.count({
|
||||
where: {
|
||||
isNewRelease: true,
|
||||
},
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { prisma } from '@/lib/db';
|
||||
import { enrichAudiobooksWithMatches } from '@/lib/utils/audiobook-matcher';
|
||||
import { enrichAudiobooksWithMatches, getAvailableAsins } from '@/lib/utils/audiobook-matcher';
|
||||
import { getCurrentUser } from '@/lib/middleware/auth';
|
||||
import { RMABLogger } from '@/lib/utils/logger';
|
||||
|
||||
@@ -24,6 +24,7 @@ export async function GET(request: NextRequest) {
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
||||
const limit = parseInt(searchParams.get('limit') || '20', 10);
|
||||
const hideAvailable = searchParams.get('hideAvailable') === 'true';
|
||||
|
||||
// Validate pagination parameters
|
||||
if (page < 1 || limit < 1 || limit > 100) {
|
||||
@@ -38,12 +39,22 @@ export async function GET(request: NextRequest) {
|
||||
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
// When hideAvailable is enabled, exclude ASINs that are in the library or have completed requests
|
||||
let excludedAsins: string[] = [];
|
||||
if (hideAvailable) {
|
||||
const availableSet = await getAvailableAsins();
|
||||
excludedAsins = [...availableSet];
|
||||
}
|
||||
|
||||
const whereClause = {
|
||||
isPopular: true,
|
||||
...(excludedAsins.length > 0 ? { asin: { notIn: excludedAsins } } : {}),
|
||||
};
|
||||
|
||||
// Query audible_cache for popular audiobooks
|
||||
const [audiobooks, totalCount] = await Promise.all([
|
||||
prisma.audibleCache.findMany({
|
||||
where: {
|
||||
isPopular: true,
|
||||
},
|
||||
where: whereClause,
|
||||
orderBy: {
|
||||
popularRank: 'asc',
|
||||
},
|
||||
@@ -66,9 +77,7 @@ export async function GET(request: NextRequest) {
|
||||
},
|
||||
}),
|
||||
prisma.audibleCache.count({
|
||||
where: {
|
||||
isPopular: true,
|
||||
},
|
||||
where: whereClause,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
||||
+36
-31
@@ -5,12 +5,12 @@
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState, useRef, useMemo } from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { AudiobookGrid } from '@/components/audiobooks/AudiobookGrid';
|
||||
import { useAudiobooks, Audiobook } from '@/lib/hooks/useAudiobooks';
|
||||
import { useAudiobooks } from '@/lib/hooks/useAudiobooks';
|
||||
import { ProtectedRoute } from '@/components/auth/ProtectedRoute';
|
||||
import { StickyPagination } from '@/components/ui/StickyPagination';
|
||||
import { UnifiedPagination } from '@/components/ui/UnifiedPagination';
|
||||
import { SectionToolbar } from '@/components/ui/SectionToolbar';
|
||||
import { usePreferences } from '@/contexts/PreferencesContext';
|
||||
|
||||
@@ -29,24 +29,20 @@ export default function HomePage() {
|
||||
isLoading: loadingPopular,
|
||||
totalPages: popularTotalPages,
|
||||
message: popularMessage,
|
||||
} = useAudiobooks('popular', 20, popularPage);
|
||||
} = useAudiobooks('popular', 20, popularPage, hideAvailable);
|
||||
|
||||
const {
|
||||
audiobooks: newReleases,
|
||||
isLoading: loadingNewReleases,
|
||||
totalPages: newReleasesTotalPages,
|
||||
message: newReleasesMessage,
|
||||
} = useAudiobooks('new-releases', 20, newReleasesPage);
|
||||
} = useAudiobooks('new-releases', 20, newReleasesPage, hideAvailable);
|
||||
|
||||
// Filter out available titles when hideAvailable is enabled
|
||||
const filteredPopular = useMemo(
|
||||
() => hideAvailable ? popular.filter((b: Audiobook) => !b.isAvailable && b.requestStatus !== 'completed') : popular,
|
||||
[popular, hideAvailable]
|
||||
);
|
||||
const filteredNewReleases = useMemo(
|
||||
() => hideAvailable ? newReleases.filter((b: Audiobook) => !b.isAvailable && b.requestStatus !== 'completed') : newReleases,
|
||||
[newReleases, hideAvailable]
|
||||
);
|
||||
// Reset to page 1 when hideAvailable changes (total pages may differ)
|
||||
useEffect(() => {
|
||||
setPopularPage(1);
|
||||
setNewReleasesPage(1);
|
||||
}, [hideAvailable]);
|
||||
|
||||
// Handle page changes with auto-scroll to section top
|
||||
const handlePopularPageChange = (page: number) => {
|
||||
@@ -100,7 +96,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
) : (
|
||||
<AudiobookGrid
|
||||
audiobooks={filteredPopular}
|
||||
audiobooks={popular}
|
||||
isLoading={loadingPopular}
|
||||
emptyMessage="No popular audiobooks available"
|
||||
cardSize={cardSize}
|
||||
@@ -145,7 +141,7 @@ export default function HomePage() {
|
||||
</div>
|
||||
) : (
|
||||
<AudiobookGrid
|
||||
audiobooks={filteredNewReleases}
|
||||
audiobooks={newReleases}
|
||||
isLoading={loadingNewReleases}
|
||||
emptyMessage="No new releases available"
|
||||
cardSize={cardSize}
|
||||
@@ -181,22 +177,31 @@ export default function HomePage() {
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{/* Sticky Pagination Controls */}
|
||||
<StickyPagination
|
||||
currentPage={popularPage}
|
||||
totalPages={popularTotalPages}
|
||||
onPageChange={handlePopularPageChange}
|
||||
sectionRef={popularSectionRef}
|
||||
{/* Unified Pagination — single context-aware pill for both sections */}
|
||||
<UnifiedPagination
|
||||
footerRef={footerRef}
|
||||
label="Popular Audiobooks"
|
||||
/>
|
||||
<StickyPagination
|
||||
currentPage={newReleasesPage}
|
||||
totalPages={newReleasesTotalPages}
|
||||
onPageChange={handleNewReleasesPageChange}
|
||||
sectionRef={newReleasesSectionRef}
|
||||
footerRef={footerRef}
|
||||
label="New Releases"
|
||||
sections={[
|
||||
{
|
||||
label: 'Popular Audiobooks',
|
||||
accentColor: 'bg-blue-500',
|
||||
currentPage: popularPage,
|
||||
totalPages: popularTotalPages,
|
||||
onPageChange: handlePopularPageChange,
|
||||
sectionRef: popularSectionRef,
|
||||
onScrollToSection: () =>
|
||||
popularSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }),
|
||||
},
|
||||
{
|
||||
label: 'New Releases',
|
||||
accentColor: 'bg-emerald-500',
|
||||
currentPage: newReleasesPage,
|
||||
totalPages: newReleasesTotalPages,
|
||||
onPageChange: handleNewReleasesPageChange,
|
||||
sectionRef: newReleasesSectionRef,
|
||||
onScrollToSection: () =>
|
||||
newReleasesSectionRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</ProtectedRoute>
|
||||
|
||||
Reference in New Issue
Block a user