-
-
-
- Accommodations in {tripDetails.location}
-
- {selectedId && (
-
-
-
- {!selectedId ? (
-
- {accommodations.map((accommodation) => (
-
setSelectedId(accommodation.id)}
- className={`flex border rounded-lg p-3 cursor-pointer transition-all ${
- accommodation.available
- ? "hover:border-blue-300 hover:shadow-md"
- : "opacity-60"
- }`}
- >
-
-

-
-
-
-
- {accommodation.name}
-
-
- {accommodation.price}
-
-
-
{accommodation.type}
-
-
-
- {accommodation.rating}
-
-
- {!accommodation.available && (
-
- Unavailable for your dates
-
- )}
-
-
- ))}
-
- ) : (
-
- {selectedAccommodation && (
- <>
-
-

-
-
-
-
- {selectedAccommodation.name}
-
-
- {selectedAccommodation.price}
-
-
-
-
-
- {selectedAccommodation.rating}
-
-
-
- Perfect accommodation in {tripDetails.location} for your{" "}
- {tripDetails.numberOfGuests} guests.
-
-
-
- Amenities:
-
-
- {selectedAccommodation.amenities.map((amenity) => (
-
- {amenity}
-
- ))}
-
-
-
-
- >
- )}
-
- )}
-
+
+
+ ))}
+
+
+
+
);
}
diff --git a/package.json b/package.json
index e7f3e98..868c037 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"@assistant-ui/react": "^0.8.0",
"@assistant-ui/react-markdown": "^0.8.0",
"@assistant-ui/react-syntax-highlighter": "^0.7.2",
+ "@faker-js/faker": "^9.5.1",
"@langchain/core": "^0.3.41",
"@langchain/google-genai": "^0.1.10",
"@langchain/langgraph": "^0.2.49",
@@ -31,6 +32,8 @@
"@tailwindcss/vite": "^4.0.9",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "date-fns": "^4.1.0",
+ "embla-carousel-react": "^8.5.2",
"esbuild": "^0.25.0",
"esbuild-plugin-tailwindcss": "^2.0.1",
"framer-motion": "^12.4.9",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 57d55c0..919b32e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -21,6 +21,9 @@ importers:
"@assistant-ui/react-syntax-highlighter":
specifier: ^0.7.2
version: 0.7.10(@assistant-ui/react-markdown@0.8.0(@assistant-ui/react@0.8.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@assistant-ui/react@0.8.0(@types/react-dom@19.0.4(@types/react@19.0.10))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react-syntax-highlighter@15.5.13)(@types/react@19.0.10)(react-syntax-highlighter@15.6.1(react@19.0.0))(react@19.0.0)
+ "@faker-js/faker":
+ specifier: ^9.5.1
+ version: 9.5.1
"@langchain/core":
specifier: ^0.3.41
version: 0.3.41(openai@4.85.4(zod@3.24.2))
@@ -69,6 +72,12 @@ importers:
clsx:
specifier: ^2.1.1
version: 2.1.1
+ date-fns:
+ specifier: ^4.1.0
+ version: 4.1.0
+ embla-carousel-react:
+ specifier: ^8.5.2
+ version: 8.5.2(react@19.0.0)
esbuild:
specifier: ^0.25.0
version: 0.25.0
@@ -921,6 +930,13 @@ packages:
}
engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 }
+ "@faker-js/faker@9.5.1":
+ resolution:
+ {
+ integrity: sha512-0fzMEDxkExR2cn731kpDaCCnBGBUOIXEi2S1N5l8Hltp6aPf4soTMJ+g4k8r2sI5oB+rpwIW8Uy/6jkwGpnWPg==,
+ }
+ engines: { node: ">=18.0.0", npm: ">=9.0.0" }
+
"@floating-ui/core@1.6.9":
resolution:
{
@@ -2483,6 +2499,12 @@ packages:
integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==,
}
+ date-fns@4.1.0:
+ resolution:
+ {
+ integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==,
+ }
+
debug@4.4.0:
resolution:
{
@@ -2606,6 +2628,28 @@ packages:
integrity: sha512-12keJGdXQWPnfVOu6/6ZzZgPPNodiDOSe3LjA8qk2yXTjnCnw2LeGUsAmtlNAmH4UW0K7tOLcz0j9lI2eJCJRA==,
}
+ embla-carousel-react@8.5.2:
+ resolution:
+ {
+ integrity: sha512-Tmx+uY3MqseIGdwp0ScyUuxpBgx5jX1f7od4Cm5mDwg/dptEiTKf9xp6tw0lZN2VA9JbnVMl/aikmbc53c6QFA==,
+ }
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
+ embla-carousel-reactive-utils@8.5.2:
+ resolution:
+ {
+ integrity: sha512-QC8/hYSK/pEmqEdU1IO5O+XNc/Ptmmq7uCB44vKplgLKhB/l0+yvYx0+Cv0sF6Ena8Srld5vUErZkT+yTahtDg==,
+ }
+ peerDependencies:
+ embla-carousel: 8.5.2
+
+ embla-carousel@8.5.2:
+ resolution:
+ {
+ integrity: sha512-xQ9oVLrun/eCG/7ru3R+I5bJ7shsD8fFwLEY7yPe27/+fDHCNj0OT5EoG5ZbFyOxOcG6yTwW8oTz/dWyFnyGpg==,
+ }
+
emoji-regex@8.0.0:
resolution:
{
@@ -5854,6 +5898,8 @@ snapshots:
"@eslint/core": 0.12.0
levn: 0.4.1
+ "@faker-js/faker@9.5.1": {}
+
"@floating-ui/core@1.6.9":
dependencies:
"@floating-ui/utils": 0.2.9
@@ -6834,6 +6880,8 @@ snapshots:
csstype@3.1.3: {}
+ date-fns@4.1.0: {}
+
debug@4.4.0:
dependencies:
ms: 2.1.3
@@ -6881,6 +6929,18 @@ snapshots:
electron-to-chromium@1.5.106: {}
+ embla-carousel-react@8.5.2(react@19.0.0):
+ dependencies:
+ embla-carousel: 8.5.2
+ embla-carousel-reactive-utils: 8.5.2(embla-carousel@8.5.2)
+ react: 19.0.0
+
+ embla-carousel-reactive-utils@8.5.2(embla-carousel@8.5.2):
+ dependencies:
+ embla-carousel: 8.5.2
+
+ embla-carousel@8.5.2: {}
+
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
diff --git a/src/components/ui/carousel.tsx b/src/components/ui/carousel.tsx
new file mode 100644
index 0000000..09cf122
--- /dev/null
+++ b/src/components/ui/carousel.tsx
@@ -0,0 +1,239 @@
+import * as React from "react";
+import useEmblaCarousel, {
+ type UseEmblaCarouselType,
+} from "embla-carousel-react";
+import { ArrowLeft, ArrowRight } from "lucide-react";
+
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
+
+type CarouselApi = UseEmblaCarouselType[1];
+type UseCarouselParameters = Parameters
;
+type CarouselOptions = UseCarouselParameters[0];
+type CarouselPlugin = UseCarouselParameters[1];
+
+type CarouselProps = {
+ opts?: CarouselOptions;
+ plugins?: CarouselPlugin;
+ orientation?: "horizontal" | "vertical";
+ setApi?: (api: CarouselApi) => void;
+};
+
+type CarouselContextProps = {
+ carouselRef: ReturnType[0];
+ api: ReturnType[1];
+ scrollPrev: () => void;
+ scrollNext: () => void;
+ canScrollPrev: boolean;
+ canScrollNext: boolean;
+} & CarouselProps;
+
+const CarouselContext = React.createContext(null);
+
+function useCarousel() {
+ const context = React.useContext(CarouselContext);
+
+ if (!context) {
+ throw new Error("useCarousel must be used within a ");
+ }
+
+ return context;
+}
+
+function Carousel({
+ orientation = "horizontal",
+ opts,
+ setApi,
+ plugins,
+ className,
+ children,
+ ...props
+}: React.ComponentProps<"div"> & CarouselProps) {
+ const [carouselRef, api] = useEmblaCarousel(
+ {
+ ...opts,
+ axis: orientation === "horizontal" ? "x" : "y",
+ },
+ plugins,
+ );
+ const [canScrollPrev, setCanScrollPrev] = React.useState(false);
+ const [canScrollNext, setCanScrollNext] = React.useState(false);
+
+ const onSelect = React.useCallback((api: CarouselApi) => {
+ if (!api) return;
+ setCanScrollPrev(api.canScrollPrev());
+ setCanScrollNext(api.canScrollNext());
+ }, []);
+
+ const scrollPrev = React.useCallback(() => {
+ api?.scrollPrev();
+ }, [api]);
+
+ const scrollNext = React.useCallback(() => {
+ api?.scrollNext();
+ }, [api]);
+
+ const handleKeyDown = React.useCallback(
+ (event: React.KeyboardEvent) => {
+ if (event.key === "ArrowLeft") {
+ event.preventDefault();
+ scrollPrev();
+ } else if (event.key === "ArrowRight") {
+ event.preventDefault();
+ scrollNext();
+ }
+ },
+ [scrollPrev, scrollNext],
+ );
+
+ React.useEffect(() => {
+ if (!api || !setApi) return;
+ setApi(api);
+ }, [api, setApi]);
+
+ React.useEffect(() => {
+ if (!api) return;
+ onSelect(api);
+ api.on("reInit", onSelect);
+ api.on("select", onSelect);
+
+ return () => {
+ api?.off("select", onSelect);
+ };
+ }, [api, onSelect]);
+
+ return (
+
+
+ {children}
+
+
+ );
+}
+
+function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
+ const { carouselRef, orientation } = useCarousel();
+
+ return (
+
+ );
+}
+
+function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
+ const { orientation } = useCarousel();
+
+ return (
+
+ );
+}
+
+function CarouselPrevious({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollPrev, canScrollPrev } = useCarousel();
+
+ return (
+
+ );
+}
+
+function CarouselNext({
+ className,
+ variant = "outline",
+ size = "icon",
+ ...props
+}: React.ComponentProps) {
+ const { orientation, scrollNext, canScrollNext } = useCarousel();
+
+ return (
+
+ );
+}
+
+export {
+ type CarouselApi,
+ Carousel,
+ CarouselContent,
+ CarouselItem,
+ CarouselPrevious,
+ CarouselNext,
+};