Better settings ui
This commit is contained in:
@@ -1,71 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useProfileSettings, useClientSettings } from "@/hooks/use-settings";
|
||||
import { setClientSettings } from "@/lib/clientSettings";
|
||||
import Settings from "./settings";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { CardContent } from "@/components/ui/card";
|
||||
import { PROFILE_SCHEMA, CLIENT_SCHEMA } from "@/types/settings";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ChevronLeftCircle } from "lucide-react";
|
||||
import axios from "axios";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [settings, setSettings] = useClientSettings();
|
||||
const [profileSettings, setProfileSettings] = useProfileSettings();
|
||||
const router = useRouter();
|
||||
const params = useSearchParams();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="ml-2 mt-2"
|
||||
onClick={() => router.back()}
|
||||
>
|
||||
<ChevronLeftCircle />
|
||||
</Button>
|
||||
<Tabs
|
||||
className="mx-auto mt-3"
|
||||
defaultValue={params.get("tab") || "client"}
|
||||
>
|
||||
<TabsList className="mx-auto">
|
||||
<TabsTrigger value="client">Client Settings</TabsTrigger>
|
||||
<TabsTrigger value="profile">Profile Settings</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="client">
|
||||
<Card className="w-xl mx-auto">
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<Settings
|
||||
settings={settings}
|
||||
setSettings={setSettings}
|
||||
schema={CLIENT_SCHEMA}
|
||||
onSave={() => {
|
||||
setClientSettings(settings);
|
||||
router.refresh();
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
<TabsContent value="profile">
|
||||
<Card className="w-xl mx-auto">
|
||||
<CardContent className="flex flex-col gap-3">
|
||||
<Settings
|
||||
settings={profileSettings}
|
||||
setSettings={setProfileSettings}
|
||||
schema={PROFILE_SCHEMA}
|
||||
onSave={async () => {
|
||||
await axios.post("/api/profile", profileSettings);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useState, useRef } from "react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { StringMap } from "@/types/typeUtils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export class RegexSetting {
|
||||
placeholder: string;
|
||||
pattern: RegExp;
|
||||
|
||||
constructor(placeholder: string, pattern: RegExp) {
|
||||
this.placeholder = placeholder;
|
||||
this.pattern = pattern;
|
||||
}
|
||||
}
|
||||
|
||||
export type SettingsSchema = StringMap<
|
||||
"boolean" | "string" | "number" | string[] | RegexSetting
|
||||
>;
|
||||
|
||||
export default function Settings<T>({
|
||||
settings,
|
||||
setSettings,
|
||||
schema,
|
||||
onSave,
|
||||
}: {
|
||||
settings: T;
|
||||
setSettings: Dispatch<SetStateAction<T>>;
|
||||
schema: SettingsSchema;
|
||||
onSave: () => void;
|
||||
}) {
|
||||
const [feedback, setFeedback] = useState<
|
||||
undefined | { kind: "error" | "info"; message: string }
|
||||
>();
|
||||
|
||||
// Ref to store debounce timer
|
||||
const debounceRef = useRef<NodeJS.Timeout>(undefined);
|
||||
|
||||
const handleChange = (key: string, value: any) => {
|
||||
setSettings((prev) => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
const handleRegexChange = (
|
||||
key: string,
|
||||
regexSetting: RegexSetting,
|
||||
value: string
|
||||
) => {
|
||||
handleChange(key, value);
|
||||
if (debounceRef.current) clearTimeout(debounceRef.current);
|
||||
debounceRef.current = setTimeout(() => {
|
||||
if (!regexSetting.pattern.test(value)) {
|
||||
setFeedback({
|
||||
kind: "error",
|
||||
message: `Value does not match the pattern!`,
|
||||
});
|
||||
} else {
|
||||
setFeedback(undefined);
|
||||
}
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
const renderInput = (key: string, type: any) => {
|
||||
if (Array.isArray(type)) {
|
||||
return (
|
||||
<Select
|
||||
value={(settings as any)[key] || []}
|
||||
onValueChange={(val) => handleChange(key, val)}
|
||||
>
|
||||
<SelectTrigger className="w-[200px]">
|
||||
<SelectValue placeholder={key} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{type.map((option) => (
|
||||
<SelectItem key={option} value={option}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
} else if (type === "boolean") {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={(settings as any)[key] || false}
|
||||
onCheckedChange={(val) => handleChange(key, val)}
|
||||
/>
|
||||
);
|
||||
} else if (type === "string") {
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
value={(settings as any)[key] || ""}
|
||||
onChange={(e) => handleChange(key, e.target.value)}
|
||||
/>
|
||||
);
|
||||
} else if (type === "number") {
|
||||
return (
|
||||
<Input
|
||||
type="number"
|
||||
value={(settings as any)[key] || 0}
|
||||
onChange={(e) => handleChange(key, Number(e.target.value))}
|
||||
/>
|
||||
);
|
||||
} else if (type instanceof RegexSetting) {
|
||||
return (
|
||||
<Input
|
||||
type="text"
|
||||
value={(settings as any)[key] || ""}
|
||||
placeholder={type.placeholder}
|
||||
onChange={(e) => handleRegexChange(key, type, e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{Object.entries(schema).map(([key, type]) => (
|
||||
<div key={key} className="flex items-center justify-between space-x-4">
|
||||
<label className="capitalize">{key}</label>
|
||||
{renderInput(key, type)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{feedback && (
|
||||
<div
|
||||
className={`text-sm rounded-md p-2 ${
|
||||
feedback.kind === "error"
|
||||
? "bg-red-500/20 text-red-400"
|
||||
: "bg-green-500/20 text-green-400"
|
||||
}`}
|
||||
>
|
||||
{feedback.message}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (feedback === undefined) onSave();
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user