mirror of
https://github.com/i701/sarlink-portal.git
synced 2025-07-01 09:13:57 +00:00
feat: implement add device functionality with validation and error handling; refactor related components for improved state management
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 11m49s
All checks were successful
Build and Push Docker Images / Build and Push Docker Images (push) Successful in 11m49s
This commit is contained in:
@ -1,85 +1,60 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
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 { addDevice } from "@/queries/devices";
|
||||
import { tryCatch } from "@/utils/tryCatch";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Loader2, Plus } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { type SubmitHandler, useForm } from "react-hook-form";
|
||||
import { useActionState, useEffect, useState } from "react"; // Import useActionState
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { GetMacAccordion } from "../how-to-get-mac";
|
||||
|
||||
export default function AddDeviceDialogForm({ user_id }: { user_id?: string }) {
|
||||
const formSchema = z.object({
|
||||
name: z.string().min(2, { message: "Name is required." }),
|
||||
mac_address: z
|
||||
.string()
|
||||
.min(2, { message: "MAC Address is required." })
|
||||
.regex(
|
||||
/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/,
|
||||
"Please enter a valid MAC address",
|
||||
),
|
||||
});
|
||||
import { addDeviceAction } from "@/queries/devices";
|
||||
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
export type AddDeviceFormState = {
|
||||
message: string;
|
||||
fieldErrors?: {
|
||||
name?: string[];
|
||||
mac_address?: string[];
|
||||
};
|
||||
payload?: FormData;
|
||||
};
|
||||
|
||||
|
||||
export const initialState: AddDeviceFormState = {
|
||||
message: "",
|
||||
fieldErrors: {},
|
||||
};
|
||||
|
||||
export default function AddDeviceDialogForm({ user_id }: { user_id?: string }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
});
|
||||
|
||||
const [state, formAction, pending] = useActionState(
|
||||
addDeviceAction,
|
||||
initialState
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state.message && state !== initialState) {
|
||||
if (state.fieldErrors && Object.keys(state.fieldErrors).length > 0) {
|
||||
toast.error(state.message);
|
||||
} else if (state.message.includes("success")) {
|
||||
toast.success(state.message);
|
||||
setOpen(false);
|
||||
} else {
|
||||
toast.error(state.message);
|
||||
}
|
||||
}
|
||||
}, [state]);
|
||||
|
||||
if (!user_id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const onSubmit: SubmitHandler<z.infer<typeof formSchema>> = async (data) => {
|
||||
console.log(data);
|
||||
setDisabled(true);
|
||||
const [error, response] = await tryCatch(
|
||||
addDevice({
|
||||
mac: data.mac_address,
|
||||
name: data.name,
|
||||
}),
|
||||
);
|
||||
if (error) {
|
||||
toast.error(error.message || "Something went wrong.");
|
||||
setDisabled(false);
|
||||
} else {
|
||||
console.log(response);
|
||||
setOpen(false);
|
||||
setDisabled(false);
|
||||
toast.success("Device successfully added!");
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
className="gap-2 items-center"
|
||||
disabled={disabled}
|
||||
variant="default"
|
||||
>
|
||||
<Button className="gap-2 items-center" disabled={pending} variant="default">
|
||||
Add Device
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
@ -93,7 +68,7 @@ export default function AddDeviceDialogForm({ user_id }: { user_id?: string }) {
|
||||
</DialogDescription>
|
||||
<GetMacAccordion />
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<form action={formAction}>
|
||||
<div className="grid gap-4 py-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div>
|
||||
@ -103,34 +78,40 @@ export default function AddDeviceDialogForm({ user_id }: { user_id?: string }) {
|
||||
<Input
|
||||
placeholder="eg: iPhone X"
|
||||
type="text"
|
||||
{...register("name")}
|
||||
defaultValue={(state?.payload?.get("name") || "") as string}
|
||||
name="name"
|
||||
id="device_name"
|
||||
className="col-span-3"
|
||||
/>
|
||||
<span className="text-red-500 text-sm">
|
||||
{errors.name?.message}
|
||||
</span>
|
||||
{state.fieldErrors?.name && (
|
||||
<span className="text-red-500 text-sm">
|
||||
{state.fieldErrors.name[0]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label htmlFor="address" className="text-right">
|
||||
<Label htmlFor="mac_address" className="text-right">
|
||||
Mac Address
|
||||
</Label>
|
||||
<Input
|
||||
placeholder="Mac address of your device"
|
||||
{...register("mac_address")}
|
||||
name="mac_address"
|
||||
defaultValue={(state?.payload?.get("mac_address") || "") as string}
|
||||
id="mac_address"
|
||||
className="col-span-3"
|
||||
/>
|
||||
<span className="text-red-500 text-sm">
|
||||
{errors.mac_address?.message}
|
||||
</span>
|
||||
{state.fieldErrors?.mac_address && (
|
||||
<span className="text-red-500 text-sm">
|
||||
{state.fieldErrors.mac_address[0]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button disabled={disabled} type="submit">
|
||||
{disabled ? <Loader2 className="animate-spin" /> : "Save"}
|
||||
<Button disabled={pending} type="submit">
|
||||
{pending ? <Loader2 className="animate-spin" /> : "Save"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
|
@ -1,6 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { authOptions } from "@/app/auth";
|
||||
import type { AddDeviceFormState, initialState } from "@/components/user/add-device-dialog";
|
||||
import type { ApiError, ApiResponse, Device } from "@/lib/backend-types";
|
||||
import { checkSession } from "@/utils/session";
|
||||
import { handleApiResponse } from "@/utils/tryCatch";
|
||||
@ -46,32 +47,63 @@ export async function getDevice({ deviceId }: { deviceId: string }) {
|
||||
return handleApiResponse<Device>(response, "getDevice");
|
||||
}
|
||||
|
||||
export async function addDevice({
|
||||
name,
|
||||
mac,
|
||||
}: {
|
||||
name: string;
|
||||
mac: string;
|
||||
}) {
|
||||
type SingleDevice = Pick<Device, "name" | "mac">;
|
||||
const session = await getServerSession(authOptions);
|
||||
const response = await fetch(
|
||||
`${process.env.SARLINK_API_BASE_URL}/api/devices/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Token ${session?.apiToken}`,
|
||||
|
||||
|
||||
|
||||
export async function addDeviceAction(
|
||||
prevState: AddDeviceFormState,
|
||||
formData: FormData
|
||||
): Promise<AddDeviceFormState> {
|
||||
const name = formData.get("name") as string;
|
||||
const mac_address = formData.get("mac_address") as string;
|
||||
|
||||
const errors: typeof initialState.fieldErrors = {};
|
||||
if (!name || name.length < 2) {
|
||||
errors.name = ["Device name is required and must be at least 2 characters."];
|
||||
}
|
||||
if (!mac_address || !/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(mac_address)) {
|
||||
errors.mac_address = ["Invalid MAC address format."];
|
||||
}
|
||||
|
||||
if (Object.keys(errors).length > 0) {
|
||||
return {
|
||||
message: "Validation failed.",
|
||||
fieldErrors: errors,
|
||||
payload: formData,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.apiToken) {
|
||||
return { message: "Authentication required.", fieldErrors: {}, payload: formData };
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${process.env.SARLINK_API_BASE_URL}/api/devices/`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Token ${session.apiToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
mac: mac_address,
|
||||
registered: true,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
mac: mac,
|
||||
registered: true,
|
||||
}),
|
||||
},
|
||||
);
|
||||
revalidatePath("/devices");
|
||||
return handleApiResponse<SingleDevice>(response, "addDevice");
|
||||
);
|
||||
|
||||
await handleApiResponse<Device>(response, "addDeviceAction");
|
||||
|
||||
revalidatePath("/devices");
|
||||
return { message: "Device successfully added!", fieldErrors: {}, payload: formData };
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error("Server Action Error:", error);
|
||||
return { message: (error as ApiError).message || "An unexpected error occurred.", fieldErrors: {}, payload: formData };
|
||||
}
|
||||
}
|
||||
|
||||
export async function blockDevice({
|
||||
|
@ -20,7 +20,7 @@ export async function handleApiResponse<T>(
|
||||
if (response.status === 403) {
|
||||
throw new Error(
|
||||
responseData.message ||
|
||||
"Forbidden; you do not have permission to access this resource.",
|
||||
"Forbidden; you do not have permission to access this resource.",
|
||||
);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user