Skip to content

Commit

Permalink
product category functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
Felix-Asante committed Feb 8, 2024
1 parent 45627b9 commit 1a978ca
Show file tree
Hide file tree
Showing 8 changed files with 375 additions and 1 deletion.
46 changes: 46 additions & 0 deletions src/actions/category.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use server";
import { apiConfig } from "@/lib/apiConfig";
import { apiHandler } from "@/lib/apiHandler";
import { CreateProductCategoryDto } from "@/rules/validations/category";
import { CountResponse, SeverActionResponse } from "@/types";
import { Category } from "@/types/category";
import { getErrorMessage } from "@/utils/helpers";
Expand Down Expand Up @@ -75,3 +76,48 @@ export async function getCategoryCount(): Promise<
return { error: getErrorMessage(error) };
}
}
interface CreateProductCategory {
name: string;
place: string;
}
export async function createProductCategory(body: CreateProductCategory) {
try {
const endpoint = apiConfig.productCategory.root();
await apiHandler<CountResponse>({
endpoint,
method: "POST",
body,
});
revalidateTag(Tags.place);
} catch (error) {
return { error: getErrorMessage(error) };
}
}
export async function updateProductCategory(
body: CreateProductCategoryDto,
categoryId: string,
) {
try {
const endpoint = apiConfig.productCategory.get(categoryId);
await apiHandler<CountResponse>({
endpoint,
method: "PUT",
body,
});
revalidateTag(Tags.place);
} catch (error) {
return { error: getErrorMessage(error) };
}
}
export async function deleteProductCategory(categoryId: string) {
try {
const endpoint = apiConfig.productCategory.get(categoryId);
await apiHandler<CountResponse>({
endpoint,
method: "DELETE",
});
revalidateTag(Tags.place);
} catch (error) {
return { error: getErrorMessage(error) };
}
}
1 change: 1 addition & 0 deletions src/actions/place.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export async function getPlace(
const place = await apiHandler<Place>({
endpoint,
method: "GET",
next: { tags: [Tags.place] },
});
return { results: place };
} catch (error) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import {
createProductCategory,
deleteProductCategory,
updateProductCategory,
} from "@/actions/category";
import TextField from "@/components/shared/input/TextField";
import Modal from "@/components/shared/modal";
import { useReactHookForm } from "@/hooks/useReactHookForm";
import { useServerAction } from "@/hooks/useServerAction";
import {
CreateProductCategoryDto,
createProductCategorySchema,
} from "@/rules/validations/category";
import { ProductCategory } from "@/types/category";
import { getErrorMessage } from "@/utils/helpers";
import { Button, useDisclosure } from "@nextui-org/react";
import { PencilIcon, TrashIcon } from "lucide-react";
import React, { useEffect, useState } from "react";
import { toast } from "sonner";

interface Props {
categories: ProductCategory[];
placeId: string;
}
export default function ProductCategoriesSection({
categories,
placeId,
}: Props) {
const [showCreateModal, setShowCreateModal] = useState(false);
const form = useReactHookForm<CreateProductCategoryDto>(
createProductCategorySchema,
);

const [runCreateCategory, { loading }] = useServerAction<
any,
typeof createProductCategory
>(createProductCategory);

const closeCreateModal = () => {
form.reset();
form.clearErrors();
setShowCreateModal(false);
};

const onSubmit = async (data: CreateProductCategoryDto) => {
try {
const response = await runCreateCategory({ ...data, place: placeId });
if (response?.error) {
toast.error(response.error);
return;
}
toast.success("Category added successfully");
closeCreateModal();
} catch (error) {
toast.error(getErrorMessage(error));
}
};

return (
<div>
<div className='flex justify-between items-center mb-5'>
<div>
<h3 className='text-lg'>Service categories</h3>
<p className='text-sm font-medium'>List of all service category</p>
</div>
<Button
disableRipple
color='primary'
radius='sm'
onClick={() => setShowCreateModal(true)}
>
Add new category
</Button>
</div>
<div className='flex flex-col gap-3'>
{categories?.map((category) => (
<ProductCategoryCard key={category?.id} {...category} />
))}
</div>

<Modal
isOpen={showCreateModal}
onClose={closeCreateModal}
title='Add service category'
content={
<form onSubmit={form.handleSubmit(onSubmit)}>
<TextField
name='name'
label='Name'
size='sm'
control={form.control}
variant='bordered'
/>
<div className='flex gap-2 mt-4'>
<Button
disableRipple
color='secondary'
radius='sm'
onClick={closeCreateModal}
type='button'
isDisabled={loading}
>
Cancel
</Button>
<Button
disabled={!form.formState.isValid}
isDisabled={!form.formState.isValid}
disableRipple
color='primary'
radius='sm'
type='submit'
isLoading={loading}
>
Add new category
</Button>
</div>
</form>
}
/>
</div>
);
}

function ProductCategoryCard(props: ProductCategory) {
const [editMode, setEditMode] = useState(false);
const { isOpen, onOpen, onClose } = useDisclosure();

const form = useReactHookForm<CreateProductCategoryDto>(
createProductCategorySchema,
{ name: props?.name },
);

const [runUpdateCategory, { loading }] = useServerAction<
any,
typeof updateProductCategory
>(updateProductCategory);

const [runDeleteCategory, { loading: deleting }] = useServerAction<
any,
typeof deleteProductCategory
>(deleteProductCategory);

const exitEditMode = () => {
form.reset();
form.clearErrors();
setEditMode(false);
};

useEffect(() => {
if (editMode) {
setTimeout(() => {
form.setValue("name", props.name);
}, 100);
}
}, [editMode, props.name]);

const onSubmit = async (data: CreateProductCategoryDto) => {
try {
const response = await runUpdateCategory(data, props?.id);
if (response?.error) {
toast.error(response.error);
return;
}
toast.success("Category updated successfully");
exitEditMode();
} catch (error) {
toast.error(getErrorMessage(error));
}
};
const onDelete = async () => {
try {
const response = await runDeleteCategory(props?.id);
if (response?.error) {
toast.error(response.error);
return;
}
toast.success("Category deleted successfully");
onClose();
} catch (error) {
toast.error(getErrorMessage(error));
}
};
return (
<div className='p-3 rounded-md bg-white flex items-center justify-between'>
{editMode ? (
<div className='w-1/2'>
<TextField
name='name'
label='Name'
size='sm'
control={form.control}
variant='bordered'
/>
</div>
) : (
<p className='font-medium uppercase'>{props?.name}</p>
)}
{props.name?.toLowerCase() !== "uncategorized" && !editMode && (
<div className='flex items-center gap-2'>
<Button
onClick={onOpen}
disableRipple
size='sm'
isIconOnly
color='danger'
>
<TrashIcon size={20} />
</Button>
<Button
onClick={() => setEditMode(true)}
disableRipple
size='sm'
isIconOnly
color='secondary'
>
<PencilIcon size={20} />
</Button>
</div>
)}
{editMode && (
<div className='flex items-center gap-2'>
<Button
disableRipple
size='sm'
onClick={exitEditMode}
color='secondary'
isDisabled={loading}
>
Cancel
</Button>
<Button
disableRipple
size='sm'
onClick={form.handleSubmit(onSubmit)}
color='primary'
isLoading={loading}
>
Save
</Button>
</div>
)}
<Modal
isOpen={isOpen}
onClose={onClose}
title='Delete service category'
description='Do you really want to delete this category? Note that this operation cannot be undone'
content={
<div className='flex gap-2 mt-4'>
<Button
disableRipple
color='secondary'
radius='sm'
onClick={onClose}
type='button'
isDisabled={deleting}
>
Cancel
</Button>
<Button
disableRipple
color='primary'
radius='sm'
isLoading={deleting}
onClick={onDelete}
>
Continue
</Button>
</div>
}
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";
import { Place } from "@/types/place";
import { Tab, Tabs } from "@nextui-org/react";
import React from "react";
import ProductCategoriesSection from "./ProductCategoriesSection";

interface Props {
place: Place;
}
export default function PlaceContent({ place }: Props) {
return (
<div className='px-4 py-5'>
<div className='w-full'>
<Tabs
size={"md"}
aria-label='place nav tabs'
className='w-full min-w-full'
>
<Tab key='productCategory' title='Product Category'>
<ProductCategoriesSection
placeId={place?.id}
categories={place?.productCategory}
/>
</Tab>
<Tab key='products' title='Products (Services/Menu)'>
<div>Menu</div>
</Tab>
<Tab key='bookings' title='Bookings'>
<div>Bookings</div>
</Tab>
<Tab key='Reviews' title='Reviews'>
<div>Reviews</div>
</Tab>
</Tabs>
</div>
</div>
);
}
5 changes: 4 additions & 1 deletion src/app/(dashboard)/places/[placeId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from "react";
import PlaceSidebar from "./_sections/PlaceSidebar";
import { getPlace } from "@/actions/place";
import WithServerError from "@/components/hoc/WithServerError";
import PlaceContent from "./_sections/placeContent";

interface PageProps {
params: {
Expand All @@ -16,7 +17,9 @@ export default async function PlaceDetailPage({ params }: PageProps) {
<div className='w-1/4 sticky top-0 min-h-full'>
<PlaceSidebar place={response?.results!} />
</div>
<p>Home</p>
<div className='w-[75%]'>
<PlaceContent place={response?.results!} />
</div>
</div>
</WithServerError>
);
Expand Down
Loading

0 comments on commit 1a978ca

Please sign in to comment.