feat: TitlesFilterPanel component
All checks were successful
Build and Deploy Go App / build (push) Successful in 6m16s
Build and Deploy Go App / deploy (push) Successful in 37s

This commit is contained in:
nihonium 2025-12-04 05:52:31 +03:00
parent ab29c33f5b
commit 4dd60f3b19
Signed by: nihonium
GPG key ID: 0251623741027CFC
3 changed files with 142 additions and 6 deletions

View file

@ -20,6 +20,7 @@ export class DefaultService {
* @param cursor * @param cursor
* @param sort * @param sort
* @param sortForward * @param sortForward
* @param extSearch
* @param word * @param word
* @param status List of title statuses to filter * @param status List of title statuses to filter
* @param rating * @param rating
@ -35,6 +36,7 @@ export class DefaultService {
cursor?: string, cursor?: string,
sort?: TitleSort, sort?: TitleSort,
sortForward: boolean = true, sortForward: boolean = true,
extSearch: boolean = false,
word?: string, word?: string,
status?: Array<TitleStatus>, status?: Array<TitleStatus>,
rating?: number, rating?: number,
@ -57,6 +59,7 @@ export class DefaultService {
'cursor': cursor, 'cursor': cursor,
'sort': sort, 'sort': sort,
'sort_forward': sortForward, 'sort_forward': sortForward,
'ext_search': extSearch,
'word': word, 'word': word,
'status': status, 'status': status,
'rating': rating, 'rating': rating,

View file

@ -0,0 +1,122 @@
import { useState } from "react";
import type { TitleStatus, ReleaseSeason } from "../../api";
import { ChevronDownIcon, ChevronUpIcon } from "@heroicons/react/24/solid";
export type TitlesFilter = {
extSearch: boolean;
status: TitleStatus | "";
rating: number | "";
releaseYear: number | "";
releaseSeason: ReleaseSeason | "";
};
type TitlesFilterPanelProps = {
filters: TitlesFilter;
setFilters: (filters: TitlesFilter) => void;
};
const STATUS_OPTIONS: (TitleStatus | "")[] = ["", "planned", "finished", "ongoing"];
const SEASON_OPTIONS: (ReleaseSeason | "")[] = ["", "winter", "spring", "summer", "fall"];
const RATING_OPTIONS = ["", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
export function TitlesFilterPanel({ filters, setFilters }: TitlesFilterPanelProps) {
const [open, setOpen] = useState(false);
const handleChange = (field: keyof TitlesFilter, value: any) => {
setFilters({ ...filters, [field]: value });
};
return (
<div className="w-full flex justify-center my-4">
<div className="bg-white shadow rounded-lg w-full max-w-3xl p-4">
{/* Заголовок панели */}
<div
className="flex justify-between items-center cursor-pointer"
onClick={() => setOpen((prev) => !prev)}
>
<h3 className="text-lg font-medium">Filters</h3>
{open ? <ChevronUpIcon className="w-5 h-5" /> : <ChevronDownIcon className="w-5 h-5" />}
</div>
{/* Контент панели */}
{open && (
<div className="mt-4 grid grid-cols-2 sm:grid-cols-3 gap-4">
{/* Extended Search */}
<div className="flex items-center gap-2">
<input
type="checkbox"
id="extSearch"
checked={filters.extSearch}
onChange={(e) => handleChange("extSearch", e.target.checked)}
className="w-4 h-4"
/>
<label htmlFor="extSearch" className="text-sm">
Extended Search
</label>
</div>
{/* Status */}
<div className="flex flex-col">
<label htmlFor="status" className="text-sm mb-1">Status</label>
<select
id="status"
value={filters.status}
onChange={(e) => handleChange("status", e.target.value || "")}
className="border rounded px-2 py-1"
>
{STATUS_OPTIONS.map((s) => (
<option key={s || "all"} value={s}>{s || "All"}</option>
))}
</select>
</div>
{/* Rating */}
<div className="flex flex-col">
<label htmlFor="rating" className="text-sm mb-1">Rating</label>
<select
id="rating"
value={filters.rating}
onChange={(e) => handleChange("rating", e.target.value ? Number(e.target.value) : "")}
className="border rounded px-2 py-1"
>
{RATING_OPTIONS.map((r) => (
<option key={r} value={r}>{r || "All"}</option>
))}
</select>
</div>
{/* Release Year */}
<div className="flex flex-col">
<label htmlFor="releaseYear" className="text-sm mb-1">Release Year</label>
<input
type="number"
id="releaseYear"
value={filters.releaseYear || ""}
onChange={(e) =>
handleChange("releaseYear", e.target.value ? Number(e.target.value) : "")
}
className="border rounded px-2 py-1"
placeholder="Any"
/>
</div>
{/* Release Season */}
<div className="flex flex-col">
<label htmlFor="releaseSeason" className="text-sm mb-1">Release Season</label>
<select
id="releaseSeason"
value={filters.releaseSeason}
onChange={(e) => handleChange("releaseSeason", e.target.value || "")}
className="border rounded px-2 py-1"
>
{SEASON_OPTIONS.map((s) => (
<option key={s || "all"} value={s}>{s || "All"}</option>
))}
</select>
</div>
</div>
)}
</div>
</div>
);
}

View file

@ -8,6 +8,7 @@ import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal"
import type { CursorObj, Title, TitleSort } from "../../api"; import type { CursorObj, Title, TitleSort } from "../../api";
import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch"; import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { type TitlesFilter, TitlesFilterPanel } from "../../components/TitlesFilterPanel/TitlesFilterPanel";
const PAGE_SIZE = 10; const PAGE_SIZE = 10;
@ -22,6 +23,14 @@ export default function TitlesPage() {
const [sortForward, setSortForward] = useState(true); const [sortForward, setSortForward] = useState(true);
const [layout, setLayout] = useState<"square" | "horizontal">("square"); const [layout, setLayout] = useState<"square" | "horizontal">("square");
const [filters, setFilters] = useState<TitlesFilter>({
extSearch: false,
status: "",
rating: "",
releaseYear: "",
releaseSeason: "",
});
const fetchPage = async (cursorObj: CursorObj | null) => { const fetchPage = async (cursorObj: CursorObj | null) => {
const cursorStr = cursorObj ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') : ""; const cursorStr = cursorObj ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') : "";
@ -30,13 +39,14 @@ export default function TitlesPage() {
cursorStr, cursorStr,
sort, sort,
sortForward, sortForward,
filters.extSearch,
search.trim() || undefined, search.trim() || undefined,
undefined, filters.status ? [filters.status] : undefined,
undefined, filters.rating || undefined,
undefined, filters.releaseYear || undefined,
undefined, filters.releaseSeason || undefined,
PAGE_SIZE,
PAGE_SIZE, PAGE_SIZE,
undefined,
"all" "all"
); );
@ -73,7 +83,7 @@ export default function TitlesPage() {
}; };
initLoad(); initLoad();
}, [search, sort, sortForward]); }, [search, sort, sortForward, filters]);
const handleLoadMore = async () => { const handleLoadMore = async () => {
@ -121,6 +131,7 @@ const handleLoadMore = async () => {
setSortForward={setSortForward} setSortForward={setSortForward}
/> />
</div> </div>
<TitlesFilterPanel filters={filters} setFilters={setFilters} />
{loading && <div className="mt-20 font-medium text-black">Loading...</div>} {loading && <div className="mt-20 font-medium text-black">Loading...</div>}