import "./globals.css";
import { Footer, Navbar } from "@/components";
export const metadata = {
title: "Car Hub",
description: "Discover world's best car showcase application",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className="relative">
<Navbar />
{children}
<Footer />
</body>
</html>
);
}
layout
: 웹 애플리케이션의 기본적인 레이아웃 구조를 구성하며, 해당 레이아웃 내에 네비게이션 바(navbar), 푸터(footer) 및 기타 컨텐츠를 렌더링 합니다.
주로 JSX 문법을 사용하여 작성되었습니다.
children은 해당 레이아웃에 포함할 내용을 나타냅니다. 즉, 해당 레이아웃 내에 표시될 페이지의 본문 컨텐츠를 children을 통해 렌더링 합니다.
import { fetchCars } from "@/utils";
import { HomeProps } from "@/types";
import { fuels, yearsOfProduction } from "@/constants";
import { CarCard, ShowMore, SearchBar, CustomFilter, Hero } from "@/components";
export default async function Home({ searchParams }: HomeProps) {
const allCars = await fetchCars({
manufacturer: searchParams.manufacturer || "",
year: searchParams.year || 2022,
fuel: searchParams.fuel || "",
limit: searchParams.limit || 10,
model: searchParams.model || "",
});
const isDataEmpty = !Array.isArray(allCars) || allCars.length < 1 || !allCars;
return (
<main className="overflow-hidden">
<Hero />
<div className="mt-12 padding-x padding-y max-width" id="discover">
<div className="home__text-container">
<h1 className="text-4xl font-extrabold">Car Catalogue</h1>
<p>Explore out cars you might like</p>
</div>
<div className="home__filters">
<SearchBar />
<div className="home__filter-container">
<CustomFilter title="fuel" options={fuels} />
<CustomFilter title="year" options={yearsOfProduction} />
</div>
</div>
{!isDataEmpty ? (
<section>
<div className="home__cars-wrapper">
{allCars?.map((car) => (
<CarCard car={car} />
))}
</div>
<ShowMore
pageNumber={(searchParams.limit || 10) / 10}
isNext={(searchParams.limit || 10) > allCars.length}
/>
</section>
) : (
<div className="home__error-container">
<h2 className="text-black text-xl font-bold">Oops, no results</h2>
<p>{allCars?.message}</p>
</div>
)}
</div>
</main>
);
}
page
: React를 사용하여 구현된 웹 애플리케이션의 홈페이지를 정의하는 컴포넌트 입니다.
React와 관련 라이브러리를 사용하여 동적인 웹 애플리케이션의 홈페이지를 구현하는데 사용됩니다. 사용자에게 자동차 목록을 보여주고 검색 필터를 통해 결과를 필터링하고 페이지네이션을 제공하는 역할을 합니다.
이 컴포넌트는 HomeProps를 매개변수로 받아옵니다.
또한 fetchCars 함수를 사용하여 자동차 데이터를 가져옵니다. 이 데이터는 searchParams 객체를 기반으로 가져오며, searchParams 객체에는 제조사, 연도, 연료 유형, 모델 등의 검색 매개변수가 포함됩니다.
isDateEmpty 변수는 가져온 자동차 데이터가 비어있는지 확인합니다. 데이터가 비어있다면 오류 메시지가 표시됩니다.
홈페이지의 레이아웃은 여러 부분으로 나누어져 있는데 Hero 컴포넌트가 렌더링 되며, 그 아래에는 일부 텍스트와 검색 필터(searchBar, CustomFilter)가 포함된 섹션이 있습니다.
데이터가 존재하는 경우 자동차 데이터를 Car Card 컴포넌트가 렌더링 됩니다. 이 컴포넌트는 자동차의 정보를 보여주는 카드 형식으로 화면에 표시됩니다. 또한 Show More 컴포넌트가 페이지네이션을 처리하고, 다음 페이지로 이동할 수 있는 옵션을 제공합니다.
"use client";
import { useState } from "react";
import Image from "next/image";
import { calculateCarRent, generateCarImageUrl } from "@/utils";
import { CarProps } from "@/types";
import CustomButton from "./CustomButton";
import CarDetails from "./CarDetails";
interface CarCardProps {
car: CarProps;
}
const CarCard = ({ car }: CarCardProps) => {
const { city_mpg, year, make, model, transmission, drive } = car;
const [isOpen, setIsOpen] = useState(false);
const carRent = calculateCarRent(city_mpg, year);
return (
<div className="car-card group">
<div className="car-card__content">
<h2 className="car-card__content-title">
{make} {model}
</h2>
</div>
<p className="flex mt-6 text-[32px] leading-[38px] font-extrabold">
<span className="self-start text-[14px] leading-[17px] font-semibold">
$
</span>
{carRent}
<span className="self-end text-[14px] leading-[17px] font-medium">
/ day
</span>
</p>
<div className="relative w-full h-40 my-3 object-contain">
<Image
src={generateCarImageUrl(car)}
alt="car model"
fill
priority
className="object-contain"
/>
</div>
<div className="relative flex w-full mt-2">
<div className="flex group-hover:invisible w-full justify-between text-grey">
<div className="flex flex-col justify-center items-center gap-2">
<Image
src="/steering-wheel.svg"
width={20}
height={20}
alt="steering wheel"
/>
<p className="text-[14px] leading-[17px]">
{transmission === "a" ? "Automatic" : "Manual"}
</p>
</div>
<div className="car-card__icon">
<Image src="/tire.svg" width={20} height={20} alt="seat" />
<p className="car-card__icon-text">{drive.toUpperCase()}</p>
</div>
<div className="car-card__icon">
<Image src="/gas.svg" width={20} height={20} alt="seat" />
<p className="car-card__icon-text">{city_mpg} MPG</p>
</div>
</div>
<div className="car-card__btn-container">
<CustomButton
title="View More"
containerStyles="w-full py-[16px] rounded-full bg-primary-blue"
textStyles="text-white text-[14px] leading-[17px] font-bold"
rightIcon="/right-arrow.svg"
handleClick={() => setIsOpen(true)}
/>
</div>
</div>
<CarDetails
isOpen={isOpen}
closeModal={() => setIsOpen(false)}
car={car}
/>
</div>
);
};
export default CarCard;
Car Card
: React를 사용하여 자동차 정보를 나타내는 카드 형식의 컴포넌트인 Car Card를 정의합니다. 이 컴포넌트는 자동차의 사양 및 이미지를 표시하고 View More 버튼을 통해 자세한 자동차 정보를 볼 수 있는 모달 창을 표시합니다.
Detail
-CarCardProps는 car라는 이름의 자동차 정보를 받는 인터페이스를 정의합니다.
-CarCard 함수 컴포넌트는 화면에 자동차 정보를 표시하는 부분입니다. car 객체에서 자동차의 다양한 속성(ex. city_mpg, year, make, model 등)을 추출해냅니다.
-isOpen이라는 상태 변수를 사용하여 자세한 자동차 정보를 표시할 모달 창의 열림/닫힘 상태를 관리합니다.
- calculateCarRent 함수를 사용하여 자동차 대여료를 계산하고 이를 carRent 변수에 저장합니다.
-자동차는 카드 형식으로 표시되며, 이 카드에는 자동차 모델 및 대여료 정보가 포함되어 있습니다. 또한 자동차 이미지도 표시됩니다.
-View More 버튼을 클릭하면, setIsOpen(true)를 호출하여 모달 창을 엽니다.
-CarDetails 컴포넌트가 모달 창을 표시하고 자세한 자동차 정보를 표시합니다.
import { Fragment } from "react";
import Image from "next/image";
import { Dialog, Transition } from "@headlessui/react";
import { CarProps } from "@/types";
import { generateCarImageUrl } from "@/utils";
interface CarDetailsProps {
isOpen: boolean;
closeModal: () => void;
car: CarProps;
}
const CarDetails = ({ isOpen, closeModal, car }: CarDetailsProps) => (
<>
<Transition appear show={isOpen} as={Fragment}>
<Dialog as="div" className="relative z-10" onClose={closeModal}>
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</Transition.Child>
<div className="fixed inset-0 overflow-y-auto">
<div className="flex min-h-full items-center justify-center p-4 text-center">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-out duration-300"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<Dialog.Panel
className="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"
>
<button
type="button"
className="absolute top-2 right-2 z-10 w-fit p-2 bg-primary-blue-100 rounded-full"
onClick={closeModal}
>
<Image
src="/close.svg"
alt="close"
width={20}
height={20}
className="object-contain"
/>
</button>
<div className="flex-1 flex flex-col gap-3">
<div className="relative w-full h-40 bg-pattern bg-cover bg-center rounded-lg">
<Image
src={generateCarImageUrl(car)}
alt="car model"
fill
priority
className="object-contain"
/>
</div>
<div className="flex gap-3">
<div className="flex-1 relative w-full h-24 bg-primary-blue-100 rounded-lg">
<Image
src={generateCarImageUrl(car, "29")}
alt="car model"
fill
priority
className="object-contain"
/>
</div>
<div className="flex-1 relative w-full h-24 bg-primary-blue-100 rounded-lg">
<Image
src={generateCarImageUrl(car, "33")}
alt="car model"
fill
priority
className="object-contain"
/>
</div>
<div className="flex-1 relative w-full h-24 bg-primary-blue-100 rounded-lg">
<Image
src={generateCarImageUrl(car, "13")}
alt="car model"
fill
priority
className="object-contain"
/>
</div>
</div>
</div>
<div className="flex-1 flex flex-col gap-2">
<h2 className="font-semibold text-xl capitalize">
{car.make} {car.model}
</h2>
<div className="mt-3 flex flex-wrap gap-4">
{Object.entries(car).map(([key, value]) => (
<div
className="flex justify-between gap-5 w-full text-right"
key={key}
>
<h4 className="text-grey capitalize">
{key.split("_").join(" ")}
</h4>
<p className="text-black-100 font-semibold">{value}</p>
</div>
))}
</div>
</div>
</Dialog.Panel>
</Transition.Child>
</div>
</div>
</Dialog>
</Transition>
</>
);
export default CarDetails;
Car Details
:자동차 상세 정보를 표시하기 위한 모달 창을 정의하는 React 컴포넌트인 Car Details입니다.
이 모달 창은 사용자가 자세한 자동차 정보를 볼 수 있는 모달 다이얼로그로 작동합니다.
해당 컴포넌트는 Car Card 컴포넌트에서 View More 버튼을 클릭하면 호출되어 팝업 창을 열고 자세한 자동차 정보를 표시합니다.
Details
-CarDetailsProps는 팝업창을 열거나 닫는데 필요한 상태와 자동차 장보를 받는 인터페이스를 정의합니다.
-CarDetails 함수 컴포넌트는 모달 창의 내용을 렌더링하는 부분입니다. isOpen, closeModal, car 객체를 받아옵니다.
-모달 창은 headlessui 라이브러리의 Transition 및 Dialog 컴포넌트를 사용하여 구현됩니다. 모달이 나타날 때와 사라질 때의 애니메이션 효과를 부여합니다.
-모달 창이 열릴 때, 화면을 어둡게 하는 배경을 생성하여, 모달 창 외부를 클릭하면 모달 창이 닫히도록 합니다. 각 이미지는 Imgage 컴포넌트를 사용하여 표시되며, 이미지 소스는 generateCarImageUrl 함수를 통하여 동적 생성됩니다.
-모달 창 오른쪽 상단에는 닫기 버튼이 있으며, 클릭하면 팝업이 닫힙니다.
-자동차 정보는 제조사, 모델, 그리고 다른 자동차 사양과 속성을 보여줍니다. 이 정보는 car 객체를 통해 동적으로 표시됩니다.
✏ headlessui 라이브러리
:headlessui UI는 React와 Vue 같은 프레임워크를 위한 UI 구성 요소 라이브러리.
하지만 보통의 "디자인이 포함된" UI 라이브러라와는 다름.
일반적인 UI 라이브러리는 이미 디자인과 스타일이 입혀져 있는 버튼, 모달, 드롭다운 같은 컴포넌트를 제공하나, headlessui UI는 "디자인 없이 기능만 제공"하는 컴포넌트를 제공함.
즉, 컴포넌트가 어떻게 보일지는 사용자가 직접 CSS나 Tailwind CSS 같은 스타일링 도구를 사용해 결정해야 함.
"use client";
import Image from "next/image";
import { CustomButtonProps } from "@/types";
const Button = ({
isDisabled,
btnType,
containerStyles,
textStyles,
title,
rightIcon,
handleClick,
}: CustomButtonProps) => (
<button
disabled={isDisabled}
type={btnType || "button"}
className={`custom-btn ${containerStyles}`}
onClick={handleClick}
>
<span className={`flex-1 ${textStyles}`}>{title}</span>
{rightIcon && (
<div className="relative w-6 h-6">
<Image
src={rightIcon}
alt="arrow_left"
fill
className="object-contain"
/>
</div>
)}
</button>
);
export default Button;
Custom Button
: React에서 재사용 가능한 버튼 컴포넌트인 Button을 정의합니다.
이 컴포넌트는 매개변수로 전달된 속성에 따라 버튼의 모양과 동작을 조절할 수 있습니다.
Details
-Custom Button Props는 버튼 컴포넌트에 전달되는 속성을 정의합니다. 이러한 속성에는 버튼의 텍스트, 스타일, 비활성화 상태, 클릭 핸들러 및 기타 속성이 포함됩니다.
-Button 함수 컴포넌트는 버튼을 렌더링하는 부분입니다. 이 함수는 전달된 속성을 사용하여 버튼을 동적으로 생성합니다.
-<button> HTML 요소가 생성됩니다. 이 요소의 속성은 disabled를 통해 isDisabled 속성에 따라 버튼이 활성화 또는 비활성화 되며, btnType 속성에 따라 버튼의 유형을 설정합니다. 기본값은 button이 됩니다.
-버튼에 적용되는 css 클래스는 custom-btn 및 containerStyles 속성을 통해 지정됩니다.
-버튼 텍스트는 title 속성에 따라 동적으로 설정 됩니다.
-right Icon 속성에 아이콘 이미지 URL이 제고오디면 이미지가 표시됩니다. 이미지는 next/image를 사용하여 효율적으로 렌더링 됩니다.
-이 버튼 컴포넌트는 매개변수로 전달된 속성을 기반으로 버튼을 렌더링 하며, 다양한 컨텍스트<footnote>React의 컴포넌트 컨텍스트를 의미함. 위에서 언급한 Button 컴포넌트는 여러 부모 컴포넌트에서 사용될 수 있고, 각 부모 컴포넌트에서는 다양한 속성을 전달하여 버튼의 동작과 모양을 조절할 수 있음. 이 때, 컨텍스트는 해당 컴포넌트가 어떤 상황이나 환경에서 사용되는지에 대한 배경을 나타냄. </footnote>에서 재사용할 수 있습니다. Custom Button Props를 통해 버튼의 모양과 동작을 설정하고 필요한 곳에서 이 컴포넌트를 사용할 수 있습니다.
✏ disabled
HTML, CSS, Javascript에서 자주 사용되는 속성으로, 주로 사용자 인터페이스 요소를 비활성화 하는데 사용됨.
특정 요소가 비활성화 되면 사용자는 그 요소와 상호작용 할 수 없음.
HTML
:폼 요소에 주로 사용됨.
예를 들어, 버튼, 입력 필트, 셀렉트 박스 등에 disabled 속성을 추가하면 사용자가 해당 요소를 클릭하거나 입력할 수 없음.
CSS
:disabled 상태에 있는 요소에 특정 스타일을 적용할 수 있음.
예를 들어, 비활성화 된 버튼은 회색으로 표시되도록 할 수 있음.
Javascript
:동적으로 disabled 속성을 추가하거나 제거할 수 있음.
예를 들어 버튼을 비활성화 하거나, 버튼을 다시 활성화 하는 방식으로 사용할 수 있음.
const button = document.querySelector('button');
button.disabled = true; // 버튼을 비활성화함
button.disabled = false; // 버튼을 다시 활성화함
Headless UI
: headless UI 컴포넌트에서도 disable 속성이 지원됨.
<Menu.Item disabled>
{({ active }) => (
<a
className={`${active ? 'bg-blue-500 text-white' : 'text-gray-900'} block px-4 py-2 text-sm`}
>
Disabled Item
</a>
)}
</Menu.Item>
이 경우, disable 속성이 적용된 메뉴 항목은 클릭할 수 없고 스타일도 달라지게 됨.
"use client";
import { Fragment, useState } from "react";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { Listbox, Transition } from "@headlessui/react";
import { CustomFilterProps } from "@/types";
import { updateSearchParams } from "@/utils";
export default function CustomFilter({ title, options }: CustomFilterProps) {
const router = useRouter();
const [selected, setSelected] = useState(options[0]);
const handleUpdateParams = (e: { title: string; value: string }) => {
const newPathName = updateSearchParams(title, e.value.toLowerCase());
router.push(newPathName);
};
return (
<div className="w-fit">
<Listbox
value={selected}
onChange={(e) => {
setSelected(e);
handleUpdateParams(e);
}}
>
<div className="relative w-fit z-10">
<Listbox.Button className="custom-filter__btn">
<span className="block truncate">{selected.title}</span>
<Image
src="/chevron-up-down.svg"
width={20}
height={20}
className="ml-4 object-contain"
alt="chevron up down"
/>
</Listbox.Button>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<Listbox.Options className="custom-filter__options">
{options.map((option) => (
<Listbox.Option
key={option.title}
className={({ active }) =>
`relative cursor-default select-none py-2 px-4 ${
active ? "bg-primary-blue text-white" : "text-gray-900"
}`
}
value={option}
>
{({ selected }) => (
<>
<span
className={`block truncate ${
selected ? "font-medium" : "font-normal"
}`}
>
{option.title}
</span>
</>
)}
</Listbox.Option>
))}
</Listbox.Options>
</Transition>
</div>
</Listbox>
</div>
);
}
Custom Filter
: Listbox를 사용하여 사용자가 특정 옵션을 선택할 수 있는 커스텀 필터를 만드는 React 컴포넌트입니다.
이 컴포넌트는 사용자에게 선택 옵션을 제공하고, 선택이 변경될 때마다 URL을 업데이트하여 페이지를 다시 로드하는 등의 기능을 수행합니다.
Details
-useState 훅을 사용하여 현재 선택된 옵션을 추적하며, 초깃값은 options 배열의 첫 번째 항목으로 설정됩니다.
-handleUpdateParams 함수는 옵션이 변경될 때 호출되는 함수로, 선택된 옵션을 기반으로 URL을 업데이트 합니다. 이 함수는 updateParams 유틸리티 함수<footnote>특정 작업을 수행하기 위해 만들어진 재사용 가능한 함수를 나타냄. 특정한 작업을 처리하거나 특정 값을 계산하는데 사용되며, 특정한 동작을 수행하는데 도움이 되는 함수들을 유틸리티 함수로 분리하여 작성하면 가독성이 높아지고 유지 보수가 쉬워짐.</footnote>를 사용하여 URL을 업데이트하고 페이지를 다시 로드합니다.
-Listbox 컴포넌트는 사용자에게 옵션을 선택할 수 있는 드롭다운 메뉴를 제공합니다. Headless UI<footnote>사용자 인터페이스를 만들기 위한 접근 방식 중 하나로, Headless UI에서 headless는 UI요소가 와관을 갖지 않고 스타일이나 레이아웃이 정의되어 있지 않다는 의미임. UI를 완전히 사용자 지정할 수 있도록 함. 이 패턴은 주로 React나 Vue 같은 컴포넌트 기반 라이브러리와 함께 사용됨. </footnote>에서 제공하는 컴포넌트로 외관 및 동작을 완전히 사용자 지정할 수 있습니다.
-Listbox.Button은 실제로 선택 상태를 보여주는 버튼 역할을 합니다.
-Listbox.Options는 옵션 목록을 나타냅니다. 이 목록은 선택한 옵션에 따라 동적으로 변합니다.
"use client";
import Image from "next/image";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import SearchManufacturer from "./SearchManufacturer";
const SearchButton = ({ otherClasses }: { otherClasses: string }) => (
<button type="submit" className={`-ml-3 z-10 ${otherClasses}`}>
<Image
src={"/magnifying-glass.svg"}
alt={"magnifying glass"}
width={40}
height={40}
className="object-contain"
/>
</button>
);
const SearchBar = () => {
const [manufacturer, setManuFacturer] = useState("");
const [model, setModel] = useState("");
const router = useRouter();
const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (manufacturer.trim() === "" && model.trim() === "") {
return alert("Please provide some input");
}
updateSearchParams(model.toLowerCase(), manufacturer.toLowerCase());
};
const updateSearchParams = (model: string, manufacturer: string) => {
const searchParams = new URLSearchParams(window.location.search);
if (model) {
searchParams.set("model", model);
} else {
searchParams.delete("model");
}
if (manufacturer) {
searchParams.set("manufacturer", manufacturer);
} else {
searchParams.delete("manufacturer");
}
const newPathname = `${
window.location.pathname
}?${searchParams.toString()}`;
router.push(newPathname);
};
return (
<form className="searchbar" onSubmit={handleSearch}>
<div className="searchbar__item">
<SearchManufacturer
manufacturer={manufacturer}
setManuFacturer={setManuFacturer}
/>
<SearchButton otherClasses="sm:hidden" />
</div>
<div className="searchbar__item">
<Image
src="/model-icon.png"
width={25}
height={25}
className="absolute w-[20px] h-[20px] ml-4"
alt="car model"
/>
<input
type="text"
name="model"
value={model}
onChange={(e) => setModel(e.target.value)}
placeholder="Tiguan..."
className="searchbar__input"
/>
<SearchButton otherClasses="sm:hidden" />
</div>
<SearchButton otherClasses="max-sm:hidden" />
</form>
);
};
export default SearchBar;
Search Bar
: 자동차 검색을 위한 검색 바를 나타냅니다.
이 컴포넌트는 자동차를 검색하는데 사용되며, 사용자가 검색어나 제조사를 입력하면 URL이 업데이트 되어 해당 검색 조건을 가진 자동차 목록을 표시합니다.
Details
-SearchManufacturer 컴포넌트를 통해 제조사 검색을 위한 드롭다운이나 자동완성 기능을 구현하고 있습니다. manufacturer 상태와 setManufacturer 함수로 상태를 업데이트 하고 있습니다.
-Search Button 검색 버튼 컴포넌트는 검색을 시작하는데 사용됩니다. 버튼에는 otherClasses라는 속성이 전달되며, 이는 추가적인 스타일을 적용하는데 사용됩니다.
-handleSearch 함수는 폼 제출 이벤트를 처리하며, 입력값이 유효한지 확인하며, 검색 매개변수를 업데이트한 다음 페이지를 다시 로드합니다.
-URL SearchParams는 updateParams 함수를 통해 URL 매개변수를 업데이트 합니다. 검색어와 제조사가 변경될 때마다 URL을 업데이트하여 해당 검색 조건을 반영합니다.
-Responsive Design 검색 바는 미디어 쿼리를 사용하여 화면의 크기에 따라 다르게 표시됩니다. 화면의 크기에 따라 드롭다운 기능이나 검색 창만 표시되도록 설정되어 있습니다.
import Image from "next/image";
import { Fragment, useState } from "react";
import { Combobox, Transition } from "@headlessui/react";
import { manufacturers } from "@/constants";
import { SearchManuFacturerProps } from "@/types";
const SearchManufacturer = ({
manufacturer,
setManuFacturer,
}: SearchManuFacturerProps) => {
const [query, setQuery] = useState("");
const filteredManufacturers =
query === ""
? manufacturers
: manufacturers.filter((item) =>
item
.toLowerCase()
.replace(/\s+/g, "")
.includes(query.toLowerCase().replace(/\s+/g, ""))
);
return (
<div className="search-manufacturer">
<Combobox value={manufacturer} onChange={setManuFacturer}>
<div className="relative w-full">
<Combobox.Button className="absolute top-[14px]">
<Image
src="/car-logo.svg"
width={20}
height={20}
className="ml-4"
alt="Car Logo"
/>
</Combobox.Button>
<Combobox.Input
className="search-manufacturer__input"
displayValue={(item: string) => item}
onChange={(event) => setQuery(event.target.value)}
placeholder="Volkswagen..."
/>
<Transition
as={Fragment}
leave="transition ease-in duration-100"
leaveFrom="opacity-100"
leaveTo="opacity-0"
afterLeave={() => setQuery("")}
>
<Combobox.Options
className="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"
static
>
{filteredManufacturers.length === 0 && query !== "" ? (
<Combobox.Option
value={query}
className="search-manufacturer__option"
>
Create "{query}"
</Combobox.Option>
) : (
filteredManufacturers.map((item) => (
<Combobox.Option
key={item}
className={({ active }) =>
`relative search-manufacturer__option ${
active ? "bg-primary-blue text-white" : "text-gray-900"
}`
}
value={item}
>
{({ selected, active }) => (
<>
<span
className={`block truncate ${
selected ? "font-medium" : "font-normal"
}`}
>
{item}
</span>
{selected ? (
<span
className={`absolute inset-y-0 left-0 flex items-center pl-3 ${
active
? "text-white"
: "text-pribg-primary-purple"
}`}
></span>
) : null}
</>
)}
</Combobox.Option>
))
)}
</Combobox.Options>
</Transition>
</div>
</Combobox>
</div>
);
};
export default SearchManufacturer;
SearchManufacturer
: 제조사를 검색하기 위한 드롭다운 또는 자동완성을 제공합니다.
제조사 검색을 쉽게 할 수 있도록 인터랙티브 하게(사용자와 상호작용할 수 있는 기능) 구현되었습니다.
Details
-Headless UI의 Combobox 컴포넌트를 사용하여 검색 및 드롭다운 기능을 구현합니다. 이를 통해 제조사 목록에서 선택하거나 새로운 제조사를 추가할 수 있습니다.
-useState에서는 query라는 상태를 사용하여 검색어를 추적하고, setQuery 함수를 통해 이를 업데이트합니다. 또한 filteredManufacturers 상태를 이용하여 현재 검색어에 따라 필터링 된 제조사 목록을 저장합니다.
-Combobox.Input은 사용자가 검색어를 입력하는 입력란입니다. onChange 핸들러를 사용하여 검색어가 변경될 때마다 setQuery 함수를 호출하여 query 상태를 업데이트합니다.
✏ useState
:React에서 상태(State)를 관리하기 위한 훅(hook)
ex.
const [state, setState] = useState(initialValue);
state: 현재 상태 값
setState: 상태 값을 업데이트하는 함수
initiaValue: 상태의 초기값
useState에서 query와 setQuery의 역할
ex.
const [query, setQuery] = useState('');
query
: 이 변수는 검색어를 저장하는 상태 값임.
예를 들어 사용자가 검색 입력란에 React라는 단어를 입력하면 query 상태는 이 값을 저장함. 즉, query는 현재 입력된 검색어를 추적하고 보관하는 역할을 함.
setQuery
:이 함수는 query 상태를 업데이트 하는데 사용됨.
사용자가 입력란에 새로운 검색어를 입력할 때마다 이 함수가 호출되어 query 상태를 최신값으로 변경함.
예를 들어, 사용자가 입력을 변경할 때마다 setQuery(newValue)를 호출하여 query의 값을 newValue(새로운 값)으로 업데이트 할 수 있음.
import { CarProps, FilterProps } from "@/types";
export const calculateCarRent = (city_mpg: number, year: number) => {
const basePricePerDay = 50;
const mileageFactor = 0.1;
const ageFactor = 0.05;
const mileageRate = city_mpg * mileageFactor;
const ageRate = (new Date().getFullYear() - year) * ageFactor;
const rentalRatePerDay = basePricePerDay + mileageRate + ageRate;
return rentalRatePerDay.toFixed(0);
};
export const updateSearchParams = (type: string, value: string) => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set(type, value);
const newPathname = `${window.location.pathname}?${searchParams.toString()}`;
return newPathname;
};
export const deleteSearchParams = (type: string) => {
const newSearchParams = new URLSearchParams(window.location.search);
newSearchParams.delete(type.toLocaleLowerCase());
const newPathname = `${
window.location.pathname
}?${newSearchParams.toString()}`;
return newPathname;
};
export async function fetchCars(filters: FilterProps) {
const { manufacturer, year, model, limit, fuel } = filters;
const headers: HeadersInit = {
"X-RapidAPI-Key": "7ffff88ee4msh3ef184e28e73bb3p172057jsndfdd6b4649d7",
// "X-RapidAPI-Key": "process.env.NEXT_PUBLIC_IMAGIN_API_KEY || "", 해당 환경 변수가 설정 되어 있을 경우 해당 값 사용, 비어있는 경우 빈 문자열 사용
"X-RapidAPI-Host": "cars-by-api-ninjas.p.rapidapi.com",
};
const response = await fetch(
`https://cars-by-api-ninjas.p.rapidapi.com/v1/cars?make=${manufacturer}&year=${year}&model=${model}&limit=${limit}&fuel_type=${fuel}`,
{
headers: headers,
}
);
const result = await response.json();
return result;
}
export const generateCarImageUrl = (car: CarProps, angle?: string) => {
const url = new URL("https://cdn.imagin.studio/getimage");
const { make, model, year } = car;
url.searchParams.append("customer", "hrjavascript-mastery");
// url.searchParams.append(
// "customer",
// process.env.NEXT_PUBLIC_IMAGIN_API_KEY || "" 해당 환경 변수(hrjavascript-mastery)가 설정 되어 있을 경우 해당 값 사용, 비어있는 경우 빈 문자열 사용
// );
url.searchParams.append("make", make);
url.searchParams.append("modelFamily", model.split(" ")[0]);
url.searchParams.append("zoomType", "fullscreen");
url.searchParams.append("modelYear", `${year}`);
url.searchParams.append("angle", `${angle}`);
return `${url}`;
};
utils - index.ts
: type script를 사용하여 React 애플리케이션에서 사용되는 여러 유틸리티 함수들을 정의하고 있습니다.
Details
-calculateCarRent
: 자동차 렌탈 요금을 계산하는 함수입니다. 주행 마일리지와 자동차 년도를 고려하여 기본 렌탈 요금을 계산합니다.
city_mpg는 주행 마일리지, year는 자동차의 생산 년도입니다. 기본 일일 렌탈 요금, 주행 마일리지에 따른 추가 비용, 그리고 자동차의 나이에 따른 추가 비용을 고려하여 총 렌탈 요금을 계산하고 그 값을 문자열로 반환합니다.
-updateSearchParams
:현재 URL의 쿼리 매개변수를 업데이트하는 함수입니다. 주어진 유형(type)과 값(value)을 사용하여 매개변수를 설정하고, 업데이트된 URL을 반환합니다.
type은 업데이트할 매개변수의 이름이고, value는 해당 매개변수에 설정할 값입니다. 그리고 업데이트 된 URL을 반환합니다.
-deleteSearchParams
:현재 URL의 특정 쿼리 매개변수를 삭제하는 함수입니다. 유형에 해당하는 매개변수를 제거하고, 업데이트된 URL을 반환합니다.
여기서의 type은 삭제할 매개변수의 이름입니다. 업데이트된 URL을 반환합니다.
-fetchCars
: 외부 API에서 자동차 정보를 가져오는 비동기 함수입니다. 주어진 필터를 사용하여 API에 요청하고, 결과를 반환합니다.
filters 객체는 제조사, 연도, 모델 등을 지정하는데 사용됩니다.
-generateCarImageUrl
:Image 스튜디오 API를 사용하여 자동차 이미지 URL을 생성하는 함수입니다. 주어진 자동차 정보와 각도(angle)을 고려하여 이미지 URL을 생성하고 반환합니다.