mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-07-01 15:23:58 +00:00
feat: add dual range slider component and integrate it into dynamic filter for device management
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 4m49s
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 4m49s
This commit is contained in:
@ -15,6 +15,7 @@ import {
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
import { DualRangeSlider } from "@/components/ui/dual-range-slider";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
@ -89,6 +90,16 @@ type FilterInputConfig<TKey extends string> =
|
||||
label: string;
|
||||
type: "radio-group";
|
||||
options: FilterOption[];
|
||||
}
|
||||
| {
|
||||
name: TKey;
|
||||
label: string;
|
||||
type: "dual-range-slider";
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
sliderLabel?: string;
|
||||
formatLabel?: (value: number | undefined) => React.ReactNode;
|
||||
};
|
||||
|
||||
// Utility type to extract the config for a specific key from the array
|
||||
@ -112,6 +123,8 @@ type FilterValues<
|
||||
? string[]
|
||||
: GetConfigForKey<K, TInputs> extends { type: "radio-group" }
|
||||
? string
|
||||
: GetConfigForKey<K, TInputs> extends { type: "dual-range-slider" }
|
||||
? number[]
|
||||
: string;
|
||||
};
|
||||
|
||||
@ -163,6 +176,16 @@ export default function DynamicFilter<
|
||||
TFilterKeys,
|
||||
TInputs
|
||||
>[typeof input.name];
|
||||
} else if (input.type === "dual-range-slider") {
|
||||
const minValue = searchParams.get(`${input.name}_min`);
|
||||
const maxValue = searchParams.get(`${input.name}_max`);
|
||||
const parsedMin = minValue ? Number(minValue) : input.min;
|
||||
const parsedMax = maxValue ? Number(maxValue) : input.max;
|
||||
(initialState as FilterValues<TFilterKeys, TInputs>)[input.name] =
|
||||
[parsedMin, parsedMax] as FilterValues<
|
||||
TFilterKeys,
|
||||
TInputs
|
||||
>[typeof input.name];
|
||||
} else {
|
||||
(initialState as FilterValues<TFilterKeys, TInputs>)[input.name] =
|
||||
(urlValue || "") as FilterValues<
|
||||
@ -203,6 +226,15 @@ export default function DynamicFilter<
|
||||
}));
|
||||
};
|
||||
|
||||
// Handler for dual range slider changes
|
||||
const handleDualRangeChange =
|
||||
(name: TFilterKeys) => (values: number[]) => {
|
||||
setInputValues((prev) => ({
|
||||
...prev,
|
||||
[name]: values,
|
||||
}));
|
||||
};
|
||||
|
||||
// Handles applying all filters
|
||||
const handleApplyFilters = () => {
|
||||
const newParams = new URLSearchParams(currentParams.toString()); // Start fresh with current params
|
||||
@ -218,6 +250,16 @@ export default function DynamicFilter<
|
||||
} else {
|
||||
newParams.delete(input.name);
|
||||
}
|
||||
} else if (input.type === "dual-range-slider") {
|
||||
const rangeValues = value as number[];
|
||||
// Only set params if values are different from the default min/max
|
||||
if (rangeValues.length === 2 && (rangeValues[0] !== input.min || rangeValues[1] !== input.max)) {
|
||||
newParams.set(`${input.name}_min`, rangeValues[0].toString());
|
||||
newParams.set(`${input.name}_max`, rangeValues[1].toString());
|
||||
} else {
|
||||
newParams.delete(`${input.name}_min`);
|
||||
newParams.delete(`${input.name}_max`);
|
||||
}
|
||||
} else {
|
||||
// String/Number inputs
|
||||
if (value) {
|
||||
@ -245,6 +287,10 @@ export default function DynamicFilter<
|
||||
(clearedInputState as FilterValues<TFilterKeys, TInputs>)[
|
||||
input.name as TFilterKeys
|
||||
] = [] as FilterValues<TFilterKeys, TInputs>[typeof input.name];
|
||||
} else if (input.type === "dual-range-slider") {
|
||||
(clearedInputState as FilterValues<TFilterKeys, TInputs>)[
|
||||
input.name as TFilterKeys
|
||||
] = [input.min, input.max] as FilterValues<TFilterKeys, TInputs>[typeof input.name];
|
||||
} else if (input.type === "radio-group" || input.type === "string" || input.type === "number") {
|
||||
(clearedInputState as FilterValues<TFilterKeys, TInputs>)[
|
||||
input.name as TFilterKeys
|
||||
@ -263,7 +309,7 @@ export default function DynamicFilter<
|
||||
const appliedFilters = useMemo(() => {
|
||||
const filters: Array<{
|
||||
key: TFilterKeys;
|
||||
value: string | string[];
|
||||
value: string | string[] | number[];
|
||||
label: string;
|
||||
config: FilterInputConfig<TFilterKeys>;
|
||||
}> = [];
|
||||
@ -282,6 +328,19 @@ export default function DynamicFilter<
|
||||
config: input,
|
||||
});
|
||||
}
|
||||
} else if (input.type === "dual-range-slider") {
|
||||
const minValue = searchParams.get(`${input.name}_min`);
|
||||
const maxValue = searchParams.get(`${input.name}_max`);
|
||||
if (minValue && maxValue) {
|
||||
const parsedMin = Number(minValue);
|
||||
const parsedMax = Number(maxValue);
|
||||
filters.push({
|
||||
key: input.name,
|
||||
value: [parsedMin, parsedMax],
|
||||
label: input.label,
|
||||
config: input,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
filters.push({
|
||||
key: input.name,
|
||||
@ -298,7 +357,7 @@ export default function DynamicFilter<
|
||||
// Dynamic `prettyPrintFilter` for badges
|
||||
const prettyPrintFilter = (
|
||||
_key: TFilterKeys,
|
||||
value: string | string[],
|
||||
value: string | string[] | number[],
|
||||
config: FilterInputConfig<TFilterKeys>,
|
||||
) => {
|
||||
if (config.type === "checkbox-group") {
|
||||
@ -312,6 +371,18 @@ export default function DynamicFilter<
|
||||
</p>
|
||||
);
|
||||
}
|
||||
if (config.type === "dual-range-slider") {
|
||||
const rangeValues = value as number[];
|
||||
const formatLabel = config.formatLabel || ((val: number | undefined) => val?.toString() || "");
|
||||
return (
|
||||
<p>
|
||||
{config.label}:{" "}
|
||||
<span className="text-muted-foreground">
|
||||
{formatLabel(rangeValues[0])} - {formatLabel(rangeValues[1])}
|
||||
</span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<p>
|
||||
{config.label}: <span className="text-muted-foreground">{value}</span>
|
||||
@ -322,15 +393,26 @@ export default function DynamicFilter<
|
||||
// Handles removing an individual filter
|
||||
const handleRemoveFilter = (keyToRemove: TFilterKeys) => {
|
||||
const newParams = new URLSearchParams(currentParams.toString());
|
||||
newParams.delete(keyToRemove);
|
||||
newParams.set("page", "1"); // Reset page after removing a filter
|
||||
|
||||
// Clear the specific input's local state
|
||||
const inputConfig = inputs.find((input) => input.name === keyToRemove);
|
||||
setInputValues((prev) => ({
|
||||
...prev,
|
||||
[keyToRemove]: inputConfig?.type === "checkbox-group" ? [] : "",
|
||||
}));
|
||||
|
||||
if (inputConfig?.type === "dual-range-slider") {
|
||||
newParams.delete(`${keyToRemove}_min`);
|
||||
newParams.delete(`${keyToRemove}_max`);
|
||||
setInputValues((prev) => ({
|
||||
...prev,
|
||||
[keyToRemove]: [inputConfig.min, inputConfig.max],
|
||||
}));
|
||||
} else {
|
||||
newParams.delete(keyToRemove);
|
||||
setInputValues((prev) => ({
|
||||
...prev,
|
||||
[keyToRemove]: inputConfig?.type === "checkbox-group" ? [] : "",
|
||||
}));
|
||||
}
|
||||
|
||||
newParams.set("page", "1"); // Reset page after removing a filter
|
||||
|
||||
startTransition(() => {
|
||||
replace(`${pathname}?${newParams.toString()}`);
|
||||
@ -384,6 +466,27 @@ export default function DynamicFilter<
|
||||
onChange={handleCheckboxGroupChange(input.name)}
|
||||
/>
|
||||
);
|
||||
case "dual-range-slider":
|
||||
return (
|
||||
<div
|
||||
key={input.name}
|
||||
className="px-2 w-full flex flex-col gap-2 p-2 border rounded-md"
|
||||
>
|
||||
<Label className="font-semibold text-sm">
|
||||
{input.label}
|
||||
</Label>
|
||||
<div className="px-3 py-2 mt-5">
|
||||
<DualRangeSlider
|
||||
label={(value) => <span className="text-xs">{value}{input.sliderLabel}</span>}
|
||||
value={inputValues[input.name] as number[]}
|
||||
onValueChange={handleDualRangeChange(input.name)}
|
||||
min={input.min}
|
||||
max={input.max}
|
||||
step={input.step || 1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case "radio-group":
|
||||
return (
|
||||
<div
|
||||
|
Reference in New Issue
Block a user