diff --git a/src/actions/place.ts b/src/actions/place.ts index 3c41d1e..e86be48 100644 --- a/src/actions/place.ts +++ b/src/actions/place.ts @@ -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"; @@ -14,6 +15,10 @@ interface PlacesResponse { items: Place[]; meta: ResponseMeta; } +interface RatingsResponse { + items: Ratings[]; + meta: ResponseMeta; +} export async function getPlaces( query: Query, @@ -152,3 +157,20 @@ export async function getPlaceService( return { error: getErrorMessage(error) }; } } +export async function getPlaceRatings( + placeId: string, + query: Query, +): Promise> { + try { + const endpoint = apiConfig.places.ratings(placeId, query); + const results = await apiHandler({ + endpoint, + method: "GET", + next: { tags: [Tags.reviews] }, + }); + return { results }; + } catch (error) { + console.log(error); + return { error: getErrorMessage(error) }; + } +} diff --git a/src/actions/ratings.ts b/src/actions/ratings.ts new file mode 100644 index 0000000..2447120 --- /dev/null +++ b/src/actions/ratings.ts @@ -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) }; + } +} diff --git a/src/app/(dashboard)/places/[placeId]/_sections/placeContent/PlaceBookings.tsx b/src/app/(dashboard)/places/[placeId]/_sections/placeContent/PlaceBookings.tsx index 27d9f94..eb8f1df 100644 --- a/src/app/(dashboard)/places/[placeId]/_sections/placeContent/PlaceBookings.tsx +++ b/src/app/(dashboard)/places/[placeId]/_sections/placeContent/PlaceBookings.tsx @@ -29,7 +29,7 @@ export default function PlaceBookings({ place }: Props) { return ( -
+

List of all bookings

{totalPages > 1 && ( diff --git a/src/app/(dashboard)/places/[placeId]/_sections/placeContent/PlaceReviews.tsx b/src/app/(dashboard)/places/[placeId]/_sections/placeContent/PlaceReviews.tsx new file mode 100644 index 0000000..8883d02 --- /dev/null +++ b/src/app/(dashboard)/places/[placeId]/_sections/placeContent/PlaceReviews.tsx @@ -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 ( +
+ {Array.from("loadin").map((l) => ( + +
+
+ ))} +
+ ); + } + + if (data?.results?.items?.length === 0 && !isPending) { + return ; + } + + const totalPages = data?.results?.meta?.totalPages || 1; + + return ( + +
+ {data?.results?.items?.map((rating) => ( + + ))} +
+ {totalPages > 1 && ( + + setPage(page)} + /> + + )} +
+ ); +} diff --git a/src/app/(dashboard)/places/[placeId]/_sections/placeContent/PlaceServices.tsx b/src/app/(dashboard)/places/[placeId]/_sections/placeContent/PlaceServices.tsx index 4393ffb..9d6df5a 100644 --- a/src/app/(dashboard)/places/[placeId]/_sections/placeContent/PlaceServices.tsx +++ b/src/app/(dashboard)/places/[placeId]/_sections/placeContent/PlaceServices.tsx @@ -58,7 +58,7 @@ export default function PlaceServices({ place }: Props) { } return ( -
+
( diff --git a/src/app/(dashboard)/places/[placeId]/_sections/placeContent/ProductCategoriesSection.tsx b/src/app/(dashboard)/places/[placeId]/_sections/placeContent/ProductCategoriesSection.tsx index 722d460..92576b2 100644 --- a/src/app/(dashboard)/places/[placeId]/_sections/placeContent/ProductCategoriesSection.tsx +++ b/src/app/(dashboard)/places/[placeId]/_sections/placeContent/ProductCategoriesSection.tsx @@ -57,7 +57,7 @@ export default function ProductCategoriesSection({ }; return ( -
+

Service categories

diff --git a/src/app/(dashboard)/places/[placeId]/_sections/placeContent/index.tsx b/src/app/(dashboard)/places/[placeId]/_sections/placeContent/index.tsx index b8f6af8..054955a 100644 --- a/src/app/(dashboard)/places/[placeId]/_sections/placeContent/index.tsx +++ b/src/app/(dashboard)/places/[placeId]/_sections/placeContent/index.tsx @@ -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 ( -
+
- + - + - + - -
Reviews
+ +
diff --git a/src/app/(dashboard)/places/[placeId]/page.tsx b/src/app/(dashboard)/places/[placeId]/page.tsx index 2c23b23..fc77b29 100644 --- a/src/app/(dashboard)/places/[placeId]/page.tsx +++ b/src/app/(dashboard)/places/[placeId]/page.tsx @@ -17,7 +17,7 @@ export default async function PlaceDetailPage({ params }: PageProps) { return ( -
+
diff --git a/src/components/shared/cards/ReviewCard.tsx b/src/components/shared/cards/ReviewCard.tsx new file mode 100644 index 0000000..a55490b --- /dev/null +++ b/src/components/shared/cards/ReviewCard.tsx @@ -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 ( +
+ + +
+ +
+

+ {rating?.user?.fullName} +

+
+
+ +
+ +

{rating?.comment}

+
+ +
+

Date:

+

+ {new Date(rating?.createdAt).toLocaleDateString()} +

+
+
+

+ {rating?.rating} {pluralize("star", rating?.rating)} +

+
+
+
+ +
+ + +
+
+ } + /> +
+ ); +} diff --git a/src/lib/apiConfig.ts b/src/lib/apiConfig.ts index 137edfd..ee9edea 100644 --- a/src/lib/apiConfig.ts +++ b/src/lib/apiConfig.ts @@ -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`, @@ -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}`, + }, }; diff --git a/src/types/ratings.ts b/src/types/ratings.ts new file mode 100644 index 0000000..4f75217 --- /dev/null +++ b/src/types/ratings.ts @@ -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; +} diff --git a/src/utils/tags.ts b/src/utils/tags.ts index 081e3e5..0dbc815 100644 --- a/src/utils/tags.ts +++ b/src/utils/tags.ts @@ -1,6 +1,7 @@ export enum Tags { places = "places", place = "place", + reviews = "reviews", place_service = "place_service", categories = "categories", users = "users",