Skip to content

Commit

Permalink
list reviews
Browse files Browse the repository at this point in the history
  • Loading branch information
Felix-Asante committed Feb 9, 2024
1 parent 7b873d0 commit ecb31bb
Show file tree
Hide file tree
Showing 12 changed files with 261 additions and 12 deletions.
22 changes: 22 additions & 0 deletions src/actions/place.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { apiHandler } from "@/lib/apiHandler";
import { CountResponse, ResponseMeta, SeverActionResponse } from "@/types";
import { User } from "@/types/auth";
import { Place, PlaceService } from "@/types/place";
import { Ratings } from "@/types/ratings";
import { Query } from "@/types/url";
import { getErrorMessage } from "@/utils/helpers";
import { Tags } from "@/utils/tags";
Expand All @@ -14,6 +15,10 @@ interface PlacesResponse {
items: Place[];
meta: ResponseMeta;
}
interface RatingsResponse {
items: Ratings[];
meta: ResponseMeta;
}

export async function getPlaces(
query: Query,
Expand Down Expand Up @@ -152,3 +157,20 @@ export async function getPlaceService(
return { error: getErrorMessage(error) };
}
}
export async function getPlaceRatings(
placeId: string,
query: Query,
): Promise<SeverActionResponse<RatingsResponse>> {
try {
const endpoint = apiConfig.places.ratings(placeId, query);
const results = await apiHandler<RatingsResponse>({
endpoint,
method: "GET",
next: { tags: [Tags.reviews] },
});
return { results };
} catch (error) {
console.log(error);
return { error: getErrorMessage(error) };
}
}
21 changes: 21 additions & 0 deletions src/actions/ratings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"use server";

import { apiConfig } from "@/lib/apiConfig";
import { apiHandler } from "@/lib/apiHandler";
import { getErrorMessage } from "@/utils/helpers";
import { Tags } from "@/utils/tags";
import { revalidateTag } from "next/cache";

export async function deleteRating(ratingId: string) {
try {
const endpoint = apiConfig.ratings.get(ratingId);
await apiHandler({
endpoint,
method: "DELETE",
});
revalidateTag(Tags.reviews);
} catch (error) {
console.log(error);
return { error: getErrorMessage(error) };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default function PlaceBookings({ place }: Props) {

return (
<WithServerError error={error?.message}>
<div className='mt-4'>
<div className='mt-4 px-3'>
<h3 className='font-semibold text-lg mb-2'>List of all bookings </h3>
<BookingListTable bookings={bookings?.results?.items ?? []} />
{totalPages > 1 && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { getPlaceRatings } from "@/actions/place";
import WithServerError from "@/components/hoc/WithServerError";
import EmptyContent from "@/components/shared/EmptyContent";
import ReviewCard from "@/components/shared/cards/ReviewCard";
import HStack from "@/components/shared/layout/HStack";
import { Place } from "@/types/place";
import { Pagination, Skeleton } from "@nextui-org/react";
import { useQuery } from "@tanstack/react-query";
import React, { useState } from "react";

interface Props {
place: Place;
}
export default function PlaceReviews({ place }: Props) {
const [page, setPage] = useState(1);

const { data, error, isPending } = useQuery({
queryKey: ["placeReviews", place?.id, page],
queryFn: () => getPlaceRatings(place?.id, { page }),
});

if (isPending) {
return (
<div className='grid grid-cols-2 xl:grid-cols-3 gap-3'>
{Array.from("loadin").map((l) => (
<Skeleton className='rounded-lg mb-3' key={l}>
<div className='h-40 rounded-lg bg-default-300'></div>
</Skeleton>
))}
</div>
);
}

if (data?.results?.items?.length === 0 && !isPending) {
return <EmptyContent title='No reviews yet' />;
}

const totalPages = data?.results?.meta?.totalPages || 1;

return (
<WithServerError error={error?.message}>
<div className='grid grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-4'>
{data?.results?.items?.map((rating) => (
<ReviewCard key={rating?.id} rating={rating} />
))}
</div>
{totalPages > 1 && (
<HStack className='justify-center mt-3'>
<Pagination
total={totalPages}
initialPage={1}
variant='bordered'
showControls
onChange={(page) => setPage(page)}
/>
</HStack>
)}
</WithServerError>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export default function PlaceServices({ place }: Props) {
}

return (
<div className='mt-6'>
<div className='mt-6 px-3'>
<HStack className='justify-between mb-4'>
<Controller
render={({ field }) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export default function ProductCategoriesSection({
};

return (
<div>
<div className='p-3'>
<div className='flex justify-between items-center mb-5'>
<div>
<h3 className='text-lg'>Service categories</h3>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,35 @@ import React from "react";
import ProductCategoriesSection from "./ProductCategoriesSection";
import PlaceServices from "./PlaceServices";
import PlaceBookings from "./PlaceBookings";
import PlaceReviews from "./PlaceReviews";

interface Props {
place: Place;
}
export default function PlaceContent({ place }: Props) {
return (
<div className='px-4 py-5'>
<div className=' pb-5'>
<div className='w-full'>
<Tabs
size={"md"}
size={"lg"}
aria-label='place nav tabs'
className='w-full min-w-full'
className='w-full min-w-full bg-gray-300'
variant='underlined'
>
<Tab key='productCategory' title='Product Category'>
<Tab className='p-4' key='productCategory' title='Product Category'>
<ProductCategoriesSection
placeId={place?.id}
categories={place?.productCategory}
/>
</Tab>
<Tab key='products' title='Products (Services/Menu)'>
<Tab className='p-4' key='products' title='Products (Services/Menu)'>
<PlaceServices place={place} />
</Tab>
<Tab key='bookings' title='Bookings'>
<Tab className='p-4' key='bookings' title='Bookings'>
<PlaceBookings place={place} />
</Tab>
<Tab key='Reviews' title='Reviews'>
<div>Reviews</div>
<Tab className='p-4' key='Reviews' title='Reviews'>
<PlaceReviews place={place} />
</Tab>
</Tabs>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/app/(dashboard)/places/[placeId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default async function PlaceDetailPage({ params }: PageProps) {

return (
<WithServerError error={error}>
<div className='flex gap-3 min-h-screen bg-gray-50'>
<div className='flex min-h-screen bg-gray-50'>
<div className='w-1/4 sticky top-0 min-h-full'>
<PlaceSidebar place={response?.results!} />
</div>
Expand Down
123 changes: 123 additions & 0 deletions src/components/shared/cards/ReviewCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use client";
import React from "react";
import {
Card,
CardHeader,
CardBody,
CardFooter,
Avatar,
Button,
useDisclosure,
} from "@nextui-org/react";
import { Ratings } from "@/types/ratings";
import { getErrorMessage, getInitials, pluralize } from "@/utils/helpers";
import Modal from "../modal";
import { useServerAction } from "@/hooks/useServerAction";
import { deleteRating } from "@/actions/ratings";
import { toast } from "sonner";
import { useQueryClient } from "@tanstack/react-query";

interface Props {
rating: Ratings;
}
export default function ReviewCard({ rating }: Props) {
const { isOpen, onOpen, onClose } = useDisclosure();

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

const queryClient = useQueryClient();

const onConfirmDelete = async () => {
try {
const response = await runDeleteReview(rating?.id);
if (response?.error) {
toast.error(response.error);
return;
}
toast.success("Review deleted");
queryClient.invalidateQueries({
queryKey: ["getReviews", rating?.booking?.place?.id],
});
onClose();
} catch (error) {
toast.error(getErrorMessage(error));
}
};

return (
<div>
<Card className='max-w-[340px] h-fit' shadow='sm'>
<CardHeader className='justify-between'>
<div className='flex gap-3'>
<Avatar
isBordered
radius='full'
size='sm'
fallback={getInitials(rating?.user?.fullName)}
/>
<div className='flex flex-col gap-1 items-start justify-center'>
<h4 className='text-small font-semibold line-clamp-1 leading-none text-default-600'>
{rating?.user?.fullName}
</h4>
</div>
</div>
<Button
onClick={onOpen}
disableRipple
color='primary'
radius='full'
size='sm'
>
Delete
</Button>
</CardHeader>
<CardBody className='px-3 py-0 text-small text-default-400'>
<p>{rating?.comment}</p>
</CardBody>
<CardFooter className='gap-3 justify-between'>
<div className='flex gap-1'>
<p className='font-semibold text-default-400 text-small'>Date:</p>
<p className='text-default-400 text-small'>
{new Date(rating?.createdAt).toLocaleDateString()}
</p>
</div>
<div className='flex gap-1'>
<p className=' text-default-400 text-small'>
{rating?.rating} {pluralize("star", rating?.rating)}
</p>
</div>
</CardFooter>
</Card>
<Modal
isOpen={isOpen}
onClose={onClose}
title='Delete review'
description='Are you sure you want to delete this review?'
content={
<div className='flex flex-col gap-3'>
<div className='flex gap-3'>
<Button
disableRipple
color='secondary'
onClick={onClose}
isDisabled={loading}
>
Cancel
</Button>
<Button
color='primary'
onClick={onConfirmDelete}
isLoading={loading}
>
Continue
</Button>
</div>
</div>
}
/>
</div>
);
}
6 changes: 6 additions & 0 deletions src/lib/apiConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export const apiConfig = {
products: (placeId: string) => `places/${placeId}/products`,
new: () => `places/new`,
popular: () => `places/popular/locations`,
ratings: (placeId: string, query: Query) =>
`places/${placeId}/ratings${toQuery(query)}`,
},
categories: {
create: () => `categories`,
Expand Down Expand Up @@ -73,4 +75,8 @@ export const apiConfig = {
count: () => `bookings/all/count`,
sales: (year: string) => `bookings/all/sales${toQuery({ year })}`,
},
ratings: {
root: () => `reviews`,
get: (ratingId: string) => `reviews/${ratingId}`,
},
};
14 changes: 14 additions & 0 deletions src/types/ratings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { User } from "./auth";
import { Booking } from "./booking";

export interface Ratings {
id: string;
createdAt: string;
updatedAt: string;
deletedAt: string | null;
rating: number;
comment: string;
date: string;
user: User;
booking: Booking;
}
1 change: 1 addition & 0 deletions src/utils/tags.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export enum Tags {
places = "places",
place = "place",
reviews = "reviews",
place_service = "place_service",
categories = "categories",
users = "users",
Expand Down

0 comments on commit ecb31bb

Please sign in to comment.