diff --git a/README.md b/README.md
index 23e4045..8dcd0ac 100644
--- a/README.md
+++ b/README.md
@@ -9,9 +9,10 @@ This is a web portal for SAR Link customers.
- [x] Add all the filters for devices table (mobile responsive)
- [x] Add cancel feature to selected devices floating button
- ### Payments
+ ### Payments1
- [x] Show payments table
- [x] Add all the filters for payment table (mobile responsive)
+ - [x] add slider range filter
- [ ] Fix bill formula linking for generated payments
### Parental Control
diff --git a/app/(dashboard)/devices/page.tsx b/app/(dashboard)/devices/page.tsx
index 7da670d..f640915 100644
--- a/app/(dashboard)/devices/page.tsx
+++ b/app/(dashboard)/devices/page.tsx
@@ -1,10 +1,10 @@
+import { redirect } from "next/navigation";
+import { getServerSession } from "next-auth";
+import { Suspense } from "react";
import { authOptions } from "@/app/auth";
import { DevicesTable } from "@/components/devices-table";
import DynamicFilter from "@/components/generic-filter";
import AddDeviceDialogForm from "@/components/user/add-device-dialog";
-import { getServerSession } from "next-auth";
-import { redirect } from "next/navigation";
-import { Suspense } from "react";
import DevicesTableSkeleton from "./device-table-skeleton";
export default async function Devices({
@@ -51,6 +51,13 @@ export default async function Devices({
label: "Vendor",
type: "string",
placeholder: "Enter vendor name",
+ }, {
+ label: "Amount of Devices",
+ name: "amount",
+ type: "dual-range-slider",
+ min: 1,
+ max: 100,
+ sliderLabel: "MVR"
}
]}
/>
diff --git a/components/generic-filter.tsx b/components/generic-filter.tsx
index 16132d2..6c00b4e 100644
--- a/components/generic-filter.tsx
+++ b/components/generic-filter.tsx
@@ -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 =
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 extends { type: "radio-group" }
? string
+ : GetConfigForKey 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)[input.name] =
+ [parsedMin, parsedMax] as FilterValues<
+ TFilterKeys,
+ TInputs
+ >[typeof input.name];
} else {
(initialState as FilterValues)[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)[
input.name as TFilterKeys
] = [] as FilterValues[typeof input.name];
+ } else if (input.type === "dual-range-slider") {
+ (clearedInputState as FilterValues)[
+ input.name as TFilterKeys
+ ] = [input.min, input.max] as FilterValues[typeof input.name];
} else if (input.type === "radio-group" || input.type === "string" || input.type === "number") {
(clearedInputState as FilterValues)[
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;
}> = [];
@@ -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,
) => {
if (config.type === "checkbox-group") {
@@ -312,6 +371,18 @@ export default function DynamicFilter<
);
}
+ if (config.type === "dual-range-slider") {
+ const rangeValues = value as number[];
+ const formatLabel = config.formatLabel || ((val: number | undefined) => val?.toString() || "");
+ return (
+
+ {config.label}:{" "}
+
+ {formatLabel(rangeValues[0])} - {formatLabel(rangeValues[1])}
+
+
+ );
+ }
return (
{config.label}: {value}
@@ -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 (
+
+
+
+ {value}{input.sliderLabel}}
+ value={inputValues[input.name] as number[]}
+ onValueChange={handleDualRangeChange(input.name)}
+ min={input.min}
+ max={input.max}
+ step={input.step || 1}
+ />
+
+
+ );
case "radio-group":
return (
{
+ labelPosition?: 'top' | 'bottom';
+ label?: (value: number | undefined) => React.ReactNode;
+}
+
+const DualRangeSlider = React.forwardRef<
+ React.ElementRef,
+ DualRangeSliderProps
+>(({ className, label, labelPosition = 'top', ...props }, ref) => {
+ const initialValue = Array.isArray(props.value) ? props.value : [props.min, props.max];
+
+ return (
+
+
+
+
+ {initialValue.map((value, index) => (
+
+
+ {label && (
+
+ {label(value)}
+
+ )}
+
+
+ ))}
+
+ );
+});
+DualRangeSlider.displayName = 'DualRangeSlider';
+
+export { DualRangeSlider };