Skip to content

Commit

Permalink
categories implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Felix-Asante committed Dec 27, 2023
1 parent 7761868 commit c085ffe
Show file tree
Hide file tree
Showing 14 changed files with 480 additions and 34 deletions.
49 changes: 47 additions & 2 deletions src/actions/category.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,61 @@
"use server";
import { apiConfig } from "@/lib/apiConfig";
import { apiHandler } from "@/lib/apiHandler";
import { SeverActionResponse } from "@/types";
import { Category } from "@/types/category";
import { getErrorMessage } from "@/utils/helpers";
import { Tags } from "@/utils/tags";
import { revalidateTag } from "next/cache";

export async function getCategory(): Promise<Category[]> {
export async function getCategory(): Promise<SeverActionResponse<Category[]>> {
try {
const endpoint = apiConfig.categories.list();
const categories = await apiHandler<Category[]>({
endpoint,
method: "GET",
next: { tags: [Tags.categories] },
});
return categories;
return { results: categories };
} catch (error) {
return { error: getErrorMessage(error) };
}
}
export async function createCategory(body: FormData) {
try {
const endpoint = apiConfig.categories.create();
await apiHandler({
endpoint,
method: "POST",
json: false,
body,
});
revalidateTag(Tags.categories);
} catch (error) {
throw new Error(getErrorMessage(error));
}
}
export async function updateCategory(categoryId: string, body: FormData) {
try {
const endpoint = apiConfig.categories.update(categoryId);
await apiHandler({
endpoint,
method: "PUT",
json: false,
body,
});
revalidateTag(Tags.categories);
} catch (error) {
throw new Error(getErrorMessage(error));
}
}
export async function deleteCategory(categoryId: string) {
try {
const endpoint = apiConfig.categories.delete(categoryId);
await apiHandler({
endpoint,
method: "DELETE",
});
revalidateTag(Tags.categories);
} catch (error) {
throw new Error(getErrorMessage(error));
}
Expand Down
26 changes: 16 additions & 10 deletions src/actions/place.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { DASHBOARD_PATHS } from "@/config/routes";
import { apiConfig } from "@/lib/apiConfig";
import { apiHandler } from "@/lib/apiHandler";
import { ResponseMeta } from "@/types";
import { ResponseMeta, SeverActionResponse } from "@/types";
import { User } from "@/types/auth";
import { CreatePlaceDto } from "@/types/dtos/places";
import { Place } from "@/types/place";
Expand All @@ -18,17 +18,19 @@ interface PlacesResponse {
meta: ResponseMeta;
}

export async function getPlaces(query: Query): Promise<PlacesResponse> {
export async function getPlaces(
query: Query,
): Promise<SeverActionResponse<PlacesResponse>> {
try {
const endpoint = apiConfig.places.list(query);
const places = await apiHandler<PlacesResponse>({
endpoint,
method: "GET",
next: { tags: [Tags.places] },
});
return places;
return { results: places };
} catch (error) {
throw new Error(getErrorMessage(error));
throw { error: getErrorMessage(error) };
}
}

Expand All @@ -54,28 +56,32 @@ export async function deletePlace(placeId: string) {
throw new Error(getErrorMessage(error));
}
}
export async function getPlaceBySlug(slug: string): Promise<Place> {
export async function getPlaceBySlug(
slug: string,
): Promise<SeverActionResponse<Place>> {
try {
const endpoint = apiConfig.places.get_by_slug(slug);
const place = await apiHandler<Place>({
endpoint,
method: "GET",
});
return place;
return { results: place };
} catch (error) {
throw new Error(getErrorMessage(error));
return { error: getErrorMessage(error) };
}
}
export async function getPlaceAdmin(placeId: string): Promise<User> {
export async function getPlaceAdmin(
placeId: string,
): Promise<SeverActionResponse<User>> {
try {
const endpoint = apiConfig.places.get_admin(placeId);
const admin = await apiHandler<User>({
endpoint,
method: "GET",
});
return admin;
return { results: admin };
} catch (error) {
throw new Error(getErrorMessage(error));
return { error: getErrorMessage(error) };
}
}

Expand Down
6 changes: 3 additions & 3 deletions src/app/(dashboard)/bookings/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,12 @@ export default async function BookingsPage({ searchParams }: PageProps) {
]);

const totalBookings = bookings?.results?.meta?.totalItems;

const error = bookings?.error ?? categories?.error;
return (
<WithServerError error={bookings?.error}>
<WithServerError error={error}>
<div className='bg-white min-h-screen'>
<BookingLists
categories={categories}
categories={categories?.results!}
bookings={bookings?.results?.items!}
totalBookings={totalBookings!}
totalPages={bookings?.results?.meta?.totalPages!}
Expand Down
38 changes: 38 additions & 0 deletions src/app/(dashboard)/categories/_sections/CategorySection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";
import HStack from "@/components/shared/layout/HStack";
import useQueryParams from "@/hooks/useQueryParam";
import { Category } from "@/types/category";
import { Pagination } from "@nextui-org/react";
import CategoryTable from "./CategoryTable";

interface Props {
categories: Category[];
}
const PAGE_SIZE = 10;

export default function CategorySection({ categories }: Props) {
const { add } = useQueryParams();

const totalCategories = categories.length;
const totalPages = Math.ceil(totalCategories / PAGE_SIZE);

return (
<section className='px-5 pt-5'>
<CategoryTable
categories={categories}
totalCategories={totalCategories}
/>
{totalPages > 1 && (
<HStack className='justify-end mt-3'>
<Pagination
total={totalPages}
initialPage={1}
variant='bordered'
showControls
onChange={(page) => add("page", page.toString())}
/>
</HStack>
)}
</section>
);
}
172 changes: 172 additions & 0 deletions src/app/(dashboard)/categories/_sections/CategoryTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import EmptyContent from "@/components/shared/EmptyContent";
import HStack from "@/components/shared/layout/HStack";
import { Category } from "@/types/category";
import {
Button,
Table,
TableBody,
TableCell,
TableColumn,
TableHeader,
TableRow,
useDisclosure,
} from "@nextui-org/react";
import { PencilIcon, Trash2Icon } from "lucide-react";
import { useEffect, useState } from "react";
import NewCategoryModal from "./NewCategoryModal";
import Modal from "@/components/shared/modal";
import { ERRORS } from "@/config/constants/errors";
import { useServerAction } from "@/hooks/useServerAction";
import { deleteCategory } from "@/actions/category";
import { toast } from "sonner";
import { getErrorMessage } from "@/utils/helpers";

interface CategoryTableProp {
categories: Category[];
totalCategories: number;
}
export default function CategoryTable(props: CategoryTableProp) {
const { categories, totalCategories } = props;

const { isOpen, onOpen, onClose } = useDisclosure();
const [selectedCategory, setSelectedCategory] = useState<Category | null>(
null,
);

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

const removeCategory = async () => {
try {
if (!selectedCategory?.id) {
toast.error("Operation not allowed");
return;
}

await runDeleteCategory(selectedCategory.id);
toast.success("Category deleted");
setSelectedCategory(null);
} catch (error) {
toast.error(getErrorMessage(error));
}
};

useEffect(() => {
if (!isOpen) {
setSelectedCategory(null);
}
}, [isOpen]);

return (
<>
<HStack className='justify-between items-center'>
<div>
<h3 className='font-semibold text-xl'>
{totalCategories} {totalCategories > 1 ? "Category" : "Categories"}
</h3>
<p className='text-gray-400'>List of all categories</p>
</div>
<Button
radius='sm'
size='md'
color='primary'
disableRipple
className='font-semibold'
onClick={onOpen}
>
Add new category
</Button>
</HStack>
<div className='p-3 mt-4 border rounded-md'>
<Table aria-label='list of categories' shadow='none' radius='none'>
<TableHeader>
<TableColumn className='w-1/2'>Name</TableColumn>
<TableColumn>{null}</TableColumn>
</TableHeader>
<TableBody
emptyContent={
<EmptyContent
// img={illustration_empty_content}
title=''
description='No places added'
/>
}
>
{categories.map((category) => (
<TableRow key={category?.id}>
<TableCell className='capitalize'>{category?.name}</TableCell>
<TableCell>
<HStack className='items-center justify-end'>
<Button
radius='sm'
size='sm'
disableRipple
color='danger'
onClick={() => setSelectedCategory(category)}
isIconOnly
>
<Trash2Icon className='text-white' size={15} />
</Button>
<Button
radius='sm'
size='sm'
disableRipple
color='success'
onClick={() => {
setSelectedCategory(category);
onOpen();
}}
isIconOnly
>
<PencilIcon className='text-white' size={20} />
</Button>
</HStack>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<NewCategoryModal
isOpen={isOpen}
onClose={onClose}
mode={selectedCategory !== null ? "edit" : "create"}
category={selectedCategory}
/>
<Modal
isOpen={selectedCategory !== null && !isOpen}
onClose={() => setSelectedCategory(null)}
title='Delete Category'
description={ERRORS.MESSAGE.DELETE_PROMPT}
content={
<HStack className='justify-end'>
<Button
radius='sm'
size='md'
variant='bordered'
disableRipple
className='font-semibold'
onClick={() => setSelectedCategory(null)}
isDisabled={loading}
>
Cancel
</Button>
<Button
radius='sm'
size='md'
color='primary'
disableRipple
className='font-semibold'
onClick={removeCategory}
isLoading={loading}
>
Continue
</Button>
</HStack>
}
/>
</div>
</>
);
}
Loading

0 comments on commit c085ffe

Please sign in to comment.