diff --git "a/public/svgs/\353\260\260\353\204\210.svg" "b/public/svgs/\353\260\260\353\204\210.svg" new file mode 100644 index 0000000..b22aa95 --- /dev/null +++ "b/public/svgs/\353\260\260\353\204\210.svg" @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/components/common/LandingBanner.tsx b/src/components/common/LandingBanner.tsx new file mode 100644 index 0000000..a7bc631 --- /dev/null +++ b/src/components/common/LandingBanner.tsx @@ -0,0 +1,73 @@ +import Image from 'next/image'; +import { BannerCard, BannerCardContent } from '../ui/BannerCard'; +import Autoplay from 'embla-carousel-autoplay'; +import { Button } from '../ui/button'; +import { + BannerCarousel, + BannerCarouselContent, + BannerCarouselItem, + BannerCarouselNext, + BannerCarouselPrevious, + CarouselApi, +} from '../ui/BannerCarousel'; +import React from 'react'; + +export function LandingBanner() { + const [api, setApi] = React.useState(); + const [current, setCurrent] = React.useState(0); + const [count, setCount] = React.useState(0); + + React.useEffect(() => { + if (!api) { + return; + } + + setCount(api.scrollSnapList().length); + setCurrent(api.selectedScrollSnap() + 1); + + api.on('select', () => { + setCurrent(api.selectedScrollSnap() + 1); + }); + }, [api]); + return ( +
+ + + + {Array.from({ length: 3 }).map((_, index) => ( + + + + {index === 0 ? ( + 배너 이미지 + ) : index === 1 ? ( + 배너 이미지 + ) : index === 2 ? ( + 배너 이미지 + ) : null} + + + + ))} + + +
+ {Array.from(Array(count).keys()).map((_, index) => ( + + ))} +
+
+
+ ); +} diff --git a/src/components/ui/BannerCard.tsx b/src/components/ui/BannerCard.tsx new file mode 100644 index 0000000..20c69c2 --- /dev/null +++ b/src/components/ui/BannerCard.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; + +import { cn } from '@/lib/utils'; + +const BannerCard = React.forwardRef>( + ({ className, ...props }, ref) =>
, +); +BannerCard.displayName = 'Card'; + +const BannerCardHeader = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +BannerCardHeader.displayName = 'CardHeader'; + +const BannerCardTitle = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ), +); +BannerCardTitle.displayName = 'CardTitle'; + +const BannerCardDescription = React.forwardRef>( + ({ className, ...props }, ref) => ( +

+ ), +); +BannerCardDescription.displayName = 'CardDescription'; + +const BannerCardContent = React.forwardRef>( + ({ className, ...props }, ref) =>

, +); +BannerCardContent.displayName = 'CardContent'; + +const BannerCardFooter = React.forwardRef>( + ({ className, ...props }, ref) => ( +
+ ), +); +BannerCardFooter.displayName = 'CardFooter'; + +export { BannerCard, BannerCardHeader, BannerCardFooter, BannerCardTitle, BannerCardDescription, BannerCardContent }; diff --git a/src/components/ui/BannerCarousel.tsx b/src/components/ui/BannerCarousel.tsx new file mode 100644 index 0000000..7022576 --- /dev/null +++ b/src/components/ui/BannerCarousel.tsx @@ -0,0 +1,229 @@ +import * as React from 'react'; +import useEmblaCarousel, { type UseEmblaCarouselType } from 'embla-carousel-react'; +import { IoIosArrowBack, IoIosArrowForward } from 'react-icons/io'; + +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; + +type CarouselApi = UseEmblaCarouselType[1]; +type UseCarouselParameters = Parameters; +type CarouselOptions = UseCarouselParameters[0]; +type CarouselPlugin = UseCarouselParameters[1]; + +type CarouselProps = { + opts?: CarouselOptions; + plugins?: CarouselPlugin; + orientation?: 'horizontal' | 'vertical'; + setApi?: (api: CarouselApi) => void; +}; + +type CarouselContextProps = { + carouselRef: ReturnType[0]; + api: ReturnType[1]; + scrollPrev: () => void; + scrollNext: () => void; + canScrollPrev: boolean; + canScrollNext: boolean; +} & CarouselProps; + +const CarouselContext = React.createContext(null); + +function useCarousel() { + const context = React.useContext(CarouselContext); + + if (!context) { + throw new Error('useCarousel must be used within a '); + } + + return context; +} + +const BannerCarousel = React.forwardRef & CarouselProps>( + ({ orientation = 'horizontal', opts, setApi, plugins, className, children, ...props }, ref) => { + const [carouselRef, api] = useEmblaCarousel( + { + ...opts, + axis: orientation === 'horizontal' ? 'x' : 'y', + }, + plugins, + ); + const [canScrollPrev, setCanScrollPrev] = React.useState(false); + const [canScrollNext, setCanScrollNext] = React.useState(false); + + const onSelect = React.useCallback((api: CarouselApi) => { + if (!api) { + return; + } + + setCanScrollPrev(api.canScrollPrev()); + setCanScrollNext(api.canScrollNext()); + }, []); + + const scrollPrev = React.useCallback(() => { + api?.scrollPrev(); + }, [api]); + + const scrollNext = React.useCallback(() => { + api?.scrollNext(); + }, [api]); + + const handleKeyDown = React.useCallback( + (event: React.KeyboardEvent) => { + if (event.key === 'ArrowLeft') { + event.preventDefault(); + scrollPrev(); + } else if (event.key === 'ArrowRight') { + event.preventDefault(); + scrollNext(); + } + }, + [scrollPrev, scrollNext], + ); + + React.useEffect(() => { + if (!api || !setApi) { + return; + } + + setApi(api); + }, [api, setApi]); + + React.useEffect(() => { + if (!api) { + return; + } + + onSelect(api); + api.on('reInit', onSelect); + api.on('select', onSelect); + + return () => { + api?.off('select', onSelect); + }; + }, [api, onSelect]); + + return ( + +
+ {children} +
+
+ ); + }, +); +BannerCarousel.displayName = 'Carousel'; + +const BannerCarouselContent = React.forwardRef>( + ({ className, ...props }, ref) => { + const { carouselRef, orientation } = useCarousel(); + + return ( +
+
+
+ ); + }, +); +BannerCarouselContent.displayName = 'CarouselContent'; + +const BannerCarouselItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const { orientation } = useCarousel(); + + return ( +
+ ); + }, +); +BannerCarouselItem.displayName = 'CarouselItem'; + +const BannerCarouselPrevious = React.forwardRef>( + ({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollPrev, canScrollPrev } = useCarousel(); + + return ( + + ); + }, +); +BannerCarouselPrevious.displayName = 'CarouselPrevious'; + +const BannerCarouselNext = React.forwardRef>( + ({ className, variant = 'outline', size = 'icon', ...props }, ref) => { + const { orientation, scrollNext, canScrollNext } = useCarousel(); + + return ( + + ); + }, +); +BannerCarouselNext.displayName = 'CarouselNext'; + +export { + type CarouselApi, + BannerCarousel, + BannerCarouselContent, + BannerCarouselItem, + BannerCarouselPrevious, + BannerCarouselNext, +}; diff --git a/src/pages/landing/index.tsx b/src/pages/landing/index.tsx index c09bf2b..095cbd8 100644 --- a/src/pages/landing/index.tsx +++ b/src/pages/landing/index.tsx @@ -1,7 +1,9 @@ +import { LandingBanner } from '@/components/common/LandingBanner'; + export default function Landing() { return ( - <> -

랜딩

- +
+ +
); }