fix(frontend): rewrite FiltersSortingPanel with shadcn, correct styling, useTransition
This commit is contained in:
@@ -1,6 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useTransition } from "react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Separator } from "@/components/ui/separator";
|
||||||
import { FeedSortOption } from "@/lib/api";
|
import { FeedSortOption } from "@/lib/api";
|
||||||
|
|
||||||
const SORT_OPTIONS: { value: FeedSortOption; label: string }[] = [
|
const SORT_OPTIONS: { value: FeedSortOption; label: string }[] = [
|
||||||
@@ -14,112 +19,148 @@ const SORT_OPTIONS: { value: FeedSortOption; label: string }[] = [
|
|||||||
export function FiltersSortingPanel() {
|
export function FiltersSortingPanel() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
|
||||||
const currentSort = (searchParams.get("sort") ?? "newest") as FeedSortOption;
|
const [sort, setSort] = useState<FeedSortOption>(
|
||||||
const originalsOnly = searchParams.get("originals_only") === "true";
|
(searchParams.get("sort") as FeedSortOption | null) ?? "newest"
|
||||||
const repliesOnly = searchParams.get("replies_only") === "true";
|
);
|
||||||
const localOnly = searchParams.get("local_only") === "true";
|
const [originalsOnly, setOriginalsOnly] = useState(
|
||||||
const hideSensitive = searchParams.get("hide_sensitive") === "true";
|
searchParams.get("originals_only") === "true"
|
||||||
|
);
|
||||||
|
const [repliesOnly, setRepliesOnly] = useState(
|
||||||
|
searchParams.get("replies_only") === "true"
|
||||||
|
);
|
||||||
|
const [localOnly, setLocalOnly] = useState(
|
||||||
|
searchParams.get("local_only") === "true"
|
||||||
|
);
|
||||||
|
const [hideSensitive, setHideSensitive] = useState(
|
||||||
|
searchParams.get("hide_sensitive") === "true"
|
||||||
|
);
|
||||||
|
|
||||||
function update(key: string, value: string | null) {
|
function pushParams(updates: Record<string, string | null>) {
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
params.delete("page");
|
params.delete("page");
|
||||||
|
for (const [key, value] of Object.entries(updates)) {
|
||||||
if (value === null) {
|
if (value === null) {
|
||||||
params.delete(key);
|
params.delete(key);
|
||||||
} else {
|
} else {
|
||||||
params.set(key, value);
|
params.set(key, value);
|
||||||
}
|
}
|
||||||
router.push(`/?${params.toString()}`);
|
}
|
||||||
|
startTransition(() => router.replace(`/?${params.toString()}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setSort(value: FeedSortOption) {
|
function handleSort(value: FeedSortOption) {
|
||||||
update("sort", value === "newest" ? null : value);
|
setSort(value);
|
||||||
|
pushParams({ sort: value === "newest" ? null : value });
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFilter(key: string, current: boolean) {
|
function handleOriginalsOnly(checked: boolean) {
|
||||||
update(key, current ? null : "true");
|
setOriginalsOnly(checked);
|
||||||
|
if (checked) setRepliesOnly(false);
|
||||||
|
const updates: Record<string, string | null> = {
|
||||||
|
originals_only: checked ? "true" : null,
|
||||||
|
};
|
||||||
|
if (checked) updates.replies_only = null;
|
||||||
|
pushParams(updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRepliesOnly(checked: boolean) {
|
||||||
|
setRepliesOnly(checked);
|
||||||
|
if (checked) setOriginalsOnly(false);
|
||||||
|
const updates: Record<string, string | null> = {
|
||||||
|
replies_only: checked ? "true" : null,
|
||||||
|
};
|
||||||
|
if (checked) updates.originals_only = null;
|
||||||
|
pushParams(updates);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLocalOnly(checked: boolean) {
|
||||||
|
setLocalOnly(checked);
|
||||||
|
pushParams({ local_only: checked ? "true" : null });
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleHideSensitive(checked: boolean) {
|
||||||
|
setHideSensitive(checked);
|
||||||
|
pushParams({ hide_sensitive: checked ? "true" : null });
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div
|
||||||
|
className={`space-y-3 transition-opacity duration-150 ${
|
||||||
|
isPending ? "opacity-50 pointer-events-none" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold mb-2">Sort by</h3>
|
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||||
<div className="space-y-1">
|
Sort by
|
||||||
|
</p>
|
||||||
|
<RadioGroup
|
||||||
|
value={sort}
|
||||||
|
onValueChange={(v) => handleSort(v as FeedSortOption)}
|
||||||
|
className="space-y-1"
|
||||||
|
>
|
||||||
{SORT_OPTIONS.map((opt) => (
|
{SORT_OPTIONS.map((opt) => (
|
||||||
<label key={opt.value} className="flex items-center gap-2 cursor-pointer">
|
<div key={opt.value} className="flex items-center gap-2">
|
||||||
<input
|
<RadioGroupItem value={opt.value} id={`sort-${opt.value}`} />
|
||||||
type="radio"
|
<Label
|
||||||
name="feed-sort"
|
htmlFor={`sort-${opt.value}`}
|
||||||
value={opt.value}
|
className="text-xs font-normal cursor-pointer"
|
||||||
checked={currentSort === opt.value}
|
>
|
||||||
onChange={() => setSort(opt.value)}
|
{opt.label}
|
||||||
className="accent-primary"
|
</Label>
|
||||||
/>
|
|
||||||
<span className="text-sm">{opt.label}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-sm font-semibold mb-2">Filter</h3>
|
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wide mb-2">
|
||||||
<div className="space-y-1">
|
Filter
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
</p>
|
||||||
<input
|
<div className="space-y-2">
|
||||||
type="checkbox"
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="originals-only"
|
||||||
checked={originalsOnly}
|
checked={originalsOnly}
|
||||||
onChange={() => {
|
onCheckedChange={(c) => handleOriginalsOnly(c === true)}
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
|
||||||
params.delete("page");
|
|
||||||
if (!originalsOnly) {
|
|
||||||
params.delete("replies_only");
|
|
||||||
params.set("originals_only", "true");
|
|
||||||
} else {
|
|
||||||
params.delete("originals_only");
|
|
||||||
}
|
|
||||||
router.push(`/?${params.toString()}`);
|
|
||||||
}}
|
|
||||||
className="accent-primary"
|
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">Originals only</span>
|
<Label htmlFor="originals-only" className="text-xs font-normal cursor-pointer">
|
||||||
</label>
|
Originals only
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
</Label>
|
||||||
<input
|
</div>
|
||||||
type="checkbox"
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="replies-only"
|
||||||
checked={repliesOnly}
|
checked={repliesOnly}
|
||||||
onChange={() => {
|
onCheckedChange={(c) => handleRepliesOnly(c === true)}
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
|
||||||
params.delete("page");
|
|
||||||
if (!repliesOnly) {
|
|
||||||
params.delete("originals_only");
|
|
||||||
params.set("replies_only", "true");
|
|
||||||
} else {
|
|
||||||
params.delete("replies_only");
|
|
||||||
}
|
|
||||||
router.push(`/?${params.toString()}`);
|
|
||||||
}}
|
|
||||||
className="accent-primary"
|
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">Replies only</span>
|
<Label htmlFor="replies-only" className="text-xs font-normal cursor-pointer">
|
||||||
</label>
|
Replies only
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
</Label>
|
||||||
<input
|
</div>
|
||||||
type="checkbox"
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="local-only"
|
||||||
checked={localOnly}
|
checked={localOnly}
|
||||||
onChange={() => toggleFilter("local_only", localOnly)}
|
onCheckedChange={(c) => handleLocalOnly(c === true)}
|
||||||
className="accent-primary"
|
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">Local only</span>
|
<Label htmlFor="local-only" className="text-xs font-normal cursor-pointer">
|
||||||
</label>
|
Local only
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
</Label>
|
||||||
<input
|
</div>
|
||||||
type="checkbox"
|
<div className="flex items-center gap-2">
|
||||||
|
<Checkbox
|
||||||
|
id="hide-sensitive"
|
||||||
checked={hideSensitive}
|
checked={hideSensitive}
|
||||||
onChange={() => toggleFilter("hide_sensitive", hideSensitive)}
|
onCheckedChange={(c) => handleHideSensitive(c === true)}
|
||||||
className="accent-primary"
|
|
||||||
/>
|
/>
|
||||||
<span className="text-sm">Hide sensitive</span>
|
<Label htmlFor="hide-sensitive" className="text-xs font-normal cursor-pointer">
|
||||||
</label>
|
Hide sensitive
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user