From d2b281281ffc43da4dd0aaef69b96645c56778a4 Mon Sep 17 00:00:00 2001 From: i701 Date: Wed, 25 Jun 2025 20:10:32 +0500 Subject: [PATCH] feat: implement add device functionality with validation and error handling; refactor related components for improved state management --- components/user/add-device-dialog.tsx | 133 +++++++++++--------------- queries/devices.ts | 82 +++++++++++----- utils/tryCatch.ts | 2 +- 3 files changed, 115 insertions(+), 102 deletions(-) diff --git a/components/user/add-device-dialog.tsx b/components/user/add-device-dialog.tsx index 61bb54b..49a105a 100644 --- a/components/user/add-device-dialog.tsx +++ b/components/user/add-device-dialog.tsx @@ -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>({ - 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> = 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 ( - @@ -93,7 +68,7 @@ export default function AddDeviceDialogForm({ user_id }: { user_id?: string }) { -
+
@@ -103,38 +78,44 @@ export default function AddDeviceDialogForm({ user_id }: { user_id?: string }) { - - {errors.name?.message} - + {state.fieldErrors?.name && ( + + {state.fieldErrors.name[0]} + + )}
-
-
); -} +} \ No newline at end of file diff --git a/queries/devices.ts b/queries/devices.ts index 48ac602..7942069 100644 --- a/queries/devices.ts +++ b/queries/devices.ts @@ -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(response, "getDevice"); } -export async function addDevice({ - name, - mac, -}: { - name: string; - mac: string; -}) { - type SingleDevice = Pick; - 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 { + 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(response, "addDevice"); + ); + + await handleApiResponse(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({ diff --git a/utils/tryCatch.ts b/utils/tryCatch.ts index 9cf6b9c..f70bbf0 100644 --- a/utils/tryCatch.ts +++ b/utils/tryCatch.ts @@ -20,7 +20,7 @@ export async function handleApiResponse( 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.", ); }