├── .editorconfig ├── .env.example ├── .eslintrc.json ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .vscode └── settings.json ├── LICENSE.md ├── README.md ├── actions └── stripe.ts ├── app ├── (app) │ ├── (home) │ │ ├── components │ │ │ ├── browse-car-types.tsx │ │ │ ├── call-to-action.tsx │ │ │ ├── car-logos │ │ │ │ ├── audi.tsx │ │ │ │ ├── bmw.tsx │ │ │ │ ├── ford.tsx │ │ │ │ ├── honda.tsx │ │ │ │ ├── hyundai.tsx │ │ │ │ ├── jeep.tsx │ │ │ │ ├── kia.tsx │ │ │ │ ├── mercedes-benz.tsx │ │ │ │ ├── mini.tsx │ │ │ │ ├── nissan.tsx │ │ │ │ ├── porsche.tsx │ │ │ │ ├── subaru.tsx │ │ │ │ ├── tesla.tsx │ │ │ │ ├── toyota.tsx │ │ │ │ ├── volkswagen.tsx │ │ │ │ └── volvo.tsx │ │ │ ├── features.tsx │ │ │ ├── hero.tsx │ │ │ ├── logo-slider.tsx │ │ │ ├── popular-destinations.tsx │ │ │ └── testimonials.tsx │ │ └── page.tsx │ ├── cars │ │ ├── [slug] │ │ │ ├── components │ │ │ │ └── reserve-card.tsx │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ ├── not-found.tsx │ │ │ └── page.tsx │ │ ├── components │ │ │ ├── car-card.tsx │ │ │ ├── car-catalog.tsx │ │ │ ├── car-details-button.tsx │ │ │ ├── filters │ │ │ │ ├── filters-button.tsx │ │ │ │ ├── filters-content.tsx │ │ │ │ ├── filters-wrapper.tsx │ │ │ │ ├── lib │ │ │ │ │ └── filters.ts │ │ │ │ ├── sections │ │ │ │ │ ├── body-style.tsx │ │ │ │ │ ├── powertrain.tsx │ │ │ │ │ ├── price-range.tsx │ │ │ │ │ ├── seating-capacity.tsx │ │ │ │ │ └── transmission-type.tsx │ │ │ │ └── types.ts │ │ │ ├── map.tsx │ │ │ └── skeletons │ │ │ │ ├── car-card.tsx │ │ │ │ ├── car-catalog.tsx │ │ │ │ └── map.tsx │ │ └── page.tsx │ ├── layout.tsx │ └── reservation │ │ ├── cars │ │ └── [slug] │ │ │ └── [[...rest]] │ │ │ ├── components │ │ │ ├── auth-section.tsx │ │ │ ├── back-button.tsx │ │ │ ├── book-details.tsx │ │ │ ├── cancellation-policy.tsx │ │ │ ├── car-details.tsx │ │ │ ├── elements-form.tsx │ │ │ ├── payment-details.tsx │ │ │ ├── price-details.tsx │ │ │ └── stripe-test-cards.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ └── confirmation │ │ └── result │ │ └── page.tsx ├── (auth) │ ├── sign-in │ │ └── [[...sign-in]] │ │ │ └── page.tsx │ └── sign-up │ │ └── [[...sign-up]] │ │ └── page.tsx ├── error.tsx ├── layout.tsx ├── manifest.webmanifest ├── opengraph-image.png └── robots.txt ├── components.json ├── components ├── cld-image.tsx ├── icons │ ├── automatic-gearbox.tsx │ ├── battery-automotive.tsx │ ├── car.tsx │ ├── caret-right.tsx │ ├── carhive-logo.tsx │ ├── check.tsx │ ├── chevron-down.tsx │ ├── chevron-left.tsx │ ├── chevron-right.tsx │ ├── circle-check.tsx │ ├── circle.tsx │ ├── click.tsx │ ├── currency-dollar.tsx │ ├── dashboard.tsx │ ├── engine.tsx │ ├── filled-star.tsx │ ├── filter-search.tsx │ ├── filter.tsx │ ├── filters.tsx │ ├── hatchback.tsx │ ├── headset.tsx │ ├── info-square.tsx │ ├── kid.tsx │ ├── manual-gearbox.tsx │ ├── map-pin.tsx │ ├── menu.tsx │ ├── minivan.tsx │ ├── minus.tsx │ ├── navigation.tsx │ ├── plus.tsx │ ├── roadster.tsx │ ├── search.tsx │ ├── selector.tsx │ ├── shield-check.tsx │ ├── suv.tsx │ ├── truck.tsx │ ├── user-circle.tsx │ ├── wifi.tsx │ └── x.tsx ├── loading-dots.tsx ├── logoLink.tsx ├── responsive-modal.tsx ├── search-panel-wrapper.tsx ├── search-panel.tsx ├── site-footer.tsx ├── site-header.tsx ├── skeletons │ └── search-panel.tsx ├── ui │ ├── alert.tsx │ ├── badge.tsx │ ├── button.tsx │ ├── calendar.tsx │ ├── carousel.tsx │ ├── command.tsx │ ├── dialog.tsx │ ├── drawer.tsx │ ├── dropdown-menu.tsx │ ├── input.tsx │ ├── label.tsx │ ├── popover.tsx │ ├── separator.tsx │ ├── skeleton.tsx │ ├── slider.tsx │ ├── toast.tsx │ ├── toaster.tsx │ ├── toggle-group.tsx │ └── toggle.tsx └── user-menu-button.tsx ├── config └── site.ts ├── data ├── car-types.js ├── cars.js ├── locations-with-images.js ├── locations.js └── testimonials.js ├── db ├── index.ts ├── migrate.ts ├── queries │ ├── car-repository.ts │ └── location-repository.ts ├── schema.ts └── seed.ts ├── drizzle.config.ts ├── hooks ├── use-debounce.ts ├── use-media-query.ts └── use-toast.ts ├── lib ├── cloudinary.ts ├── constants.ts ├── dates.ts ├── fonts.ts ├── stripe │ ├── index.ts │ └── utils │ │ ├── get-stripejs.ts │ │ └── stripe-helpers.ts ├── types.ts └── utils.ts ├── middleware.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public ├── apple-icon.png ├── assets │ └── images │ │ ├── cars │ │ ├── hatchback.jpg │ │ ├── minivan.jpg │ │ ├── pickup-truck.jpg │ │ ├── sedan.jpg │ │ ├── sports-car.jpg │ │ └── suv.jpg │ │ ├── locations │ │ ├── cancun.jpg │ │ ├── dubai.jpg │ │ ├── paris.jpg │ │ ├── rio.jpg │ │ ├── rome.jpg │ │ └── sydney.jpg │ │ └── profiles │ │ ├── alex.jpg │ │ ├── david.jpg │ │ ├── emily.jpg │ │ ├── james.jpg │ │ ├── jennifer.jpg │ │ ├── mark.jpg │ │ └── sarah.jpg ├── favicon.ico ├── icon-192.png ├── icon-512.png └── icon.svg ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json └── tsconfig.scripts.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | POSTGRES_URL="" 2 | POSTGRES_PRISMA_URL="" 3 | POSTGRES_URL_NO_SSL="" 4 | POSTGRES_URL_NON_POOLING="" 5 | POSTGRES_USER="" 6 | POSTGRES_HOST="" 7 | POSTGRES_PASSWORD="" 8 | POSTGRES_DATABASE="" 9 | NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME="" 10 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="" 11 | CLERK_SECRET_KEY="" 12 | NEXT_PUBLIC_CLERK_SIGN_IN_URL="/sign-in" 13 | NEXT_PUBLIC_CLERK_SIGN_UP_URL="/sign-up" 14 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="" 15 | STRIPE_SECRET_KEY="" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc", 3 | "root": true, 4 | "extends": [ 5 | "next/core-web-vitals", 6 | "prettier", 7 | "plugin:tailwindcss/recommended" 8 | ], 9 | "plugins": [ 10 | "tailwindcss" 11 | ], 12 | "rules": { 13 | "tailwindcss/no-custom-classname": "off", 14 | "tailwindcss/classnames-order": "error" 15 | }, 16 | "settings": { 17 | "tailwindcss": { 18 | "callees": [ 19 | "cn", 20 | "cva" 21 | ], 22 | "config": "tailwind.config.js" 23 | }, 24 | "next": { 25 | "rootDir": true 26 | } 27 | }, 28 | "overrides": [ 29 | { 30 | "files": [ 31 | "*.ts", 32 | "*.tsx" 33 | ], 34 | "parser": "@typescript-eslint/parser" 35 | } 36 | ] 37 | } -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | .env 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.11.1 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tailwindCSS.experimental.classRegex": [ 3 | [ 4 | "cva\\(([^)]*)\\)", 5 | "[\"'`]([^\"'`]*).*?[\"'`]" 6 | ], 7 | [ 8 | "cn\\(([^)]*)\\)", 9 | "[\"'`]([^\"'`]*).*?[\"'`]" 10 | ] 11 | ], 12 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 eduamdev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Carhive 2 | 3 | > **Warning** 4 | > This project is a work in progress and may not function consistently. It is primarily a testing demo created for educational purposes and to explore new technologies. 5 | 6 | ![carhive](https://github.com/user-attachments/assets/5e4cdffe-dcd0-44ee-957d-66fdb82b47ee) 7 | 8 | ## Features 9 | 10 | - Dynamic Routing 11 | - **Server Components** and **Server Actions** 12 | - **Loading UI** and **Streaming with Suspense** for a smoother user experience 13 | - SEO-friendly metadata 14 | - Fully responsive design 15 | - Styled with **Tailwind CSS** 16 | - UI components built with **Shadcn/ui** 17 | - Interactive maps using **Leaflet** 18 | - User authentication and management via **Clerk** 19 | - Serverless SQL powered by **Vercel Postgres** (and Neon) 20 | - Image Management through **Cloudinary** 21 | - Infinite Logo Slider 22 | - Search functionality with data filtering capabilities 23 | - Code Linting for consistent formatting 24 | - Written in **TypeScript** for enhanced type safety 25 | - **Drizzle ORM** for type-safe database interaction, schema generation, and migrations 26 | - **Stripe** integration for payment processing 27 | 28 | ## Requirements 29 | 30 | Ensure the following are installed: 31 | 32 | - Node.js (v18+) 33 | - `pnpm` as the package manager 34 | 35 | ## Prerequisites 36 | 37 | Before running the app, make sure you have: 38 | 39 | - A [Vercel account](https://vercel.com/) and a [Vercel Postgres Database](https://vercel.com/docs/storage/vercel-postgres) 40 | - A [Cloudinary account](https://cloudinary.com/) for image management 41 | - A [Clerk account](https://clerk.com/) for authentication 42 | - A [Stripe account](https://stripe.com/) for payment processing 43 | 44 | ## Running the Project Locally 45 | 46 | 1. Install dependencies: 47 | 48 | ```bash 49 | pnpm install 50 | ``` 51 | 52 | 2. Set up environment variables: 53 | 54 | - Copy the `.env.example` file to `.env` at the root of the project: 55 | 56 | ```bash 57 | cp .env.example .env 58 | ``` 59 | 60 | - Update the `.env` file with your configuration details. 61 | 62 | 3. Generate the database: 63 | 64 | ```bash 65 | pnpm db:generate 66 | ``` 67 | 68 | 4. Seed the initial data: 69 | 70 | ```bash 71 | pnpm db:seed 72 | ``` 73 | 74 | 5. Start the development server: 75 | 76 | ```bash 77 | pnpm dev 78 | ``` 79 | -------------------------------------------------------------------------------- /actions/stripe.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import type { Stripe } from "stripe" 4 | 5 | import { stripe } from "@/lib/stripe" 6 | import { formatAmountForStripe } from "@/lib/stripe/utils/stripe-helpers" 7 | 8 | export async function createPaymentIntent( 9 | data: FormData 10 | ): Promise<{ client_secret: string }> { 11 | const currency = data.get("currency") as string 12 | 13 | const paymentIntent: Stripe.PaymentIntent = 14 | await stripe.paymentIntents.create({ 15 | amount: formatAmountForStripe( 16 | Number(data.get("amount") as string), 17 | currency 18 | ), 19 | automatic_payment_methods: { enabled: true }, 20 | currency, 21 | }) 22 | 23 | return { client_secret: paymentIntent.client_secret as string } 24 | } 25 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/browse-car-types.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import Link from "next/link" 3 | import { carTypes } from "@/data/car-types" 4 | 5 | import { SearchParams } from "@/lib/types" 6 | import { Button } from "@/components/ui/button" 7 | import { 8 | Carousel, 9 | CarouselContent, 10 | CarouselItem, 11 | CarouselNext, 12 | CarouselPrevious, 13 | } from "@/components/ui/carousel" 14 | 15 | export function BrowseCarTypes() { 16 | return ( 17 |
18 |
19 |

20 | Pick Your Perfect Match 21 |

22 |
23 |
24 |
25 |
26 | 27 | 28 | {carTypes.map(({ id, slug, name, imageUrl }) => { 29 | return ( 30 | 34 | 49 |
50 | 51 | {name} 52 | 53 | {name} 62 |
63 |
64 | ) 65 | })} 66 |
67 | 68 | 69 |
70 |
71 |
72 |
73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/call-to-action.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { Button } from "@/components/ui/button" 4 | 5 | export function CallToAction() { 6 | return ( 7 |
8 |
9 |
10 |

11 | Ready to Hit the Road?
12 | Start Your Adventure Today! 13 |

14 |
15 | 18 |
19 |
20 |
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/audi.tsx: -------------------------------------------------------------------------------- 1 | export function AudiIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/bmw.tsx: -------------------------------------------------------------------------------- 1 | export function BMWIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/honda.tsx: -------------------------------------------------------------------------------- 1 | export function HondaIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/hyundai.tsx: -------------------------------------------------------------------------------- 1 | export function HyundaiIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/jeep.tsx: -------------------------------------------------------------------------------- 1 | export function JeepIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/kia.tsx: -------------------------------------------------------------------------------- 1 | export function KiaIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/mercedes-benz.tsx: -------------------------------------------------------------------------------- 1 | export function MercedesBenzIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/mini.tsx: -------------------------------------------------------------------------------- 1 | export function MiniIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/nissan.tsx: -------------------------------------------------------------------------------- 1 | export function NissanIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/porsche.tsx: -------------------------------------------------------------------------------- 1 | export function PorscheIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/subaru.tsx: -------------------------------------------------------------------------------- 1 | export function SubaruIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/tesla.tsx: -------------------------------------------------------------------------------- 1 | export function TeslaIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/toyota.tsx: -------------------------------------------------------------------------------- 1 | export function ToyotaIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/volkswagen.tsx: -------------------------------------------------------------------------------- 1 | export function VolkswagenIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/car-logos/volvo.tsx: -------------------------------------------------------------------------------- 1 | export function VolvoIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/features.tsx: -------------------------------------------------------------------------------- 1 | import { ClickIcon } from "@/components/icons/click" 2 | import { FilterSearchIcon } from "@/components/icons/filter-search" 3 | import { MapPinIcon } from "@/components/icons/map-pin" 4 | import { ShieldCheckIcon } from "@/components/icons/shield-check" 5 | 6 | const features = [ 7 | { 8 | icon: ClickIcon, 9 | title: "Hassle-Free Booking", 10 | description: 11 | "Book your perfect car in just a few clicks. Enjoy seamless reservations and fantastic deals.", 12 | }, 13 | { 14 | icon: ShieldCheckIcon, 15 | title: "Secure & Reliable Rentals", 16 | description: 17 | "Drive with confidence. Our vehicles are thoroughly inspected and fully insured for your peace of mind.", 18 | }, 19 | { 20 | icon: MapPinIcon, 21 | title: "Simple Navigation", 22 | description: 23 | "Navigate with ease. Our user-friendly tools make every journey smooth and enjoyable.", 24 | }, 25 | { 26 | icon: FilterSearchIcon, 27 | title: "Customizable Search Filters", 28 | description: 29 | "Find exactly what you need. Apply filters to tailor your search, ensuring you get the best match for your preferences.", 30 | }, 31 | ] 32 | 33 | export function Features() { 34 | return ( 35 |
36 |
37 |

38 | Discover What Sets Us Apart 39 |

40 |
41 |
42 | {features.map(({ icon: Icon, title, description }, index) => ( 43 |
44 |
45 | 46 |

47 | {title} 48 |

49 |
50 |

51 | {description} 52 |

53 |
54 | ))} 55 |
56 |
57 |
58 |
59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/hero.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react" 2 | 3 | import { SearchPanelWrapper } from "@/components/search-panel-wrapper" 4 | import { SearchPanelSkeleton } from "@/components/skeletons/search-panel" 5 | 6 | import { LogoSlider } from "./logo-slider" 7 | 8 | export async function Hero() { 9 | return ( 10 |
11 |
12 |
13 |

14 | Your Road Trip Starts Here 15 |

16 |
17 |
18 |
19 |
20 | }> 21 | 22 | 23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/popular-destinations.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import Link from "next/link" 3 | import { locationsWithImages } from "@/data/locations-with-images" 4 | 5 | import { SearchParams } from "@/lib/types" 6 | import { formatAmountForDisplay } from "@/lib/utils" 7 | import { Button } from "@/components/ui/button" 8 | 9 | export function PopularDestinations() { 10 | const featuredLocations = locationsWithImages.filter( 11 | (location) => location.featured === true 12 | ) 13 | 14 | return ( 15 |
16 |
17 |

18 | Where to Rent Next 19 |

20 |
21 |
22 | {featuredLocations.map( 23 | ( 24 | { 25 | id, 26 | slug, 27 | imageUrl, 28 | latitude, 29 | longitude, 30 | name, 31 | startingPrice, 32 | }, 33 | index 34 | ) => ( 35 |
39 | 56 |
57 | {name} 67 |
68 |
69 |

70 | {name} 71 |

72 |

73 | Cars from{" "} 74 | {formatAmountForDisplay(startingPrice, "usd", true)}+ 75 |

76 |
77 |
78 | ) 79 | )} 80 |
81 |
82 |
83 |
84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /app/(app)/(home)/components/testimonials.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import { testimonials } from "@/data/testimonials" 3 | 4 | import { 5 | Carousel, 6 | CarouselContent, 7 | CarouselItem, 8 | CarouselNext, 9 | CarouselPrevious, 10 | } from "@/components/ui/carousel" 11 | import { FilledStarIcon } from "@/components/icons/filled-star" 12 | 13 | export function Testimonials() { 14 | return ( 15 |
16 |
17 |

18 | What Our Customers Are Saying 19 |

20 |
21 |
22 |
23 | 24 | 25 | {testimonials.map(({ id, name, comment, imageUrl, rating }) => { 26 | return ( 27 | 31 |
32 | {/* Rating Section */} 33 |
37 | {[...Array(rating)].map((_, index) => ( 38 | 42 | ))} 43 |
44 |
45 | {/* Comment Section */} 46 |
47 | “{comment}” 48 |
49 |
50 |
51 | {/* Reviewer Information */} 52 |
53 | {name} 58 |

59 | {name} 60 |

61 |
62 |
63 |
64 |
65 | ) 66 | })} 67 |
68 | 69 | 70 |
71 |
72 |
73 |
74 |
75 |
76 | ) 77 | } 78 | -------------------------------------------------------------------------------- /app/(app)/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import { SiteHeader } from "@/components/site-header" 2 | 3 | import { BrowseCarTypes } from "./components/browse-car-types" 4 | import { CallToAction } from "./components/call-to-action" 5 | import { Features } from "./components/features" 6 | import { Hero } from "./components/hero" 7 | import { PopularDestinations } from "./components/popular-destinations" 8 | import { Testimonials } from "./components/testimonials" 9 | 10 | export default function HomePage() { 11 | return ( 12 | <> 13 |
14 |
15 | 16 |
17 |
18 |
19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 |
28 | 29 |
30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/(app)/cars/[slug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react" 2 | import Link from "next/link" 3 | 4 | import { Button } from "@/components/ui/button" 5 | import { ChevronLeftIcon } from "@/components/icons/chevron-left" 6 | import { SiteHeader } from "@/components/site-header" 7 | 8 | export default function CarLayout({ children }: { children: ReactNode }) { 9 | return ( 10 |
11 |
12 |
13 |
14 |
15 |
16 | 29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | 37 |
38 |
39 |
40 |
41 | {children} 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /app/(app)/cars/[slug]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | export default function Loading() { 4 | return ( 5 |
14 |
15 |
16 | 17 |
18 |
19 |
20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 43 |
44 | 45 |
46 |
47 |
48 | 49 |
50 | 51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /app/(app)/cars/[slug]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { CaretRightIcon } from "@/components/icons/caret-right" 5 | 6 | export default function NotFound() { 7 | return ( 8 |
9 |
10 |

404 - Car Not Found

11 |

12 | The car you're looking for seems to have taken a detour. No 13 | worries, though! We have a wide selection of vehicles waiting just for 14 | you. 15 |

16 | 24 |
25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /app/(app)/cars/components/car-card.tsx: -------------------------------------------------------------------------------- 1 | import { SelectCar } from "@/db/schema" 2 | 3 | import { formatAmountForDisplay } from "@/lib/utils" 4 | import CldImage from "@/components/cld-image" 5 | import { AutomaticGearboxIcon } from "@/components/icons/automatic-gearbox" 6 | import { BatteryAutomotiveIcon } from "@/components/icons/battery-automotive" 7 | import { EngineIcon } from "@/components/icons/engine" 8 | import { FilledStarIcon } from "@/components/icons/filled-star" 9 | import { ManualGearboxIcon } from "@/components/icons/manual-gearbox" 10 | 11 | import { CarDetailsButton } from "./car-details-button" 12 | 13 | interface CarCardProps { 14 | car: SelectCar 15 | } 16 | 17 | export async function CarCard({ car }: CarCardProps) { 18 | if (!car) { 19 | return null 20 | } 21 | 22 | return ( 23 |
24 |
25 | 33 |
34 |
35 |
36 | {car.name} 37 |
38 | 39 | 40 | {car.rating}{" "} 41 | 42 | {Number(car.reviewCount) > 0 && `(${car.reviewCount})`} 43 | 44 | 45 |
46 |
47 |
48 |
49 | {car.powertrain.toLowerCase() === "electric" ? ( 50 | 51 | ) : ( 52 | 53 | )} 54 | {car.powertrain} 55 |
56 |
57 | {car.transmission.toLowerCase() === "automatic" ? ( 58 | 59 | ) : ( 60 | 61 | )} 62 | {car.transmission} 63 |
64 |
65 |
66 | 67 | {formatAmountForDisplay( 68 | Math.round(Number(car.pricePerDay)), 69 | car.currency, 70 | true 71 | )} 72 | 73 | day 74 |
75 |
76 | 77 |
78 |
79 |
80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /app/(app)/cars/components/car-catalog.tsx: -------------------------------------------------------------------------------- 1 | import { getCars } from "@/db/queries/car-repository" 2 | 3 | import { SearchParams } from "@/lib/types" 4 | import { slugify } from "@/lib/utils" 5 | 6 | import { CarCard } from "./car-card" 7 | 8 | interface CarCatalogProps { 9 | searchParams: { 10 | [SearchParams.MIN_PRICE]?: string 11 | [SearchParams.MAX_PRICE]?: string 12 | [SearchParams.BODY_STYLE]?: string[] 13 | [SearchParams.POWERTRAIN]?: string 14 | [SearchParams.TRANSMISSION]?: string[] 15 | [SearchParams.MIN_SEATS]?: string 16 | } 17 | } 18 | 19 | export default async function CarCatalog({ searchParams }: CarCatalogProps) { 20 | const cars = await getCars() 21 | 22 | const { 23 | [SearchParams.MIN_PRICE]: minPrice, 24 | [SearchParams.MAX_PRICE]: maxPrice, 25 | [SearchParams.BODY_STYLE]: bodyStyles, 26 | [SearchParams.POWERTRAIN]: powertrain, 27 | [SearchParams.TRANSMISSION]: transmissions, 28 | [SearchParams.MIN_SEATS]: minSeats, 29 | } = searchParams 30 | 31 | const filteredCars = cars.filter((car) => { 32 | return ( 33 | (!minPrice || Number(car.pricePerDay) >= Number(minPrice)) && 34 | (!maxPrice || Number(car.pricePerDay) <= Number(maxPrice)) && 35 | (!bodyStyles || bodyStyles.includes(slugify(car.bodyStyle))) && 36 | (!powertrain || powertrain === car.powertrain) && 37 | (!transmissions || transmissions.includes(slugify(car.transmission))) && 38 | (!minSeats || car.seats >= Number(minSeats)) 39 | ) 40 | }) 41 | 42 | if (!filteredCars.length) 43 | return ( 44 |
45 |

No exact matches

46 |

47 | Try changing or removing some of your filters. 48 |

49 |
50 | ) 51 | 52 | return ( 53 | <> 54 | {filteredCars.map((car) => ( 55 | 56 | ))} 57 | 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /app/(app)/cars/components/car-details-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import { useSearchParams } from "next/navigation" 5 | 6 | import { SearchParams } from "@/lib/types" 7 | import { constructUrlWithParams } from "@/lib/utils" 8 | import { Button } from "@/components/ui/button" 9 | 10 | export function CarDetailsButton({ carSlug }: { carSlug: string }) { 11 | const searchParams = useSearchParams() 12 | const newParams = new URLSearchParams(searchParams.toString()) 13 | 14 | const location = searchParams.get(SearchParams.LOCATION) 15 | const checkin = searchParams.get(SearchParams.CHECKIN) 16 | const checkout = searchParams.get(SearchParams.CHECKOUT) 17 | 18 | if (location) newParams.set(SearchParams.LOCATION, location) 19 | if (checkin) newParams.set(SearchParams.CHECKIN, checkin) 20 | if (checkout) newParams.set(SearchParams.CHECKOUT, checkout) 21 | 22 | const href = constructUrlWithParams(`/cars/${carSlug}`, newParams) 23 | 24 | return ( 25 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /app/(app)/cars/components/filters/filters-content.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, ReactNode, SetStateAction } from "react" 2 | 3 | import { Separator } from "@/components/ui/separator" 4 | 5 | import { BodyStyleFilters } from "./sections/body-style" 6 | import { PowertrainFilters } from "./sections/powertrain" 7 | import { PriceRangeFilters } from "./sections/price-range" 8 | import { SeatingCapacityFilters } from "./sections/seating-capacity" 9 | import { TransmissionTypeFilters } from "./sections/transmission-type" 10 | import { SelectedFilters } from "./types" 11 | 12 | interface FiltersContentProps { 13 | selectedFilters: SelectedFilters 14 | setSelectedFilters: Dispatch> 15 | initialMinPrice: number 16 | initialMaxPrice: number 17 | } 18 | 19 | export function FiltersContent({ 20 | selectedFilters, 21 | setSelectedFilters, 22 | initialMinPrice, 23 | initialMaxPrice, 24 | }: FiltersContentProps) { 25 | return ( 26 |
27 | 28 | 32 | 33 | 34 | 35 | 41 | 42 | 43 | 44 | 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | 62 | 63 |
64 | ) 65 | } 66 | 67 | function FilterSection({ children }: { children: ReactNode }) { 68 | return
{children}
69 | } 70 | 71 | function FilterSeparator() { 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /app/(app)/cars/components/filters/filters-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react" 2 | import { getCars } from "@/db/queries/car-repository" 3 | 4 | import { FiltersButton } from "./filters-button" 5 | 6 | interface FiltersProps { 7 | trigger?: ReactNode 8 | } 9 | 10 | export default async function Filters({ trigger }: FiltersProps) { 11 | const cars = await getCars() 12 | 13 | const { MIN_PRICE, MAX_PRICE } = cars.reduce( 14 | (acc, car) => { 15 | acc.MIN_PRICE = Math.min(acc.MIN_PRICE, Number(car.pricePerDay)) 16 | acc.MAX_PRICE = Math.max(acc.MAX_PRICE, Number(car.pricePerDay)) 17 | return acc 18 | }, 19 | { MIN_PRICE: Infinity, MAX_PRICE: -Infinity } 20 | ) 21 | 22 | return ( 23 | 28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /app/(app)/cars/components/filters/lib/filters.ts: -------------------------------------------------------------------------------- 1 | import { SearchParams } from "@/lib/types" 2 | 3 | import { BodyStyle, Powertrain, SelectedFilters, Transmission } from "../types" 4 | 5 | export function initializeFiltersFromParams( 6 | searchParams: URLSearchParams, 7 | initialMinPrice: number, 8 | initialMaxPrice: number 9 | ): SelectedFilters { 10 | return { 11 | minPrice: 12 | Number(searchParams.get(SearchParams.MIN_PRICE)) || initialMinPrice, 13 | maxPrice: 14 | Number(searchParams.get(SearchParams.MAX_PRICE)) || initialMaxPrice, 15 | seats: Number(searchParams.get(SearchParams.MIN_SEATS)) || undefined, 16 | bodyStyles: searchParams.getAll(SearchParams.BODY_STYLE) as BodyStyle[], 17 | powertrain: searchParams.get(SearchParams.POWERTRAIN) as Powertrain, 18 | transmissions: searchParams.getAll( 19 | SearchParams.TRANSMISSION 20 | ) as Transmission[], 21 | } 22 | } 23 | 24 | export function applyFiltersToParams( 25 | newParams: URLSearchParams, 26 | selectedFilters: SelectedFilters, 27 | initialMinPrice: number, 28 | initialMaxPrice: number 29 | ) { 30 | const { minPrice, maxPrice, seats, bodyStyles, powertrain, transmissions } = 31 | selectedFilters 32 | 33 | if (minPrice !== initialMinPrice) 34 | newParams.set(SearchParams.MIN_PRICE, minPrice.toString()) 35 | else newParams.delete(SearchParams.MIN_PRICE) 36 | 37 | if (maxPrice !== initialMaxPrice) 38 | newParams.set(SearchParams.MAX_PRICE, maxPrice.toString()) 39 | else newParams.delete(SearchParams.MAX_PRICE) 40 | 41 | if (seats) newParams.set(SearchParams.MIN_SEATS, seats.toString()) 42 | else newParams.delete(SearchParams.MIN_SEATS) 43 | 44 | if (powertrain) newParams.set(SearchParams.POWERTRAIN, powertrain) 45 | else newParams.delete(SearchParams.POWERTRAIN) 46 | 47 | newParams.delete(SearchParams.BODY_STYLE) 48 | bodyStyles.forEach((bodyStyle) => 49 | newParams.append(SearchParams.BODY_STYLE, bodyStyle) 50 | ) 51 | 52 | newParams.delete(SearchParams.TRANSMISSION) 53 | transmissions.forEach((transmission) => 54 | newParams.append(SearchParams.TRANSMISSION, transmission) 55 | ) 56 | } 57 | 58 | export function countActiveFilters( 59 | searchParams: URLSearchParams, 60 | initialMinPrice: number, 61 | initialMaxPrice: number 62 | ): number { 63 | let totalCount = 0 64 | 65 | const minPrice = Number(searchParams.get(SearchParams.MIN_PRICE)) 66 | const maxPrice = Number(searchParams.get(SearchParams.MAX_PRICE)) 67 | const seats = Number(searchParams.get(SearchParams.MIN_SEATS)) 68 | const bodyStyles = searchParams.getAll(SearchParams.BODY_STYLE) 69 | const powertrain = searchParams.get(SearchParams.POWERTRAIN) 70 | const transmissions = searchParams.getAll(SearchParams.TRANSMISSION) 71 | 72 | if (minPrice && minPrice !== initialMinPrice) totalCount += 1 73 | if (maxPrice && maxPrice !== initialMaxPrice) totalCount += 1 74 | if (seats) totalCount += 1 75 | if (powertrain) totalCount += 1 76 | 77 | totalCount += bodyStyles.length 78 | totalCount += transmissions.length 79 | 80 | return totalCount 81 | } 82 | -------------------------------------------------------------------------------- /app/(app)/cars/components/filters/sections/body-style.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from "react" 2 | 3 | import { Toggle } from "@/components/ui/toggle" 4 | import { CarIcon } from "@/components/icons/car" 5 | import { HatchbackIcon } from "@/components/icons/hatchback" 6 | import { MinivanIcon } from "@/components/icons/minivan" 7 | import { RoadsterIcon } from "@/components/icons/roadster" 8 | import { SUVIcon } from "@/components/icons/suv" 9 | import { TruckIcon } from "@/components/icons/truck" 10 | 11 | import { BodyStyle, SelectedFilters } from "../types" 12 | 13 | export const bodyStyles: { 14 | slug: BodyStyle 15 | name: string 16 | icon: (props: React.HTMLAttributes) => JSX.Element 17 | }[] = [ 18 | { 19 | slug: "hatchback", 20 | name: "Hatchback", 21 | icon: HatchbackIcon, 22 | }, 23 | { slug: "minivan", name: "Minivan", icon: MinivanIcon }, 24 | { 25 | slug: "pickup-truck", 26 | name: "Pickup Truck", 27 | icon: TruckIcon, 28 | }, 29 | { 30 | slug: "sports-car", 31 | name: "Sports Car", 32 | icon: RoadsterIcon, 33 | }, 34 | { slug: "suv", name: "SUV", icon: SUVIcon }, 35 | { slug: "sedan", name: "Sedan", icon: CarIcon }, 36 | ] 37 | 38 | interface BodyStyleFiltersProps { 39 | selectedFilters: SelectedFilters 40 | setSelectedFilters: Dispatch> 41 | } 42 | 43 | export function BodyStyleFilters({ 44 | selectedFilters, 45 | setSelectedFilters, 46 | }: BodyStyleFiltersProps) { 47 | const handleBodyStyleToggle = (bodyStyle: BodyStyle) => { 48 | setSelectedFilters((prevFilters) => { 49 | const bodyStylesSelected = prevFilters.bodyStyles.includes(bodyStyle) 50 | ? prevFilters.bodyStyles.filter((selected) => selected !== bodyStyle) 51 | : [...prevFilters.bodyStyles, bodyStyle] 52 | 53 | return { ...prevFilters, bodyStyles: bodyStylesSelected } 54 | }) 55 | } 56 | 57 | return ( 58 |
59 |

Body Style

60 |
61 |
62 | {bodyStyles.map(({ slug, name, icon: Icon }) => ( 63 | handleBodyStyleToggle(slug)} 69 | > 70 | 71 | 72 | {name} 73 | 74 | 75 | ))} 76 |
77 |
78 |
79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /app/(app)/cars/components/filters/sections/powertrain.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from "react" 2 | 3 | import { Separator } from "@/components/ui/separator" 4 | import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" 5 | 6 | import { Powertrain, SelectedFilters } from "../types" 7 | 8 | const powertrains: { 9 | slug: Powertrain 10 | name: string 11 | }[] = [ 12 | { 13 | slug: "gasoline", 14 | name: "Gasoline", 15 | }, 16 | { slug: "diesel", name: "Diesel" }, 17 | { slug: "hybrid", name: "Hybrid" }, 18 | { 19 | slug: "electric", 20 | name: "Electric", 21 | }, 22 | ] 23 | 24 | interface PowertrainFiltersProps { 25 | selectedFilters: SelectedFilters 26 | setSelectedFilters: Dispatch> 27 | } 28 | 29 | export function PowertrainFilters({ 30 | selectedFilters, 31 | setSelectedFilters, 32 | }: PowertrainFiltersProps) { 33 | const handlePowertrainChange = (powertrain: Powertrain | undefined) => { 34 | setSelectedFilters((prevFilters) => ({ 35 | ...prevFilters, 36 | powertrain: powertrain ?? undefined, 37 | })) 38 | } 39 | 40 | return ( 41 |
42 |

Powertrain

43 |
44 | 48 | handlePowertrainChange(value as Powertrain | undefined) 49 | } 50 | className="grid min-h-14 w-full auto-cols-fr grid-flow-col gap-0 rounded-2xl border border-neutral-300 p-1" 51 | > 52 | {powertrains.map(({ slug, name }) => ( 53 | 58 | 59 | {name} 60 | 61 | 65 | 66 | ))} 67 | 68 |
69 |
70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /app/(app)/cars/components/filters/sections/seating-capacity.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction, useState } from "react" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { MinusIcon } from "@/components/icons/minus" 5 | import { PlusIcon } from "@/components/icons/plus" 6 | 7 | import { SelectedFilters } from "../types" 8 | 9 | interface SeatingCapacityFiltersProps { 10 | selectedFilters: SelectedFilters 11 | setSelectedFilters: Dispatch> 12 | } 13 | 14 | export function SeatingCapacityFilters({ 15 | selectedFilters, 16 | setSelectedFilters, 17 | }: SeatingCapacityFiltersProps) { 18 | const [counter, setCounter] = useState(selectedFilters.seats || 0) 19 | 20 | const handleMinusClick = () => { 21 | setCounter((prevCounter) => { 22 | const newCounter = prevCounter - 1 23 | setSelectedFilters({ 24 | ...selectedFilters, 25 | seats: newCounter > 0 ? newCounter : undefined, 26 | }) 27 | return newCounter 28 | }) 29 | } 30 | 31 | const handlePlusClick = () => { 32 | setCounter((prevCounter) => { 33 | const newCounter = prevCounter + 1 34 | setSelectedFilters({ 35 | ...selectedFilters, 36 | seats: newCounter > 0 ? newCounter : undefined, 37 | }) 38 | return newCounter 39 | }) 40 | } 41 | 42 | return ( 43 |
44 |
45 |

Seats

46 |
47 | 56 | 57 |
58 | {!selectedFilters.seats 59 | ? "Any" 60 | : selectedFilters.seats === 7 61 | ? `${selectedFilters.seats}+` 62 | : selectedFilters.seats} 63 |
64 | 65 | 74 |
75 |
76 |
77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /app/(app)/cars/components/filters/sections/transmission-type.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, SetStateAction } from "react" 2 | 3 | import { Toggle } from "@/components/ui/toggle" 4 | import { AutomaticGearboxIcon } from "@/components/icons/automatic-gearbox" 5 | import { ManualGearboxIcon } from "@/components/icons/manual-gearbox" 6 | 7 | import { SelectedFilters, Transmission } from "../types" 8 | 9 | const transmissions: { 10 | slug: Transmission 11 | name: string 12 | icon: (props: React.HTMLAttributes) => JSX.Element 13 | }[] = [ 14 | { 15 | slug: "automatic", 16 | name: "Automatic", 17 | icon: AutomaticGearboxIcon, 18 | }, 19 | { slug: "manual", name: "Manual", icon: ManualGearboxIcon }, 20 | ] 21 | 22 | interface TransmissionTypeFiltersProps { 23 | selectedFilters: SelectedFilters 24 | setSelectedFilters: Dispatch> 25 | } 26 | 27 | export function TransmissionTypeFilters({ 28 | selectedFilters, 29 | setSelectedFilters, 30 | }: TransmissionTypeFiltersProps) { 31 | const handleTransmissionToggle = (transmission: Transmission) => { 32 | setSelectedFilters((prevFilters) => { 33 | const transmissionsSelected = prevFilters.transmissions.includes( 34 | transmission 35 | ) 36 | ? prevFilters.transmissions.filter( 37 | (selected) => selected !== transmission 38 | ) 39 | : [...prevFilters.transmissions, transmission] 40 | 41 | return { ...prevFilters, transmissions: transmissionsSelected } 42 | }) 43 | } 44 | 45 | return ( 46 |
47 |

Transmission

48 |
49 |
50 | {transmissions.map(({ slug, name, icon: Icon }) => ( 51 | handleTransmissionToggle(slug)} 57 | > 58 | 59 | 60 | {name} 61 | 62 | 63 | ))} 64 |
65 |
66 |
67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /app/(app)/cars/components/filters/types.ts: -------------------------------------------------------------------------------- 1 | export type SelectedFilters = { 2 | minPrice: number 3 | maxPrice: number 4 | seats: number | undefined 5 | bodyStyles: BodyStyle[] 6 | powertrain: Powertrain | undefined 7 | transmissions: Transmission[] 8 | } 9 | 10 | export type BodyStyle = 11 | | "suv" 12 | | "minivan" 13 | | "pickup-truck" 14 | | "sports-car" 15 | | "hatchback" 16 | | "sedan" 17 | export type Powertrain = "gasoline" | "diesel" | "hybrid" | "electric" 18 | export type Transmission = "automatic" | "manual" 19 | -------------------------------------------------------------------------------- /app/(app)/cars/components/map.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useRef } from "react" 4 | import { useSearchParams } from "next/navigation" 5 | import type { Map as LeafletMap } from "leaflet" 6 | import { MapContainer, TileLayer, useMap } from "react-leaflet" 7 | 8 | import "leaflet/dist/leaflet.css" 9 | 10 | import { 11 | MAP_INITIAL_ZOOM_LEVEL, 12 | MAP_LOCATION_ZOOM_LEVEL, 13 | } from "@/lib/constants" 14 | import { SearchParams } from "@/lib/types" 15 | import { useToast } from "@/hooks/use-toast" 16 | 17 | export default function Map() { 18 | const searchParams = useSearchParams() 19 | const mapRef = useRef(null) 20 | 21 | function Recenter() { 22 | const map = useMap() 23 | const { toast } = useToast() 24 | 25 | useEffect(() => { 26 | if ( 27 | searchParams.has(SearchParams.LAT) && 28 | searchParams.has(SearchParams.LNG) 29 | ) { 30 | const lat = Number(searchParams.get(SearchParams.LAT)) 31 | const lng = Number(searchParams.get(SearchParams.LNG)) 32 | 33 | if (!isNaN(lat) && !isNaN(lng)) { 34 | map.setView({ lat, lng }, MAP_LOCATION_ZOOM_LEVEL) 35 | } else { 36 | console.error("Invalid latitude or longitude values:", { lat, lng }) 37 | toast({ 38 | variant: "destructive", 39 | title: "Invalid Location Data", 40 | description: 41 | "Either the latitude or longitude search parameters in the URL are not valid numbers. Please check the URL and try again.", 42 | }) 43 | } 44 | } 45 | }, [map, toast]) 46 | 47 | return null 48 | } 49 | 50 | return ( 51 | 57 | 61 | 62 | 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /app/(app)/cars/components/skeletons/car-card.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | export function CarCardSkeleton() { 4 | return ( 5 |
6 |
7 | 8 |
9 |
10 |
11 | 12 |
13 |
14 | 15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /app/(app)/cars/components/skeletons/car-catalog.tsx: -------------------------------------------------------------------------------- 1 | import { CarCardSkeleton } from "./car-card" 2 | 3 | export function CarCatalogSkeleton() { 4 | return ( 5 | <> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /app/(app)/cars/components/skeletons/map.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingDots } from "@/components/loading-dots" 2 | 3 | export function MapSkeleton() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /app/(app)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { SiteFooter } from "@/components/site-footer" 2 | 3 | export default function AppLayout({ children }: { children: React.ReactNode }) { 4 | return ( 5 | <> 6 | {children} 7 | 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /app/(app)/reservation/cars/[slug]/[[...rest]]/components/auth-section.tsx: -------------------------------------------------------------------------------- 1 | import { SignInButton, SignUpButton } from "@clerk/nextjs" 2 | 3 | import { Button } from "@/components/ui/button" 4 | 5 | export function AuthSection() { 6 | return ( 7 | <> 8 |

Log in or sign up to book

9 |
10 |
11 | 14 | 22 |
23 |
24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/(app)/reservation/cars/[slug]/[[...rest]]/components/back-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { usePathname, useRouter, useSearchParams } from "next/navigation" 4 | import { VariantProps } from "class-variance-authority" 5 | 6 | import { Button, buttonVariants } from "@/components/ui/button" 7 | 8 | interface BackButtonProps extends VariantProps { 9 | className?: string 10 | children?: React.ReactNode 11 | } 12 | 13 | export function BackButton({ 14 | variant = "ghost", 15 | size = "icon", 16 | className, 17 | children, 18 | }: BackButtonProps) { 19 | const router = useRouter() 20 | const pathname = usePathname() 21 | const searchParams = useSearchParams() 22 | 23 | const handleRedirect = () => { 24 | const updatedPath = pathname.replace("/reservation/cars", "/cars") 25 | const targetUrl = `${updatedPath}?${searchParams.toString()}` 26 | router.push(targetUrl) 27 | } 28 | 29 | return ( 30 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/(app)/reservation/cars/[slug]/[[...rest]]/components/book-details.tsx: -------------------------------------------------------------------------------- 1 | import { formatDateRangeForDisplay } from "@/lib/dates" 2 | 3 | export function BookDetails({ 4 | checkinDate, 5 | checkoutDate, 6 | }: { 7 | checkinDate: Date 8 | checkoutDate: Date 9 | }) { 10 | if (!checkinDate || !checkoutDate) { 11 | throw new Error("Both check-in and check-out dates must be provided.") 12 | } 13 | 14 | return ( 15 | <> 16 |

Your trip

17 |
18 |
19 |

20 | Dates 21 |

22 |

23 | {formatDateRangeForDisplay( 24 | checkinDate.toISOString(), 25 | checkoutDate.toISOString() 26 | )} 27 |

28 |
29 |
30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /app/(app)/reservation/cars/[slug]/[[...rest]]/components/cancellation-policy.tsx: -------------------------------------------------------------------------------- 1 | export function CancellationPolicy() { 2 | return ( 3 | <> 4 |

Cancellation policy

5 |
6 |

7 | Free cancelation before 17 Aug. Learn more 8 |

9 |
10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /app/(app)/reservation/cars/[slug]/[[...rest]]/components/car-details.tsx: -------------------------------------------------------------------------------- 1 | import { SelectCar } from "@/db/schema" 2 | 3 | import CldImage from "@/components/cld-image" 4 | import { FilledStarIcon } from "@/components/icons/filled-star" 5 | 6 | export function CarDetails({ car }: { car: SelectCar }) { 7 | return ( 8 |
9 |
10 | 18 |
19 |
20 | {car.name} 21 | {car.transmission} 22 | {car.powertrain} 23 |
24 | 25 | {car.rating} 26 | ({car.reviewCount} reviews) 27 |
28 |
29 |
30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /app/(app)/reservation/cars/[slug]/[[...rest]]/components/payment-details.tsx: -------------------------------------------------------------------------------- 1 | import { currentUser } from "@clerk/nextjs/server" 2 | 3 | import ElementsForm from "./elements-form" 4 | 5 | export async function PaymentDetails({ 6 | amount, 7 | currency, 8 | }: { 9 | amount: number 10 | currency: string 11 | }) { 12 | const user = await currentUser() 13 | 14 | return ( 15 | <> 16 |

Pay with

17 | 18 |
19 | 24 |
25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /app/(app)/reservation/cars/[slug]/[[...rest]]/components/price-details.tsx: -------------------------------------------------------------------------------- 1 | import { formatAmountForDisplay } from "@/lib/utils" 2 | import { Separator } from "@/components/ui/separator" 3 | 4 | export function PriceDetails({ 5 | days, 6 | currency, 7 | subtotal, 8 | taxes, 9 | }: { 10 | days: number 11 | currency: string 12 | subtotal: number 13 | taxes: number 14 | }) { 15 | return ( 16 | <> 17 |

Your total

18 |
19 |
20 |
21 | 22 | {days} {days === 1 ? "day" : "days"} 23 | 24 | {formatAmountForDisplay(subtotal, currency)} 25 |
26 |
27 | Taxes 28 | {formatAmountForDisplay(taxes, currency)} 29 |
30 | 31 |
32 | Total ({currency.toUpperCase()}) 33 | 34 | {formatAmountForDisplay(subtotal + taxes, currency)} 35 | 36 |
37 |
38 |
39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /app/(app)/reservation/cars/[slug]/[[...rest]]/components/stripe-test-cards.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, AlertDescription } from "@/components/ui/alert" 2 | 3 | export function StripeTestCards() { 4 | return ( 5 | 6 | 7 |

8 | Use any of{" "} 9 | 15 | Stripe's test cards 16 | 17 | , like{" "} 18 | 19 | 4242 4242 4242 4242 20 | 21 | . No real charges will apply. 22 |

23 |
24 |
25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/(app)/reservation/cars/[slug]/[[...rest]]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { LoadingDots } from "@/components/loading-dots" 2 | 3 | export default function LoadingReservationPage() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /app/(app)/reservation/confirmation/result/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Stripe } from "stripe" 2 | 3 | import { stripe } from "@/lib/stripe" 4 | 5 | export default async function ResultPage({ 6 | searchParams, 7 | }: { 8 | searchParams: { payment_intent: string } 9 | }): Promise { 10 | if (!searchParams.payment_intent) 11 | throw new Error("Please provide a valid payment_intent (`pi_...`)") 12 | 13 | const paymentIntent: Stripe.PaymentIntent = 14 | await stripe.paymentIntents.retrieve(searchParams.payment_intent) 15 | 16 | const formattedContent: string = JSON.stringify(paymentIntent, null, 2) 17 | 18 | return ( 19 | <> 20 |

Status: {paymentIntent.status}

21 |

Payment Intent response:

22 | 23 |
{formattedContent}
24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /app/(auth)/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs" 2 | 3 | import { LoadingDots } from "@/components/loading-dots" 4 | 5 | export default function SignInPage() { 6 | return ( 7 |
8 |
9 | 10 |
11 | 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/(auth)/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs" 2 | 3 | import { LoadingDots } from "@/components/loading-dots" 4 | 5 | export default function SignUpPage() { 6 | return ( 7 |
8 |
9 | 10 |
11 | 12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect } from "react" 4 | 5 | import { Button } from "@/components/ui/button" 6 | import { SiteHeader } from "@/components/site-header" 7 | 8 | export default function Error({ 9 | error, 10 | reset, 11 | }: { 12 | error: Error & { digest?: string } 13 | reset: () => void 14 | }) { 15 | useEffect(() => { 16 | // Optionally log the error to an error reporting service 17 | console.error(error) 18 | }, [error]) 19 | 20 | return ( 21 | <> 22 |
23 |
24 | 25 |
26 |
27 |
28 |
29 |

30 | Something went wrong! 31 |

32 |

33 | It seems like there's a hiccup on our end. Our team is working 34 | hard to fix the issue. We appreciate your patience and 35 | understanding. 36 |

37 | 46 | 56 |
57 |
58 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from "next" 2 | import { ClerkProvider } from "@clerk/nextjs" 3 | 4 | import { Toaster } from "@/components/ui/toaster" 5 | 6 | import "../styles/globals.css" 7 | 8 | import { siteConfig } from "@/config/site" 9 | import { fontSans } from "@/lib/fonts" 10 | 11 | export const metadata: Metadata = { 12 | metadataBase: new URL(siteConfig.url), 13 | title: { 14 | default: siteConfig.name, 15 | template: `%s - ${siteConfig.name}`, 16 | }, 17 | description: siteConfig.description, 18 | authors: { 19 | name: siteConfig.author.name, 20 | url: siteConfig.author.url, 21 | }, 22 | creator: siteConfig.author.name, 23 | openGraph: { 24 | type: "website", 25 | locale: "en_US", 26 | url: siteConfig.url, 27 | title: siteConfig.name, 28 | description: siteConfig.description, 29 | siteName: siteConfig.name, 30 | }, 31 | twitter: { 32 | card: "summary_large_image", 33 | title: siteConfig.name, 34 | description: siteConfig.description, 35 | creator: `@${siteConfig.author.name}`, 36 | }, 37 | icons: { 38 | icon: [ 39 | { 40 | url: "/favicon.ico", 41 | sizes: "32x32", 42 | }, 43 | { url: "/icon.svg", type: "image/svg+xml" }, 44 | { url: "/icon-192.png", type: "image/png", sizes: "192x192" }, 45 | { url: "/icon-512.png", type: "image/png", sizes: "512x512" }, 46 | ], 47 | apple: [{ url: "/apple-icon.png", type: "image/png" }], 48 | }, 49 | } 50 | 51 | export const viewport: Viewport = { 52 | themeColor: "white", 53 | colorScheme: "light", 54 | width: "device-width", 55 | initialScale: 1, 56 | } 57 | 58 | interface RootLayoutProps { 59 | children: React.ReactNode 60 | } 61 | 62 | export default function RootLayout({ children }: RootLayoutProps) { 63 | return ( 64 | 65 | 66 | 67 | {children} 68 | 69 | 70 | 71 | 72 | ) 73 | } 74 | -------------------------------------------------------------------------------- /app/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Carhive", 3 | "short_name": "Carhive", 4 | "icons": [ 5 | { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" }, 6 | { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" } 7 | ], 8 | "theme_color": "#ffffff", 9 | "background-color": "#ffffff", 10 | "display": "standalone" 11 | } 12 | -------------------------------------------------------------------------------- /app/opengraph-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/app/opengraph-image.png -------------------------------------------------------------------------------- /app/robots.txt: -------------------------------------------------------------------------------- 1 | # Block OpenAI 2 | User-agent: GPTBot 3 | Disallow: / 4 | User-agent: ChatGPT-User 5 | Disallow: / 6 | 7 | # Block Google Bard AI 8 | User-agent: Google-Extended 9 | Disallow: / 10 | 11 | # Block Common Crawl 12 | User-agent: CCBot 13 | Disallow: / -------------------------------------------------------------------------------- /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": "styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": false 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /components/cld-image.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { CldImage as CldImageDefault, CldImageProps } from "next-cloudinary" 4 | 5 | const CldImage = (props: CldImageProps) => { 6 | return 7 | } 8 | 9 | export default CldImage 10 | -------------------------------------------------------------------------------- /components/icons/automatic-gearbox.tsx: -------------------------------------------------------------------------------- 1 | export function AutomaticGearboxIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /components/icons/battery-automotive.tsx: -------------------------------------------------------------------------------- 1 | export function BatteryAutomotiveIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /components/icons/car.tsx: -------------------------------------------------------------------------------- 1 | export function CarIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/icons/caret-right.tsx: -------------------------------------------------------------------------------- 1 | export function CaretRightIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/icons/check.tsx: -------------------------------------------------------------------------------- 1 | export function CheckIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/chevron-down.tsx: -------------------------------------------------------------------------------- 1 | export function ChevronDownIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/chevron-left.tsx: -------------------------------------------------------------------------------- 1 | export function ChevronLeftIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/chevron-right.tsx: -------------------------------------------------------------------------------- 1 | export function ChevronRightIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/circle-check.tsx: -------------------------------------------------------------------------------- 1 | export function CircleCheckIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/icons/circle.tsx: -------------------------------------------------------------------------------- 1 | export function CircleIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/click.tsx: -------------------------------------------------------------------------------- 1 | export function ClickIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/currency-dollar.tsx: -------------------------------------------------------------------------------- 1 | export function CurrencyDollarIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/icons/dashboard.tsx: -------------------------------------------------------------------------------- 1 | export function DashboardIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/engine.tsx: -------------------------------------------------------------------------------- 1 | export function EngineIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /components/icons/filled-star.tsx: -------------------------------------------------------------------------------- 1 | export function FilledStarIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /components/icons/filter-search.tsx: -------------------------------------------------------------------------------- 1 | export function FilterSearchIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/filter.tsx: -------------------------------------------------------------------------------- 1 | export function FilterIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/filters.tsx: -------------------------------------------------------------------------------- 1 | export function FiltersIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /components/icons/hatchback.tsx: -------------------------------------------------------------------------------- 1 | export function HatchbackIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/headset.tsx: -------------------------------------------------------------------------------- 1 | export function HeadsetIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /components/icons/info-square.tsx: -------------------------------------------------------------------------------- 1 | export function InfoSquareIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/kid.tsx: -------------------------------------------------------------------------------- 1 | export function KidIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /components/icons/manual-gearbox.tsx: -------------------------------------------------------------------------------- 1 | export function ManualGearboxIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/icons/map-pin.tsx: -------------------------------------------------------------------------------- 1 | export function MapPinIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /components/icons/menu.tsx: -------------------------------------------------------------------------------- 1 | export function MenuIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/icons/minivan.tsx: -------------------------------------------------------------------------------- 1 | export function MinivanIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/icons/minus.tsx: -------------------------------------------------------------------------------- 1 | export function MinusIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/navigation.tsx: -------------------------------------------------------------------------------- 1 | export function NavigationIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/icons/plus.tsx: -------------------------------------------------------------------------------- 1 | export function PlusIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/icons/roadster.tsx: -------------------------------------------------------------------------------- 1 | export function RoadsterIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/search.tsx: -------------------------------------------------------------------------------- 1 | export function SearchIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/icons/selector.tsx: -------------------------------------------------------------------------------- 1 | export function SelectorIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/icons/shield-check.tsx: -------------------------------------------------------------------------------- 1 | export function ShieldCheckIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/icons/suv.tsx: -------------------------------------------------------------------------------- 1 | export function SUVIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/icons/truck.tsx: -------------------------------------------------------------------------------- 1 | export function TruckIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /components/icons/user-circle.tsx: -------------------------------------------------------------------------------- 1 | export function UserCircleIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 4 | 8 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/icons/wifi.tsx: -------------------------------------------------------------------------------- 1 | export function WifiIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /components/icons/x.tsx: -------------------------------------------------------------------------------- 1 | export function XIcon(props: React.HTMLAttributes) { 2 | return ( 3 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/loading-dots.tsx: -------------------------------------------------------------------------------- 1 | export function LoadingDots() { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /components/logoLink.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import { CarhiveLogo } from './icons/carhive-logo'; 3 | import { Button } from './ui/button'; 4 | 5 | export function LogoLink() { 6 | return ( 7 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /components/responsive-modal.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react" 2 | 3 | import { DESKTOP_MEDIA_QUERY } from "@/lib/constants" 4 | import { useMediaQuery } from "@/hooks/use-media-query" 5 | import { 6 | Dialog, 7 | DialogContent, 8 | DialogDescription, 9 | DialogFooter, 10 | DialogHeader, 11 | DialogTitle, 12 | DialogTrigger, 13 | } from "@/components/ui/dialog" 14 | import { 15 | Drawer, 16 | DrawerContent, 17 | DrawerDescription, 18 | DrawerFooter, 19 | DrawerHeader, 20 | DrawerTitle, 21 | DrawerTrigger, 22 | } from "@/components/ui/drawer" 23 | 24 | interface ResponsiveModalProps { 25 | open: boolean 26 | onOpenChange: (open: boolean) => void 27 | trigger: ReactNode 28 | title: string 29 | description: string 30 | children: ReactNode 31 | footer?: ReactNode 32 | } 33 | 34 | export function ResponsiveModal({ 35 | open, 36 | onOpenChange, 37 | trigger, 38 | title, 39 | description, 40 | children, 41 | footer, 42 | }: ResponsiveModalProps) { 43 | const isDesktop = useMediaQuery(DESKTOP_MEDIA_QUERY) 44 | 45 | if (isDesktop) { 46 | return ( 47 | 48 | {trigger} 49 | 50 | 51 | 52 | {title} 53 | 54 | 55 | {description} 56 | 57 | 58 |
{children}
59 | {footer && ( 60 | 61 | {footer} 62 | 63 | )} 64 |
65 |
66 | ) 67 | } 68 | 69 | return ( 70 | 71 | {trigger} 72 | 73 | 74 | {title} 75 | 76 | {description} 77 | 78 | 79 |
80 | {children} 81 |
82 | {footer && {footer}} 83 |
84 |
85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /components/search-panel-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { getLocations } from "@/db/queries/location-repository" 2 | 3 | import { SearchPanel } from "./search-panel" 4 | 5 | export async function SearchPanelWrapper(props: any) { 6 | const locations = await getLocations() 7 | 8 | if (!locations) return null 9 | 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /components/site-footer.tsx: -------------------------------------------------------------------------------- 1 | import { siteConfig } from "@/config/site" 2 | 3 | import { LogoLink } from "./logoLink" 4 | import { Button } from "./ui/button" 5 | 6 | const footerLinks = [ 7 | { 8 | title: "Services", 9 | links: [ 10 | "Car Rentals", 11 | "Insurance Options", 12 | "Corporate Rentals", 13 | "Special Offers", 14 | "FAQs", 15 | ], 16 | }, 17 | { 18 | title: "Resources", 19 | links: [ 20 | "Help Center", 21 | "Privacy Policy", 22 | "Terms of Service", 23 | "Accessibility", 24 | "Vehicle Guides", 25 | "Customer Testimonials", 26 | ], 27 | }, 28 | { 29 | title: "Company", 30 | links: [ 31 | "About", 32 | "Contact Us", 33 | "Blog", 34 | "Partners", 35 | "Customers", 36 | "Careers", 37 | "Press", 38 | ], 39 | }, 40 | { 41 | title: "Social", 42 | links: ["Youtube", "Twitter", "Instagram", "Facebook"], 43 | }, 44 | ] 45 | 46 | export function SiteFooter() { 47 | const githubUrl = siteConfig.links.github 48 | 49 | return ( 50 |
51 |
52 |
53 |
54 | 55 |
56 | 83 |
84 |

85 | Built by{" "} 86 | 99 | . 100 |

101 |
102 |
103 |
104 |
105 | ) 106 | } 107 | -------------------------------------------------------------------------------- /components/site-header.tsx: -------------------------------------------------------------------------------- 1 | import { LogoLink } from "./logoLink" 2 | import { UserMenuButton } from "./user-menu-button" 3 | 4 | export function SiteHeader() { 5 | return ( 6 |
7 | 8 |
9 | 10 |
11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /components/skeletons/search-panel.tsx: -------------------------------------------------------------------------------- 1 | import { Separator } from "@/components/ui/separator" 2 | import { Skeleton } from "@/components/ui/skeleton" 3 | 4 | export function SearchPanelSkeleton() { 5 | return ( 6 |
7 |
8 |
9 |
10 | 14 |
15 |
16 |
17 | 18 | 19 |
20 |
21 |
22 |
23 |
24 | 28 |
29 |
30 |
31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | 42 | 43 |
44 |
45 |
46 |
47 |
48 |
49 | 50 |
51 |
52 |
53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border border-neutral-300 p-6 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-6 [&>svg]:top-6 [&>svg]:text-neutral-950 [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-white text-neutral-950 ", 12 | destructive: "border-red-600 text-red-500 [&>svg]:text-red-500", 13 | }, 14 | }, 15 | defaultVariants: { 16 | variant: "default", 17 | }, 18 | } 19 | ) 20 | 21 | const Alert = React.forwardRef< 22 | HTMLDivElement, 23 | React.HTMLAttributes & VariantProps 24 | >(({ className, variant, ...props }, ref) => ( 25 |
31 | )) 32 | Alert.displayName = "Alert" 33 | 34 | const AlertTitle = React.forwardRef< 35 | HTMLParagraphElement, 36 | React.HTMLAttributes 37 | >(({ className, ...props }, ref) => ( 38 |
43 | )) 44 | AlertTitle.displayName = "AlertTitle" 45 | 46 | const AlertDescription = React.forwardRef< 47 | HTMLParagraphElement, 48 | React.HTMLAttributes 49 | >(({ className, ...props }, ref) => ( 50 |
55 | )) 56 | AlertDescription.displayName = "AlertDescription" 57 | 58 | export { Alert, AlertTitle, AlertDescription } 59 | -------------------------------------------------------------------------------- /components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border border-neutral-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-neutral-950 focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-neutral-900 text-neutral-50 hover:bg-neutral-900/80", 13 | secondary: 14 | "border-transparent bg-neutral-100 text-neutral-900 hover:bg-neutral-100/80", 15 | destructive: 16 | "border-transparent bg-red-500 text-neutral-50 hover:bg-red-500/80", 17 | outline: "text-neutral-950", 18 | counter: 19 | "size-5 justify-center border-2 border-white bg-neutral-900 px-1 py-0 text-[10px] font-bold text-neutral-50", 20 | }, 21 | }, 22 | defaultVariants: { 23 | variant: "default", 24 | }, 25 | } 26 | ) 27 | 28 | export interface BadgeProps 29 | extends React.HTMLAttributes, 30 | VariantProps {} 31 | 32 | function Badge({ className, variant, ...props }: BadgeProps) { 33 | return ( 34 |
35 | ) 36 | } 37 | 38 | export { Badge, badgeVariants } 39 | -------------------------------------------------------------------------------- /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-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-neutral-900 text-neutral-50 hover:bg-neutral-900/90", 13 | destructive: "bg-red-500 text-neutral-50 hover:bg-red-500/90", 14 | outline: 15 | "border border-neutral-300 bg-white hover:bg-neutral-100 hover:text-neutral-900", 16 | secondary: "bg-neutral-100 text-neutral-900 hover:bg-neutral-100/80", 17 | ghost: "hover:bg-neutral-100 hover:text-neutral-900 ", 18 | link: "text-neutral-600 hover:text-black", 19 | unstyled: 20 | "rounded-none font-normal ring-0 focus-visible:outline focus-visible:ring-0", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | xs: "h-7 px-2", 25 | sm: "h-9 px-3", 26 | lg: "h-12 px-6", 27 | icon: "size-10 rounded-full", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { DayPicker } from "react-day-picker" 5 | 6 | import { cn } from "@/lib/utils" 7 | import { buttonVariants } from "@/components/ui/button" 8 | 9 | import { ChevronLeftIcon } from "../icons/chevron-left" 10 | import { ChevronRightIcon } from "../icons/chevron-right" 11 | 12 | export type CalendarProps = React.ComponentProps 13 | 14 | function Calendar({ 15 | className, 16 | classNames, 17 | showOutsideDays = true, 18 | ...props 19 | }: CalendarProps) { 20 | return ( 21 | ( 59 | 60 | ), 61 | IconRight: ({ ...props }) => ( 62 | 63 | ), 64 | }} 65 | {...props} 66 | /> 67 | ) 68 | } 69 | Calendar.displayName = "Calendar" 70 | 71 | export { Calendar } 72 | -------------------------------------------------------------------------------- /components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Drawer as DrawerPrimitive } from "vaul" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Drawer = ({ 9 | shouldScaleBackground = true, 10 | ...props 11 | }: React.ComponentProps) => ( 12 | 16 | ) 17 | Drawer.displayName = "Drawer" 18 | 19 | const DrawerTrigger = DrawerPrimitive.Trigger 20 | 21 | const DrawerPortal = DrawerPrimitive.Portal 22 | 23 | const DrawerClose = DrawerPrimitive.Close 24 | 25 | const DrawerOverlay = React.forwardRef< 26 | React.ElementRef, 27 | React.ComponentPropsWithoutRef 28 | >(({ className, ...props }, ref) => ( 29 | 34 | )) 35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName 36 | 37 | const DrawerContent = React.forwardRef< 38 | React.ElementRef, 39 | React.ComponentPropsWithoutRef 40 | >(({ className, children, ...props }, ref) => ( 41 | 42 | 43 | 51 |
52 | {children} 53 | 54 | 55 | )) 56 | DrawerContent.displayName = "DrawerContent" 57 | 58 | const DrawerHeader = ({ 59 | className, 60 | ...props 61 | }: React.HTMLAttributes) => ( 62 |
66 | ) 67 | DrawerHeader.displayName = "DrawerHeader" 68 | 69 | const DrawerFooter = ({ 70 | className, 71 | ...props 72 | }: React.HTMLAttributes) => ( 73 |
77 | ) 78 | DrawerFooter.displayName = "DrawerFooter" 79 | 80 | const DrawerTitle = React.forwardRef< 81 | React.ElementRef, 82 | React.ComponentPropsWithoutRef 83 | >(({ className, ...props }, ref) => ( 84 | 92 | )) 93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName 94 | 95 | const DrawerDescription = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, ...props }, ref) => ( 99 | 104 | )) 105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName 106 | 107 | export { 108 | Drawer, 109 | DrawerPortal, 110 | DrawerOverlay, 111 | DrawerTrigger, 112 | DrawerClose, 113 | DrawerContent, 114 | DrawerHeader, 115 | DrawerFooter, 116 | DrawerTitle, 117 | DrawerDescription, 118 | } 119 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /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/ui/slider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SliderPrimitive from "@radix-ui/react-slider" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Slider = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => { 12 | const initialValue = Array.isArray(props.value) 13 | ? props.value 14 | : [props.min, props.max] 15 | 16 | return ( 17 | 25 | 26 | 27 | 28 | {initialValue.map((value, index) => ( 29 | 30 | 31 | 32 | ))} 33 | 34 | ) 35 | }) 36 | Slider.displayName = SliderPrimitive.Root.displayName 37 | 38 | export { Slider } 39 | -------------------------------------------------------------------------------- /components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useToast } from "@/hooks/use-toast" 4 | import { 5 | Toast, 6 | ToastClose, 7 | ToastDescription, 8 | ToastProvider, 9 | ToastTitle, 10 | ToastViewport, 11 | } from "@/components/ui/toast" 12 | 13 | export function Toaster() { 14 | const { toasts } = useToast() 15 | 16 | return ( 17 | 18 | {toasts.map(function ({ id, title, description, action, ...props }) { 19 | return ( 20 | 21 |
22 | {title && {title}} 23 | {description && ( 24 | {description} 25 | )} 26 |
27 | {action} 28 | 29 |
30 | ) 31 | })} 32 | 33 |
34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /components/ui/toggle-group.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group" 5 | import { type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { toggleVariants } from "@/components/ui/toggle" 9 | 10 | const ToggleGroupContext = React.createContext< 11 | VariantProps 12 | >({ 13 | size: "default", 14 | variant: "default", 15 | }) 16 | 17 | const ToggleGroup = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef & 20 | VariantProps 21 | >(({ className, variant, size, children, ...props }, ref) => ( 22 | 27 | 28 | {children} 29 | 30 | 31 | )) 32 | 33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName 34 | 35 | const ToggleGroupItem = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef & 38 | VariantProps 39 | >(({ className, children, variant, size, ...props }, ref) => { 40 | const context = React.useContext(ToggleGroupContext) 41 | 42 | return ( 43 | 54 | {children} 55 | 56 | ) 57 | }) 58 | 59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName 60 | 61 | export { ToggleGroup, ToggleGroupItem } 62 | -------------------------------------------------------------------------------- /components/ui/toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TogglePrimitive from "@radix-ui/react-toggle" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const toggleVariants = cva( 10 | "inline-flex items-center justify-center rounded-md text-sm font-medium text-neutral-900 ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 11 | { 12 | variants: { 13 | variant: { 14 | default: "bg-transparent", 15 | outline: 16 | "border border-neutral-300 bg-transparent hover:border-black data-[state=on]:border-black data-[state=on]:bg-neutral-50 data-[state=on]:ring-1 data-[state=on]:ring-black", 17 | }, 18 | size: { 19 | default: "h-10 px-3", 20 | sm: "h-9 px-2.5", 21 | lg: "h-11 px-5", 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: "default", 26 | size: "default", 27 | }, 28 | } 29 | ) 30 | 31 | const Toggle = React.forwardRef< 32 | React.ElementRef, 33 | React.ComponentPropsWithoutRef & 34 | VariantProps 35 | >(({ className, variant, size, ...props }, ref) => ( 36 | 41 | )) 42 | 43 | Toggle.displayName = TogglePrimitive.Root.displayName 44 | 45 | export { Toggle, toggleVariants } 46 | -------------------------------------------------------------------------------- /components/user-menu-button.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | SignedIn, 3 | SignedOut, 4 | SignInButton, 5 | SignUpButton, 6 | UserButton, 7 | } from "@clerk/nextjs" 8 | 9 | import { siteConfig } from "@/config/site" 10 | import { 11 | DropdownMenu, 12 | DropdownMenuContent, 13 | DropdownMenuGroup, 14 | DropdownMenuItem, 15 | DropdownMenuSeparator, 16 | DropdownMenuTrigger, 17 | } from "@/components/ui/dropdown-menu" 18 | 19 | import { MenuIcon } from "./icons/menu" 20 | import { UserCircleIcon } from "./icons/user-circle" 21 | import { Button } from "./ui/button" 22 | 23 | export function UserMenuButton() { 24 | const githubUrl = siteConfig.links.github 25 | 26 | return ( 27 | <> 28 | 29 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 66 | Gift Cards 67 | 68 | 69 | 70 | 76 | Help Center 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | ) 85 | } 86 | -------------------------------------------------------------------------------- /config/site.ts: -------------------------------------------------------------------------------- 1 | export const siteConfig = { 2 | name: "Carhive", 3 | url: "https://carhive.eduam.dev", 4 | author: { 5 | name: "eduamdev", 6 | url: "https://eduam.dev", 7 | }, 8 | description: 9 | "Explore the freedom of the open road with our premium car rental services! Discover a wide range of meticulously maintained vehicles, from sleek sedans to spacious SUVs, perfect for every occasion. With competitive prices, flexible rental options, and exceptional customer service, we make your journey unforgettable. Book your dream car today and embark on a seamless travel experience.", 10 | links: { 11 | github: "https://github.com/eduamdev", 12 | }, 13 | } 14 | 15 | export type SiteConfig = typeof siteConfig 16 | -------------------------------------------------------------------------------- /data/car-types.js: -------------------------------------------------------------------------------- 1 | import hatchback from "../public/assets/images/cars/hatchback.jpg" 2 | import minivan from "../public/assets/images/cars/minivan.jpg" 3 | import pickupTruck from "../public/assets/images/cars/pickup-truck.jpg" 4 | import sedan from "../public/assets/images/cars/sedan.jpg" 5 | import sportsCar from "../public/assets/images/cars/sports-car.jpg" 6 | import suv from "../public/assets/images/cars/suv.jpg" 7 | 8 | export const carTypes = [ 9 | { 10 | id: "hatchback", 11 | slug: "hatchback", 12 | name: "Hatchback", 13 | imageUrl: hatchback, 14 | }, 15 | { 16 | id: "minivan", 17 | slug: "minivan", 18 | name: "Minivan", 19 | imageUrl: minivan, 20 | }, 21 | { 22 | id: "sports-car", 23 | slug: "sports-car", 24 | name: "Sports Car", 25 | imageUrl: sportsCar, 26 | }, 27 | { 28 | id: "pickup-truck", 29 | slug: "pickup-truck", 30 | name: "Pickup Truck", 31 | imageUrl: pickupTruck, 32 | }, 33 | { 34 | id: "suv", 35 | slug: "suv", 36 | name: "SUV", 37 | imageUrl: suv, 38 | }, 39 | { 40 | id: "sedan", 41 | slug: "sedan", 42 | name: "Sedan", 43 | imageUrl: sedan, 44 | }, 45 | ] 46 | -------------------------------------------------------------------------------- /data/locations-with-images.js: -------------------------------------------------------------------------------- 1 | import Cancun from "../public/assets/images/locations/cancun.jpg" 2 | import Dubai from "../public/assets/images/locations/dubai.jpg" 3 | import Paris from "../public/assets/images/locations/paris.jpg" 4 | import Rio from "../public/assets/images/locations/rio.jpg" 5 | import Rome from "../public/assets/images/locations/rome.jpg" 6 | import Sydney from "../public/assets/images/locations/sydney.jpg" 7 | import { locations } from "./locations" 8 | 9 | // Add images to the respective locations 10 | export const locationsWithImages = locations.map((location) => { 11 | switch (location.slug) { 12 | case "cancun": 13 | return { ...location, imageUrl: Cancun } 14 | case "dubai": 15 | return { ...location, imageUrl: Dubai } 16 | case "paris": 17 | return { ...location, imageUrl: Paris } 18 | case "rio": 19 | return { ...location, imageUrl: Rio } 20 | case "rome": 21 | return { ...location, imageUrl: Rome } 22 | case "sydney": 23 | return { ...location, imageUrl: Sydney } 24 | default: 25 | return location // For locations without images 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /data/locations.js: -------------------------------------------------------------------------------- 1 | export const locations = [ 2 | { 3 | id: "fae436f3-6341-486a-8691-0633f64e1997", 4 | slug: "amsterdam", 5 | name: "Amsterdam, Netherlands", 6 | latitude: 52.3547, 7 | longitude: 4.904, 8 | imageUrl: "", 9 | featured: false, 10 | startingPrice: 44, 11 | }, 12 | { 13 | id: "6132dc81-cc1a-4a2e-93c3-d176139bec4f", 14 | slug: "barcelona", 15 | name: "Barcelona, Spain", 16 | latitude: 41.3925, 17 | longitude: 2.1404, 18 | imageUrl: "", 19 | featured: false, 20 | startingPrice: 59, 21 | }, 22 | { 23 | id: "92331cbf-254a-4acf-9671-a77810faef4c", 24 | slug: "cancun", 25 | name: "Cancún, México", 26 | latitude: 21.1617, 27 | longitude: -86.851, 28 | imageUrl: "", 29 | featured: true, 30 | startingPrice: 49, 31 | }, 32 | { 33 | id: "e27e9e7c-5bf9-44f9-b24f-3b5df6417c77", 34 | slug: "dubai", 35 | name: "Dubai, United Arab Emirates", 36 | latitude: 25.2652, 37 | longitude: 55.2928, 38 | imageUrl: "", 39 | featured: true, 40 | startingPrice: 67, 41 | }, 42 | { 43 | id: "fd1e4b0d-5e1a-41db-a89b-6e33eed72ace", 44 | slug: "new-york", 45 | name: "New York, United States", 46 | latitude: 40.6975, 47 | longitude: -73.9795, 48 | imageUrl: "", 49 | featured: false, 50 | startingPrice: 100, 51 | }, 52 | { 53 | id: "45d07433-25e2-4ce7-b039-f0317e694048", 54 | slug: "paris", 55 | name: "Paris, France", 56 | latitude: 48.8589, 57 | longitude: 2.3469, 58 | imageUrl: "", 59 | featured: true, 60 | startingPrice: 69, 61 | }, 62 | { 63 | id: "d9b23370-3be4-4936-ae23-3ad54b310fd8", 64 | slug: "rio", 65 | name: "Rio de Janeiro, Brazil", 66 | latitude: -22.9148, 67 | longitude: -43.4075, 68 | imageUrl: "", 69 | featured: true, 70 | startingPrice: 66, 71 | }, 72 | { 73 | id: "2538dcf8-b531-4c68-a87a-b49a42be0c23", 74 | slug: "rome", 75 | name: "Rome, Italy", 76 | latitude: 41.8931, 77 | longitude: 12.4832, 78 | imageUrl: "", 79 | featured: true, 80 | startingPrice: 78, 81 | }, 82 | { 83 | id: "b31d9e0c-77c6-427b-9a19-37382ea62d7b", 84 | slug: "sydney", 85 | name: "Sydney, Australia", 86 | latitude: -33.8693, 87 | longitude: 151.209, 88 | imageUrl: "", 89 | featured: true, 90 | startingPrice: 57, 91 | }, 92 | { 93 | id: "ff841d10-0682-4e51-9330-47c5abb00643", 94 | slug: "tokyo", 95 | name: "Tokyo, Japan", 96 | latitude: 35.6841, 97 | longitude: 139.7742, 98 | imageUrl: "", 99 | featured: false, 100 | startingPrice: 54, 101 | }, 102 | ] 103 | -------------------------------------------------------------------------------- /data/testimonials.js: -------------------------------------------------------------------------------- 1 | import alex from "../public/assets/images/profiles/alex.jpg" 2 | import david from "../public/assets/images/profiles/david.jpg" 3 | import emily from "../public/assets/images/profiles/emily.jpg" 4 | import james from "../public/assets/images/profiles/james.jpg" 5 | import jennifer from "../public/assets/images/profiles/jennifer.jpg" 6 | import mark from "../public/assets/images/profiles/mark.jpg" 7 | import sarah from "../public/assets/images/profiles/sarah.jpg" 8 | 9 | export const testimonials = [ 10 | { 11 | id: "t01", 12 | name: "Sarah J.", 13 | imageUrl: sarah, 14 | comment: 15 | "Absolutely seamless experience! Booking was quick, and the car was in perfect condition. I'll definitely use this service again.", 16 | rating: 5, 17 | }, 18 | { 19 | id: "t02", 20 | name: "Mark T.", 21 | imageUrl: mark, 22 | comment: 23 | "Great selection of vehicles and very affordable rates. The customer service team was incredibly helpful when I needed to make changes to my reservation.", 24 | rating: 5, 25 | }, 26 | { 27 | id: "t03", 28 | name: "James L.", 29 | imageUrl: james, 30 | comment: 31 | "The navigation tools were a lifesaver! They made it so easy to explore the city without getting lost. Highly recommended!", 32 | rating: 5, 33 | }, 34 | { 35 | id: "t04", 36 | name: "Alex P.", 37 | imageUrl: alex, 38 | comment: 39 | "Fantastic service! The car was clean, well-maintained, and the pickup process was a breeze. I’ll be using this service for all my future trips.", 40 | rating: 5, 41 | }, 42 | { 43 | id: "t05", 44 | name: "David S.", 45 | imageUrl: david, 46 | comment: 47 | "Great value for money! The booking process was quick, and the customer support was responsive. Overall, a very positive experience.", 48 | rating: 5, 49 | }, 50 | { 51 | id: "t06", 52 | name: "Jennifer K.", 53 | imageUrl: jennifer, 54 | comment: 55 | "The rental experience was smooth and hassle-free. The car was in great condition, and the rates were competitive. Would definitely recommend.", 56 | rating: 4, 57 | }, 58 | { 59 | id: "t07", 60 | name: "Emily R.", 61 | imageUrl: emily, 62 | comment: 63 | "I was a bit nervous about renting a car, but the process was so easy and transparent. I felt very secure with their insurance coverage.", 64 | rating: 4, 65 | }, 66 | ] 67 | -------------------------------------------------------------------------------- /db/index.ts: -------------------------------------------------------------------------------- 1 | import { sql } from "@vercel/postgres" 2 | import { config } from "dotenv" 3 | import { drizzle } from "drizzle-orm/vercel-postgres" 4 | 5 | import * as schema from "./schema" 6 | 7 | config({ path: ".env" }) 8 | 9 | export const db = drizzle(sql, { schema }) 10 | -------------------------------------------------------------------------------- /db/migrate.ts: -------------------------------------------------------------------------------- 1 | import { migrate } from "drizzle-orm/vercel-postgres/migrator" 2 | 3 | import { db } from "." 4 | 5 | const main = async () => { 6 | try { 7 | await migrate(db, { 8 | migrationsFolder: "db/migrations", 9 | }) 10 | 11 | console.log("Migration successful") 12 | } catch (error) { 13 | console.error(error) 14 | process.exit(1) 15 | } 16 | } 17 | 18 | main() 19 | -------------------------------------------------------------------------------- /db/queries/car-repository.ts: -------------------------------------------------------------------------------- 1 | import { eq } from "drizzle-orm" 2 | 3 | import { db } from ".." 4 | import { SelectCar } from "../schema" 5 | 6 | export async function getCars() { 7 | return db.query.carsTable.findMany() 8 | } 9 | 10 | export async function getCarBySlug(slug: SelectCar["slug"]) { 11 | return db.query.carsTable.findFirst({ 12 | where: (fields, operators) => eq(fields.slug, slug), 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /db/queries/location-repository.ts: -------------------------------------------------------------------------------- 1 | import { db } from ".." 2 | 3 | export async function getLocations() { 4 | return db.query.locationsTable.findMany() 5 | } 6 | -------------------------------------------------------------------------------- /db/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | boolean, 3 | decimal, 4 | doublePrecision, 5 | integer, 6 | jsonb, 7 | pgTable, 8 | smallint, 9 | text, 10 | timestamp, 11 | } from "drizzle-orm/pg-core" 12 | 13 | export const locationsTable = pgTable("locations", { 14 | id: integer("id").primaryKey().generatedAlwaysAsIdentity(), 15 | slug: text("slug").notNull().unique(), 16 | name: text("name").notNull(), 17 | latitude: doublePrecision("latitude").notNull(), 18 | longitude: doublePrecision("longitude").notNull(), 19 | imageUrl: text("image_url"), 20 | featured: boolean("featured").default(false), 21 | createdAt: timestamp("created_at").notNull().defaultNow(), 22 | updatedAt: timestamp("updated_at") 23 | .notNull() 24 | .defaultNow() 25 | .$onUpdate(() => new Date()), 26 | }) 27 | 28 | export const carsTable = pgTable("cars", { 29 | id: integer("id").primaryKey().generatedAlwaysAsIdentity(), 30 | slug: text("slug").notNull().unique(), 31 | name: text("name").notNull(), 32 | bodyStyle: text("body_style").notNull(), 33 | powertrain: text("powertrain").notNull(), 34 | transmission: text("transmission").notNull(), 35 | seats: smallint("seats").notNull(), 36 | description: text("description").notNull(), 37 | features: text("features").array().notNull(), 38 | rating: decimal("rating", { precision: 2, scale: 1 }).notNull(), 39 | reviewCount: decimal("review_count", { precision: 10, scale: 0 }).notNull(), 40 | unlimitedMileage: boolean("unlimited_mileage").default(false), 41 | imageUrl: text("image_url").notNull(), 42 | pricePerDay: decimal("price_per_day", { 43 | precision: 10, 44 | scale: 2, 45 | }).notNull(), 46 | currency: text("currency").notNull().default("usd"), 47 | priceId: text("price_id").default(""), 48 | status: text("status").default("active"), 49 | metadata: jsonb("metadata"), 50 | createdAt: timestamp("created_at").notNull().defaultNow(), 51 | updatedAt: timestamp("updated_at") 52 | .notNull() 53 | .defaultNow() 54 | .$onUpdate(() => new Date()), 55 | }) 56 | 57 | export type InsertLocation = typeof locationsTable.$inferInsert 58 | export type SelectLocation = typeof locationsTable.$inferSelect 59 | 60 | export type InsertCar = typeof carsTable.$inferInsert 61 | export type SelectCar = typeof carsTable.$inferSelect 62 | -------------------------------------------------------------------------------- /db/seed.ts: -------------------------------------------------------------------------------- 1 | import { cars } from "@/data/cars" 2 | import { locations } from "@/data/locations" 3 | import { db } from "@/db" 4 | import { carsTable, locationsTable } from "@/db/schema" 5 | 6 | import { getCarImagePublicId } from "@/lib/cloudinary" 7 | 8 | const main = async () => { 9 | try { 10 | console.log("Seeding database") 11 | // Delete all data 12 | await db.delete(locationsTable) 13 | await db.delete(carsTable) 14 | 15 | for (const location of locations) { 16 | await db 17 | .insert(locationsTable) 18 | .values({ 19 | slug: location.slug, 20 | name: location.name, 21 | latitude: location.latitude, 22 | longitude: location.longitude, 23 | featured: location.featured, 24 | imageUrl: location.imageUrl, 25 | }) 26 | .onConflictDoNothing() 27 | } 28 | 29 | console.log("Locations seeded successfully.") 30 | 31 | for (const car of cars) { 32 | await db 33 | .insert(carsTable) 34 | .values({ 35 | slug: car.slug, 36 | name: car.name, 37 | description: car.description, 38 | imageUrl: getCarImagePublicId(car.bodyStyle) ?? "", 39 | status: car.status, 40 | pricePerDay: car.pricePerDay.toString(), 41 | currency: car.currency, 42 | bodyStyle: car.bodyStyle, 43 | powertrain: car.powertrain, 44 | transmission: car.transmission, 45 | seats: car.seats, 46 | priceId: car.priceId, 47 | features: car.features, 48 | rating: car.rating.toString(), 49 | reviewCount: car.reviewCount.toString(), 50 | unlimitedMileage: car.unlimitedMileage, 51 | metadata: car.metadata, 52 | }) 53 | .onConflictDoNothing() 54 | } 55 | 56 | console.log("Cars seeded successfully.") 57 | } catch (error) { 58 | console.error(error) 59 | process.exit(1) 60 | } 61 | } 62 | 63 | main() 64 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv" 2 | import { defineConfig } from "drizzle-kit" 3 | 4 | config({ path: ".env" }) 5 | 6 | export default defineConfig({ 7 | schema: "./db/schema.ts", 8 | out: "./db/migrations", 9 | dialect: "postgresql", 10 | dbCredentials: { 11 | url: process.env.POSTGRES_URL!, 12 | }, 13 | verbose: true, 14 | strict: true, 15 | }) 16 | -------------------------------------------------------------------------------- /hooks/use-debounce.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | /** 4 | * Custom hook to debounce a value. 5 | * @param value - The value to debounce. 6 | * @param delay - The delay in milliseconds to wait before updating the debounced value. 7 | * @returns The debounced value. 8 | */ 9 | export function useDebounce(value: T, delay: number): T { 10 | const [debouncedValue, setDebouncedValue] = useState(value) 11 | 12 | useEffect(() => { 13 | // Set up a timeout to update the debounced value after the specified delay 14 | const handler = setTimeout(() => { 15 | setDebouncedValue(value) 16 | }, delay) 17 | 18 | // Cleanup the timeout if the value or delay changes before the timeout completes 19 | return () => { 20 | clearTimeout(handler) 21 | } 22 | }, [value, delay]) 23 | 24 | return debouncedValue 25 | } 26 | -------------------------------------------------------------------------------- /hooks/use-media-query.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | 3 | export function useMediaQuery(query: string) { 4 | const [matches, setMatches] = useState(false) 5 | 6 | useEffect(() => { 7 | if (typeof window !== "undefined") { 8 | const media = window.matchMedia(query) 9 | if (media.matches !== matches) { 10 | setMatches(media.matches) 11 | } 12 | const listener = () => { 13 | setMatches(media.matches) 14 | } 15 | media.addEventListener("change", listener) 16 | return () => { 17 | media.removeEventListener("change", listener) 18 | } 19 | } 20 | }, [query]) 21 | 22 | return matches 23 | } 24 | -------------------------------------------------------------------------------- /lib/cloudinary.ts: -------------------------------------------------------------------------------- 1 | import { SelectCar } from "@/db/schema" 2 | 3 | export const getCarImagePublicId = (bodyStyle: SelectCar["bodyStyle"]) => { 4 | switch (bodyStyle) { 5 | case "hatchback": 6 | return "carhive/cars/hatchback_hawhtv" 7 | 8 | case "minivan": 9 | return "carhive/cars/minivan_vlkx4g" 10 | 11 | case "sedan": 12 | return "carhive/cars/sedan_rfl011" 13 | 14 | case "sports-car": 15 | return "carhive/cars/sports-car_hmxtaj" 16 | 17 | case "pickup-truck": 18 | return "carhive/cars/pickup-truck_ihwn41" 19 | 20 | case "suv": 21 | return "carhive/cars/suv_rdgyby" 22 | 23 | default: 24 | return null 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const DESKTOP_MEDIA_QUERY = "(min-width: 1024px)" 2 | 3 | // logo slider 4 | export const CAR_LOGO_WIDTH = "120px" 5 | export const CLONE_SETS_COUNT = 2 6 | 7 | // map 8 | export const MAP_INITIAL_ZOOM_LEVEL = 2 9 | export const MAP_LOCATION_ZOOM_LEVEL = 11 10 | -------------------------------------------------------------------------------- /lib/dates.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats a date string into a readable format: "Mon DD" 3 | * @param dateString - The ISO date string to format. 4 | * @returns A formatted date string in the format "Mon DD". 5 | */ 6 | function formatDateToShortMonthDay(dateString: string): string { 7 | const date = new Date(dateString) 8 | 9 | // Check for invalid date 10 | if (isNaN(date.getTime())) { 11 | throw new Error(`Invalid date string: ${dateString}`) 12 | } 13 | 14 | const month = date.toLocaleDateString("en-US", { month: "short" }) 15 | const day = date.getDate() 16 | 17 | return `${month} ${day}` 18 | } 19 | 20 | /** 21 | * Formats a date range (check-in to check-out) into a readable format. 22 | * @param checkinDate - The check-in date string in ISO format. 23 | * @param checkoutDate - The check-out date string in ISO format. 24 | * @returns A formatted date range string in the format "Mon DD – DD, YYYY" or "Mon DD, YYYY – Mon DD, YYYY". 25 | */ 26 | export function formatDateRangeForDisplay( 27 | checkinDate: string, 28 | checkoutDate: string 29 | ): string { 30 | const checkin = new Date(checkinDate) 31 | const checkout = new Date(checkoutDate) 32 | 33 | // Check for invalid dates 34 | if (isNaN(checkin.getTime()) || isNaN(checkout.getTime())) { 35 | throw new Error( 36 | `Invalid date string(s): checkin "${checkinDate}", checkout "${checkoutDate}"` 37 | ) 38 | } 39 | 40 | // Ensure check-in date is before or equal to check-out date 41 | if (checkin > checkout) { 42 | throw new Error("Check-in date cannot be after the check-out date.") 43 | } 44 | 45 | const formattedCheckin = formatDateToShortMonthDay(checkinDate) 46 | const formattedCheckout = checkout.getDate().toString() 47 | const year = checkin.getFullYear() 48 | 49 | // Check if both dates share the same month and year 50 | const sameMonthAndYear = 51 | checkin.getMonth() === checkout.getMonth() && 52 | checkin.getFullYear() === checkout.getFullYear() 53 | 54 | if (sameMonthAndYear) { 55 | return `${formattedCheckin} – ${formattedCheckout}, ${year}` 56 | } else { 57 | const formattedCheckinWithYear = `${formattedCheckin}, ${year}` 58 | const formattedCheckoutWithYear = 59 | formatDateToShortMonthDay(checkoutDate) + `, ${checkout.getFullYear()}` 60 | return `${formattedCheckinWithYear} – ${formattedCheckoutWithYear}` 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Inter as FontSans } from "next/font/google" 2 | 3 | export const fontSans = FontSans({ 4 | subsets: ["latin"], 5 | variable: "--font-inter", 6 | display: "swap", 7 | weight: "variable", 8 | }) 9 | -------------------------------------------------------------------------------- /lib/stripe/index.ts: -------------------------------------------------------------------------------- 1 | import "server-only" 2 | 3 | import Stripe from "stripe" 4 | 5 | import { siteConfig } from "@/config/site" 6 | 7 | export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY as string, { 8 | // https://github.com/stripe/stripe-node#configuration 9 | apiVersion: "2024-06-20", 10 | appInfo: { 11 | name: siteConfig.name, 12 | url: siteConfig.url, 13 | }, 14 | telemetry: false, 15 | }) 16 | -------------------------------------------------------------------------------- /lib/stripe/utils/get-stripejs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is a singleton to ensure we only instantiate Stripe once. 3 | */ 4 | import { loadStripe, Stripe } from "@stripe/stripe-js" 5 | 6 | let stripePromise: Promise 7 | 8 | export default function getStripe(): Promise { 9 | if (!stripePromise) 10 | stripePromise = loadStripe( 11 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string 12 | ) 13 | 14 | return stripePromise 15 | } 16 | -------------------------------------------------------------------------------- /lib/stripe/utils/stripe-helpers.ts: -------------------------------------------------------------------------------- 1 | export function formatAmountForStripe( 2 | amount: number, 3 | currency: string 4 | ): number { 5 | let numberFormat = new Intl.NumberFormat(["en-US"], { 6 | style: "currency", 7 | currency: currency, 8 | currencyDisplay: "symbol", 9 | }) 10 | const parts = numberFormat.formatToParts(amount) 11 | let zeroDecimalCurrency: boolean = true 12 | for (let part of parts) { 13 | if (part.type === "decimal") { 14 | zeroDecimalCurrency = false 15 | } 16 | } 17 | return zeroDecimalCurrency ? amount : Math.round(amount * 100) 18 | } 19 | -------------------------------------------------------------------------------- /lib/types.ts: -------------------------------------------------------------------------------- 1 | export enum SearchParams { 2 | CAR = "car", 3 | LOCATION = "location", 4 | LAT = "lat", 5 | LNG = "lng", 6 | CHECKIN = "checkin", 7 | CHECKOUT = "checkout", 8 | MIN_PRICE = "min-price", 9 | MAX_PRICE = "max-price", 10 | BODY_STYLE = "body-style", 11 | POWERTRAIN = "powertrain", 12 | MIN_SEATS = "min-seats", 13 | TRANSMISSION = "transmission", 14 | } 15 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { ReadonlyURLSearchParams } from "next/navigation" 2 | import clsx, { ClassValue } from "clsx" 3 | import { twMerge } from "tailwind-merge" 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)) 7 | } 8 | 9 | export const setCSSVariable = (name: string, value: string): void => { 10 | if (typeof window !== "undefined") { 11 | document.documentElement?.style.setProperty(name, value) 12 | } 13 | } 14 | 15 | export const slugify = (str: string): string => 16 | str 17 | .trim() // Trim leading/trailing whitespace 18 | .toLowerCase() 19 | .replace(/[^a-z0-9\s-]+/g, "") // Remove non-alphanumeric characters 20 | .replace(/\s+/g, "-") // Replace spaces with hyphens 21 | .replace(/-+/g, "-") // Replace multiple hyphens with a single one 22 | 23 | export const constructUrlWithParams = ( 24 | pathname: string, 25 | params: URLSearchParams | ReadonlyURLSearchParams 26 | ): string => { 27 | const queryString = params.toString() 28 | return queryString ? `${pathname}?${queryString}` : pathname 29 | } 30 | 31 | export function absoluteUrl(path: string) { 32 | return `${process.env.NEXT_PUBLIC_APP_URL}${path}` 33 | } 34 | 35 | export const convertImageUrlToDataUrl = async ( 36 | imageUrl: string 37 | ): Promise => { 38 | try { 39 | const response = await fetch(imageUrl) 40 | if (!response.ok) 41 | throw new Error(`Failed to fetch image: ${response.statusText}`) 42 | 43 | const arrayBuffer = await response.arrayBuffer() 44 | const base64 = Buffer.from(arrayBuffer).toString("base64") 45 | const contentType = 46 | response.headers.get("content-type") ?? "application/octet-stream" 47 | 48 | return `data:${contentType};base64,${base64}` 49 | } catch (error) { 50 | console.error("Error converting image URL to data URL:", error) 51 | return null 52 | } 53 | } 54 | 55 | export const formatAmountForDisplay = ( 56 | amount: number, 57 | currency: string, 58 | removeCents: boolean = false 59 | ): string => { 60 | if (isNaN(amount)) return "" 61 | let numberFormat = new Intl.NumberFormat(["en-US"], { 62 | style: "currency", 63 | currency: currency, 64 | currencyDisplay: "symbol", 65 | minimumFractionDigits: removeCents ? 0 : 2, 66 | maximumFractionDigits: removeCents ? 0 : 2, 67 | }) 68 | return numberFormat.format(amount) 69 | } 70 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { clerkMiddleware } from '@clerk/nextjs/server'; 2 | 3 | export default clerkMiddleware(); 4 | 5 | export const config = { 6 | matcher: [ 7 | // Skip Next.js internals and all static files, unless found in search params 8 | '/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)', 9 | // Always run for API routes 10 | '/(api|trpc)(.*)', 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | images: { 6 | formats: ["image/avif", "image/webp"], 7 | remotePatterns: [ 8 | { 9 | protocol: "https", 10 | hostname: "carhive.eduam.dev", 11 | }, 12 | { 13 | protocol: "https", 14 | hostname: "carhive.vercel.app", 15 | }, 16 | { 17 | protocol: "https", 18 | hostname: "res.cloudinary.com", 19 | }, 20 | ], 21 | }, 22 | } 23 | 24 | module.exports = nextConfig 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "carhive", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbo", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "lint:fix": "next lint --fix", 11 | "db:generate": "drizzle-kit generate", 12 | "db:migrate": "tsx --tsconfig ./tsconfig.scripts.json ./db/migrate.ts", 13 | "db:push": "drizzle-kit push", 14 | "db:seed": "tsx --tsconfig ./tsconfig.scripts.json ./db/seed.ts" 15 | }, 16 | "dependencies": { 17 | "@clerk/nextjs": "^5.3.3", 18 | "@radix-ui/react-dialog": "^1.1.1", 19 | "@radix-ui/react-dropdown-menu": "^2.1.1", 20 | "@radix-ui/react-label": "^2.1.0", 21 | "@radix-ui/react-popover": "^1.1.1", 22 | "@radix-ui/react-separator": "^1.1.0", 23 | "@radix-ui/react-slider": "^1.2.0", 24 | "@radix-ui/react-slot": "^1.1.0", 25 | "@radix-ui/react-toast": "^1.2.1", 26 | "@radix-ui/react-toggle": "^1.1.0", 27 | "@radix-ui/react-toggle-group": "^1.1.0", 28 | "@stripe/react-stripe-js": "^2.8.0", 29 | "@stripe/stripe-js": "^4.4.0", 30 | "@vercel/postgres": "^0.10.0", 31 | "class-variance-authority": "^0.7.0", 32 | "clsx": "^2.1.1", 33 | "cmdk": "^1.0.0", 34 | "date-fns": "^2.30.0", 35 | "drizzle-orm": "^0.33.0", 36 | "embla-carousel-react": "^8.2.0", 37 | "eslint": "^8.57.0", 38 | "leaflet": "^1.9.4", 39 | "next": "^14.2.6", 40 | "next-cloudinary": "^5.20.0", 41 | "react": "^18.3.1", 42 | "react-day-picker": "^8.10.1", 43 | "react-dom": "^18.3.1", 44 | "react-leaflet": "^4.2.1", 45 | "server-only": "^0.0.1", 46 | "sharp": "^0.32.6", 47 | "stripe": "^16.9.0", 48 | "tailwind-merge": "^2.5.2", 49 | "tailwindcss-animate": "^1.0.7", 50 | "typescript": "5.2.2", 51 | "vaul": "^0.9.1" 52 | }, 53 | "devDependencies": { 54 | "@ianvs/prettier-plugin-sort-imports": "^4.3.1", 55 | "@types/leaflet": "^1.9.12", 56 | "@types/node": "^20.16.1", 57 | "@types/react": "18.3.3", 58 | "@types/react-dom": "^18.3.0", 59 | "@typescript-eslint/parser": "^8.3.0", 60 | "autoprefixer": "^10.4.20", 61 | "dotenv": "^16.4.5", 62 | "drizzle-kit": "^0.24.2", 63 | "eslint-config-next": "14.2.5", 64 | "eslint-config-prettier": "^9.1.0", 65 | "eslint-plugin-tailwindcss": "^3.17.4", 66 | "postcss": "^8.4.41", 67 | "prettier": "^3.3.3", 68 | "prettier-plugin-tailwindcss": "^0.5.14", 69 | "tailwindcss": "^3.4.10", 70 | "tsx": "^4.19.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | endOfLine: "lf", 4 | semi: false, 5 | singleQuote: false, 6 | tabWidth: 2, 7 | trailingComma: "es5", 8 | importOrder: [ 9 | "^(react/(.*)$)|^(react$)", 10 | "^(next/(.*)$)|^(next$)", 11 | "", 12 | "", 13 | "^types$", 14 | "^@/types/(.*)$", 15 | "^@/config/(.*)$", 16 | "^@/lib/(.*)$", 17 | "^@/hooks/(.*)$", 18 | "^@/components/ui/(.*)$", 19 | "^@/components/(.*)$", 20 | "^@/registry/(.*)$", 21 | "^@/styles/(.*)$", 22 | "^@/app/(.*)$", 23 | "", 24 | "^[./]", 25 | ], 26 | importOrderSeparation: false, 27 | importOrderSortSpecifiers: true, 28 | importOrderBuiltinModulesToTop: true, 29 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"], 30 | importOrderMergeDuplicateImports: true, 31 | importOrderCombineTypeAndValueImports: true, 32 | plugins: ["@ianvs/prettier-plugin-sort-imports"], 33 | }; 34 | -------------------------------------------------------------------------------- /public/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/apple-icon.png -------------------------------------------------------------------------------- /public/assets/images/cars/hatchback.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/cars/hatchback.jpg -------------------------------------------------------------------------------- /public/assets/images/cars/minivan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/cars/minivan.jpg -------------------------------------------------------------------------------- /public/assets/images/cars/pickup-truck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/cars/pickup-truck.jpg -------------------------------------------------------------------------------- /public/assets/images/cars/sedan.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/cars/sedan.jpg -------------------------------------------------------------------------------- /public/assets/images/cars/sports-car.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/cars/sports-car.jpg -------------------------------------------------------------------------------- /public/assets/images/cars/suv.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/cars/suv.jpg -------------------------------------------------------------------------------- /public/assets/images/locations/cancun.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/locations/cancun.jpg -------------------------------------------------------------------------------- /public/assets/images/locations/dubai.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/locations/dubai.jpg -------------------------------------------------------------------------------- /public/assets/images/locations/paris.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/locations/paris.jpg -------------------------------------------------------------------------------- /public/assets/images/locations/rio.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/locations/rio.jpg -------------------------------------------------------------------------------- /public/assets/images/locations/rome.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/locations/rome.jpg -------------------------------------------------------------------------------- /public/assets/images/locations/sydney.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/locations/sydney.jpg -------------------------------------------------------------------------------- /public/assets/images/profiles/alex.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/profiles/alex.jpg -------------------------------------------------------------------------------- /public/assets/images/profiles/david.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/profiles/david.jpg -------------------------------------------------------------------------------- /public/assets/images/profiles/emily.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/profiles/emily.jpg -------------------------------------------------------------------------------- /public/assets/images/profiles/james.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/profiles/james.jpg -------------------------------------------------------------------------------- /public/assets/images/profiles/jennifer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/profiles/jennifer.jpg -------------------------------------------------------------------------------- /public/assets/images/profiles/mark.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/profiles/mark.jpg -------------------------------------------------------------------------------- /public/assets/images/profiles/sarah.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/assets/images/profiles/sarah.jpg -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/favicon.ico -------------------------------------------------------------------------------- /public/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/icon-192.png -------------------------------------------------------------------------------- /public/icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eduamdev/carhive/3864e53ef1ab9068fd0230e54ccedccbeff1a6e4/public/icon-512.png -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --font-sans: var(--font-inter); 8 | --font-sans-fallback: "InterVariable", "Inter", -apple-system, 9 | BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", 10 | "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 11 | 12 | --site-header-height: 60px; 13 | 14 | --search-panel-height: 68px; 15 | } 16 | 17 | @media (prefers-reduced-motion: no-preference) { 18 | html { 19 | scroll-behavior: smooth; 20 | } 21 | } 22 | 23 | html { 24 | color-scheme: light; 25 | /* Enable panning and pinch zoom gestures, but disable additional non-standard gestures such as double-tap to zoom */ 26 | touch-action: manipulation; 27 | } 28 | 29 | html, 30 | body { 31 | background-color: white; 32 | font-family: var(--font-sans), var(--font-sans-fallback); 33 | font-synthesis-weight: none; 34 | font-feature-settings: 35 | "liga" 1, 36 | "calt" 1; 37 | text-rendering: optimizeLegibility; 38 | text-size-adjust: 100%; 39 | -webkit-font-smoothing: antialiased; 40 | -moz-osx-font-smoothing: grayscale; 41 | 42 | user-select: text; 43 | overflow-x: clip; 44 | } 45 | body { 46 | overscroll-behavior-y: contain; 47 | margin: 0; 48 | font-size: 100%; 49 | min-height: 100%; 50 | min-width: 360px; 51 | max-width: 100vw; 52 | } 53 | 54 | a { 55 | transition-delay: 0s; 56 | transition-duration: 0.2s; 57 | transition-property: all; 58 | transition-timing-function: ease; 59 | } 60 | 61 | strong { 62 | font-weight: 600; 63 | color: theme("colors.neutral.900"); 64 | } 65 | 66 | /* https://bugs.webkit.org/show_bug.cgi?id=243601 67 | https://nextjs.org/docs/pages/api-reference/components/image#known-browser-bugs 68 | Safari 15 and 16 display a gray border while loading */ 69 | @supports (font: -apple-system-body) and (-webkit-appearance: none) { 70 | img[loading="lazy"] { 71 | clip-path: inset(0.6px); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | future: { 4 | hoverOnlyWhenSupported: true, 5 | }, 6 | content: [ 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 9 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 10 | ], 11 | theme: { 12 | extend: { 13 | screens: { 14 | sm: "550px", 15 | md: "950px", 16 | lg: "1128px", 17 | }, 18 | fontFamily: { 19 | sans: ["var(--font-sans)"], 20 | }, 21 | maxWidth: { 22 | "8xl": "86rem", 23 | }, 24 | keyframes: { 25 | slider: { 26 | "0%": { transform: "translateX(0)" }, 27 | "100%": { 28 | transform: "translateX(calc(-100% / var(--slider-total-clones)))", 29 | }, 30 | }, 31 | "dot-pulse": { 32 | "0%,100%": { 33 | transform: "Scale(1)", 34 | opacity: 1, 35 | }, 36 | "50%": { 37 | transform: "scale(1.25)", 38 | opacity: 0.3, 39 | }, 40 | }, 41 | }, 42 | animation: { 43 | slider: "slider 56s linear infinite", 44 | "dot-pulse": "dot-pulse 1s infinite ease-in-out", 45 | }, 46 | }, 47 | }, 48 | plugins: [require("tailwindcss-animate")], 49 | } 50 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "lib": [ 6 | "dom", 7 | "dom.iterable", 8 | "esnext" 9 | ], 10 | "allowJs": true, 11 | "skipLibCheck": true, 12 | "strict": true, 13 | "forceConsistentCasingInFileNames": true, 14 | "noEmit": true, 15 | "incremental": true, 16 | "esModuleInterop": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "jsx": "preserve", 22 | "baseUrl": ".", 23 | "plugins": [ 24 | { 25 | "name": "next" 26 | } 27 | ], 28 | "paths": { 29 | "@/*": [ 30 | "./*" 31 | ] 32 | } 33 | }, 34 | "include": [ 35 | "next-env.d.ts", 36 | ".next/types/**/*.ts", 37 | "**/*.ts", 38 | "**/*.tsx", 39 | "data/*.js", 40 | ], 41 | "exclude": [ 42 | "node_modules" 43 | ] 44 | } -------------------------------------------------------------------------------- /tsconfig.scripts.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "target": "es6", 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "isolatedModules": false 10 | }, 11 | "include": [ 12 | "scripts/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } --------------------------------------------------------------------------------