From 55ea1ab4784696800bf010a1313fcaebd7320b9c Mon Sep 17 00:00:00 2001 From: Felix-Asante Date: Sat, 9 Dec 2023 19:26:28 +0100 Subject: [PATCH] delete place api integration --- src/actions/place.ts | 19 ++ .../places/sections/PlaceTable.tsx | 184 +++++++++++++----- .../places/sections/PlaceTableFilters.tsx | 22 ++- .../places/sections/PlacesContentSection.tsx | 48 ++++- src/components/shared/modal/index.tsx | 47 +++++ src/hooks/useServerAction.ts | 50 +++++ src/utils/helpers.ts | 8 +- src/utils/tags.ts | 5 + tsconfig.json | 51 ++--- 9 files changed, 345 insertions(+), 89 deletions(-) create mode 100644 src/components/shared/modal/index.tsx create mode 100644 src/hooks/useServerAction.ts create mode 100644 src/utils/tags.ts diff --git a/src/actions/place.ts b/src/actions/place.ts index 9d95577..3a627e1 100644 --- a/src/actions/place.ts +++ b/src/actions/place.ts @@ -1,11 +1,15 @@ "use server"; +import { DASHBOARD_PATHS } from "@/config/routes"; import { apiConfig } from "@/lib/apiConfig"; import { apiHandler } from "@/lib/apiHandler"; import { ResponseMeta } from "@/types"; import { Place } from "@/types/place"; import { Query } from "@/types/url"; import { getErrorMessage } from "@/utils/helpers"; +import { Tags } from "@/utils/tags"; +import { revalidateTag } from "next/cache"; +import { redirect } from "next/navigation"; interface PlacesResponse { items: Place[]; @@ -18,9 +22,24 @@ export async function getPlaces(query: Query): Promise { const places = await apiHandler({ endpoint, method: "GET", + next: { tags: [Tags.places] }, }); return places; } catch (error) { throw new Error(getErrorMessage(error)); } } +export async function deletePlace(placeId: string, redirectHome = false) { + try { + const endpoint = apiConfig.places.delete(placeId); + await apiHandler({ + endpoint, + method: "DELETE", + }); + revalidateTag(Tags.places); + redirectHome && redirect(DASHBOARD_PATHS.places.root); + } catch (error) { + console.log(error); + throw new Error(getErrorMessage(error)); + } +} diff --git a/src/app/(dashboard)/places/sections/PlaceTable.tsx b/src/app/(dashboard)/places/sections/PlaceTable.tsx index 0c2cc3b..c7421dc 100644 --- a/src/app/(dashboard)/places/sections/PlaceTable.tsx +++ b/src/app/(dashboard)/places/sections/PlaceTable.tsx @@ -6,6 +6,7 @@ import { DASHBOARD_PATHS } from "@/config/routes"; import { Place } from "@/types/place"; import { Avatar, + Button, Table, TableBody, TableCell, @@ -15,8 +16,13 @@ import { } from "@nextui-org/react"; import { PencilIcon, TrashIcon } from "lucide-react"; import Link from "next/link"; -import React, { useState } from "react"; +import React, { useState, useTransition } from "react"; import illustration_empty_content from "../../../../../public/svg/illustration_empty_content.svg"; +import Modal from "@/components/shared/modal"; +import { toast } from "sonner"; +import { getErrorMessage } from "@/utils/helpers"; +import { deletePlace } from "@/actions/place"; +import { useServerAction } from "@/hooks/useServerAction"; const columns = [ { @@ -39,64 +45,138 @@ const columns = [ interface TableProps { places: Place[]; + selectedKeys: any; + onSelectionChange: any; } -export default function PlaceTable({ places }: TableProps) { +export default function PlaceTable({ + places, + selectedKeys, + onSelectionChange, +}: TableProps) { const [selectionBehavior, setSelectionBehavior] = useState< "toggle" | "replace" | undefined >("toggle"); + const [placeToBeDeleted, setPlaceToBeDeleted] = useState(null); + + const [run, { loading }] = useServerAction( + deletePlace, + ); + + const openModal = placeToBeDeleted != null && placeToBeDeleted?.length > 0; + + const closeModal = () => setPlaceToBeDeleted(null); + + const deletePlaceHandler = async () => { + try { + if (placeToBeDeleted) { + await run(placeToBeDeleted); + toast.success("Place successfully deleted"); + closeModal(); + } + } catch (error) { + toast.error(getErrorMessage(error)); + } + }; return ( - - - {columns.map((column) => ( - {column.label} - ))} + <> +
+ + {columns.map((column) => ( + {column.label} + ))} - - - + + + + } + > + {places.map((row) => ( + + + + +
+

{row?.name}

+

{row?.email}

+
+
+
+ {row?.address} + {row?.category?.name} + + {DEFAULT_CURRENCY.symbol} {row?.deliveryFee} + + + + + + + + + +
+ ))} +
+
+ + + + } - > - {places.map((row) => ( - - - - -
-

{row?.name}

-

{row?.email}

-
-
-
- {row?.address} - {row?.category?.name} - - {DEFAULT_CURRENCY.symbol} {row?.deliveryFee} - - - - - - - - - -
- ))} - - + isOpen={openModal} + onClose={closeModal} + /> + ); } diff --git a/src/app/(dashboard)/places/sections/PlaceTableFilters.tsx b/src/app/(dashboard)/places/sections/PlaceTableFilters.tsx index c35cd52..b799f07 100644 --- a/src/app/(dashboard)/places/sections/PlaceTableFilters.tsx +++ b/src/app/(dashboard)/places/sections/PlaceTableFilters.tsx @@ -12,11 +12,18 @@ interface FormFields { search: string; category: string; } +interface Props { + categories: Category[]; + disableDelete: boolean; + onDelete: () => void; + deleting: boolean; +} export default function PlaceTableFilters({ categories, -}: { - categories: Category[]; -}) { + disableDelete, + onDelete, + deleting, +}: Props) { const { control, watch } = useForm(); const search = useDebounce(watch("search"), 1000); @@ -73,7 +80,14 @@ export default function PlaceTableFilters({ variant='bordered' /> - diff --git a/src/app/(dashboard)/places/sections/PlacesContentSection.tsx b/src/app/(dashboard)/places/sections/PlacesContentSection.tsx index d64bf13..be7fbfc 100644 --- a/src/app/(dashboard)/places/sections/PlacesContentSection.tsx +++ b/src/app/(dashboard)/places/sections/PlacesContentSection.tsx @@ -3,14 +3,17 @@ import HStack from "@/components/shared/layout/HStack"; import { DASHBOARD_PATHS } from "@/config/routes"; import { Button, Pagination } from "@nextui-org/react"; import { PlusIcon } from "lucide-react"; -import React from "react"; +import React, { useState } from "react"; import PlaceTableFilters from "./PlaceTableFilters"; import PlaceTable from "./PlaceTable"; import { Place } from "@/types/place"; import { Category } from "@/types/category"; -import { pluralize } from "@/utils/helpers"; +import { getErrorMessage, pluralize } from "@/utils/helpers"; import { ResponseMeta } from "@/types"; import useQueryParams from "@/hooks/useQueryParam"; +import { useServerAction } from "@/hooks/useServerAction"; +import { deletePlace } from "@/actions/place"; +import { toast } from "sonner"; interface Props { places: Place[]; @@ -25,6 +28,34 @@ export default function PlacesContentSection({ }: Props) { const totalPlace = meta?.totalItems; const { add } = useQueryParams(); + const [selectedPlaces, setSelectedPlaces] = useState>( + new Set([]), + ); + + const [run, { loading }] = useServerAction( + deletePlace, + ); + + const deleteMorePlaceHandler = async () => { + let placeIds: string[] = []; + try { + if (typeof selectedPlaces === "string") { + placeIds = places?.map((place) => place.id); + } else { + placeIds = [...selectedPlaces]; + } + if (placeIds.length === 0) { + toast.error("Make sure you have selected a place"); + return; + } + await Promise.all(placeIds.map((placeId) => run(placeId))); + setSelectedPlaces(new Set([])); + toast.success("Places successfully deleted"); + } catch (error) { + toast.error(getErrorMessage(error)); + } + }; + return ( <> @@ -42,8 +73,17 @@ export default function PlacesContentSection({
- - + + {meta?.totalPages > 1 && ( void; + isOpen: boolean; +} +export default function Modal(props: ModalProps) { + const { + title, + description, + content, + size = "sm", + closeOnOutsideClick = false, + backdrop = "opaque", + onClose, + isOpen, + } = props; + return ( + + + {title} + + {description &&

{description}

} + {content &&
{content}
} +
+
+
+ ); +} diff --git a/src/hooks/useServerAction.ts b/src/hooks/useServerAction.ts new file mode 100644 index 0000000..274fed4 --- /dev/null +++ b/src/hooks/useServerAction.ts @@ -0,0 +1,50 @@ +import { useState, useTransition } from "react"; + +interface ServerActionResponse { + loading: boolean; + error: any; + data: T | undefined; +} + +type DefaultAction = (...args: any[]) => Promise; + +/** + * A custom React hook for handling server actions. + * + * @template T - The type of data returned by the server action. + * @template A - The type of the server action function (default: DefaultAction). + * + * @param {A} action - The server action function to be executed. + * + * @returns {[Function, ServerActionResponse]} - A tuple containing a function to execute the server action + * and an object representing the state of the action (loading, error, and data). + */ +export function useServerAction( + action: A = (() => Promise.resolve()) as A, +): [ + (...args: Parameters) => Promise>, + ServerActionResponse, +] { + const [loading, startTransition] = useTransition(); + const [error, setError] = useState(); + const [data, setData] = useState(); + + const run = (...args: Parameters) => { + return new Promise>((resolve, reject) => { + startTransition(async () => { + try { + setError(undefined); + const result = await action(...args); + resolve(result); + setData(result); + } catch (err: any) { + setError(err); + setData(undefined); + reject(err); + } + }); + }); + }; + + return [run, { loading, error, data }]; +} diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 99a1338..5f7fc5b 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -47,16 +47,16 @@ export function cn(...inputs: ClassValue[]) { } export function getErrorMessage(error: any) { - if (!error?.response) { + if (!error?.response && !error?.message) { return ERRORS.MESSAGE.NETWORK; } - const { status, data } = error?.response; - if (status === 403) { + const { statusCode, data, message = "" } = error; + if (statusCode === 403) { // logout console.log("logout"); } - return data?.message; + return data?.message || message; } export function pluralize(text: string, total = 2): string { diff --git a/src/utils/tags.ts b/src/utils/tags.ts new file mode 100644 index 0000000..bf50254 --- /dev/null +++ b/src/utils/tags.ts @@ -0,0 +1,5 @@ +export enum Tags { + places = "places", + categories = "categories", + users = "users", +} diff --git a/tsconfig.json b/tsconfig.json index e59724b..aa331a3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,27 +1,28 @@ { - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./src/*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "downlevelIteration": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] }