Better settings ui

This commit is contained in:
2025-10-13 14:01:13 +02:00
parent 3b98a94c19
commit 2c061a6f8e
6 changed files with 131 additions and 83 deletions
+6 -9
View File
@@ -33,6 +33,7 @@ import ProfilePicture from "./ProfilePicture";
import auth, { makeAddress } from "@/lib/auth";
import { toast } from "sonner";
import { StringMap } from "@/types/typeUtils";
import SettingsDialog from "./settings/SettingsDialog";
export default function AppSidebar({
children,
@@ -195,9 +196,7 @@ export default function AppSidebar({
</Dialog>
<footer className="mt-auto">
<Link href="/settings">
<SettingsIcon />
</Link>
<SettingsDialog />
</footer>
</div>
@@ -271,12 +270,10 @@ function DMItem({
<span className="text-xs text-muted-foreground">{status}</span>
</div>
{settings && (
<Button
onClick={() => router.push(`/settings?tab=profile`)}
className="bg-transparent text-accent-foreground hover:text-accent ml-auto w-7 h-7 p-0 flex items-center justify-center rounded-md"
>
<SettingsIcon size={16} />
</Button>
<SettingsDialog
className="ml-auto w-7 h-7 p-0 flex items-center justify-center"
tab="profile"
/>
)}
</Card>
);
+154
View File
@@ -0,0 +1,154 @@
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 SettingSchema<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>
<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>
</>
);
}
+66
View File
@@ -0,0 +1,66 @@
"use client";
import { useClientSettings } from "@/hooks/use-settings";
import { setClientSettings } from "@/lib/clientSettings";
import SettingSchema from "./SettingSchema";
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,
ProfileSettings,
} from "@/types/settings";
import { useParams, useRouter, useSearchParams } from "next/navigation";
import axios from "axios";
export default function Settings({
tab,
profileSettings,
setProfileSettings,
}: {
tab?: string;
profileSettings: ProfileSettings;
setProfileSettings: React.Dispatch<React.SetStateAction<ProfileSettings>>;
}) {
const [settings, setSettings] = useClientSettings();
const router = useRouter();
return (
<Tabs className="mx-auto mt-3" defaultValue={tab || "client"}>
<TabsList className="mx-auto">
<TabsTrigger value="client">Client Settings</TabsTrigger>
<TabsTrigger value="profile">Profile Settings</TabsTrigger>
</TabsList>
<TabsContent value="client">
<Card className="mx-auto">
<CardContent className="flex flex-col gap-3">
<SettingSchema
settings={settings}
setSettings={setSettings}
schema={CLIENT_SCHEMA}
onSave={() => {
setClientSettings(settings);
router.refresh();
}}
/>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="profile">
<Card className="mx-auto">
<CardContent className="flex flex-col gap-3">
<SettingSchema
settings={profileSettings}
setSettings={setProfileSettings}
schema={PROFILE_SCHEMA}
onSave={async () => {
await axios.post("/api/profile", profileSettings);
}}
/>
</CardContent>
</Card>
</TabsContent>
</Tabs>
);
}
@@ -0,0 +1,53 @@
import { SettingsIcon } from "lucide-react";
import { Button } from "../ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "../ui/dialog";
import { cn } from "@/lib/utils";
import Settings from "./Settings";
import { ProfileSettings } from "@/types/settings";
import { useProfileSettings } from "@/hooks/use-settings";
export default function SettingsDialog({
className,
tab,
}: {
className?: string;
tab?: string;
profile?: ProfileSettings;
}) {
const [profileSettings, setProfileSettings] = useProfileSettings();
return (
<Dialog>
<DialogTrigger asChild>
<Button
className={cn(
"bg-transparent text-accent-foreground hover:text-accent",
className
)}
>
<SettingsIcon />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Settings</DialogTitle>
</DialogHeader>
<div className="w-full">
<Settings
tab={tab}
profileSettings={profileSettings}
setProfileSettings={setProfileSettings}
/>
</div>
</DialogContent>
</Dialog>
);
}