feat: TitlesFilterPanel component
This commit is contained in:
parent
ab29c33f5b
commit
4dd60f3b19
3 changed files with 142 additions and 6 deletions
|
|
@ -20,6 +20,7 @@ export class DefaultService {
|
|||
* @param cursor
|
||||
* @param sort
|
||||
* @param sortForward
|
||||
* @param extSearch
|
||||
* @param word
|
||||
* @param status List of title statuses to filter
|
||||
* @param rating
|
||||
|
|
@ -35,6 +36,7 @@ export class DefaultService {
|
|||
cursor?: string,
|
||||
sort?: TitleSort,
|
||||
sortForward: boolean = true,
|
||||
extSearch: boolean = false,
|
||||
word?: string,
|
||||
status?: Array<TitleStatus>,
|
||||
rating?: number,
|
||||
|
|
@ -57,6 +59,7 @@ export class DefaultService {
|
|||
'cursor': cursor,
|
||||
'sort': sort,
|
||||
'sort_forward': sortForward,
|
||||
'ext_search': extSearch,
|
||||
'word': word,
|
||||
'status': status,
|
||||
'rating': rating,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import { TitleCardHorizontal } from "../../components/cards/TitleCardHorizontal"
|
|||
import type { CursorObj, Title, TitleSort } from "../../api";
|
||||
import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch";
|
||||
import { Link } from "react-router-dom";
|
||||
import { type TitlesFilter, TitlesFilterPanel } from "../../components/TitlesFilterPanel/TitlesFilterPanel";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
|
|
@ -22,6 +23,14 @@ export default function TitlesPage() {
|
|||
const [sortForward, setSortForward] = useState(true);
|
||||
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 cursorStr = cursorObj ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '') : "";
|
||||
|
||||
|
|
@ -30,13 +39,14 @@ export default function TitlesPage() {
|
|||
cursorStr,
|
||||
sort,
|
||||
sortForward,
|
||||
filters.extSearch,
|
||||
search.trim() || undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
filters.status ? [filters.status] : undefined,
|
||||
filters.rating || undefined,
|
||||
filters.releaseYear || undefined,
|
||||
filters.releaseSeason || undefined,
|
||||
PAGE_SIZE,
|
||||
PAGE_SIZE,
|
||||
undefined,
|
||||
"all"
|
||||
);
|
||||
|
||||
|
|
@ -73,7 +83,7 @@ export default function TitlesPage() {
|
|||
};
|
||||
|
||||
initLoad();
|
||||
}, [search, sort, sortForward]);
|
||||
}, [search, sort, sortForward, filters]);
|
||||
|
||||
|
||||
const handleLoadMore = async () => {
|
||||
|
|
@ -121,6 +131,7 @@ const handleLoadMore = async () => {
|
|||
setSortForward={setSortForward}
|
||||
/>
|
||||
</div>
|
||||
<TitlesFilterPanel filters={filters} setFilters={setFilters} />
|
||||
|
||||
{loading && <div className="mt-20 font-medium text-black">Loading...</div>}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue