Skip to content

Commit

Permalink
홈페이지 슬라이딩 배너 (#20)
Browse files Browse the repository at this point in the history
* feat: 배너 기본 컴포넌트 구현

* refactor: 이미지 컨테이너 컴포넌트 분리

* feat: 스토리북 추가

* feat: 슬라이더 버튼 추가

* style: 홈페이지 마진
  • Loading branch information
howons authored May 17, 2024
1 parent 3f9679c commit 6f73f4d
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 14 deletions.
1 change: 1 addition & 0 deletions app/lib/constants/ banner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const BANNER_IMAGES = ["/당근.svg", "/번개장터.svg", "/중고나라.svg"];
6 changes: 4 additions & 2 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import Banner from "@ui/Banner";
import Search from "@ui/Search";

export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-12 md:p-24 ">
<Search className="w-full" />
<main className="flex min-h-screen flex-col items-center justify-between pb-28 pt-14">
<Banner className="mb-8" />
<Search className="mt-6 w-full" />
</main>
);
}
25 changes: 25 additions & 0 deletions app/ui/Banner/ImageContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { BANNER_IMAGES } from "@lib/constants/ banner";
import Image from "next/image";
import { HTMLAttributes } from "react";

function ImageContainer({
className = "",
...props
}: HTMLAttributes<HTMLDivElement>) {
return (
<div className={`flex shrink-0 ${className}`} {...props}>
{BANNER_IMAGES.map((image) => (
<Image
key={image}
src={image}
alt="banner"
width={360}
height={256}
className="h-full w-screen bg-stone-300 object-contain"
/>
))}
</div>
);
}

export default ImageContainer;
46 changes: 46 additions & 0 deletions app/ui/Banner/SliderButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"use client";

import { BANNER_IMAGES } from "@lib/constants/ banner";
import { Dispatch, HTMLAttributes, MouseEvent, SetStateAction } from "react";

interface SliderButtonProps extends HTMLAttributes<HTMLDivElement> {
curImage: number;
setCurImage: Dispatch<SetStateAction<number>>;
intervalRef: React.MutableRefObject<NodeJS.Timeout | null>;
}

function SliderButton({
curImage,
setCurImage,
intervalRef,
className,
...props
}: SliderButtonProps) {
const handleClick = (idx: number) => (e: MouseEvent<HTMLButtonElement>) => {
setCurImage(idx);

clearInterval(intervalRef.current!);
intervalRef.current = setInterval(() => {
setCurImage((prev) => prev + 1);
}, 3000);
};

const buttonDefaultStyle =
"w-3 h-3 rounded-full bg-stone-400 hover:bg-stone-300 transition-all duration-300 origin-center";
const buttonActiveStyle = (idx: number) =>
idx === curImage % BANNER_IMAGES.length ? "cs:bg-stone-200 scale-150" : "";

return (
<div className={`flex justify-center gap-3 p-4 ${className}`} {...props}>
{BANNER_IMAGES.map((image, idx) => (
<button
key={image}
className={`${buttonDefaultStyle} ${buttonActiveStyle(idx)}`}
onClick={handleClick(idx)}
/>
))}
</div>
);
}

export default SliderButton;
65 changes: 65 additions & 0 deletions app/ui/Banner/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"use client";

import { BANNER_IMAGES } from "@lib/constants/ banner";
import ImageContainer from "@ui/Banner/ImageContainer";
import SliderButton from "@ui/Banner/SliderButton";
import { HTMLAttributes, useEffect, useRef, useState } from "react";

function Banner({ className = "", ...props }: HTMLAttributes<HTMLDivElement>) {
const [curImage, setCurImage] = useState(0);
const [isReturning, setIsReturning] = useState(false);
const intervalRef = useRef<NodeJS.Timeout | null>(null);

useEffect(() => {
intervalRef.current = setInterval(() => {
setCurImage((prev) => prev + 1);
}, 3000);

return () => clearInterval(intervalRef.current!);
}, []);

useEffect(() => {
if (curImage >= BANNER_IMAGES.length) {
setTimeout(() => {
setIsReturning(true);
setCurImage(0);

setTimeout(() => {
setIsReturning(false);
}, 100);
}, 500);
}
}, [curImage]);

const imageTransitionStyle = isReturning
? ""
: "transition-transform duration-500";

const imageTranslateStyle = [
"",
"-translate-x-[100vw]",
"-translate-x-[200vw]",
"-translate-x-[300vw]",
];

return (
<div
className={`relative flex h-64 w-full overflow-hidden ${className}`}
{...props}>
<ImageContainer
className={`${imageTransitionStyle} ${imageTranslateStyle[curImage]}`}
/>
<ImageContainer
className={`${imageTransitionStyle} ${imageTranslateStyle[curImage]}`}
/>
<SliderButton
curImage={curImage}
setCurImage={setCurImage}
intervalRef={intervalRef}
className="absolute bottom-0 left-0 w-full"
/>
</div>
);
}

export default Banner;
23 changes: 11 additions & 12 deletions app/ui/Header/HeaderSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,26 +11,25 @@ import { SearchStoreProvider } from "@lib/providers/SearchStoreProvider";
import SearchBar from "@ui/SearchBar";
import SearchList from "@ui/SearchList";
import { usePathname } from "next/navigation";
import { MouseEventHandler } from "react";
import { MouseEvent } from "react";

interface HeaderSearchProps extends PopoverProps {}

function HeaderSearch({ className = "", ...props }: HeaderSearchProps) {
const pathname = usePathname();
if (pathname === "/") return null;

const handlePopoverButtonClick: (
open: boolean
) => MouseEventHandler<HTMLDivElement> = (open) => (e) => {
if (open) {
e.preventDefault();
}
const handlePopoverButtonClick =
(open: boolean) => (e: MouseEvent<HTMLDivElement>) => {
if (open) {
e.preventDefault();
}

const eventTarget = e.target as HTMLElement;
if (eventTarget.tagName === "INPUT") {
eventTarget.focus();
}
};
const eventTarget = e.target as HTMLElement;
if (eventTarget.tagName === "INPUT") {
eventTarget.focus();
}
};

const popoverDefaultStyle =
"flex h-14 flex-col items-center overflow-hidden transition-all duration-300";
Expand Down
16 changes: 16 additions & 0 deletions stories/Banner.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { Meta, StoryObj } from "@storybook/react";
import Banner from "@ui/Banner";

const meta = {
title: "ui/Banner",
component: Banner,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
} satisfies Meta<typeof Banner>;

export default meta;
type Story = StoryObj<typeof meta>;

export const HomeBanner: Story = {};

0 comments on commit 6f73f4d

Please sign in to comment.