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 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,
|
||||||
|
|
|
||||||
|
|
@ -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 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>}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue