feat: /titles page implementation with cursor pagination

This commit is contained in:
nihonium 2025-11-19 10:54:52 +03:00
parent a515769823
commit 397d2bcf70
Signed by: nihonium
GPG key ID: 0251623741027CFC
37 changed files with 797 additions and 1247 deletions

View file

@ -1,114 +1,52 @@
import React, { useEffect, useState } from "react";
import { ListView } from "../../components/ListView/ListView";
import { DefaultService } from "../../api/services/DefaultService";
import type { Title } from "../../api/models/Title";
import styles from "./TitlesPage.module.css";
import { TitleCardSquare } from "../../components/cards/TitleCardSquare";
import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal";
import type { Title } from "../../api";
import { useState, useEffect } from "react";
const LIMIT = 20;
const TitlesPage: React.FC = () => {
const [titles, setTitles] = useState<Title[]>([]);
const PAGE_SIZE = 20;
export default function TitlesPage() {
const [search, setSearch] = useState("");
const [offset, setOffset] = useState(0);
const [loading, setLoading] = useState(true);
const [loadingMore, setLoadingMore] = useState(false);
const [error, setError] = useState<string | null>(null);
const loadTitles = async (cursor: string, limit: number) => {
const result = await DefaultService.getTitles(
cursor,
undefined,
true,
search,
undefined,
undefined,
undefined,
undefined,
limit,
undefined,
'all'
);
const fetchTitles = async (reset: boolean) => {
try {
if (reset) {
setLoading(true);
setOffset(0);
} else {
setLoadingMore(true);
}
const result = await DefaultService.getTitles(
search || undefined,
undefined, // status
undefined, // rating
undefined, // release_year
undefined, // release_season
LIMIT,
reset ? 0 : offset,
"all"
);
if (reset) {
setTitles(result);
} else {
setTitles(prev => [...prev, ...result]);
}
if (result.length > 0) {
setOffset(prev => prev + LIMIT);
}
} catch (err) {
console.error(err);
setError("Failed to fetch titles.");
} finally {
setLoading(false);
setLoadingMore(false);
}
return {
items: result.data ?? [],
cursor: result.cursor ?? null,
};
};
useEffect(() => {
fetchTitles(true);
}, [search]);
if (loading) return <div className={styles.loader}>Loading...</div>;
if (error) return <div className={styles.error}>{error}</div>;
return (
<div className={styles.container}>
<div className={styles.header}>
<h1>Titles</h1>
<div className="w-full min-h-screen bg-gray-50 p-6 text-black flex flex-col items-center">
<h1 className="text-4xl font-bold mb-6 text-center">Titles</h1>
<input
className={styles.searchInput}
placeholder="Search titles..."
value={search}
onChange={e => setSearch(e.target.value)}
/>
</div>
<div className={styles.list}>
{titles.map((t) => (
<div key={t.id} className={styles.card}>
{t.poster_id ? (
<img
src={`/images/${t.poster_id}.png`}
alt="Poster"
className={styles.poster}
/>
) : (
<div className={styles.posterPlaceholder}>No Image</div>
)}
<div className={styles.cardInfo}>
<h3 className={styles.titleName}>{t.name}</h3>
<p className={styles.meta}>
{t.release_year} {t.release_season}
</p>
<p className={styles.rating}>Rating: {t.rating}</p>
<p className={styles.status}>{t.status}</p>
</div>
</div>
))}
</div>
{titles.length > 0 && (
<button
className={styles.loadMore}
onClick={() => fetchTitles(false)}
disabled={loadingMore}
>
{loadingMore ? "Loading..." : "Load More"}
</button>
)}
<ListView<Title>
pageSize={PAGE_SIZE}
fetchItems={loadTitles}
searchPlaceholder="Search titles..."
renderItem={(title, layout) =>
layout === "square"
? <TitleCardSquare title={title} />
: <TitleCardHorizontal title={title} />
}
setSearch={setSearch}
/>
</div>
);
};
}
export default TitlesPage;