feat(lst): added update settings into the entire app

This commit is contained in:
2025-03-05 12:09:51 -06:00
parent 6fb615a743
commit 5fcadb9fc8
14 changed files with 601 additions and 29 deletions

View File

@@ -0,0 +1,121 @@
import {Button} from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {Input} from "@/components/ui/input";
import {Label} from "@/components/ui/label";
import {Settings} from "./SettingsPage";
import {toast} from "sonner";
import {useState} from "react";
import {useForm} from "react-hook-form";
import {z} from "zod";
import {zodResolver} from "@hookform/resolvers/zod";
import {useQuery} from "@tanstack/react-query";
import {getSettings} from "@/utils/querys/settings";
import {useSessionStore} from "@/lib/store/sessionStore";
import axios from "axios";
const FormSchema = z.object({
value: z.string().min(1, "You must enter a value greater than 0"),
});
export function ChangeSetting({setting}: {setting: Settings}) {
const {token} = useSessionStore();
const {refetch} = useQuery(getSettings(token ?? ""));
const [open, setOpen] = useState(false);
const [saving, setSaving] = useState(false);
const {
register,
handleSubmit,
reset,
formState: {errors},
} = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
value: setting.value || "",
},
});
const onSubmit = async (data: z.infer<typeof FormSchema>) => {
setSaving(!saving);
const update = {...data, name: setting.name};
// console.log(update);
try {
const result = await axios.patch("/api/server/settings", update, {
headers: {Authorization: `Bearer ${token}`},
});
if (result.data.success) {
setOpen(!open);
setSaving(false);
refetch();
toast.success(result.data.message);
}
} catch (error) {
console.log(error);
}
};
return (
<>
<Dialog
open={open}
onOpenChange={(isOpen) => {
if (!open) {
reset();
}
setOpen(isOpen);
// toast.message("Model was something", {
// description: isOpen ? "Modal is open" : "Modal is closed",
// });
}}
>
<DialogTrigger asChild>
<Button variant="outline">Edit</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{setting.name}</DialogTitle>
<DialogDescription>
Update the setting and press save to complete the changes.
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<>
<Label htmlFor={"value"} className="m-1">
Value
</Label>
<Input
{...register("value")}
className={errors.value ? "border-red-500" : ""}
aria-invalid={!!errors.value}
/>
{errors.value && <p className="text-red-500 text-sm mt-1">{errors.value.message}</p>}
</>
</div>
<DialogFooter>
<div className="flex justify-end mt-2">
<Button type="submit" disabled={saving}>
{saving ? (
<>
<span>Saving....</span>
</>
) : (
<span>Save setting</span>
)}
</Button>
</div>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
</>
);
}

View File

@@ -1,21 +1,24 @@
import {LstCard} from "@/components/extendedUI/LstCard";
import {Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow} from "@/components/ui/table";
import {useSessionStore} from "@/lib/store/sessionStore";
import {useModuleStore} from "@/lib/store/useModuleStore";
import {useQuery} from "@tanstack/react-query";
import {useRouter} from "@tanstack/react-router";
import axios from "axios";
import {ChangeSetting} from "./SettingForm";
import {getSettings} from "@/utils/querys/settings";
export type Settings = {
settings_id?: string;
name?: string;
value?: string;
description?: string;
};
export default function SettingsPage() {
const token = localStorage.getItem("auth_token");
const {user} = useSessionStore();
const {user, token} = useSessionStore();
const {modules} = useModuleStore();
const router = useRouter();
const fetchSettings = async () => {
const {data} = await axios.get("/api/server/settings", {headers: {Authorization: `Bearer ${token}`}});
return data.data;
};
const adminModule = modules.filter((n) => n.name === "admin");
const userLevel = user?.roles.filter((r) => r.module_id === adminModule[0].module_id) || [];
@@ -23,12 +26,7 @@ export default function SettingsPage() {
router.navigate({to: "/"});
}
const {data, isError, error, isLoading} = useQuery({
queryKey: ["settings"],
queryFn: fetchSettings,
enabled: !!token,
refetchOnWindowFocus: true,
});
const {data, isError, error, isLoading} = useQuery(getSettings(token ?? ""));
if (isLoading) {
return <div>Loading.....</div>;
@@ -38,10 +36,29 @@ export default function SettingsPage() {
}
return (
<div>
{data.map((i: any) => {
return <LstCard key={i.settings_id}>{i.name}</LstCard>;
})}
</div>
<LstCard className="m-2 flex place-content-center w-dvh">
<Table>
<TableCaption>All Settings</TableCaption>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Value</TableHead>
<TableHead>Change</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.map((setting: Settings) => (
<TableRow key={setting.settings_id}>
<TableCell className="font-medium">{setting.name}</TableCell>
<TableCell className="font-medium">{setting.value}</TableCell>
<TableCell className="font-medium">{setting.description}</TableCell>
<TableCell className="font-medium">
<ChangeSetting setting={setting} />
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</LstCard>
);
}

View File

@@ -0,0 +1,133 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content>) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,58 @@
import * as React from "react";
import {Slot} from "@radix-ui/react-slot";
import {cva, type VariantProps} from "class-variance-authority";
import {cn} from "@/lib/utils";
const spinnerVariants = cva("relative block opacity-[0.65]", {
variants: {
size: {
sm: "w-4 h-4",
md: "w-6 h-6",
lg: "w-8 h-8",
},
},
defaultVariants: {
size: "sm",
},
});
export interface SpinnerProps extends React.HTMLAttributes<HTMLSpanElement>, VariantProps<typeof spinnerVariants> {
loading?: boolean;
asChild?: boolean;
}
const Spinner = React.forwardRef<HTMLSpanElement, SpinnerProps>(
({className, size, loading = true, asChild = false, ...props}, ref) => {
const Comp = asChild ? Slot : "span";
const [bgColorClass, filteredClassName] = React.useMemo(() => {
const bgClass = className?.match(/(?:dark:bg-|bg-)[a-zA-Z0-9-]+/g) || [];
const filteredClasses = className?.replace(/(?:dark:bg-|bg-)[a-zA-Z0-9-]+/g, "").trim();
return [bgClass, filteredClasses];
}, [className]);
if (!loading) return null;
return (
<Comp className={cn(spinnerVariants({size, className: filteredClassName}))} ref={ref} {...props}>
{Array.from({length: 8}).map((_, i) => (
<span
key={i}
className="absolute top-0 left-1/2 w-[12.5%] h-full animate-spinner-leaf-fade"
style={{
transform: `rotate(${i * 45}deg)`,
animationDelay: `${-(7 - i) * 100}ms`,
}}
>
<span className={cn("block w-full h-[30%] rounded-full", bgColorClass)}></span>
</span>
))}
</Comp>
);
}
);
Spinner.displayName = "Spinner";
export {Spinner};

View File

@@ -0,0 +1,114 @@
import * as React from "react"
import { cn } from "@/lib/utils"
function Table({ className, ...props }: React.ComponentProps<"table">) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn("w-full caption-bottom text-sm", className)}
{...props}
/>
</div>
)
}
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
return (
<thead
data-slot="table-header"
className={cn("[&_tr]:border-b", className)}
{...props}
/>
)
}
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
return (
<tbody
data-slot="table-body"
className={cn("[&_tr:last-child]:border-0", className)}
{...props}
/>
)
}
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
return (
<tfoot
data-slot="table-footer"
className={cn(
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
className
)}
{...props}
/>
)
}
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
return (
<tr
data-slot="table-row"
className={cn(
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
}
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
return (
<th
data-slot="table-head"
className={cn(
"text-muted-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
return (
<td
data-slot="table-cell"
className={cn(
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
className
)}
{...props}
/>
)
}
function TableCaption({
className,
...props
}: React.ComponentProps<"caption">) {
return (
<caption
data-slot="table-caption"
className={cn("text-muted-foreground mt-4 text-sm", className)}
{...props}
/>
)
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
}

View File

@@ -17,9 +17,10 @@ import {
import {SessionProvider} from "../components/providers/Providers";
import {Toaster} from "sonner";
import {Button} from "../components/ui/button";
import {useLogout} from "../lib/hooks/useLogout";
import {useSession} from "../lib/hooks/useSession";
import {useSessionStore} from "../lib/store/sessionStore";
import {useSession} from "@/hooks/useSession";
import {useLogout} from "@/hooks/useLogout";
// same as the layout
export const Route = createRootRoute({

View File

@@ -0,0 +1,3 @@
export function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

View File

@@ -0,0 +1,17 @@
import {queryOptions} from "@tanstack/react-query";
import axios from "axios";
export function getSettings(token: string) {
return queryOptions({
queryKey: ["settings"],
queryFn: () => fetchSettings(token),
enabled: !!token,
staleTime: 1000,
refetchOnWindowFocus: true,
});
}
const fetchSettings = async (token: string) => {
const {data} = await axios.get("/api/server/settings", {headers: {Authorization: `Bearer ${token}`}});
return data.data;
};