This commit is contained in:
xCyanGrizzly
2026-02-18 14:26:36 +01:00
commit 3a5726e82b
167 changed files with 104081 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
"use client";
import { useTransition } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "sonner";
import { useTheme } from "next-themes";
import { settingsSchema, type SettingsInput } from "@/schemas/settings.schema";
import { CURRENCIES, UNITS } from "@/lib/constants";
import { updateSettings } from "../actions";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
interface SettingsFormProps {
settings: {
lowStockThreshold: number;
currency: string;
theme: string;
units: string;
};
}
export function SettingsForm({ settings }: SettingsFormProps) {
const [isPending, startTransition] = useTransition();
const { setTheme } = useTheme();
const form = useForm<SettingsInput>({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolver: zodResolver(settingsSchema) as any,
defaultValues: {
lowStockThreshold: settings.lowStockThreshold,
currency: settings.currency as SettingsInput["currency"],
theme: settings.theme as SettingsInput["theme"],
units: settings.units as SettingsInput["units"],
},
});
function onSubmit(values: SettingsInput) {
startTransition(async () => {
const result = await updateSettings(values);
if (!result.success) {
toast.error(result.error);
return;
}
setTheme(values.theme);
toast.success("Settings updated");
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Inventory Settings</CardTitle>
<CardDescription>
Configure stock thresholds and display preferences.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<FormField
control={form.control}
name="lowStockThreshold"
render={({ field }) => (
<FormItem>
<FormLabel>Low Stock Threshold (%)</FormLabel>
<FormControl>
<Input type="number" min="0" max="100" step="1" {...field} />
</FormControl>
<FormDescription>
Items with remaining stock below this percentage will be flagged as
low stock.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="currency"
render={({ field }) => (
<FormItem>
<FormLabel>Currency</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{CURRENCIES.map((c) => (
<SelectItem key={c} value={c}>
{c}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Display currency for cost values.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="units"
render={({ field }) => (
<FormItem>
<FormLabel>Unit System</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{UNITS.map((u) => (
<SelectItem key={u} value={u}>
{u.charAt(0).toUpperCase() + u.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Measurement system for weight and volume.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
<Separator />
<Card>
<CardHeader>
<CardTitle>Appearance</CardTitle>
<CardDescription>Customize the look and feel.</CardDescription>
</CardHeader>
<CardContent>
<FormField
control={form.control}
name="theme"
render={({ field }) => (
<FormItem>
<FormLabel>Theme</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="system">System</SelectItem>
</SelectContent>
</Select>
<FormDescription>
Choose your preferred color theme.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
<div className="flex justify-end">
<Button type="submit" disabled={isPending}>
{isPending ? "Saving..." : "Save Settings"}
</Button>
</div>
</form>
</Form>
);
}

View File

@@ -0,0 +1,43 @@
"use server";
import { auth } from "@/lib/auth";
import { prisma } from "@/lib/prisma";
import { settingsSchema } from "@/schemas/settings.schema";
import { revalidatePath } from "next/cache";
import type { ActionResult } from "@/types/api.types";
export async function updateSettings(input: unknown): Promise<ActionResult> {
const session = await auth();
if (!session?.user?.id) return { success: false, error: "Unauthorized" };
const parsed = settingsSchema.safeParse(input);
if (!parsed.success) return { success: false, error: "Validation failed" };
try {
await prisma.userSettings.upsert({
where: { userId: session.user.id },
update: {
lowStockThreshold: parsed.data.lowStockThreshold,
currency: parsed.data.currency,
theme: parsed.data.theme,
units: parsed.data.units,
},
create: {
userId: session.user.id,
lowStockThreshold: parsed.data.lowStockThreshold,
currency: parsed.data.currency,
theme: parsed.data.theme,
units: parsed.data.units,
},
});
revalidatePath("/settings");
revalidatePath("/dashboard");
revalidatePath("/filaments");
revalidatePath("/resins");
revalidatePath("/paints");
return { success: true, data: undefined };
} catch {
return { success: false, error: "Failed to update settings" };
}
}

View File

@@ -0,0 +1,24 @@
import { Skeleton } from "@/components/ui/skeleton";
export default function SettingsLoading() {
return (
<div className="space-y-6">
<div>
<Skeleton className="h-8 w-32" />
<Skeleton className="mt-1 h-4 w-64" />
</div>
<div className="max-w-2xl space-y-6">
<div className="rounded-lg border p-6 space-y-4">
<Skeleton className="h-6 w-40" />
<div className="space-y-3">
<Skeleton className="h-9 w-full" />
<Skeleton className="h-9 w-full" />
<Skeleton className="h-9 w-full" />
<Skeleton className="h-9 w-full" />
</div>
<Skeleton className="h-9 w-24" />
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,26 @@
import { auth } from "@/lib/auth";
import { redirect } from "next/navigation";
import { getUserSettings } from "@/data/settings.queries";
import { PageHeader } from "@/components/shared/page-header";
import { SettingsForm } from "./_components/settings-form";
export default async function SettingsPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const settings = await getUserSettings(session.user.id);
return (
<div className="space-y-6">
<PageHeader
title="Settings"
description="Manage your application preferences"
/>
<div className="max-w-2xl">
<SettingsForm
settings={JSON.parse(JSON.stringify(settings))}
/>
</div>
</div>
);
}