frontend/components/filter-sidebar.tsx

225 lines
7.7 KiB
TypeScript

"use client";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Star, X } from "lucide-react";
import { useGetGenres } from "@/api/mangamochi";
import { useAuth } from "@/contexts/auth-context";
import { Switch } from "@/components/ui/switch";
import { Skeleton } from "@/components/ui/skeleton";
interface FilterSidebarProps {
selectedGenres: number[];
selectedStatus: string[];
minRating: number;
userFavorites: boolean;
showAdultContent: boolean;
onGenresChange: (genres: number[]) => void;
onStatusChange: (status: string[]) => void;
onRatingChange: (rating: number) => void;
onUserFavoritesChange: (favorites: boolean) => void;
onShowAdultContentChange: (showAdult: boolean) => void;
}
const STATUSES = ["Ongoing", "Completed", "Hiatus"];
const RATINGS = [
{ label: "8.5+ Stars", value: 8.5 },
{ label: "7.0+ Stars", value: 7.0 },
{ label: "5.0+ Stars", value: 5.0 },
{ label: "All Ratings", value: 0 },
];
export function FilterSidebar({
selectedGenres,
selectedStatus,
minRating,
userFavorites,
showAdultContent,
onGenresChange,
onStatusChange,
onRatingChange,
onUserFavoritesChange,
onShowAdultContentChange,
}: FilterSidebarProps) {
const { data: genresData, isPending: isPendingGenres } = useGetGenres();
const { isAuthenticated } = useAuth();
const toggleGenre = (genre: number) => {
if (selectedGenres.includes(genre)) {
onGenresChange(selectedGenres.filter((g) => g !== genre));
} else {
onGenresChange([...selectedGenres, genre]);
}
};
const toggleStatus = (status: string) => {
if (selectedStatus.includes(status)) {
onStatusChange(selectedStatus.filter((s) => s !== status));
} else {
onStatusChange([...selectedStatus, status]);
}
};
const clearAllFilters = () => {
onGenresChange([]);
onStatusChange([]);
onRatingChange(0);
onUserFavoritesChange(false);
onShowAdultContentChange(false);
};
const hasActiveFilters =
selectedGenres.length > 0 ||
selectedStatus.length > 0 ||
minRating > 0 ||
userFavorites;
return (
<aside className="w-64 border-r border-border bg-sidebar">
<div className="sticky top-0 flex h-screen flex-col">
{/* Header */}
<div className="flex items-center justify-between border-b border-sidebar-border px-6 py-6">
<h2 className="text-lg font-semibold text-sidebar-foreground">
Filters
</h2>
{hasActiveFilters && (
<Button
variant="ghost"
size="sm"
onClick={clearAllFilters}
className="h-8 px-2 text-xs"
>
Clear
</Button>
)}
</div>
{/* Filters */}
<div className="flex-1 overflow-y-auto px-6 py-6">
<div className="space-y-8">
{isAuthenticated && (
<>
<div>
<h3 className="mb-3 text-sm font-medium text-sidebar-foreground">
Content
</h3>
<div className="flex items-center justify-between rounded-md px-3 py-2">
<label className="text-sm text-sidebar-foreground">
Show Only Favorites
</label>
<Switch
checked={userFavorites}
onCheckedChange={onUserFavoritesChange}
/>
</div>
<div className="flex items-center justify-between rounded-md px-3 py-2">
<label className="text-sm text-sidebar-foreground">
Show Adult Content
</label>
<Switch
checked={showAdultContent}
onCheckedChange={onShowAdultContentChange}
/>
</div>
</div>
<Separator className="bg-sidebar-border" />
</>
)}
{/* Genres */}
<div>
<h3 className="mb-3 text-sm font-medium text-sidebar-foreground">
Genres
</h3>
<div className="flex flex-wrap gap-2">
{isPendingGenres && <Skeleton className="h-[240px] w-full" />}
{!isPendingGenres &&
genresData?.data?.map((genre) => {
const isSelected = selectedGenres.includes(genre.id);
return (
<Badge
key={genre.id}
variant={isSelected ? "default" : "outline"}
className={`cursor-pointer transition-colors ${
isSelected
? "bg-sidebar-primary text-sidebar-primary-foreground hover:bg-sidebar-primary/90"
: "border-sidebar-border text-sidebar-foreground hover:bg-sidebar-accent"
}`}
onClick={() => toggleGenre(genre.id)}
>
{genre.name}
{isSelected && <X className="ml-1 h-3 w-3" />}
</Badge>
);
})}
</div>
</div>
<Separator className="bg-sidebar-border" />
{/* Status */}
<div>
<h3 className="mb-3 text-sm font-medium text-sidebar-foreground">
Status
</h3>
<div className="flex flex-wrap gap-2">
{STATUSES.map((status) => {
const isSelected = selectedStatus.includes(status);
return (
<Badge
key={status}
variant={isSelected ? "default" : "outline"}
className={`cursor-pointer transition-colors ${
isSelected
? "bg-sidebar-primary text-sidebar-primary-foreground hover:bg-sidebar-primary/90"
: "border-sidebar-border text-sidebar-foreground hover:bg-sidebar-accent"
}`}
onClick={() => toggleStatus(status)}
>
{status}
{isSelected && <X className="ml-1 h-3 w-3" />}
</Badge>
);
})}
</div>
</div>
<Separator className="bg-sidebar-border" />
{/* Rating */}
<div>
<h3 className="mb-3 text-sm font-medium text-sidebar-foreground">
Minimum Rating
</h3>
<div className="space-y-2">
{RATINGS.map((rating) => {
const isSelected = minRating === rating.value;
return (
<button
key={rating.value}
onClick={() => onRatingChange(rating.value)}
className={`flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm transition-colors ${
isSelected
? "bg-sidebar-accent text-sidebar-accent-foreground"
: "text-sidebar-foreground hover:bg-sidebar-accent/50"
}`}
>
<Star
className={`h-4 w-4 ${isSelected ? "fill-sidebar-primary text-sidebar-primary" : "text-muted-foreground"}`}
/>
<span>{rating.label}</span>
</button>
);
})}
</div>
</div>
</div>
</div>
</div>
</aside>
);
}