├── .eslintrc.json ├── .env.example ├── next.config.js ├── postcss.config.js ├── app ├── favicon.ico ├── layout.tsx ├── search │ ├── loading.tsx │ └── page.tsx ├── page.tsx └── globals.css ├── lib ├── utils.ts └── fetchResults.ts ├── components ├── ui │ ├── skeleton.tsx │ ├── label.tsx │ ├── input.tsx │ ├── popover.tsx │ ├── button.tsx │ ├── calendar.tsx │ └── form.tsx ├── SearchForm.tsx └── Header.tsx ├── components.json ├── typings.ts ├── .gitignore ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── package.json ├── data └── trending.ts ├── README.md └── tailwind.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OXYLABS_USERNAME=oxylabs_username_goes_here 2 | OXYLABS_PASSWORD=secret_password_goes_here 3 | ``` -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonnysangha/booking.com-clone-nextjs-14-shadcn-tailwind-typescript-oxylabs/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /typings.ts: -------------------------------------------------------------------------------- 1 | export type Listing = { 2 | url: string; 3 | title: string; 4 | rating: string | null; 5 | description: string; 6 | price: string; 7 | link: string; 8 | booking_metadata: string; 9 | rating_word: string; 10 | rating_count: string | null; 11 | }; 12 | 13 | export type Result = { 14 | content: { 15 | listings: Listing[]; 16 | total_listings: string; 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import "./globals.css"; 3 | import Header from "@/components/Header"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Booking.com clone", 7 | description: "Generated by create next app", 8 | }; 9 | 10 | export default function RootLayout({ 11 | children, 12 | }: { 13 | children: React.ReactNode; 14 | }) { 15 | return ( 16 | 17 | 18 |
19 | 20 | {children} 21 | 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /app/search/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | 3 | function LoadingResults() { 4 | return ( 5 |
6 |
7 |

8 | Sit tight - were just scanning the market for the best deals! 9 |

10 |
11 | 12 |
13 |
14 |
15 | 16 |
17 | {[...Array(10)].map((_, i) => ( 18 |
19 | 20 | 21 |
22 | ))} 23 |
24 |
25 | ); 26 | } 27 | 28 | export default LoadingResults; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "booking-clone-youtube", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@headlessui/react": "^1.7.17", 13 | "@heroicons/react": "^2.0.18", 14 | "@hookform/resolvers": "^3.3.2", 15 | "@radix-ui/react-label": "^2.0.2", 16 | "@radix-ui/react-popover": "^1.0.7", 17 | "@radix-ui/react-slot": "^1.0.2", 18 | "class-variance-authority": "^0.7.0", 19 | "clsx": "^2.0.0", 20 | "date-fns": "^2.30.0", 21 | "lucide-react": "^0.294.0", 22 | "next": "14.0.3", 23 | "react": "^18", 24 | "react-day-picker": "^8.9.1", 25 | "react-dom": "^18", 26 | "react-hook-form": "^7.48.2", 27 | "tailwind-merge": "^2.0.0", 28 | "tailwindcss-animate": "^1.0.7", 29 | "zod": "^3.22.4" 30 | }, 31 | "devDependencies": { 32 | "@types/node": "^20", 33 | "@types/react": "^18", 34 | "@types/react-dom": "^18", 35 | "autoprefixer": "^10.0.1", 36 | "eslint": "^8", 37 | "eslint-config-next": "14.0.3", 38 | "postcss": "^8", 39 | "tailwindcss": "^3.3.0", 40 | "typescript": "^5" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /data/trending.ts: -------------------------------------------------------------------------------- 1 | export const trending_data = [ 2 | { 3 | id: 1, 4 | src: "https://r-xx.bstatic.com/xdata/images/city/526x420/977220.jpg?k=ee4b7b42c35b8cbf09c8ddb7630092b40cd706fec153c41904ed6e252a883938&o=", 5 | title: "Dubai", 6 | location: "United Arab Emirates", 7 | description: "15 Deals", 8 | }, 9 | { 10 | id: 2, 11 | src: "https://cf.bstatic.com/xdata/images/xphoto/540x405/288594543.webp?k=7a96a2b4190146f63068bd0604a916c0c885c4899a527e24a9b39487d4ae50b8&o=", 12 | title: "South Korea", 13 | location: "Asia", 14 | description: "32 Deals", 15 | }, 16 | { 17 | id: 3, 18 | src: "https://cf.bstatic.com/xdata/images/xphoto/540x405/289320924.webp?k=99a00f2907495aaeb6396695c053e3d8b95fb05619b10e76c89fb1f7d1fec427&o=", 19 | title: "London", 20 | location: "United Kingdom", 21 | description: "45 Deals", 22 | }, 23 | { 24 | id: 4, 25 | src: "https://cf.bstatic.com/xdata/images/xphoto/540x405/290483794.webp?k=916f7bac0ccdb08efcb269ad29cc10816ab66cd1671359066d23d32fb17b5c39&o=", 26 | title: "Singapore", 27 | location: "Asia", 28 | description: "88 Deals", 29 | }, 30 | { 31 | id: 5, 32 | src: "https://cf.bstatic.com/xdata/images/xphoto/540x405/173724501.webp?k=5e8a2353c6cd4efef3c5f992ea36d65598c52f2662d45cedc460d1a6a759109f&o=", 33 | title: "New York City", 34 | location: "United States of America", 35 | description: "92 Deals", 36 | }, 37 | ]; 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import SearchForm from "@/components/SearchForm"; 2 | import { trending_data } from "@/data/trending"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |
8 |

Find your Next Stay

9 |

10 | Search low prices on hotels, homes and much more... 11 |

12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 |
20 |

Trending Destinations

21 |

22 | Most popular choices for travellers from around the world 23 |

24 |
25 | 26 |
27 | {trending_data.map((item) => ( 28 |
29 | 35 | 36 |

{item.title}

37 |

{item.location}

38 |

{item.description}

39 |
40 | ))} 41 |
42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | './pages/**/*.{ts,tsx}', 6 | './components/**/*.{ts,tsx}', 7 | './app/**/*.{ts,tsx}', 8 | './src/**/*.{ts,tsx}', 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | input: "hsl(var(--input))", 22 | ring: "hsl(var(--ring))", 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | primary: { 26 | DEFAULT: "hsl(var(--primary))", 27 | foreground: "hsl(var(--primary-foreground))", 28 | }, 29 | secondary: { 30 | DEFAULT: "hsl(var(--secondary))", 31 | foreground: "hsl(var(--secondary-foreground))", 32 | }, 33 | destructive: { 34 | DEFAULT: "hsl(var(--destructive))", 35 | foreground: "hsl(var(--destructive-foreground))", 36 | }, 37 | muted: { 38 | DEFAULT: "hsl(var(--muted))", 39 | foreground: "hsl(var(--muted-foreground))", 40 | }, 41 | accent: { 42 | DEFAULT: "hsl(var(--accent))", 43 | foreground: "hsl(var(--accent-foreground))", 44 | }, 45 | popover: { 46 | DEFAULT: "hsl(var(--popover))", 47 | foreground: "hsl(var(--popover-foreground))", 48 | }, 49 | card: { 50 | DEFAULT: "hsl(var(--card))", 51 | foreground: "hsl(var(--card-foreground))", 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | "accordion-down": "accordion-down 0.2s ease-out", 71 | "accordion-up": "accordion-up 0.2s ease-out", 72 | }, 73 | }, 74 | }, 75 | plugins: [require("tailwindcss-animate")], 76 | } -------------------------------------------------------------------------------- /components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ChevronLeft, ChevronRight } from "lucide-react" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { buttonVariants } from "@/components/ui/button" 9 | 10 | export type CalendarProps = React.ComponentProps 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | , 58 | IconRight: ({ ...props }) => , 59 | }} 60 | {...props} 61 | /> 62 | ) 63 | } 64 | Calendar.displayName = "Calendar" 65 | 66 | export { Calendar } 67 | -------------------------------------------------------------------------------- /app/search/page.tsx: -------------------------------------------------------------------------------- 1 | import { fetchResults } from "@/lib/fetchResults"; 2 | import Link from "next/link"; 3 | import { notFound } from "next/navigation"; 4 | 5 | type Props = { 6 | searchParams: SearchParams; 7 | }; 8 | 9 | export type SearchParams = { 10 | url: URL; 11 | group_adults: string; 12 | group_children: string; 13 | no_rooms: string; 14 | checkin: string; 15 | checkout: string; 16 | }; 17 | 18 | async function SearchPage({ searchParams }: Props) { 19 | if (!searchParams.url) return notFound(); 20 | 21 | const results = await fetchResults(searchParams); 22 | 23 | if (!results) return
No results...
; 24 | 25 | return ( 26 |
27 |
28 |

Your Trip Results

29 | 30 |

31 | Dates of trip: 32 | 33 | {searchParams.checkin} to {searchParams.checkout} 34 | 35 |

36 | 37 |
38 | 39 |

40 | {results.content.total_listings} 41 |

42 | 43 |
44 | {results.content.listings.map((item, i) => ( 45 |
49 | image of property 54 | 55 |
56 |
57 | 61 | {item.title} 62 | 63 |

{item.description}

64 |
65 | 66 |
67 |
68 |
69 |

{item.rating_word}

70 |

{item.rating_count}

71 |
72 | 73 |

74 | {item.rating || "N/A"} 75 |

76 |
77 | 78 |
79 |

{item.booking_metadata}

80 |

{item.price}

81 |
82 |
83 |
84 |
85 | ))} 86 |
87 |
88 |
89 | ); 90 | } 91 | 92 | export default SearchPage; 93 | -------------------------------------------------------------------------------- /lib/fetchResults.ts: -------------------------------------------------------------------------------- 1 | import { SearchParams } from "@/app/search/page"; 2 | import { Result } from "@/typings"; 3 | 4 | export async function fetchResults(searchParams: SearchParams) { 5 | const username = process.env.OXYLABS_USERNAME; 6 | const password = process.env.OXYLABS_PASSWORD; 7 | 8 | const url = new URL(searchParams.url); 9 | Object.keys(searchParams).forEach((key) => { 10 | if (key === "url" || key === "location") return; 11 | 12 | const value = searchParams[key as keyof SearchParams]; 13 | 14 | if (typeof value === "string") { 15 | url.searchParams.append(key, value); 16 | } 17 | }); 18 | 19 | console.log("scraping url >>>", url.href); 20 | 21 | const body = { 22 | source: "universal", 23 | url: url.href, 24 | parse: true, 25 | render: "html", 26 | parsing_instructions: { 27 | listings: { 28 | _fns: [ 29 | { 30 | _fn: "xpath", 31 | _args: ["//div[@data-testid='property-card-container']"], 32 | }, 33 | ], 34 | _items: { 35 | title: { 36 | _fns: [ 37 | { 38 | _fn: "xpath_one", 39 | _args: [".//div[@data-testid='title']/text()"], 40 | }, 41 | ], 42 | }, 43 | description: { 44 | _fns: [ 45 | { 46 | _fn: "xpath_one", 47 | _args: [ 48 | ".//h4[contains(@class, 'abf093bdfe e8f7c070a7')]/text()", 49 | ], 50 | }, 51 | ], 52 | }, 53 | booking_metadata: { 54 | _fns: [ 55 | { 56 | _fn: "xpath_one", 57 | _args: [ 58 | ".//div[contains(@class, 'c5ca594cb1 f19ed67e4b')]/div[contains(@class, 'abf093bdfe f45d8e4c32')]/text()", 59 | ], 60 | }, 61 | ], 62 | }, 63 | link: { 64 | _fns: [ 65 | { 66 | _fn: "xpath_one", 67 | _args: [".//a[contains(@class, 'a78ca197d0')]/@href"], 68 | }, 69 | ], 70 | }, 71 | price: { 72 | _fns: [ 73 | { 74 | _fn: "xpath_one", 75 | _args: [ 76 | `.//span[contains(@class, 'f6431b446c fbfd7c1165 e84eb96b1f')]/text()`, 77 | ], 78 | }, 79 | ], 80 | }, 81 | url: { 82 | _fns: [ 83 | { 84 | _fn: "xpath_one", 85 | _args: [".//img/@src"], 86 | }, 87 | ], 88 | }, 89 | rating_word: { 90 | _fns: [ 91 | { 92 | _fn: "xpath_one", 93 | _args: [ 94 | ".//div[@class='a3b8729ab1 e6208ee469 cb2cbb3ccb']/text()", 95 | ], 96 | }, 97 | ], 98 | }, 99 | rating: { 100 | _fns: [ 101 | { 102 | _fn: "xpath_one", 103 | _args: [".//div[@class='a3b8729ab1 d86cee9b25']/text()"], 104 | }, 105 | ], 106 | }, 107 | rating_count: { 108 | _fns: [ 109 | { 110 | _fn: "xpath_one", 111 | _args: [ 112 | ".//div[@class='abf093bdfe f45d8e4c32 d935416c47']/text()", 113 | ], 114 | }, 115 | ], 116 | }, 117 | }, 118 | }, 119 | total_listings: { 120 | _fns: [ 121 | { 122 | _fn: "xpath_one", 123 | _args: [".//h1/text()"], 124 | }, 125 | ], 126 | }, 127 | }, 128 | }; 129 | 130 | const response = await fetch("https://realtime.oxylabs.io/v1/queries", { 131 | method: "POST", 132 | body: JSON.stringify(body), 133 | next: { 134 | revalidate: 60 * 60, // cache for 1 hour 135 | }, 136 | headers: { 137 | "Content-Type": "application/json", 138 | Authorization: 139 | "Basic " + Buffer.from(`${username}:${password}`).toString("base64"), 140 | }, 141 | }) 142 | .then((response) => response.json()) 143 | .then((data) => { 144 | if (data.results.length === 0) return; 145 | const result: Result = data.results[0]; 146 | 147 | return result; 148 | }) 149 | .catch((err) => console.log(err)); 150 | 151 | return response; 152 | } 153 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |