mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-07-01 15:23:58 +00:00
chore: upgrade to tailwind v4 and add a generic filter for dynamic filter handling
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 7m51s
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 7m51s
This commit is contained in:
@ -1,4 +1,7 @@
|
||||
"use client";
|
||||
import { ListFilter, Loader2, X } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useState, useTransition } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@ -13,11 +16,8 @@ import {
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label"; // Assuming shadcn Label
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ListFilter, Loader2, X } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useMemo, useState, useTransition } from "react";
|
||||
import { RadioGroup, RadioGroupItem } from "./ui/radio-group";
|
||||
|
||||
interface CheckboxGroupProps {
|
||||
@ -48,7 +48,10 @@ const CheckboxGroup: React.FC<CheckboxGroupProps> = ({
|
||||
<Label className="font-semibold text-sm">{label}</Label>
|
||||
<div className="flex flex-col gap-2">
|
||||
{options.map((option) => (
|
||||
<div key={`${name}-${option.value}`} className="flex items-center space-x-2">
|
||||
<div
|
||||
key={`${name}-${option.value}`}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<Checkbox
|
||||
id={`${name}-${option.value}`}
|
||||
checked={selectedValues.includes(option.value)}
|
||||
@ -64,7 +67,6 @@ const CheckboxGroup: React.FC<CheckboxGroupProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
type FilterOption = { value: string; label: string };
|
||||
|
||||
type FilterInputConfig<TKey extends string> =
|
||||
@ -90,8 +92,10 @@ type FilterInputConfig<TKey extends string> =
|
||||
};
|
||||
|
||||
// Utility type to extract the config for a specific key from the array
|
||||
type GetConfigForKey<TKey, TConfigArray extends FilterInputConfig<string>[]> =
|
||||
TConfigArray extends ReadonlyArray<infer U>
|
||||
type GetConfigForKey<
|
||||
TKey,
|
||||
TConfigArray extends FilterInputConfig<string>[],
|
||||
> = TConfigArray extends ReadonlyArray<infer U>
|
||||
? U extends FilterInputConfig<string> & { name: TKey }
|
||||
? U
|
||||
: never
|
||||
@ -102,7 +106,9 @@ type FilterValues<
|
||||
TFilterKeys extends string,
|
||||
TInputs extends FilterInputConfig<string>[],
|
||||
> = {
|
||||
[K in TFilterKeys]: GetConfigForKey<K, TInputs> extends { type: "checkbox-group" }
|
||||
[K in TFilterKeys]: GetConfigForKey<K, TInputs> extends {
|
||||
type: "checkbox-group";
|
||||
}
|
||||
? string[]
|
||||
: GetConfigForKey<K, TInputs> extends { type: "radio-group" }
|
||||
? string
|
||||
@ -124,7 +130,6 @@ interface DynamicFilterProps<
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// --- Main DynamicFilter Component ---
|
||||
export default function DynamicFilter<
|
||||
TFilterKeys extends string,
|
||||
TInputs extends FilterInputConfig<TFilterKeys>[],
|
||||
@ -153,17 +158,24 @@ export default function DynamicFilter<
|
||||
const urlValue = searchParams.get(input.name);
|
||||
if (input.type === "checkbox-group") {
|
||||
const deserialize = input.deserialize || defaultDeserialize;
|
||||
(initialState as FilterValues<TFilterKeys, TInputs>)[input.name] = deserialize(urlValue || "") as FilterValues<TFilterKeys, TInputs>[typeof input.name];
|
||||
(initialState as FilterValues<TFilterKeys, TInputs>)[input.name] =
|
||||
deserialize(urlValue || "") as FilterValues<
|
||||
TFilterKeys,
|
||||
TInputs
|
||||
>[typeof input.name];
|
||||
} else {
|
||||
(initialState as FilterValues<TFilterKeys, TInputs>)[input.name] = (urlValue || "") as FilterValues<TFilterKeys, TInputs>[typeof input.name];
|
||||
(initialState as FilterValues<TFilterKeys, TInputs>)[input.name] =
|
||||
(urlValue || "") as FilterValues<
|
||||
TFilterKeys,
|
||||
TInputs
|
||||
>[typeof input.name];
|
||||
}
|
||||
}
|
||||
return initialState as FilterValues<TFilterKeys, TInputs>;
|
||||
}, [inputs, searchParams]); // Re-initialize if inputs config or URL searchParams change
|
||||
|
||||
const [inputValues, setInputValues] = useState<
|
||||
FilterValues<TFilterKeys, TInputs>
|
||||
>(initialInputState);
|
||||
const [inputValues, setInputValues] =
|
||||
useState<FilterValues<TFilterKeys, TInputs>>(initialInputState);
|
||||
|
||||
// Update local state if URL searchParams change while drawer is closed
|
||||
// This is important if filters are applied externally or on page load
|
||||
@ -173,7 +185,6 @@ export default function DynamicFilter<
|
||||
}
|
||||
}, [isOpen, initialInputState]);
|
||||
|
||||
|
||||
// Handler for text/number input changes
|
||||
const handleTextOrNumberInputChange =
|
||||
(name: TFilterKeys) => (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@ -217,31 +228,34 @@ export default function DynamicFilter<
|
||||
}
|
||||
}
|
||||
|
||||
newParams.set("page", "1"); // Always reset page on filter apply
|
||||
newParams.set("page", "1");
|
||||
|
||||
startTransition(() => {
|
||||
replace(`${pathname}?${newParams.toString()}`);
|
||||
setIsOpen(false); // Close the drawer after applying filters
|
||||
setIsOpen(false);
|
||||
});
|
||||
};
|
||||
|
||||
// Handles clearing all filters
|
||||
const handleClearFilters = () => {
|
||||
|
||||
// Reset local input values
|
||||
const clearedInputState: Partial<FilterValues<TFilterKeys, TInputs>> = {};
|
||||
for (const input of inputs) {
|
||||
if (input.type === "checkbox-group") {
|
||||
(clearedInputState as FilterValues<TFilterKeys, TInputs>)[input.name as TFilterKeys] = [] as any;
|
||||
(clearedInputState as FilterValues<TFilterKeys, TInputs>)[
|
||||
input.name as TFilterKeys
|
||||
] = [] as any;
|
||||
} else {
|
||||
(clearedInputState as FilterValues<TFilterKeys, TInputs>)[input.name as TFilterKeys] = "" as any;
|
||||
(clearedInputState as FilterValues<TFilterKeys, TInputs>)[
|
||||
input.name as TFilterKeys
|
||||
] = "" as any;
|
||||
}
|
||||
}
|
||||
setInputValues(clearedInputState as FilterValues<TFilterKeys, TInputs>);
|
||||
|
||||
startTransition(() => {
|
||||
replace(pathname); // Navigate to base path
|
||||
setIsOpen(false); // Close the drawer
|
||||
replace(pathname);
|
||||
setIsOpen(false);
|
||||
});
|
||||
};
|
||||
|
||||
@ -289,13 +303,12 @@ export default function DynamicFilter<
|
||||
) => {
|
||||
if (config.type === "checkbox-group") {
|
||||
const labels = (value as string[])
|
||||
.map(
|
||||
(v) => config.options.find((opt) => opt.value === v)?.label || v,
|
||||
)
|
||||
.map((v) => config.options.find((opt) => opt.value === v)?.label || v)
|
||||
.join(", ");
|
||||
return (
|
||||
<p>
|
||||
{config.label.replace(/%20/g, " ")}: <span className="text-muted-foreground">{labels}</span>
|
||||
{config.label.replace(/%20/g, " ")}:{" "}
|
||||
<span className="text-muted-foreground">{labels}</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
@ -373,8 +386,13 @@ export default function DynamicFilter<
|
||||
);
|
||||
case "radio-group":
|
||||
return (
|
||||
<div key={input.name} className="flex flex-col gap-2 p-2 border rounded-md">
|
||||
<Label className="font-semibold text-sm">{input.label}</Label>
|
||||
<div
|
||||
key={input.name}
|
||||
className="flex flex-col gap-2 p-2 border rounded-md"
|
||||
>
|
||||
<Label className="font-semibold text-sm">
|
||||
{input.label}
|
||||
</Label>
|
||||
<RadioGroup
|
||||
value={inputValues[input.name] as string}
|
||||
onValueChange={(value) =>
|
||||
@ -385,9 +403,17 @@ export default function DynamicFilter<
|
||||
}
|
||||
>
|
||||
{input.options.map((option) => (
|
||||
<div key={`${input.name}-${option.value}`} className="flex items-center space-x-2">
|
||||
<RadioGroupItem value={option.value} id={`${input.name}-${option.value}`} />
|
||||
<Label htmlFor={`${input.name}-${option.value}`}>{option.label}</Label>
|
||||
<div
|
||||
key={`${input.name}-${option.value}`}
|
||||
className="flex items-center space-x-2"
|
||||
>
|
||||
<RadioGroupItem
|
||||
value={option.value}
|
||||
id={`${input.name}-${option.value}`}
|
||||
/>
|
||||
<Label htmlFor={`${input.name}-${option.value}`}>
|
||||
{option.label}
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</RadioGroup>
|
||||
@ -424,10 +450,9 @@ export default function DynamicFilter<
|
||||
<Badge
|
||||
aria-disabled={disabled}
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"flex p-2 gap-2 items-center justify-between",
|
||||
{ "opacity-50 pointer-events-none": disabled },
|
||||
)}
|
||||
className={cn("flex p-2 gap-2 items-center justify-between", {
|
||||
"opacity-50 pointer-events-none": disabled,
|
||||
})}
|
||||
key={filter.key}
|
||||
>
|
||||
<span className="text-md">
|
||||
@ -449,4 +474,4 @@ export default function DynamicFilter<
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user