Skip to content

Commit

Permalink
refactor: 홈 화면 배너 구현 방식 변경 (#42)
Browse files Browse the repository at this point in the history
  • Loading branch information
coggiee authored Jun 4, 2024
1 parent da898b2 commit 3e01b5c
Show file tree
Hide file tree
Showing 7 changed files with 143 additions and 148 deletions.
26 changes: 26 additions & 0 deletions apps/user/src/pages/home/_components/carousel/Carousel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@tailwind components;

@layer components {
.embla__controls {
@apply absolute bottom-[5%] left-2/4 z-50 mt-[1.8rem] grid -translate-x-2/4 grid-cols-[auto_1fr] justify-between gap-[1.2rem];
}

.embla__dots {
@apply mr-[calc((2.6rem_-_1.4rem)_/_2_*_-1)] flex flex-nowrap items-center justify-end gap-0.5;
}

.embla__dot {
@apply m-0 flex h-4 w-4 cursor-pointer touch-manipulation appearance-none items-center justify-center rounded-[50%] border-0 bg-transparent p-0 no-underline;
-webkit-tap-highlight-color: rgba(49, 49, 49, 0.5);
-webkit-appearance: none;
}

.embla__dot:after {
@apply flex h-[0.7rem] w-[0.7rem] items-center rounded-[50%] content-[""];
@apply bg-gray-400 shadow-lg;
}

.embla__dot--selected:after {
@apply bg-white shadow-lg;
}
}
134 changes: 68 additions & 66 deletions apps/user/src/pages/home/_components/carousel/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -1,91 +1,93 @@
import { useCallback, useEffect, useState } from "react";
import useEmblaCarousel from "embla-carousel-react";
import { EmblaCarouselType, EmblaOptionsType } from "embla-carousel";
import { useEffect, useState } from "react";
import {
Carousel,
CarouselApi,
CarouselContent,
CarouselItem,
} from "@uket/ui/components/ui/carousel";
import { Card, CardContent } from "@uket/ui/components/ui/card";

import Indicator from "@/components/Indicator";

import { useDotButton } from "@/hooks/useDotButton";

import { FestivalInfo } from "@/types/univType";

import { LazyLoadImage } from "./CarouselLazyImage";
import { DotButton } from "./CarouselDotButton";
import "@/styles/Carousel.css";
import CarouselDotButtonList from "./CarouselDotButtonList";

interface PropType {
slides: FestivalInfo["banners"] | undefined;
}

const OPTIONS: EmblaOptionsType = { align: "start", loop: true };

const Carousel = (props: PropType) => {
const CarouselT = (props: PropType) => {
const { slides } = props;
const [emblaRef, emblaApi] = useEmblaCarousel(OPTIONS);
const [emblaApi, setEmblaApi] = useState<CarouselApi>();
const [slidesInView, setSlidesInView] = useState<number[]>([]);

const { selectedIndex, scrollSnaps, onDotButtonClick } =
useDotButton(emblaApi);
useEffect(() => {
if (!emblaApi) {
return;
}

const updateSlidesInView = useCallback((emblaApi: EmblaCarouselType) => {
setSlidesInView(slidesInView => {
if (slidesInView.length === emblaApi.slideNodes().length) {
emblaApi.off("slidesInView", updateSlidesInView);
}
const inView = emblaApi
.slidesInView()
.filter((index: number) => !slidesInView.includes(index));
return slidesInView.concat(inView);
});
}, []);
const updateSlidesInView = () => {
setSlidesInView(slidesInView => {
if (slidesInView.length === emblaApi.slideNodes().length) {
emblaApi.off("slidesInView", updateSlidesInView);
}
const inView = emblaApi
.slidesInView()
.filter((index: number) => !slidesInView.includes(index));
return slidesInView.concat(inView);
});
};

emblaApi.on("slidesInView", updateSlidesInView);
emblaApi.on("reInit", updateSlidesInView);
}, [emblaApi]);

const slideComponent = !slides ? (
<div className="embla__slide">
<p className="flex h-full w-full flex-col items-center justify-center rounded-lg bg-gray-100 text-gray-500">
배너가 존재하지 않아요 😢
</p>
</div>
<CarouselItem>
<div className="p-1">
<Card>
<CardContent className="flex aspect-square items-center justify-center p-6">
<p className="flex h-full w-full flex-col items-center justify-center rounded-lg bg-gray-100 text-gray-500">
배너가 존재하지 않아요 😢
</p>
</CardContent>
</Card>
</div>
</CarouselItem>
) : (
slides.map(({ title, url }, index) => (
<div className="embla__slide relative" key={url}>
<LazyLoadImage
index={index}
imgSrc={url}
inView={slidesInView.indexOf(index) > -1}
/>
<Indicator title={title} variant={"banner"} />
</div>
<CarouselItem key={url} className="basis-full">
<div className="p-1">
<Card className="border-none">
<CardContent className="relative h-44 rounded-lg p-0 shadow-md sm:h-80 lg:h-96">
<LazyLoadImage
imgSrc={url}
inView={slidesInView.indexOf(index) > -1}
/>
<Indicator
title={title}
variant={"banner"}
className="left-3 top-3"
/>
</CardContent>
</Card>
</div>
</CarouselItem>
))
);

useEffect(() => {
if (!emblaApi) return;

updateSlidesInView(emblaApi);
emblaApi.on("slidesInView", updateSlidesInView);
emblaApi.on("reInit", updateSlidesInView);
}, [emblaApi, updateSlidesInView]);

return (
<section className="embla h-44 w-full rounded-lg shadow-md sm:h-80 lg:h-96">
<div className="embla__viewport h-full w-full" ref={emblaRef}>
<div className="embla__container h-full cursor-pointer">
{slideComponent}
</div>
<div className="embla__controls">
<div className="embla__dots">
{scrollSnaps.map((_, index) => (
<DotButton
key={index}
onClick={() => onDotButtonClick(index)}
className={"embla__dot".concat(
index === selectedIndex ? " embla__dot--selected" : "",
)}
/>
))}
</div>
</div>
</div>
</section>
<Carousel
className="w-full max-w-full"
opts={{ align: "start" }}
setApi={setEmblaApi}
>
<CarouselContent>{slideComponent}</CarouselContent>
<CarouselDotButtonList emblaApi={emblaApi} />
</Carousel>
);
};

export default Carousel;
export default CarouselT;
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
import React, { PropsWithChildren } from "react";
import { cn } from "@uket/ui/lib/utils";

type PropType = PropsWithChildren<
type CarouselDotButtonProps = PropsWithChildren<
React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
>
>;
> & { selected: boolean };

export const DotButton = (props: PropType) => {
const { children, ...restProps } = props;
export const CarouselDotButton = (props: CarouselDotButtonProps) => {
const { children, className, selected, ...rest } = props;

return (
<button type="button" {...restProps}>
<button
type="button"
className={cn(
"embla__dot",
className,
selected && " embla__dot--selected",
)}
{...rest}
>
{children}
</button>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { CarouselApi } from "@uket/ui/components/ui/carousel";

import { useDotButton } from "@/hooks/useDotButton";

import { CarouselDotButton } from "./CarouselDotButton";

import "./Carousel.css";

interface CarouselDotButtonListProps {
emblaApi: CarouselApi;
}

const CarouselDotButtonList = (props: CarouselDotButtonListProps) => {
const { emblaApi } = props;
const { selectedIndex, scrollSnaps, onDotButtonClick } =
useDotButton(emblaApi);

return (
<div className="embla__controls">
<div className="embla__dots">
{scrollSnaps.map((_, index) => (
<CarouselDotButton
key={index}
onClick={() => onDotButtonClick(index)}
selected={index === selectedIndex}
/>
))}
</div>
</div>
);
};

export default CarouselDotButtonList;
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { useState, useCallback } from "react";
import { Skeleton } from "@uket/ui/components/ui/skeleton";

interface PropType {
interface LazyLoadImageProps {
imgSrc: string;
inView: boolean;
index: number;
}

export const LazyLoadImage = (props: PropType) => {
export const LazyLoadImage = (props: LazyLoadImageProps) => {
const { imgSrc, inView } = props;
const [hasLoaded, setHasLoaded] = useState(false);

Expand Down
72 changes: 0 additions & 72 deletions apps/user/src/styles/Carousel.css

This file was deleted.

2 changes: 0 additions & 2 deletions packages/ui/src/globals.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
/* @import "@/styles/Carousel.css"; */

@tailwind base;
@tailwind components;
@tailwind utilities;
Expand Down

0 comments on commit 3e01b5c

Please sign in to comment.