From 5412948dc0902c2b66b8075321d908bbd31d6d80 Mon Sep 17 00:00:00 2001 From: Olaleye Blessing Date: Fri, 12 Apr 2024 12:05:18 +0100 Subject: [PATCH] Both: Enable users to search by number of ratings --- backend/src/controllers/room.ts | 1 + .../app/search/_components/sub-search.tsx | 115 ------------------ .../_components/sub-search/amenities.tsx | 23 ++++ .../search/_components/sub-search/beds.tsx | 33 +++++ .../search/_components/sub-search/budget.tsx | 33 +++++ .../search/_components/sub-search/index.tsx | 37 ++++++ .../search/_components/sub-search/ratings.tsx | 47 +++++++ .../search/_components/sub-search/type.tsx | 44 +++++++ frontend/app/search/_hooks/use-result.ts | 4 + frontend/app/search/_types/index.ts | 1 + frontend/app/search/page.tsx | 2 + .../components/custom/form-radio-field.tsx | 4 +- frontend/components/custom/rating-star.tsx | 56 +++++++++ 13 files changed, 283 insertions(+), 117 deletions(-) delete mode 100644 frontend/app/search/_components/sub-search.tsx create mode 100644 frontend/app/search/_components/sub-search/amenities.tsx create mode 100644 frontend/app/search/_components/sub-search/beds.tsx create mode 100644 frontend/app/search/_components/sub-search/budget.tsx create mode 100644 frontend/app/search/_components/sub-search/index.tsx create mode 100644 frontend/app/search/_components/sub-search/ratings.tsx create mode 100644 frontend/app/search/_components/sub-search/type.tsx create mode 100644 frontend/components/custom/rating-star.tsx diff --git a/backend/src/controllers/room.ts b/backend/src/controllers/room.ts index 2b6c1df..69e1d97 100644 --- a/backend/src/controllers/room.ts +++ b/backend/src/controllers/room.ts @@ -30,6 +30,7 @@ export const setRoomsFilter: RequestHandler = (req, _res, next) => { 'price', 'maxNumOfGuests', 'numOfBathrooms', + 'ratings', ], }; diff --git a/frontend/app/search/_components/sub-search.tsx b/frontend/app/search/_components/sub-search.tsx deleted file mode 100644 index 84870b9..0000000 --- a/frontend/app/search/_components/sub-search.tsx +++ /dev/null @@ -1,115 +0,0 @@ -"use client"; - -import { FormField } from "@/components/custom/form-field"; -import { SearchData } from "../_types"; -import { Controller, useFormContext } from "react-hook-form"; -import { FormRadioField } from "@/components/custom/form-radio-field"; -import Amenities from "@/app/hotels/new/_components/amenities"; -import ToggleContainer from "./toggle-container"; - -const types = [ - { - id: "hotels", - value: "hotels", - }, - { - id: "rooms", - value: "rooms", - }, -]; - -export default function SubSearch() { - const form = useFormContext(); - const type = form.watch("type"); - - return ( - // -
- -
-
-

Search Type

- ( - - )} - /> -
- {type === "rooms" && ( - <> -
-

Your budget per 24 hours

-
- - -
-
-
-

Number of beds

-
- - -
-
- - )} -
-

Filter by Amenities

- -
- {/* TODO: */} - {/* Ratings Later */} -
-
-
- ); -} diff --git a/frontend/app/search/_components/sub-search/amenities.tsx b/frontend/app/search/_components/sub-search/amenities.tsx new file mode 100644 index 0000000..bae2d48 --- /dev/null +++ b/frontend/app/search/_components/sub-search/amenities.tsx @@ -0,0 +1,23 @@ +import AmenitiesComp from "@/app/hotels/new/_components/amenities"; + +import { UseFormReturn } from "react-hook-form"; +import { SearchData } from "../../_types"; + +interface AmenitiesProps { + form: UseFormReturn; + type: "hotels" | "rooms"; +} + +export default function Amenities({ form, type }: AmenitiesProps) { + return ( +
+

Filter by Amenities

+ +
+ ); +} diff --git a/frontend/app/search/_components/sub-search/beds.tsx b/frontend/app/search/_components/sub-search/beds.tsx new file mode 100644 index 0000000..6aa2145 --- /dev/null +++ b/frontend/app/search/_components/sub-search/beds.tsx @@ -0,0 +1,33 @@ +import { FormField } from "@/components/custom/form-field"; +import { UseFormReturn } from "react-hook-form"; +import { SearchData } from "../../_types"; + +interface BedProps { + form: UseFormReturn; +} + +export default function Beds({ form }: BedProps) { + return ( +
+

Number of beds

+
+ + +
+
+ ); +} diff --git a/frontend/app/search/_components/sub-search/budget.tsx b/frontend/app/search/_components/sub-search/budget.tsx new file mode 100644 index 0000000..fa2a9f3 --- /dev/null +++ b/frontend/app/search/_components/sub-search/budget.tsx @@ -0,0 +1,33 @@ +import { FormField } from "@/components/custom/form-field"; +import { UseFormReturn } from "react-hook-form"; +import { SearchData } from "../../_types"; + +interface BudgetProps { + form: UseFormReturn; +} + +export default function Budget({ form }: BudgetProps) { + return ( +
+

Your budget per 24 hours

+
+ + +
+
+ ); +} diff --git a/frontend/app/search/_components/sub-search/index.tsx b/frontend/app/search/_components/sub-search/index.tsx new file mode 100644 index 0000000..ee94c05 --- /dev/null +++ b/frontend/app/search/_components/sub-search/index.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { SearchData } from "./../../_types"; +import { useFormContext } from "react-hook-form"; +import ToggleContainer from "./../toggle-container"; +import Type from "./type"; +import Budget from "./budget"; +import Beds from "./beds"; +import Amenities from "./amenities"; +import Ratings from "./ratings"; + +export default function SubSearch() { + const form = useFormContext(); + const type = form.watch("type"); + + return ( + // +
+ +
+ + + {type === "rooms" && ( + <> + + + + )} + +
+
+
+ ); +} diff --git a/frontend/app/search/_components/sub-search/ratings.tsx b/frontend/app/search/_components/sub-search/ratings.tsx new file mode 100644 index 0000000..0293dd3 --- /dev/null +++ b/frontend/app/search/_components/sub-search/ratings.tsx @@ -0,0 +1,47 @@ +import RatingStar from "@/components/custom/rating-star"; +import { SearchData } from "../../_types"; +import { Controller, UseFormReturn } from "react-hook-form"; +import { FormRadioField } from "@/components/custom/form-radio-field"; + +interface RatingsProps { + form: UseFormReturn; +} + +const ratings = [4.5, 4, 3.5, 3, 2.5, 2, 1.5, 1, 0.5, 0].map((rate) => ({ + id: `${rate}`, + value: `${rate}`, + label: ( + + + {rate} & up + + ), +})); + +export default function Ratings({ form }: RatingsProps) { + return ( +
+

Ratings

+
+ ( + + )} + /> +
+
+ ); +} diff --git a/frontend/app/search/_components/sub-search/type.tsx b/frontend/app/search/_components/sub-search/type.tsx new file mode 100644 index 0000000..a97fa12 --- /dev/null +++ b/frontend/app/search/_components/sub-search/type.tsx @@ -0,0 +1,44 @@ +import { Controller, UseFormReturn } from "react-hook-form"; +import { SearchData } from "../../_types"; +import { FormRadioField } from "@/components/custom/form-radio-field"; + +interface TypeProps { + form: UseFormReturn; +} + +const types = [ + { + id: "hotels", + value: "hotels", + }, + { + id: "rooms", + value: "rooms", + }, +]; + +export default function Type({ form }: TypeProps) { + return ( +
+

Search Type

+ ( + + )} + /> +
+ ); +} diff --git a/frontend/app/search/_hooks/use-result.ts b/frontend/app/search/_hooks/use-result.ts index 8d1f2ec..6475144 100644 --- a/frontend/app/search/_hooks/use-result.ts +++ b/frontend/app/search/_hooks/use-result.ts @@ -35,6 +35,10 @@ export const useResult = () => { search["price[lte]"] = +search.maxPrice; delete search.maxPrice; } + if (search.ratings) { + search["ratings[gte]"] = search.ratings; + delete search.ratings; + } // TODO: Implement searching by booking date if (search.from) delete search.from; if (search.to) delete search.to; diff --git a/frontend/app/search/_types/index.ts b/frontend/app/search/_types/index.ts index e083b0a..4cf1a04 100644 --- a/frontend/app/search/_types/index.ts +++ b/frontend/app/search/_types/index.ts @@ -6,6 +6,7 @@ export interface SearchData { city: string; name: string; type: "hotels" | "rooms"; + ratings: string; amenities: string[]; price: { gte: number | string; diff --git a/frontend/app/search/page.tsx b/frontend/app/search/page.tsx index 3b08512..0d7ab72 100644 --- a/frontend/app/search/page.tsx +++ b/frontend/app/search/page.tsx @@ -19,6 +19,7 @@ export default function Page() { city: searchParams.get("city") || "", name: searchParams.get("name") || "", type: searchParams.get("type") === "rooms" ? "rooms" : "hotels", + ratings: searchParams.get("ratings") || "", amenities: searchParams.get("amenities")?.split(",") || [], price: { gte: Number(searchParams.get("minPrice")) || "", @@ -50,6 +51,7 @@ export default function Page() { maxPrice: +data.price.lte, minBeds: +data.beds.gte, maxBeds: +data.beds.lte, + ratings: +data.ratings, // page: 1, // limit: 5, } as any; diff --git a/frontend/components/custom/form-radio-field.tsx b/frontend/components/custom/form-radio-field.tsx index 33054e6..bd60b9a 100644 --- a/frontend/components/custom/form-radio-field.tsx +++ b/frontend/components/custom/form-radio-field.tsx @@ -1,6 +1,6 @@ "use client"; -import { forwardRef } from "react"; +import { forwardRef, ReactNode } from "react"; import { Label } from "@/components/ui/label"; import { RadioGroup, @@ -11,7 +11,7 @@ import { interface FormRadioFieldProps { label: string; options: { - label?: string; + label?: ReactNode; id: string; value: string; }[]; diff --git a/frontend/components/custom/rating-star.tsx b/frontend/components/custom/rating-star.tsx new file mode 100644 index 0000000..18ee070 --- /dev/null +++ b/frontend/components/custom/rating-star.tsx @@ -0,0 +1,56 @@ +import { Star, StarHalf } from "lucide-react"; + +interface RatingStarProps { + rating: number; + className?: string; +} + +const maxRating = 5; + +export default function RatingStar({ rating, className }: RatingStarProps) { + if (rating > 5) { + rating = 5; + } else if (rating < 0) { + rating = 0; + } + // 0.0 - 0.3 -> empty + // 0.4 - 0.7 -> half + // 0.8 - 0.9 -> full + let full = 0, + half = 0, + empty = 0; + + full = Math.floor(rating); + + const remainder = rating - full; + + if (remainder <= 0.3) { + empty = maxRating - full; + } else if (remainder >= 0.4 && remainder <= 0.7) { + half = 1; + empty = maxRating - full - half; + } else if (remainder >= 0.8 && remainder <= 0.99) { + full += 1; + empty = maxRating - full; + } + + return ( +
+ {Array.from({ length: full }, (_, idx) => idx).map((val) => ( + + + + ))} + {Array.from({ length: half }, (_, idx) => idx).map((val) => ( + + + + ))} + {Array.from({ length: empty }, (_, idx) => idx).map((val) => ( + + + + ))} +
+ ); +}