mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-05-11 06:11:15 +00:00
Init
This commit is contained in:
205
src/app/(app)/settings/_components/settings-form.tsx
Normal file
205
src/app/(app)/settings/_components/settings-form.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
43
src/app/(app)/settings/actions.ts
Normal file
43
src/app/(app)/settings/actions.ts
Normal 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" };
|
||||
}
|
||||
}
|
||||
24
src/app/(app)/settings/loading.tsx
Normal file
24
src/app/(app)/settings/loading.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
26
src/app/(app)/settings/page.tsx
Normal file
26
src/app/(app)/settings/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user