├── app
├── favicon.ico
├── favicon-16x16.png
├── favicon-32x32.png
├── apple-touch-icon.png
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── layout.tsx
├── page.tsx
└── globals.css
├── public
├── hero.png
├── hero2.png
├── hero-bg.png
├── pattern.png
├── model-icon.png
├── chevron-up-down.svg
├── arrow-down.svg
├── close.svg
├── heart-outline.svg
├── heart-filled.svg
├── right-arrow.svg
├── vercel.svg
├── linkedin.svg
├── facebook.svg
├── discord.svg
├── magnifying-glass.svg
├── car-logo.svg
├── github.svg
├── tire.svg
├── steering-wheel.svg
├── twitter.svg
├── gas.svg
├── next.svg
└── logo.svg
├── postcss.config.js
├── .idea
├── .gitignore
├── vcs.xml
├── modules.xml
└── car-showcase-project.iml
├── .vscode
└── settings.json
├── next.config.js
├── .gitignore
├── components
├── index.ts
├── CustomButton.tsx
├── Navbar.tsx
├── ShowMore.tsx
├── Hero.tsx
├── Footer.tsx
├── CarCard.tsx
├── CustomFilter.tsx
├── SearchBar.tsx
├── SearchManufacturer.tsx
└── CarDetails.tsx
├── package.json
├── tsconfig.json
├── tailwind.config.js
├── types
└── index.ts
├── README.md
├── constants.ts
└── utils
└── index.ts
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexisnovas/autoverse-cars-showcase/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/public/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexisnovas/autoverse-cars-showcase/HEAD/public/hero.png
--------------------------------------------------------------------------------
/public/hero2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexisnovas/autoverse-cars-showcase/HEAD/public/hero2.png
--------------------------------------------------------------------------------
/public/hero-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexisnovas/autoverse-cars-showcase/HEAD/public/hero-bg.png
--------------------------------------------------------------------------------
/public/pattern.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexisnovas/autoverse-cars-showcase/HEAD/public/pattern.png
--------------------------------------------------------------------------------
/app/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexisnovas/autoverse-cars-showcase/HEAD/app/favicon-16x16.png
--------------------------------------------------------------------------------
/app/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexisnovas/autoverse-cars-showcase/HEAD/app/favicon-32x32.png
--------------------------------------------------------------------------------
/public/model-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexisnovas/autoverse-cars-showcase/HEAD/public/model-icon.png
--------------------------------------------------------------------------------
/app/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexisnovas/autoverse-cars-showcase/HEAD/app/apple-touch-icon.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/app/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexisnovas/autoverse-cars-showcase/HEAD/app/android-chrome-192x192.png
--------------------------------------------------------------------------------
/app/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexisnovas/autoverse-cars-showcase/HEAD/app/android-chrome-512x512.png
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules\\typescript\\lib",
3 | "typescript.enablePromptUseWorkspaceTsdk": true
4 | }
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/chevron-up-down.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/public/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: ["cdn.imagin.studio"]
5 | },
6 | typescript: {
7 | ignoreBuildErrors: true
8 | },
9 | experimental: {
10 | appDir: true
11 | }
12 | }
13 |
14 | module.exports = nextConfig
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/public/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/public/heart-outline.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/public/heart-filled.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/.idea/car-showcase-project.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.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
37 |
--------------------------------------------------------------------------------
/public/right-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './globals.css'
2 | import type { Metadata } from 'next'
3 | import { Navbar, Footer} from "@/components";
4 |
5 |
6 | export const metadata: Metadata = {
7 | title: 'AutoVerse',
8 | description: 'Showcasing the most amazing cars in the world!',
9 | }
10 |
11 | export default function RootLayout({
12 | children,
13 | }: {
14 | children: React.ReactNode
15 | }) {
16 | return (
17 |
18 |
19 |
20 | {children}
21 |
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/components/index.ts:
--------------------------------------------------------------------------------
1 | import Hero from './Hero'
2 | import CustomButton from './CustomButton'
3 | import Navbar from "./Navbar";
4 | import Footer from "./Footer";
5 | import CarCard from "./CarCard";
6 | import ShowMore from "./ShowMore";
7 | import SearchBar from "./SearchBar";
8 | import CustomFilter from "./CustomFilter";
9 | import SearchManufacturer from "./SearchManufacturer";
10 |
11 | export {
12 | Hero,
13 | CustomButton,
14 | Navbar,
15 | Footer,
16 | CarCard,
17 | ShowMore,
18 | SearchBar,
19 | CustomFilter,
20 | SearchManufacturer
21 | }
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/linkedin.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "car-showcase-project",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@headlessui/react": "^1.7.15",
13 | "@types/node": "20.4.1",
14 | "@types/react": "18.2.14",
15 | "@types/react-dom": "18.2.6",
16 | "autoprefixer": "10.4.14",
17 | "next": "13.2.4",
18 | "postcss": "8.4.25",
19 | "react": "18.2.0",
20 | "react-dom": "18.2.0",
21 | "tailwindcss": "3.3.2",
22 | "typescript": "5.1.6"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/public/facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/public/discord.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/magnifying-glass.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "incremental": true,
17 | "plugins": [
18 | {
19 | "name": "next"
20 | }
21 | ],
22 | "paths": {
23 | "@/*": ["./*"]
24 | }
25 | },
26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
27 | "exclude": ["node_modules"]
28 | }
29 |
--------------------------------------------------------------------------------
/public/car-logo.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/github.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/tire.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/components/CustomButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { CustomButtonProps } from "../types";
4 | import Image from "next/image";
5 |
6 |
7 | const Button = ({ isDisabled, btnType, containerStyles, textStyles, title, rightIcon, handleClick }: CustomButtonProps) => (
8 |
14 | {title}
15 | {rightIcon && (
16 |
17 |
23 |
24 | )}
25 |
26 | );
27 |
28 | export default Button;
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
5 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
7 | ],
8 | mode: "jit",
9 | theme: {
10 | extend: {
11 | fontFamily: {
12 | inter: ["Inter", "sans-serif"],
13 | },
14 | colors: {
15 | "black-100": "#2B2C35",
16 | "primary-blue": {
17 | DEFAULT: "#2B59FF",
18 | 100: "#F5F8FF",
19 | },
20 | "secondary-orange": "#f79761",
21 | "light-white": {
22 | DEFAULT: "rgba(59,60,152,0.03)",
23 | 100: "rgba(59,60,152,0.02)",
24 | },
25 | grey: "#747A88",
26 | },
27 | backgroundImage: {
28 | 'pattern': "url('/pattern.png')",
29 | 'hero-bg': "url('/hero-bg.png')"
30 | }
31 | },
32 | },
33 | plugins: [],
34 | };
--------------------------------------------------------------------------------
/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link";
2 | import Image from "next/image";
3 |
4 | import CustomButton from "./CustomButton";
5 |
6 | const NavBar = () => (
7 |
8 |
9 |
10 |
17 |
18 |
19 |
24 |
25 |
26 | );
27 |
28 | export default NavBar;
--------------------------------------------------------------------------------
/public/steering-wheel.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/public/twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/components/ShowMore.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useRouter } from "next/navigation";
4 |
5 | import { ShowMoreProps } from "@/types";
6 | import { updateSearchParams } from "@/utils";
7 | import { CustomButton } from "@/components";
8 |
9 | const ShowMore = ({ pageNumber, isNext }: ShowMoreProps) => {
10 | const router = useRouter();
11 |
12 | const handleNavigation = () => {
13 | // Calculate the new limit based on the page number and navigation type
14 | const newLimit = (pageNumber + 1) * 10;
15 |
16 | // Update the "limit" search parameter in the URL with the new value
17 | const newPathname = updateSearchParams("limit", `${newLimit}`);
18 |
19 | router.push(newPathname);
20 | };
21 |
22 | return (
23 |
24 | {!isNext && (
25 |
31 | )}
32 |
33 | );
34 | };
35 |
36 | export default ShowMore;
--------------------------------------------------------------------------------
/public/gas.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/components/Hero.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import CustomButton from "./CustomButton";
5 |
6 | const Hero = () => {
7 | const handleScroll = () => {
8 | const nextSection = document.getElementById("discover");
9 |
10 | if (nextSection) {
11 | nextSection.scrollIntoView({ behavior: "smooth" });
12 | }
13 | };
14 |
15 | return (
16 |
17 |
18 |
19 | Find, book, rent a car—quick and super easy!
20 |
21 |
22 |
23 | Streamline your car rental experience with our effortless booking
24 | process.
25 |
26 |
27 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | );
42 | };
43 |
44 | export default Hero;
--------------------------------------------------------------------------------
/types/index.ts:
--------------------------------------------------------------------------------
1 | import { MouseEventHandler } from "react";
2 |
3 | export interface CarProps {
4 | city_mpg: number;
5 | class: string;
6 | combination_mpg: number;
7 | cylinders: number;
8 | displacement: number;
9 | drive: string;
10 | fuel_type: string;
11 | highway_mpg: number;
12 | make: string;
13 | model: string;
14 | transmission: string;
15 | year: number;
16 | }
17 |
18 | export interface FilterProps {
19 | manufacturer?: string;
20 | year?: number;
21 | model?: string;
22 | limit?: number;
23 | fuel?: string;
24 | }
25 |
26 | export interface HomeProps {
27 | searchParams: FilterProps;
28 | }
29 |
30 | export interface CarCardProps {
31 | model: string;
32 | make: string;
33 | mpg: number;
34 | transmission: string;
35 | year: number;
36 | drive: string;
37 | cityMPG: number;
38 | }
39 |
40 | export interface CustomButtonProps {
41 | isDisabled?: boolean;
42 | btnType?: "button" | "submit";
43 | containerStyles?: string;
44 | textStyles?: string;
45 | title: string;
46 | rightIcon?: string;
47 | handleClick?: MouseEventHandler;
48 | }
49 |
50 | export interface OptionProps {
51 | title: string;
52 | value: string;
53 | }
54 |
55 | export interface CustomFilterProps {
56 | title: string;
57 | options: OptionProps[];
58 | }
59 |
60 | export interface ShowMoreProps {
61 | pageNumber: number;
62 | isNext: boolean;
63 | }
64 |
65 | export interface SearchManuFacturerProps {
66 | manufacturer: string;
67 | setManuFacturer: (manufacturer: string) => void;
68 | }
--------------------------------------------------------------------------------
/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import Link from "next/link";
3 |
4 | import { footerLinks } from "@/constants";
5 |
6 | const Footer = () => (
7 |
50 | );
51 |
52 | export default Footer;
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { fetchCars } from "@/utils";
2 | import type { HomeProps } from "@/types";
3 | import { fuels, yearsOfProduction } from "@/constants";
4 | import { CarCard, ShowMore, SearchBar, CustomFilter, Hero } from "@/components";
5 |
6 | export default async function Home({ searchParams }: HomeProps) {
7 | const allCars = await fetchCars({
8 | manufacturer: searchParams.manufacturer || "",
9 | year: searchParams.year || 2022,
10 | fuel: searchParams.fuel || "",
11 | limit: searchParams.limit || 10,
12 | model: searchParams.model || "",
13 | });
14 |
15 | const isDataEmpty = !Array.isArray(allCars) || allCars.length < 1 || !allCars;
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
Car Catalogue
24 |
Explore out cars you might like
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {!isDataEmpty ? (
37 |
38 |
39 | {allCars?.map((car) => (
40 |
41 | ))}
42 |
43 |
44 | allCars.length}
47 | />
48 |
49 | ) : (
50 |
51 |
Oops, no results
52 |
{allCars?.message}
53 |
54 | )}
55 |
56 |
57 | );
58 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Cars Showcase Web Project
2 |
3 | This is AutoVerse! A Cars Showcase web project developed using React, NextJS 13, Typescript, and Tailwind CSS. The website aims to provide an immersive experience for users interested in exploring various car models.
4 |
5 | ## Quick Demo
6 | 
7 |
8 | ## Features
9 |
10 | 1. **Hero Section**: The website features a visually stunning hero section that immediately grabs the attention of visitors, showcasing the most captivating cars in a captivating manner.
11 |
12 | 2. **Car Catalogue**: The project includes a comprehensive car catalogue where users can browse through a wide range of car models. The catalogue allows users to filter cars based on various criteria, including maker, model, fuel type, and year.
13 |
14 | 3. **Filtering Functionality**: Users can conveniently filter the car catalogue by selecting specific options for makers, models, fuel types, and years. This enables them to narrow down their search and find the exact car they are looking for.
15 |
16 | 4. **Car Details**: Clicking on a car card in the catalogue provides users with more detailed information about the selected car. Users can explore the manufacturer's details and view pictures of the car from different angles, gaining a comprehensive understanding of its design and features.
17 |
18 | ## Technologies Used
19 |
20 | - **React**: The project is built using the React library, which provides a robust foundation for developing interactive user interfaces.
21 |
22 | - **NextJS 13**: NextJS, a popular React framework, is employed to enhance the project's performance, scalability, and SEO capabilities.
23 |
24 | - **Typescript**: The use of TypeScript ensures the project's codebase is strongly typed, leading to improved maintainability and fewer runtime errors.
25 |
26 | - **Tailwind CSS**: The project utilizes Tailwind CSS, a utility-first CSS framework, to facilitate rapid UI development and enable responsive and visually appealing designs.
27 |
28 | ## Installation
29 |
30 | To set up the project locally, follow these steps:
31 |
32 | 1. Clone the repository:
33 |
34 | ```bash
35 | git clone https://github.com/your-username/your-repo.git
36 |
37 | 2. Install the dependencies:
38 |
39 | ```bash
40 | cd your-repo
41 | npm install
42 |
43 | 3. Start the development server:
44 | ```bash
45 | npm run dev
46 |
47 | This command will launch the project on a local development server.
48 |
--------------------------------------------------------------------------------
/constants.ts:
--------------------------------------------------------------------------------
1 | export const manufacturers = [
2 | "Acura",
3 | "Alfa Romeo",
4 | "Aston Martin",
5 | "Audi",
6 | "Bentley",
7 | "BMW",
8 | "Buick",
9 | "Cadillac",
10 | "Chevrolet",
11 | "Chrysler",
12 | "Citroen",
13 | "Dodge",
14 | "Ferrari",
15 | "Fiat",
16 | "Ford",
17 | "GMC",
18 | "Honda",
19 | "Hyundai",
20 | "Infiniti",
21 | "Jaguar",
22 | "Jeep",
23 | "Kia",
24 | "Lamborghini",
25 | "Land Rover",
26 | "Lexus",
27 | "Lincoln",
28 | "Maserati",
29 | "Mazda",
30 | "McLaren",
31 | "Mercedes-Benz",
32 | "MINI",
33 | "Mitsubishi",
34 | "Nissan",
35 | "Porsche",
36 | "Ram",
37 | "Rolls-Royce",
38 | "Subaru",
39 | "Tesla",
40 | "Toyota",
41 | "Volkswagen",
42 | "Volvo",
43 | ];
44 |
45 | export const yearsOfProduction = [
46 | { title: "Year", value: "" },
47 | { title: "2015", value: "2015" },
48 | { title: "2016", value: "2016" },
49 | { title: "2017", value: "2017" },
50 | { title: "2018", value: "2018" },
51 | { title: "2019", value: "2019" },
52 | { title: "2020", value: "2020" },
53 | { title: "2021", value: "2021" },
54 | { title: "2022", value: "2022" },
55 | { title: "2023", value: "2023" },
56 | ];
57 |
58 | export const fuels = [
59 | {
60 | title: "Fuel",
61 | value: "",
62 | },
63 | {
64 | title: "Gas",
65 | value: "Gas",
66 | },
67 | {
68 | title: "Electricity",
69 | value: "Electricity",
70 | },
71 | ];
72 |
73 | export const footerLinks = [
74 | {
75 | title: "About",
76 | links: [
77 | { title: "How it works", url: "/" },
78 | { title: "Featured", url: "/" },
79 | { title: "Partnership", url: "/" },
80 | { title: "Bussiness Relation", url: "/" },
81 | ],
82 | },
83 | {
84 | title: "Company",
85 | links: [
86 | { title: "Events", url: "/" },
87 | { title: "Blog", url: "/" },
88 | { title: "Podcast", url: "/" },
89 | { title: "Invite a friend", url: "/" },
90 | ],
91 | },
92 | {
93 | title: "Socials",
94 | links: [
95 | { title: "Discord", url: "/" },
96 | { title: "Instagram", url: "/" },
97 | { title: "Twitter", url: "/" },
98 | { title: "Facebook", url: "/" },
99 | ],
100 | },
101 | ];
--------------------------------------------------------------------------------
/utils/index.ts:
--------------------------------------------------------------------------------
1 | import { CarProps, FilterProps } from "../types";
2 |
3 | export const calculateCarRent = (city_mpg: number, year: number) => {
4 | const basePricePerDay = 50; // Base rental price per day in dollars
5 | const mileageFactor = 0.1; // Additional rate per mile driven
6 | const ageFactor = 0.05; // Additional rate per year of vehicle age
7 |
8 | // Calculate additional rate based on mileage and age
9 | const mileageRate = city_mpg * mileageFactor;
10 | const ageRate = (new Date().getFullYear() - year) * ageFactor;
11 |
12 | // Calculate total rental rate per day
13 | const rentalRatePerDay = basePricePerDay + mileageRate + ageRate;
14 |
15 | return rentalRatePerDay.toFixed(0);
16 | };
17 |
18 | export const updateSearchParams = (type: string, value: string) => {
19 | // Get the current URL search params
20 | const searchParams = new URLSearchParams(window.location.search);
21 |
22 | // Set the specified search parameter to the given value
23 | searchParams.set(type, value);
24 |
25 | // Set the specified search parameter to the given value
26 | const newPathname = `${window.location.pathname}?${searchParams.toString()}`;
27 |
28 | return newPathname;
29 | };
30 |
31 | export const deleteSearchParams = (type: string) => {
32 | // Set the specified search parameter to the given value
33 | const newSearchParams = new URLSearchParams(window.location.search);
34 |
35 | // Delete the specified search parameter
36 | newSearchParams.delete(type.toLocaleLowerCase());
37 |
38 | // Construct the updated URL pathname with the deleted search parameter
39 | const newPathname = `${window.location.pathname}?${newSearchParams.toString()}`;
40 |
41 | return newPathname;
42 | };
43 |
44 | export async function fetchCars(filters: FilterProps) {
45 | const { manufacturer, year, model, limit, fuel } = filters;
46 |
47 | // Set the required headers for the API request
48 | const headers: HeadersInit = {
49 | "X-RapidAPI-Key": process.env.NEXT_PUBLIC_RAPID_API_KEY || "",
50 | "X-RapidAPI-Host": "cars-by-api-ninjas.p.rapidapi.com",
51 | };
52 |
53 | // Set the required headers for the API request
54 | const response = await fetch(
55 | `https://cars-by-api-ninjas.p.rapidapi.com/v1/cars?make=${manufacturer}&year=${year}&model=${model}&limit=${limit}&fuel_type=${fuel}`,
56 | {
57 | headers: headers,
58 | }
59 | );
60 |
61 | // Parse the response as JSON
62 | const result = await response.json();
63 |
64 | return result;
65 | }
66 |
67 | export const generateCarImageUrl = (car: CarProps, angle?: string) => {
68 | const url = new URL("https://cdn.imagin.studio/getimage");
69 | const { make, model, year } = car;
70 |
71 | url.searchParams.append('customer', process.env.NEXT_PUBLIC_IMAGIN_API_KEY || '');
72 | url.searchParams.append('make', make);
73 | url.searchParams.append('modelFamily', model.split(" ")[0]);
74 | url.searchParams.append('zoomType', 'fullscreen');
75 | url.searchParams.append('modelYear', `${year}`);
76 | // url.searchParams.append('zoomLevel', zoomLevel);
77 | url.searchParams.append('angle', `${angle}`);
78 |
79 | return `${url}`;
80 | }
--------------------------------------------------------------------------------
/components/CarCard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import Image from "next/image";
5 |
6 | import type { CarProps } from "@/types";
7 | import { calculateCarRent, generateCarImageUrl } from "@/utils";
8 | import CustomButton from "./CustomButton";
9 | import CarDetails from "./CarDetails";
10 |
11 | interface CarCardProps {
12 | car: CarProps;
13 | }
14 |
15 | const CarCard = ({ car }: CarCardProps) => {
16 | const { city_mpg, year, make, model, transmission, drive } = car;
17 |
18 | const [isOpen, setIsOpen] = useState(false);
19 |
20 | const carRent = calculateCarRent(city_mpg, year);
21 |
22 | return (
23 |
24 |
25 |
26 | {make} {model}
27 |
28 |
29 |
30 |
31 | $
32 | {carRent}
33 | /day
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {transmission === "a" ? "Automatic" : "Manual"}
46 |
47 |
48 |
49 |
50 |
{drive.toUpperCase()}
51 |
52 |
53 |
54 |
{city_mpg} MPG
55 |
56 |
57 |
58 |
59 | setIsOpen(true)}
65 | />
66 |
67 |
68 |
69 |
setIsOpen(false)} car={car} />
70 |
71 | );
72 | };
73 |
74 | export default CarCard;
--------------------------------------------------------------------------------
/components/CustomFilter.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { Fragment, useState } from "react";
4 | import Image from "next/image";
5 | import { useRouter } from "next/navigation";
6 | import { Listbox, Transition } from "@headlessui/react";
7 |
8 | import { CustomFilterProps } from "@/types";
9 | import { updateSearchParams } from "@/utils";
10 |
11 | export default function CustomFilter({ title, options }: CustomFilterProps) {
12 | const router = useRouter();
13 | const [selected, setSelected] = useState(options[0]); // State for storing the selected option
14 |
15 | // update the URL search parameters and navigate to the new URL
16 | const handleUpdateParams = (e: { title: string; value: string }) => {
17 | const newPathName = updateSearchParams(title, e.value.toLowerCase());
18 |
19 | router.push(newPathName);
20 | };
21 |
22 | return (
23 |
24 |
{
27 | setSelected(e); // Update the selected option in state
28 | handleUpdateParams(e); // Update the URL search parameters and navigate to the new URL
29 | }}
30 | >
31 |
32 | {/* Button for the listbox */}
33 |
34 | {selected.title}
35 |
36 |
37 | {/* Transition for displaying the options */}
38 | >
40 | leave='transition ease-in duration-100'
41 | leaveFrom='opacity-100'
42 | leaveTo='opacity-0'
43 | >
44 |
45 | {/* Map over the options and display them as listbox options */}
46 | {options.map((option) => (
47 |
50 | `relative cursor-default select-none py-2 px-4 ${
51 | active ? "bg-primary-blue text-white" : "text-gray-900"
52 | }`
53 | }
54 | value={option}
55 | >
56 | {({ selected }) => (
57 | <>
58 |
59 | {option.title}
60 |
61 | >
62 | )}
63 |
64 | ))}
65 |
66 |
67 |
68 |
69 |
70 | );
71 | }
--------------------------------------------------------------------------------
/components/SearchBar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import React, { useState } from "react";
5 | import { useRouter } from "next/navigation";
6 | import SearchManufacturer from "./SearchManufacturer";
7 |
8 | const SEARCH_BAR_ID = 'searchbar-id'
9 |
10 | const SearchButton = ({ otherClasses }: { otherClasses: string }) => (
11 |
12 |
19 |
20 | );
21 |
22 | const SearchBar = () => {
23 | const [manufacturer, setManuFacturer] = useState("");
24 | const [model, setModel] = useState("");
25 |
26 | const router = useRouter();
27 |
28 | const handleSearch = (e: React.FormEvent) => {
29 | e.preventDefault();
30 |
31 | if (manufacturer.trim() === "" && model.trim() === "") {
32 | return alert("Please provide some input");
33 | }
34 |
35 | window.scrollTo({ top: 1000 })
36 | updateSearchParams(model.toLowerCase(), manufacturer.toLowerCase());
37 |
38 | // To temporary fix Next JS 13 scroll to top issue.
39 | // const searchBar = document.getElementById(SEARCH_BAR_ID);
40 | // searchBar.scrollIntoView()
41 | };
42 |
43 | const updateSearchParams = (model: string, manufacturer: string) => {
44 | // Create a new URLSearchParams object using the current URL search parameters
45 | const searchParams = new URLSearchParams(window.location.search);
46 |
47 | // Update or delete the 'model' search parameter based on the 'model' value
48 | if (model) {
49 | searchParams.set("model", model);
50 | } else {
51 | searchParams.delete("model");
52 | }
53 |
54 | // Update or delete the 'manufacturer' search parameter based on the 'manufacturer' value
55 | if (manufacturer) {
56 | searchParams.set("manufacturer", manufacturer);
57 | } else {
58 | searchParams.delete("manufacturer");
59 | }
60 |
61 | // Generate the new pathname with the updated search parameters
62 | const newPathname = `${window.location.pathname}?${searchParams.toString()}`;
63 |
64 | router.push(newPathname);
65 | };
66 |
67 | return (
68 |
96 | );
97 | };
98 |
99 | export default SearchBar;
--------------------------------------------------------------------------------
/components/SearchManufacturer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 | import { Fragment, useState } from "react";
5 | import { Combobox, Transition } from "@headlessui/react";
6 |
7 | import { manufacturers } from "@/constants";
8 | import { SearchManuFacturerProps } from "@/types";
9 |
10 | const SearchManufacturer = ({ manufacturer, setManuFacturer }: SearchManuFacturerProps) => {
11 | const [query, setQuery] = useState("");
12 |
13 | const filteredManufacturers =
14 | query === ""
15 | ? manufacturers
16 | : manufacturers.filter((item) =>
17 | item
18 | .toLowerCase()
19 | .replace(/\s+/g, "")
20 | .includes(query.toLowerCase().replace(/\s+/g, ""))
21 | );
22 |
23 | return (
24 |
25 |
26 |
27 | {/* Button for the combobox. Click on the icon to see the complete dropdown */}
28 |
29 |
36 |
37 |
38 | {/* Input field for searching */}
39 | item}
42 | onChange={(event) => setQuery(event.target.value)} // Update the search query when the input changes
43 | placeholder='Volkswagen...'
44 | />
45 |
46 | {/* Transition for displaying the options */}
47 | >
49 | leave='transition ease-in duration-100'
50 | leaveFrom='opacity-100'
51 | leaveTo='opacity-0'
52 | afterLeave={() => setQuery("")} // Reset the search query after the transition completes
53 | >
54 |
58 | {filteredManufacturers.length === 0 && query !== "" ? (
59 |
63 | Create "{query}"
64 |
65 | ) : (
66 | filteredManufacturers.map((item) => (
67 |
70 | `relative search-manufacturer__option ${
71 | active ? "bg-primary-blue text-white" : "text-gray-900"
72 | }`
73 | }
74 | value={item}
75 | >
76 | {({ selected, active }) => (
77 | <>
78 |
79 | {item}
80 |
81 |
82 | {/* Show an active blue background color if the option is selected */}
83 | {selected ? (
84 |
86 | ) : null}
87 | >
88 | )}
89 |
90 | ))
91 | )}
92 |
93 |
94 |
95 |
96 |
97 | );
98 | };
99 |
100 | export default SearchManufacturer;
--------------------------------------------------------------------------------
/components/CarDetails.tsx:
--------------------------------------------------------------------------------
1 | import { Fragment } from "react";
2 | import Image from "next/image";
3 |
4 | import { Dialog, Transition } from "@headlessui/react";
5 | import { CarProps } from "../types";
6 | import { generateCarImageUrl } from "../utils";
7 |
8 | interface CarDetailsProps {
9 | isOpen: boolean;
10 | closeModal: () => void;
11 | car: CarProps;
12 | }
13 |
14 | const CarDetails = ({ isOpen, closeModal, car }: CarDetailsProps) => (
15 | <>
16 |
17 |
18 |
27 |
28 |
29 |
30 |
31 |
32 |
41 |
42 |
47 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | {car.make} {car.model}
77 |
78 |
79 |
80 | {Object.entries(car).map(([key, value]) => (
81 |
82 |
83 | {key.split("_").join(" ")}
84 |
85 |
86 | {value}
87 |
88 |
89 | ))}
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | >
99 | );
100 |
101 | export default CarDetails;
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @import url("https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700;800&display=swap");
2 |
3 | @tailwind base;
4 | @tailwind components;
5 | @tailwind utilities;
6 |
7 | * {
8 | margin: 0;
9 | padding: 0;
10 | box-sizing: border-box;
11 | font-family: "Manrope", sans-serif;
12 | }
13 |
14 | /* START: General styles */
15 | .max-width {
16 | @apply max-w-[1440px] mx-auto;
17 | }
18 |
19 | .padding-x {
20 | @apply sm:px-16 px-6;
21 | }
22 |
23 | .padding-y {
24 | @apply py-4;
25 | }
26 |
27 | .flex-center {
28 | @apply flex items-center justify-center;
29 | }
30 |
31 | .flex-between {
32 | @apply flex justify-between items-center;
33 | }
34 |
35 | .custom-btn {
36 | @apply flex flex-row relative justify-center items-center py-3 px-6 outline-none;
37 | }
38 | /* END: General styles */
39 |
40 | /* START: Hero styles */
41 | .hero {
42 | @apply flex xl:flex-row flex-col gap-5 relative z-0 max-w-[1440px] mx-auto;
43 | }
44 |
45 | .hero__title {
46 | @apply 2xl:text-[72px] sm:text-[64px] text-[50px] font-extrabold;
47 | }
48 |
49 | .hero__subtitle {
50 | @apply text-[27px] text-black-100 font-light mt-5;
51 | }
52 |
53 | .hero__image-container {
54 | @apply xl:flex-[1.5] flex justify-end items-end w-full xl:h-screen;
55 | }
56 |
57 | .hero__image {
58 | @apply relative xl:w-full w-[90%] xl:h-full h-[590px] z-0;
59 | }
60 |
61 | .hero__image-overlay {
62 | @apply absolute xl:-top-24 xl:-right-1/2 -right-1/4 bg-hero-bg bg-repeat-round -z-10 w-full xl:h-screen h-[590px] overflow-hidden;
63 | }
64 | /* END: Hero styles */
65 |
66 | /* START: Home styles */
67 |
68 | .home__text-container {
69 | @apply flex flex-col items-start justify-start gap-y-2.5 text-black-100;
70 | }
71 |
72 | .home__filters {
73 | @apply mt-12 w-full flex-between items-center flex-wrap gap-5;
74 | }
75 |
76 | .home__filter-container {
77 | @apply flex justify-start flex-wrap items-center gap-2;
78 | }
79 |
80 | .home__cars-wrapper {
81 | @apply grid 2xl:grid-cols-4 xl:grid-cols-3 md:grid-cols-2 grid-cols-1 w-full gap-8 pt-14;
82 | }
83 |
84 | .home__error-container {
85 | @apply mt-16 flex justify-center items-center flex-col gap-2;
86 | }
87 | /* END: Home styles */
88 |
89 | /* START: Car Card styles */
90 | .car-card {
91 | @apply flex flex-col p-6 justify-center items-start text-black-100 bg-primary-blue-100 hover:bg-white hover:shadow-md rounded-3xl;
92 | }
93 |
94 | .car-card__content {
95 | @apply w-full flex justify-between items-start gap-2;
96 | }
97 |
98 | .car-card__content-title {
99 | @apply text-[22px] leading-[26px] font-bold capitalize;
100 | }
101 |
102 | .car-card__price {
103 | @apply flex mt-6 text-[32px] leading-[38px] font-extrabold;
104 | }
105 |
106 | .car-card__price-dollar {
107 | @apply self-start text-[14px] leading-[17px] font-semibold;
108 | }
109 |
110 | .car-card__price-day {
111 | @apply self-end text-[14px] leading-[17px] font-medium;
112 | }
113 |
114 | .car-card__image {
115 | @apply relative w-full h-40 my-3 object-contain;
116 | }
117 |
118 | .car-card__icon-container {
119 | @apply flex group-hover:invisible w-full justify-between text-grey;
120 | }
121 |
122 | .car-card__icon {
123 | @apply flex flex-col justify-center items-center gap-2;
124 | }
125 |
126 | .car-card__icon-text {
127 | @apply text-[14px] leading-[17px];
128 | }
129 |
130 | .car-card__btn-container {
131 | @apply hidden group-hover:flex absolute bottom-0 w-full z-10;
132 | }
133 | /* END: Car Card styles */
134 |
135 | /* START: Car Details styles */
136 | .car-details__dialog-panel {
137 | @apply relative w-full max-w-lg max-h-[90vh] overflow-y-auto transform rounded-2xl bg-white p-6 text-left shadow-xl transition-all flex flex-col gap-5;
138 | }
139 |
140 | .car-details__close-btn {
141 | @apply absolute top-2 right-2 z-10 w-fit p-2 bg-primary-blue-100 rounded-full;
142 | }
143 |
144 | .car-details__main-image {
145 | @apply relative w-full h-40 bg-pattern bg-cover bg-center rounded-lg;
146 | }
147 | /* END: Car Details styles */
148 |
149 | /* START: Custom Filter styles */
150 | .custom-filter__btn {
151 | @apply relative w-full min-w-[127px] flex justify-between items-center cursor-default rounded-lg bg-white py-2 px-3 text-left shadow-md sm:text-sm border;
152 | }
153 |
154 | .custom-filter__options {
155 | @apply absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm;
156 | }
157 | /* END: Custom Filter styles */
158 |
159 | /* START: Footer styles */
160 | .footer {
161 | @apply flex flex-col text-black-100 mt-5 border-t border-gray-100;
162 | }
163 |
164 | .footer__links-container {
165 | @apply flex max-md:flex-col flex-wrap justify-between gap-5 sm:px-16 px-6 py-10;
166 | }
167 |
168 | .footer__rights {
169 | @apply flex flex-col justify-start items-start gap-6;
170 | }
171 |
172 | .footer__links {
173 | @apply flex-1 w-full flex md:justify-end flex-wrap max-md:mt-10 gap-20;
174 | }
175 |
176 | .footer__link {
177 | @apply flex flex-col gap-6 text-base min-w-[170px];
178 | }
179 |
180 | .footer__copyrights {
181 | @apply flex justify-between items-center flex-wrap mt-10 border-t border-gray-100 sm:px-16 px-6 py-10;
182 | }
183 |
184 | .footer__copyrights-link {
185 | @apply flex-1 flex sm:justify-end justify-center max-sm:mt-4 gap-10;
186 | }
187 | /* END: Footer styles */
188 |
189 | /* START: searchbar styles */
190 | .searchbar {
191 | @apply flex items-center justify-start max-sm:flex-col w-full relative max-sm:gap-4 max-w-3xl;
192 | }
193 |
194 | .searchbar__item {
195 | @apply flex-1 max-sm:w-full flex justify-start items-center relative;
196 | }
197 |
198 | .searchbar__input {
199 | @apply w-full h-[48px] pl-12 p-4 bg-light-white rounded-r-full max-sm:rounded-full outline-none cursor-pointer text-sm;
200 | }
201 | /* END: searchbar styles */
202 |
203 | /* START: search manufacturer styles */
204 | .search-manufacturer {
205 | @apply flex-1 max-sm:w-full flex justify-start items-center;
206 | }
207 |
208 | .search-manufacturer__input {
209 | @apply w-full h-[48px] pl-12 p-4 rounded-l-full max-sm:rounded-full bg-light-white outline-none cursor-pointer text-sm;
210 | }
211 |
212 | .search-manufacturer__options {
213 | @apply absolute mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm;
214 | }
215 |
216 | .search-manufacturer__option {
217 | @apply cursor-default select-none py-2 pl-10 pr-4;
218 | }
219 | /* END: search manufacturer styles */
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------