├── app
├── favicon.ico
├── layout.tsx
├── page.tsx
└── globals.css
├── public
├── hero.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
├── magnifying-glass.svg
├── discord.svg
├── car-logo.svg
├── github.svg
├── tire.svg
├── steering-wheel.svg
├── twitter.svg
├── gas.svg
├── next.svg
└── logo.svg
├── postcss.config.js
├── next.config.js
├── components
├── index.ts
├── CustomButton.tsx
├── Navbar.tsx
├── ShowMore.tsx
├── Hero.tsx
├── Footer.tsx
├── CarCard.tsx
├── CustomFilter.tsx
├── Searchbar.tsx
├── SearchManufacturer.tsx
└── CarDetails.tsx
├── .gitignore
├── package.json
├── tsconfig.json
├── tailwind.config.js
├── types
└── index.ts
├── constants
└── index.ts
├── utils
└── index.ts
└── README.md
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DarkMage108/project_next13_car_showcase/HEAD/app/favicon.ico
--------------------------------------------------------------------------------
/public/hero.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DarkMage108/project_next13_car_showcase/HEAD/public/hero.png
--------------------------------------------------------------------------------
/public/hero-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DarkMage108/project_next13_car_showcase/HEAD/public/hero-bg.png
--------------------------------------------------------------------------------
/public/pattern.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DarkMage108/project_next13_car_showcase/HEAD/public/pattern.png
--------------------------------------------------------------------------------
/public/model-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/DarkMage108/project_next13_car_showcase/HEAD/public/model-icon.png
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | domains: ["cdn.imagin.studio"]
5 | }
6 | }
7 |
8 | module.exports = nextConfig
9 |
--------------------------------------------------------------------------------
/public/chevron-up-down.svg:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/public/arrow-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/public/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/components/index.ts:
--------------------------------------------------------------------------------
1 | import CarCard from "./CarCard";
2 | import CustomButton from "./CustomButton";
3 | import CustomFilter from "./CustomFilter";
4 | import Footer from "./Footer";
5 | import NavBar from "./Navbar";
6 | import ShowMore from "./ShowMore";
7 | import SearchBar from "./Searchbar";
8 | import Hero from "./Hero";
9 |
10 | export {
11 | Hero,
12 | CarCard,
13 | CustomButton,
14 | CustomFilter,
15 | Footer,
16 | NavBar,
17 | ShowMore,
18 | SearchBar,
19 | };
20 |
--------------------------------------------------------------------------------
/public/heart-outline.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/public/heart-filled.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import "./globals.css";
2 |
3 | import { Footer, NavBar } from "@components";
4 |
5 | export const metadata = {
6 | title: "Car Hub",
7 | description: "Discover world's best car showcase application",
8 | };
9 |
10 | export default function RootLayout({ children }: { children: React.ReactNode }) {
11 | return (
12 |
13 |
14 |
15 | {children}
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/.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 |
30 | # vercel
31 | .vercel
32 |
33 | # typescript
34 | *.tsbuildinfo
35 | next-env.d.ts
36 |
37 | # env
38 | .env
--------------------------------------------------------------------------------
/public/right-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "carhub",
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.14",
13 | "@types/node": "20.2.1",
14 | "@types/react": "18.2.6",
15 | "@types/react-dom": "18.2.4",
16 | "autoprefixer": "10.4.14",
17 | "next": "13.4.3",
18 | "postcss": "8.4.23",
19 | "react": "18.2.0",
20 | "react-dom": "18.2.0",
21 | "tailwindcss": "3.3.2",
22 | "typescript": "5.0.4"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/public/linkedin.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/facebook.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/public/magnifying-glass.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/public/discord.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/components/CustomButton.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 |
5 | import { CustomButtonProps } from "@types";
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;
29 |
--------------------------------------------------------------------------------
/public/github.svg:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/public/tire.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/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;
29 |
--------------------------------------------------------------------------------
/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 | };
35 |
--------------------------------------------------------------------------------
/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;
37 |
--------------------------------------------------------------------------------
/public/steering-wheel.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/public/twitter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/components/Hero.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import Image from "next/image";
4 |
5 | import { CustomButton } from "@components";
6 |
7 | const Hero = () => {
8 | const handleScroll = () => {
9 | const nextSection = document.getElementById("discover");
10 |
11 | if (nextSection) {
12 | nextSection.scrollIntoView({ behavior: "smooth" });
13 | }
14 | };
15 |
16 | return (
17 |
18 |
19 |
20 | Find, book, rent a car—quick and super easy!
21 |
22 |
23 |
24 | Streamline your car rental experience with our effortless booking
25 | process.
26 |
27 |
28 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | export default Hero;
46 |
--------------------------------------------------------------------------------
/public/gas.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 | }
69 |
--------------------------------------------------------------------------------
/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;
53 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { fetchCars } from "@utils";
2 | import { 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 | }
59 |
--------------------------------------------------------------------------------
/constants/index.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 | ];
102 |
--------------------------------------------------------------------------------
/components/CarCard.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import Image from "next/image";
5 |
6 | import { calculateCarRent, generateCarImageUrl } from "@utils";
7 | import { CarProps } from "@types";
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;
75 |
--------------------------------------------------------------------------------
/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 | }
72 |
--------------------------------------------------------------------------------
/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 |
7 | import SearchManufacturer from "./SearchManufacturer";
8 |
9 | const SearchButton = ({ otherClasses }: { otherClasses: string }) => (
10 |
11 |
18 |
19 | );
20 |
21 | const SearchBar = () => {
22 | const [manufacturer, setManuFacturer] = useState("");
23 | const [model, setModel] = useState("");
24 |
25 | const router = useRouter();
26 |
27 | const handleSearch = (e: React.FormEvent) => {
28 | e.preventDefault();
29 |
30 | if (manufacturer.trim() === "" && model.trim() === "") {
31 | return alert("Please provide some input");
32 | }
33 |
34 | updateSearchParams(model.toLowerCase(), manufacturer.toLowerCase());
35 | };
36 |
37 | const updateSearchParams = (model: string, manufacturer: string) => {
38 | // Create a new URLSearchParams object using the current URL search parameters
39 | const searchParams = new URLSearchParams(window.location.search);
40 |
41 | // Update or delete the 'model' search parameter based on the 'model' value
42 | if (model) {
43 | searchParams.set("model", model);
44 | } else {
45 | searchParams.delete("model");
46 | }
47 |
48 | // Update or delete the 'manufacturer' search parameter based on the 'manufacturer' value
49 | if (manufacturer) {
50 | searchParams.set("manufacturer", manufacturer);
51 | } else {
52 | searchParams.delete("manufacturer");
53 | }
54 |
55 | // Generate the new pathname with the updated search parameters
56 | const newPathname = `${window.location.pathname}?${searchParams.toString()}`;
57 |
58 | router.push(newPathname);
59 | };
60 |
61 | return (
62 |
90 | );
91 | };
92 |
93 | export default SearchBar;
94 |
--------------------------------------------------------------------------------
/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 | }
81 |
--------------------------------------------------------------------------------
/components/SearchManufacturer.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image";
2 | import { Fragment, useState } from "react";
3 | import { Combobox, Transition } from "@headlessui/react";
4 |
5 | import { manufacturers } from "@constants";
6 | import { SearchManuFacturerProps } from "@types";
7 |
8 | const SearchManufacturer = ({ manufacturer, setManuFacturer }: SearchManuFacturerProps) => {
9 | const [query, setQuery] = useState("");
10 |
11 | const filteredManufacturers =
12 | query === ""
13 | ? manufacturers
14 | : manufacturers.filter((item) =>
15 | item
16 | .toLowerCase()
17 | .replace(/\s+/g, "")
18 | .includes(query.toLowerCase().replace(/\s+/g, ""))
19 | );
20 |
21 | return (
22 |
23 |
24 |
25 | {/* Button for the combobox. Click on the icon to see the complete dropdown */}
26 |
27 |
34 |
35 |
36 | {/* Input field for searching */}
37 | item}
40 | onChange={(event) => setQuery(event.target.value)} // Update the search query when the input changes
41 | placeholder='Volkswagen...'
42 | />
43 |
44 | {/* Transition for displaying the options */}
45 | >
47 | leave='transition ease-in duration-100'
48 | leaveFrom='opacity-100'
49 | leaveTo='opacity-0'
50 | afterLeave={() => setQuery("")} // Reset the search query after the transition completes
51 | >
52 |
56 | {filteredManufacturers.length === 0 && query !== "" ? (
57 |
61 | Create "{query}"
62 |
63 | ) : (
64 | filteredManufacturers.map((item) => (
65 |
68 | `relative search-manufacturer__option ${
69 | active ? "bg-primary-blue text-white" : "text-gray-900"
70 | }`
71 | }
72 | value={item}
73 | >
74 | {({ selected, active }) => (
75 | <>
76 |
77 | {item}
78 |
79 |
80 | {/* Show an active blue background color if the option is selected */}
81 | {selected ? (
82 |
84 | ) : null}
85 | >
86 | )}
87 |
88 | ))
89 | )}
90 |
91 |
92 |
93 |
94 |
95 | );
96 | };
97 |
98 | export default SearchManufacturer;
99 |
--------------------------------------------------------------------------------
/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;
102 |
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
14 |
23 |
25 |
30 |
33 |
35 |
38 |
41 |
44 |
45 |
46 |
47 |
52 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/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 */
220 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
14 |
A Car Showcase Website
15 |
16 |
17 | Build this project step by step with our detailed tutorial on
JavaScript Mastery YouTube. Join the JSM family!
18 |
19 |
20 |
21 | ## 📋 Table of Contents
22 |
23 | 1. 🤖 [Introduction](#introduction)
24 | 2. ⚙️ [Tech Stack](#tech-stack)
25 | 3. 🔋 [Features](#features)
26 | 4. 🤸 [Quick Start](#quick-start)
27 | 5. 🕸️ [Snippets](#snippets)
28 | 6. 🔗 [Links](#links)
29 | 7. 🚀 [More](#more)
30 |
31 | ## 🚨 Tutorial
32 |
33 | This repository contains the code corresponding to an in-depth tutorial available on our YouTube channel, JavaScript Mastery .
34 |
35 | If you prefer visual learning, this is the perfect resource for you. Follow our tutorial to learn how to build projects like these step-by-step in a beginner-friendly manner!
36 |
37 | ## 🤖 Introduction
38 |
39 | Developed with Next.js and leveraging its server-side rendering capabilities, the Car Showcase website presents various car types, showcasing comprehensive information in a well-designed format with advanced filtering and pagination support for an enhanced user experience.
40 |
41 | If you're getting started and need assistance or face any bugs, join our active Discord community with over 27k+ members. It's a place where people help each other out.
42 |
43 | ## ⚙️ Tech Stack
44 |
45 | - Next.js
46 | - TypeScript
47 | - Tailwind CSS
48 |
49 | ## 🔋 Features
50 |
51 | 👉 **Home Page**: Showcases a visually appealing display of cars fetched from a third-party API, providing a captivating introduction to the diverse range of vehicles available.
52 |
53 | 👉 **Exploration and Filtering**: Explore a wide variety of cars from around the world, utilizing a search and filter system based on criteria such as model, manufacturer, year, fuel type, and make.
54 |
55 | 👉 **Transition to Server-Side Rendering**: A seamless transition from client-side rendering to server-side rendering, enhancing performance and providing a smoother browsing experience.
56 |
57 | 👉 **Pagination**: For easy navigation through a large dataset of cars, allowing users to explore multiple pages effortlessly.
58 |
59 | 👉 **Metadata Optimization and SEO**: Optimize metadata for car listing, enhancing search engine optimization (SEO) and ensuring better visibility on search engine results pages.
60 |
61 | 👉 **TypeScript Types**: Utilize TypeScript to provide robust typing for enhanced code quality and better development
62 |
63 | 👉 **Responsive Website Design**: The website is designed to be visually pleasing and responsive, ensuring an optimal user experience across various devices.
64 |
65 | and many more, including code architecture and reusability
66 |
67 | ## 🤸 Quick Start
68 |
69 | Follow these steps to set up the project locally on your machine.
70 |
71 | **Prerequisites**
72 |
73 | Make sure you have the following installed on your machine:
74 |
75 | - [Git](https://git-scm.com/)
76 | - [Node.js](https://nodejs.org/en)
77 | - [npm](https://www.npmjs.com/) (Node Package Manager)
78 |
79 | **Cloning the Repository**
80 |
81 | ```bash
82 | git clone https://github.com/DarkMage108/project_next13_car_showcase.git
83 | cd project_next13_car_showcase
84 | ```
85 |
86 | **Installation**
87 |
88 | Install the project dependencies using npm:
89 |
90 | ```bash
91 | npm install
92 | ```
93 |
94 | **Set Up Environment Variables**
95 |
96 | Create a new file named `.env` in the root of your project and add the following content:
97 |
98 | ```env
99 | NEXT_PUBLIC_RAPID_API_KEY=
100 | NEXT_PUBLIC_IMAGIN_API_KEY=hrjavascript-mastery
101 | ```
102 |
103 | Replace the placeholder values with your actual credentials. You can obtain these credentials by signing up on the corresponding websites from [Rapid API](https://www.youtube.com/redirect?event=video_description&redir_token=QUFFLUhqbmI1TlE1NHFGZ1JLdHU3dnAxSTU5a2R5UUM4QXxBQ3Jtc0tsUDY0aW8xMFhUZVdxMUNzSUlKUExRTG5UaDZoR3hWVFprN2tJV0k2dnk4MXo2NVFMVkk0NWhGS19Nd0g5cGRfN2JjcTdaSlJJRHJKYzlfT3lSS1M4TDVNVTV5Wl91c1lIR2VPZUYzbHJ2Tll2QkJ0aw&q=https%3A%2F%2Frapidapi.com%2Fapininjas%2Fapi%2Fcars-by-api-ninjas%3Futm_source%3Dyoutube.com%2FJavaScriptMastery%26utm_medium%3Dreferral%26utm_campaign%3DDevRel&v=pUNSHPyVryU) to [Imagin Cars](https://www.imagin.studio/solutions/api)
104 |
105 | **Running the Project**
106 |
107 | ```bash
108 | npm run dev
109 | ```
110 |
111 | Open [http://localhost:3000](http://localhost:3000) in your browser to view the project.
112 |
113 | ## 🕸️ Snippets
114 |
115 |
116 | constants.ts
117 |
118 | ```typescript
119 | export const manufacturers = [
120 | "Acura",
121 | "Alfa Romeo",
122 | "Aston Martin",
123 | "Audi",
124 | "Bentley",
125 | "BMW",
126 | "Buick",
127 | "Cadillac",
128 | "Chevrolet",
129 | "Chrysler",
130 | "Citroen",
131 | "Dodge",
132 | "Ferrari",
133 | "Fiat",
134 | "Ford",
135 | "GMC",
136 | "Honda",
137 | "Hyundai",
138 | "Infiniti",
139 | "Jaguar",
140 | "Jeep",
141 | "Kia",
142 | "Lamborghini",
143 | "Land Rover",
144 | "Lexus",
145 | "Lincoln",
146 | "Maserati",
147 | "Mazda",
148 | "McLaren",
149 | "Mercedes-Benz",
150 | "MINI",
151 | "Mitsubishi",
152 | "Nissan",
153 | "Porsche",
154 | "Ram",
155 | "Rolls-Royce",
156 | "Subaru",
157 | "Tesla",
158 | "Toyota",
159 | "Volkswagen",
160 | "Volvo",
161 | ];
162 |
163 | export const yearsOfProduction = [
164 | { title: "Year", value: "" },
165 | { title: "2015", value: "2015" },
166 | { title: "2016", value: "2016" },
167 | { title: "2017", value: "2017" },
168 | { title: "2018", value: "2018" },
169 | { title: "2019", value: "2019" },
170 | { title: "2020", value: "2020" },
171 | { title: "2021", value: "2021" },
172 | { title: "2022", value: "2022" },
173 | { title: "2023", value: "2023" },
174 | ];
175 |
176 | export const fuels = [
177 | {
178 | title: "Fuel",
179 | value: "",
180 | },
181 | {
182 | title: "Gas",
183 | value: "Gas",
184 | },
185 | {
186 | title: "Electricity",
187 | value: "Electricity",
188 | },
189 | ];
190 |
191 | export const footerLinks = [
192 | {
193 | title: "About",
194 | links: [
195 | { title: "How it works", url: "/" },
196 | { title: "Featured", url: "/" },
197 | { title: "Partnership", url: "/" },
198 | { title: "Bussiness Relation", url: "/" },
199 | ],
200 | },
201 | {
202 | title: "Company",
203 | links: [
204 | { title: "Events", url: "/" },
205 | { title: "Blog", url: "/" },
206 | { title: "Podcast", url: "/" },
207 | { title: "Invite a friend", url: "/" },
208 | ],
209 | },
210 | {
211 | title: "Socials",
212 | links: [
213 | { title: "Discord", url: "/" },
214 | { title: "Instagram", url: "/" },
215 | { title: "Twitter", url: "/" },
216 | { title: "Facebook", url: "/" },
217 | ],
218 | },
219 | ];
220 | ```
221 |
222 |
223 |
224 |
225 | globals.css
226 |
227 | ```css
228 | @import url("https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700;800&display=swap");
229 |
230 | @tailwind base;
231 | @tailwind components;
232 | @tailwind utilities;
233 |
234 | * {
235 | margin: 0;
236 | padding: 0;
237 | box-sizing: border-box;
238 | font-family: "Manrope", sans-serif;
239 | }
240 |
241 | /* START: General styles */
242 | .max-width {
243 | @apply max-w-[1440px] mx-auto;
244 | }
245 |
246 | .padding-x {
247 | @apply sm:px-16 px-6;
248 | }
249 |
250 | .padding-y {
251 | @apply py-4;
252 | }
253 |
254 | .flex-center {
255 | @apply flex items-center justify-center;
256 | }
257 |
258 | .flex-between {
259 | @apply flex justify-between items-center;
260 | }
261 |
262 | .custom-btn {
263 | @apply flex flex-row relative justify-center items-center py-3 px-6 outline-none;
264 | }
265 | /* END: General styles */
266 |
267 | /* START: Hero styles */
268 | .hero {
269 | @apply flex xl:flex-row flex-col gap-5 relative z-0 max-w-[1440px] mx-auto;
270 | }
271 |
272 | .hero__title {
273 | @apply 2xl:text-[72px] sm:text-[64px] text-[50px] font-extrabold;
274 | }
275 |
276 | .hero__subtitle {
277 | @apply text-[27px] text-black-100 font-light mt-5;
278 | }
279 |
280 | .hero__image-container {
281 | @apply xl:flex-[1.5] flex justify-end items-end w-full xl:h-screen;
282 | }
283 |
284 | .hero__image {
285 | @apply relative xl:w-full w-[90%] xl:h-full h-[590px] z-0;
286 | }
287 |
288 | .hero__image-overlay {
289 | @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;
290 | }
291 | /* END: Hero styles */
292 |
293 | /* START: Home styles */
294 |
295 | .home__text-container {
296 | @apply flex flex-col items-start justify-start gap-y-2.5 text-black-100;
297 | }
298 |
299 | .home__filters {
300 | @apply mt-12 w-full flex-between items-center flex-wrap gap-5;
301 | }
302 |
303 | .home__filter-container {
304 | @apply flex justify-start flex-wrap items-center gap-2;
305 | }
306 |
307 | .home__cars-wrapper {
308 | @apply grid 2xl:grid-cols-4 xl:grid-cols-3 md:grid-cols-2 grid-cols-1 w-full gap-8 pt-14;
309 | }
310 |
311 | .home__error-container {
312 | @apply mt-16 flex justify-center items-center flex-col gap-2;
313 | }
314 | /* END: Home styles */
315 |
316 | /* START: Car Card styles */
317 | .car-card {
318 | @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;
319 | }
320 |
321 | .car-card__content {
322 | @apply w-full flex justify-between items-start gap-2;
323 | }
324 |
325 | .car-card__content-title {
326 | @apply text-[22px] leading-[26px] font-bold capitalize;
327 | }
328 |
329 | .car-card__price {
330 | @apply flex mt-6 text-[32px] leading-[38px] font-extrabold;
331 | }
332 |
333 | .car-card__price-dollar {
334 | @apply self-start text-[14px] leading-[17px] font-semibold;
335 | }
336 |
337 | .car-card__price-day {
338 | @apply self-end text-[14px] leading-[17px] font-medium;
339 | }
340 |
341 | .car-card__image {
342 | @apply relative w-full h-40 my-3 object-contain;
343 | }
344 |
345 | .car-card__icon-container {
346 | @apply flex group-hover:invisible w-full justify-between text-grey;
347 | }
348 |
349 | .car-card__icon {
350 | @apply flex flex-col justify-center items-center gap-2;
351 | }
352 |
353 | .car-card__icon-text {
354 | @apply text-[14px] leading-[17px];
355 | }
356 |
357 | .car-card__btn-container {
358 | @apply hidden group-hover:flex absolute bottom-0 w-full z-10;
359 | }
360 | /* END: Car Card styles */
361 |
362 | /* START: Car Details styles */
363 | .car-details__dialog-panel {
364 | @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;
365 | }
366 |
367 | .car-details__close-btn {
368 | @apply absolute top-2 right-2 z-10 w-fit p-2 bg-primary-blue-100 rounded-full;
369 | }
370 |
371 | .car-details__main-image {
372 | @apply relative w-full h-40 bg-pattern bg-cover bg-center rounded-lg;
373 | }
374 | /* END: Car Details styles */
375 |
376 | /* START: Custom Filter styles */
377 | .custom-filter__btn {
378 | @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;
379 | }
380 |
381 | .custom-filter__options {
382 | @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;
383 | }
384 | /* END: Custom Filter styles */
385 |
386 | /* START: Footer styles */
387 | .footer {
388 | @apply flex flex-col text-black-100 mt-5 border-t border-gray-100;
389 | }
390 |
391 | .footer__links-container {
392 | @apply flex max-md:flex-col flex-wrap justify-between gap-5 sm:px-16 px-6 py-10;
393 | }
394 |
395 | .footer__rights {
396 | @apply flex flex-col justify-start items-start gap-6;
397 | }
398 |
399 | .footer__links {
400 | @apply flex-1 w-full flex md:justify-end flex-wrap max-md:mt-10 gap-20;
401 | }
402 |
403 | .footer__link {
404 | @apply flex flex-col gap-6 text-base min-w-[170px];
405 | }
406 |
407 | .footer__copyrights {
408 | @apply flex justify-between items-center flex-wrap mt-10 border-t border-gray-100 sm:px-16 px-6 py-10;
409 | }
410 |
411 | .footer__copyrights-link {
412 | @apply flex-1 flex sm:justify-end justify-center max-sm:mt-4 gap-10;
413 | }
414 | /* END: Footer styles */
415 |
416 | /* START: searchbar styles */
417 | .searchbar {
418 | @apply flex items-center justify-start max-sm:flex-col w-full relative max-sm:gap-4 max-w-3xl;
419 | }
420 |
421 | .searchbar__item {
422 | @apply flex-1 max-sm:w-full flex justify-start items-center relative;
423 | }
424 |
425 | .searchbar__input {
426 | @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;
427 | }
428 | /* END: searchbar styles */
429 |
430 | /* START: search manufacturer styles */
431 | .search-manufacturer {
432 | @apply flex-1 max-sm:w-full flex justify-start items-center;
433 | }
434 |
435 | .search-manufacturer__input {
436 | @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;
437 | }
438 |
439 | .search-manufacturer__options {
440 | @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;
441 | }
442 |
443 | .search-manufacturer__option {
444 | @apply cursor-default select-none py-2 pl-10 pr-4;
445 | }
446 | /* END: search manufacturer styles */
447 | ```
448 |
449 |
450 |
451 |
452 | tailwind.config.js
453 |
454 | ```javascript
455 | /** @type {import('tailwindcss').Config} */
456 | module.exports = {
457 | content: [
458 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
459 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
460 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
461 | ],
462 | mode: "jit",
463 | theme: {
464 | extend: {
465 | fontFamily: {
466 | inter: ["Inter", "sans-serif"],
467 | },
468 | colors: {
469 | "black-100": "#2B2C35",
470 | "primary-blue": {
471 | DEFAULT: "#2B59FF",
472 | 100: "#F5F8FF",
473 | },
474 | "secondary-orange": "#f79761",
475 | "light-white": {
476 | DEFAULT: "rgba(59,60,152,0.03)",
477 | 100: "rgba(59,60,152,0.02)",
478 | },
479 | grey: "#747A88",
480 | },
481 | backgroundImage: {
482 | 'pattern': "url('/pattern.png')",
483 | 'hero-bg': "url('/hero-bg.png')"
484 | }
485 | },
486 | },
487 | plugins: [],
488 | };
489 | ```
490 |
491 |
492 |
493 | ## 🔗 Links
494 |
495 | Assets used in the project are [here](https://drive.google.com/file/d/1Ague8aTHA6JSrzy3kscEZmrJQdtDxqwy/view)
496 |
497 | ## 🚀 More
498 |
499 | **Advance your skills with Next.js 14 Pro Course**
500 |
501 | Enjoyed creating this project? Dive deeper into our PRO courses for a richer learning adventure. They're packed with detailed explanations, cool features, and exercises to boost your skills. Give it a go!
502 |
503 |
504 |
505 |
506 |
507 |
508 |
509 |
510 | **Accelerate your professional journey with the Expert Training program**
511 |
512 | And if you're hungry for more than just a course and want to understand how we learn and tackle tech challenges, hop into our personalized masterclass. We cover best practices, different web skills, and offer mentorship to boost your confidence. Let's learn and grow together!
513 |
514 |
515 |
516 |
517 |
518 | #
519 |
--------------------------------------------------------------------------------