Initial commit
This commit is contained in:
+146
@@ -0,0 +1,146 @@
|
||||
import { listImages, countImages, libraryStats, libraryLetterCounts } from "@/lib/db/queries";
|
||||
import { LibraryGrid } from "@/components/grid/LibraryGrid";
|
||||
import { getAppSetting } from "@/lib/db/appSettings";
|
||||
import { UploadCard } from "@/components/ingest/UploadCard";
|
||||
import { RegisterVisible } from "@/components/select/RegisterVisible";
|
||||
import { FilterBar } from "@/components/grid/FilterBar";
|
||||
import { LetterBar } from "@/components/grid/LetterBar";
|
||||
import type { LibraryView } from "@/components/grid/ViewToggle";
|
||||
import { resolveSort } from "@/lib/sortServer";
|
||||
import { parseFilterCriteria, anyActive as hasAnyCriteria, statusToFlags } from "@/lib/filters";
|
||||
import { Disc3 } from "lucide-react";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function Home({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<Record<string, string | string[] | undefined>>;
|
||||
}) {
|
||||
const sp = await searchParams;
|
||||
const criteria = parseFilterCriteria(sp);
|
||||
const sort = await resolveSort(typeof sp.sort === "string" ? sp.sort : undefined);
|
||||
const rawLetter = (typeof sp.letter === "string" ? sp.letter : "").toUpperCase();
|
||||
const letter = rawLetter === "#" ? "#" : (/^[A-Z]$/.test(rawLetter) ? rawLetter : null);
|
||||
const search = (typeof sp.q === "string" ? sp.q.trim() : "") || undefined;
|
||||
const view: LibraryView = sp.view === "portrait" ? "portrait" : "landscape";
|
||||
const anyActive = hasAnyCriteria(criteria) || letter != null || !!search;
|
||||
|
||||
// URL pagination — `page` is the anchor (1-based). Page size from
|
||||
// user settings (Settings → Appearance → Items Per Page). Negative
|
||||
// / non-numeric params clamp to 1.
|
||||
const PAGE_SIZE = Math.max(25, Math.min(500, getAppSetting("coverPageSize") ?? 100));
|
||||
const rawPage = typeof sp.page === "string" ? Number(sp.page) : NaN;
|
||||
const page = Number.isFinite(rawPage) && rawPage >= 1 ? Math.floor(rawPage) : 1;
|
||||
const offset = (page - 1) * PAGE_SIZE;
|
||||
|
||||
const filterOpts = {
|
||||
sort,
|
||||
letter: letter ?? undefined,
|
||||
search,
|
||||
...statusToFlags(criteria.status),
|
||||
marks: criteria.marks,
|
||||
actressIds: criteria.ids.actresses,
|
||||
actressMode: criteria.mode.actresses,
|
||||
studioIds: criteria.ids.studios,
|
||||
seriesIds: criteria.ids.series,
|
||||
genreIds: criteria.ids.genres,
|
||||
genreMode: criteria.mode.genres,
|
||||
collectionIds: criteria.ids.collections,
|
||||
collectionMode: criteria.mode.collections,
|
||||
tagIds: criteria.ids.tags,
|
||||
tagMode: criteria.mode.tags,
|
||||
categoryIds: criteria.ids.categories,
|
||||
categoryMode: criteria.mode.categories,
|
||||
};
|
||||
const totalCount = countImages(filterOpts);
|
||||
const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE));
|
||||
// Clamp out-of-range page back to last; cheap re-fetch since we only
|
||||
// care about offset and we already have the count.
|
||||
const effectivePage = Math.min(page, totalPages);
|
||||
const effectiveOffset = (effectivePage - 1) * PAGE_SIZE;
|
||||
|
||||
const items = listImages({ ...filterOpts, limit: PAGE_SIZE, offset: effectiveOffset });
|
||||
const stats = libraryStats();
|
||||
const letterCounts = libraryLetterCounts({
|
||||
...statusToFlags(criteria.status),
|
||||
marks: criteria.marks,
|
||||
actressIds: criteria.ids.actresses,
|
||||
actressMode: criteria.mode.actresses,
|
||||
studioIds: criteria.ids.studios,
|
||||
seriesIds: criteria.ids.series,
|
||||
genreIds: criteria.ids.genres,
|
||||
genreMode: criteria.mode.genres,
|
||||
collectionIds: criteria.ids.collections,
|
||||
collectionMode: criteria.mode.collections,
|
||||
tagIds: criteria.ids.tags,
|
||||
tagMode: criteria.mode.tags,
|
||||
categoryIds: criteria.ids.categories,
|
||||
categoryMode: criteria.mode.categories,
|
||||
search,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="max-w-[1600px] mx-auto px-6 py-8 fade-in">
|
||||
<section className="mb-6 grid grid-cols-1 lg:grid-cols-[1fr_minmax(280px,360px)] gap-6 items-start">
|
||||
<div>
|
||||
<h1 className="text-4xl font-semibold tracking-tight">
|
||||
Your <span className="text-gradient-accent">Cover Library</span>
|
||||
</h1>
|
||||
<p className="text-[var(--color-fg-dim)] mt-2 max-w-prose">
|
||||
Drop cover images to import. Codes are parsed from filenames; metadata can be filled in
|
||||
manually or seeded from a sibling <span className="font-mono">.nfo</span> file. Tag, collect,
|
||||
rate, and search.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-6 mt-6 text-sm">
|
||||
<Stat label="Covers" value={stats.images} />
|
||||
<Stat label="Actresses" value={stats.actresses} />
|
||||
<Stat label="Studios" value={stats.studios} />
|
||||
<Stat label="Tags" value={stats.tags} />
|
||||
<Stat label="Collections" value={stats.collections} />
|
||||
</div>
|
||||
</div>
|
||||
<UploadCard />
|
||||
</section>
|
||||
|
||||
<FilterBar current={{ kind: "all" }} criteria={criteria} sort={sort} view={view} />
|
||||
<div className="my-6">
|
||||
<LetterBar active={letter} counts={letterCounts} />
|
||||
</div>
|
||||
|
||||
<RegisterVisible ids={items.map((i) => i.id)} />
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="glass rounded-2xl p-card text-center">
|
||||
<Disc3 className="w-10 h-10 mx-auto text-[var(--color-cyan)] mb-label" />
|
||||
<h2 className="text-xl font-medium">
|
||||
{anyActive ? "Nothing Matches" : "Nothing Here Yet"}
|
||||
</h2>
|
||||
<p className="text-[var(--color-fg-dim)] mt-1">
|
||||
{anyActive ? "All filtered out — switch back to All." : "Drag a few covers above to get started."}
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div key={`${view}-${letter ?? "all"}-${effectivePage}`} className="fade-in">
|
||||
<LibraryGrid
|
||||
initialItems={items}
|
||||
initialPage={effectivePage}
|
||||
totalPages={totalPages}
|
||||
totalCount={totalCount}
|
||||
view={view}
|
||||
infiniteScrollEnabled
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Stat({ label, value }: { label: string; value: number }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="text-2xl font-mono font-semibold tabular-nums">{value}</div>
|
||||
<div className="text-xs uppercase tracking-wider text-[var(--color-fg-muted)]">{label}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user