Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat(design-system): 홈 배너 상단 캐러셀 구현 #80

Merged
merged 18 commits into from
Jan 16, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions apps/client/src/pages/home/page/home.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { style } from '@vanilla-extract/css';
import { themeVars } from '@confeti/design-system/styles';

export const mainStyle = style({
paddingTop: '3rem',
background: themeVars.color.confeti_purple_grad,
});
12 changes: 10 additions & 2 deletions apps/client/src/pages/home/page/home.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Footer, Navigation } from '@confeti/design-system';
import { Footer, TopCarousel, Navigation } from '@confeti/design-system';
import { PERFORMANCE_DATA } from '@shared/mocks/top-carousel-mock';
import * as styles from './home.css';

import { TAB_MENU } from '../constants/menu';

const Home = () => {
@@ -11,10 +14,15 @@ const Home = () => {
</Navigation.List>
<Navigation.Panels>
{/* TODO: 추후 페이지 연결 */}
<Navigation.Panel>홈페이지</Navigation.Panel>
<Navigation.Panel>
<div className={styles.mainStyle}>
<TopCarousel performData={PERFORMANCE_DATA}></TopCarousel>
</div>
</Navigation.Panel>
<Navigation.Panel>타임테이블</Navigation.Panel>
</Navigation.Panels>
</Navigation.Root>

<Footer />
</>
);
65 changes: 65 additions & 0 deletions apps/client/src/shared/mocks/top-carousel-mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
export const PERFORMANCE_DATA = [
{
performanceId: 1,
type: 'concert',
title: '오아시스 내한공연1',
subTitle: 'LIVE NATION PRESENTS COLDPLAY',
performanceAt: '2025.10.21',
posterUrl:
'https://s3-alpha-sig.figma.com/img/961e/48da/36bf2a709612a4562cfb56af542eb961?Expires=1737936000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=gTKYxz-Jsf68n0V2ZbrooxAwXN4r8tDUw1xLkmhhk1OnG2-nrLIxsnTOQiDaZOS1cC5ZvPZ3MyoVIbCYSa5qnnKIkyJ68oemZ-UD8Nmj2wCs7PsbbCF7yhGTUhhGnivzVFpYkpBrM91J0ZEhaFP4N4pdBoA~-ad15QfDaBJEcYKlD49bOUcYSN6Eu3kb3OzDPaAZjrBOBRTPM7iDIDMrQD5qIbSJgew9OPXnrHB0ZS4yD1QXrI5-2Zrvx9RpTEPxHstR88q8yw5VgXsKReKdSpV1CKVhh3oQ~uWmn0gSN1IQW097zpPR6b0E8fQHB1HLig1EhriCZhQmUYBjDOcBgg__',
},
{
performanceId: 2,
type: 'festival',
title: '오아시스 내한공연2',
subTitle: '',
performanceAt: '2025.10.21',
posterUrl:
'https://s3-alpha-sig.figma.com/img/f03d/9a2c/4854283ea449b35300d949bc3d7e3bcb?Expires=1737936000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=bJBmX7kA6jAfx1I5CLhFl763ycOSD6eL3Gkg~Immdm8cGWDjSmOcW0VU1X23~q-wgDp0uTya6O9oMTy6dpHlkedb8RbZnTHMNlhVXbbNU5-TVwVJSTpIkCj8BkC8ble~jKhESsWt6OZq3f2uA5j4ESb-VaYMuq7iK-LlBTXcgKFfyZC5o7z2Y-7uASgM6-uWitKCsUUUXON4tft1PMI4~asveZyc4k2-ZGCyd0dwpNONefXkwXMwgYjmoOK12uzB7dKoSrGJPcWEDMYsdZHbG8x5rLsBVe87U~eTmfN5s-U5dDu0Sv0~u8s~W4vddQh0CaZSlqSViO5I373KI7pHOQ__',
},
{
performanceId: 3,
type: 'festival',
title: '오아시스 내한공연3',
subTitle: 'LIVE NATION PRESENTS COLDPLAY',
performanceAt: '2025.10.21',
posterUrl:
'https://s3-alpha-sig.figma.com/img/961e/48da/36bf2a709612a4562cfb56af542eb961?Expires=1737936000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=gTKYxz-Jsf68n0V2ZbrooxAwXN4r8tDUw1xLkmhhk1OnG2-nrLIxsnTOQiDaZOS1cC5ZvPZ3MyoVIbCYSa5qnnKIkyJ68oemZ-UD8Nmj2wCs7PsbbCF7yhGTUhhGnivzVFpYkpBrM91J0ZEhaFP4N4pdBoA~-ad15QfDaBJEcYKlD49bOUcYSN6Eu3kb3OzDPaAZjrBOBRTPM7iDIDMrQD5qIbSJgew9OPXnrHB0ZS4yD1QXrI5-2Zrvx9RpTEPxHstR88q8yw5VgXsKReKdSpV1CKVhh3oQ~uWmn0gSN1IQW097zpPR6b0E8fQHB1HLig1EhriCZhQmUYBjDOcBgg__',
},
{
performanceId: 4,
type: 'festival',
title: '오아시스 내한공연4',
subTitle: '',
performanceAt: '2025.10.21',
posterUrl:
'https://s3-alpha-sig.figma.com/img/46b2/a7e8/7992b9ede09843fff6ebf8b566713967?Expires=1737936000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=lyWMYVAsud4Do0xSlFj4Ex59sruTIo9ZQhQLPbLtxW9W5wOn0ppLfC81tFHXWgiyXKK4OiHc79CouyY0jFdN1AIrsGM3eKdJDjEh8yU0zQxH5Pyp5aOqX4uaiWqMP17Cyj7xaLrKJgDWPAwJAOsonzns~ihB67MrZKxdEqKo0F5nM-nYJwgYLQSlL5kmQwAm3ig2QUvSRNHhDWw2XPMD6Ng9JktfeWv4npBoRRG8WpboXSt2O72cVoNZbxPFuh1fBY5jRC4oNbucMiHRKCWRst-bRrDNKI-gS8segXXyGAXOyPTIls0SZ~bjOzFfSDmVzvmOvQ-Q4~xh502bV1tziQ__',
},
{
performanceId: 5,
type: 'festival',
title: '오아시스 내한공연5',
subTitle: 'LIVE NATION PRESENTS COLDPLAY',
performanceAt: '2025.10.21',
posterUrl:
'https://s3-alpha-sig.figma.com/img/abe0/2e69/dd0f1adcebbc0d20cddfb46a0c10804a?Expires=1737936000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=frNVG9N10Y91eyJMOmBpHIfdL8MLvmJQYxcwGaeT0so8tFo57AyRO7N9Km5xUQ5Oi~afEqV1GVaMdh5cNH5CdU3qBrrMnMR2sEt4czvwPaGv-MraYWdXqvw-zoetcpDGihs9L-lRJ-jicTq~Fqz6UxKlREMGkAZ6kuItMFsuiKH~jQL0CjI7EwO1Bct6-iFJp7iKtO8I8h0tXCA2wScjmpCUAaro1adoMKVcLOeFNtKZo5TzirO5giGTtHCFRilR9uc0wl-P~ZehLklPRr~9BoPwUUs4O~67RlAFv38yoDFP~gdQ6zDuiU81u5Nmfc1ic52vKhzD4kU3TDDJXRXDZQ__',
},
{
performanceId: 6,
type: 'festival',
title: '오아시스 내한공연6',
subTitle: 'LIVE NATION PRESENTS COLDPLAY',
performanceAt: '2025.10.21',
posterUrl:
'https://s3-alpha-sig.figma.com/img/ddaf/3f4b/77a4449254ce1360254377e752a0f278?Expires=1737936000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=DCAfi~ihZuq2YEIag2pfg8fj~BlBK9lhdf9l7MQe7Fb-kp8Jn0hqDyAD87jpbwQ3fgl-ZTkabk9zyfyZSUtD-eEuza-Or-706rik8uYXbZ8aCKydal66nuBDWVMNqe2xeg9qOf0m7xI5ePr1CHMbTs7zo3d2p-x~tIMfN16i4dz94aVrLgOnHq8LiG77Z7euT-RTQXoejMiTB8DJnME2peonChLdhTUKKApnfAw5HWZ8eHDk06d66LeVeQANuiITXu01Bd52Xd88-JjpQ~7TNjICViq8k96JQ3MJBo3wo~7msgf1e7WvXT59yaL~TNpu~1XeRtyO5IqoGiV-Uq3BSw__',
},
{
performanceId: 7,
type: 'festival',
title: '오아시스 내한공연7',
subTitle: '',
performanceAt: '2025.10.21',
posterUrl:
'https://s3-alpha-sig.figma.com/img/cd74/1748/2c7fa3513d56a30582a1b24ea7ceb408?Expires=1737936000&Key-Pair-Id=APKAQ4GOSFWCVNEHN3O4&Signature=X0l2RAVxVnOjB6RIM81l-5cVWBY8w4Dm~uMXKQIybjN3I9q5i8rVEgI698FBwi0PG8~LTtqiON6wF6-LldHtV~syc~dOzMHTYWp~tN4s1qyoiHARHiR78SxWYHA3~h1ZSQloVvCUx0Kq4mCQ9yBBANwE1y5SLnx8P9IQpAIE48iyrk35sKVlF80rlfDaFAXTJXv940YoT7GOFqnYdkd5ep1bDQpj2wNmTNnl54jlV8k450yvvn~f5f9hV7YN62AgRAJ6YI-62UpSGBKIXoNXHRja2nl0RZngtgoXMccv33kvZ3KPE8iH~boEeolwVt6FL-lKlFqG8G5O4mZS994yZQ__',
},
];
5 changes: 4 additions & 1 deletion packages/design-system/package.json
Original file line number Diff line number Diff line change
@@ -40,12 +40,15 @@
"vite": "^5.4.8"
},
"dependencies": {
"@types/react-slick": "^0.23.13",
"@vanilla-extract/css": "^1.17.0",
"@vanilla-extract/recipes": "^0.5.5",
"@vanilla-extract/sprinkles": "^1.6.3",
"clsx": "^2.1.1",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.1.1"
"react-router-dom": "^7.1.1",
"react-slick": "^0.30.3",
"slick-carousel": "^1.8.1"
}
}
1 change: 1 addition & 0 deletions packages/design-system/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ export { default as FloatingButton } from './floating-button/floating-button';
export { default as ToastContainer } from './toast/toast-container';
export { toast } from './toast/utils/toast';
export { default as Header } from './header/header';
export { default as TopCarousel } from './top-carousel/top-carousel';
export { default as ArtistCard } from './artist-card/artist-card';
export { default as FestivalCard } from './festival-card/festival-card';
export { default as Spacing } from './spacing/spacing';
37 changes: 37 additions & 0 deletions packages/design-system/src/components/top-carousel/dots.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { globalStyle } from '@vanilla-extract/css';
import { themeVars } from '../../styles';

globalStyle('.dots_custom', {
display: 'flex',
justifyContent: 'center',
verticalAlign: 'middle',
margin: 0,
paddingTop: '0rem',
paddingBottom: '1.6rem',
});

globalStyle('.dots_custom li', {
listStyle: 'none',
cursor: 'pointer',
display: 'inline-block',
marginRight: '0.8rem',
padding: 0,
});

globalStyle('.dots_custom li button', {
border: 'none',
background: themeVars.color.white,
opacity: 0.2,
cursor: 'pointer',
display: 'block',
height: '0.6rem',
width: '0.6rem',
borderRadius: '100%',
padding: 0,
color: 'transparent',
});

globalStyle('.dots_custom li.slick-active button', {
backgroundColor: themeVars.color.confeti_lime3,
opacity: 1,
});
112 changes: 112 additions & 0 deletions packages/design-system/src/components/top-carousel/slick.css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { globalStyle } from '@vanilla-extract/css';
import { themeVars } from '../../styles';

/* Slider */
globalStyle('.slick-slider', {
...themeVars.display.flexColumn,
boxSizing: 'border-box',

WebkitUserSelect: 'none',
MozUserSelect: 'none',
msUserSelect: 'none',
userSelect: 'none',

WebkitTouchCallout: 'none',
msTouchAction: 'pan-y',
touchAction: 'pan-y',
WebkitTapHighlightColor: 'transparent',
});

globalStyle('.slick-list', {
gap: '2rem',
overflow: 'hidden',
margin: '0',
});

globalStyle('.slick-list:focus', {
outline: 'none',
});

globalStyle('.slick-list.dragging', {
cursor: 'pointer',
});

globalStyle('.slick-slider .slick-track, .slick-slider .slick-list', {
WebkitTransform: 'translate3d(0, 0, 0)',
MozTransform: 'translate3d(0, 0, 0)',
msTransform: 'translate3d(0, 0, 0)',
OTransform: 'translate3d(0, 0, 0)',
transform: 'translate3d(0, 0, 0)',
});

globalStyle('.slick-track', {
margin: 'auto',
paddingBottom: '2rem',
});

globalStyle('.slick-track:before, .slick-track:after', {
display: 'table',
content: "''",
});

globalStyle('.slick-track:after', {
clear: 'both',
});

globalStyle('.slick-loading .slick-track', {
visibility: 'hidden',
});

globalStyle('.slick-slide', {
display: 'none',
float: 'left',
height: '100%',
minHeight: '1px',
});

globalStyle("[dir='rtl'] .slick-slide", {
float: 'right',
});

globalStyle('.slick-slide img', {
display: 'block',
});

globalStyle('.slick-slide.slick-loading img', {
display: 'none',
});

globalStyle('.slick-slide.dragging img', {
pointerEvents: 'none',
});

globalStyle('.slick-initialized .slick-slide', {
display: 'block',
});

globalStyle('.slick-loading .slick-slide', {
visibility: 'hidden',
});

globalStyle('.slick-vertical .slick-slide', {
display: 'block',
height: 'auto',
border: '1px solid transparent',
});

globalStyle('.slick-arrow.slick-hidden', {
display: 'none',
});

/* 기본 슬라이드 스타일 */
globalStyle('.slick-slide', {
transition: 'transform 0.5s ease, opacity 0.5s ease',
opacity: 0.7,
transform: 'scale(0.8)',
});

/* 중앙 슬라이드 스타일 */
globalStyle('.slick-center', {
opacity: 1,
transform: 'scale(1)',
});
seueooo marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { globalStyle, keyframes } from '@vanilla-extract/css';
import { themeVars } from '../../styles';

export const fadeInOut = keyframes({
'0%': { opacity: 0 },
'50%': { opacity: 0.5 },
'100%': { opacity: 1 },
});

globalStyle('.banner-title', {
marginTop: '3rem',
height: '10.3rem',
});

globalStyle('.title-date', {
...themeVars.fontStyles.subtitle5_sb_12,
color: themeVars.color.white,
textAlign: 'center',
marginBottom: '1.2rem',
animation: `${fadeInOut} 1s ease-out forwards`,
});

globalStyle('.title-name', {
...themeVars.fontStyles.title1_b_24,
color: themeVars.color.white,
textAlign: 'center',
marginBottom: '4px',
animation: `${fadeInOut} 1s ease-out forwards`,
});

globalStyle('.title-sub', {
...themeVars.fontStyles.body3_m_14,
color: themeVars.color.gray500,
textAlign: 'center',
animation: `${fadeInOut} 1s ease-out forwards`,
});

globalStyle('.card', {
zIndex: 1,
width: '19.7rem',
height: '26.2rem',
borderRadius: '1rem',
boxShadow: '0px 3px 6px 1px rgba(0, 0, 0, 0.25)',
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { Meta } from '@storybook/react';
import TopCarousel from './top-carousel';

const MOCK_PERFORM_DATA = [
{
performanceId: 1,
type: 'concert',
title: '오아시스 내한공연',
subTitle: 'LIVE NATION PRESENTS COLDPLAY',
performanceAt: '2025.10.21',
posterUrl: 'https://dummyimage.com/197x262',
},
{
performanceId: 2,
type: 'festival',
title: '오아시스 내한공연',
subTitle: '',
performanceAt: '2025.10.21',
posterUrl: 'https://dummyimage.com/197x262',
},
{
performanceId: 3,
type: 'festival',
title: '오아시스 내한공연3',
subTitle: 'LIVE NATION PRESENTS COLDPLAY',
performanceAt: '2025.10.21',
posterUrl: 'https://dummyimage.com/197x262',
},
{
performanceId: 4,
type: 'festival',
title: '오아시스 내한공연4',
subTitle: '',
performanceAt: '2025.10.21',
posterUrl: 'https://dummyimage.com/197x262',
},
{
performanceId: 5,
type: 'festival',
title: '오아시스 내한공연5',
subTitle: 'LIVE NATION PRESENTS COLDPLAY',
performanceAt: '2025.10.21',
posterUrl: 'https://dummyimage.com/197x262',
},
{
performanceId: 6,
type: 'festival',
title: '오아시스 내한공연6',
subTitle: 'LIVE NATION PRESENTS COLDPLAY',
performanceAt: '2025.10.21',
posterUrl: 'https://dummyimage.com/197x262',
},
{
performanceId: 7,
type: 'festival',
title: '오아시스 내한공연7',
subTitle: '',
performanceAt: '2025.10.21',
posterUrl: 'https://dummyimage.com/197x262',
},
];

const meta: Meta<typeof TopCarousel> = {
title: 'Common/TopCarousel',
component: TopCarousel,
args: {
performData: MOCK_PERFORM_DATA, // mock 데이터를 args로 전달
},
parameters: {
layout: 'centered',
},
decorators: [
(Story) => {
return (
<div
style={{
width: '375px',
height: '100%',
background:
'linear-gradient(180deg, #131433 -3.3%, #9747FF 89.71%)',
}}
>
<Story />
</div>
);
},
],
tags: ['autodocs'],
};

export default meta;
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { useRef, useState, useEffect } from 'react';
import Slider from 'react-slick';
import 'slick-carousel/slick/slick-theme.css';
import './slick.css';
import './dots.css';
import './top-carousel.css';

interface PerformData {
performanceId: number;
type: string;
title: string;
subTitle: string;
performanceAt: string;
posterUrl: string;
}

interface DataProps {
performData: PerformData[];
}

const TopCarousel = ({ performData }: DataProps) => {
const sliderRef = useRef<Slider | null>(null);
const [currentId, setCurrentId] = useState(3);

useEffect(() => {
const interval = setInterval(() => {
if (sliderRef.current) {
sliderRef.current?.slickNext();
}
}, 4000);

return () => clearInterval(interval);
}, []);

const settings = {
ref: sliderRef,
className: 'center',
dots: true,
centerMode: true,
infinite: true,
centerPadding: '115px',
slidesToShow: 1,
sliceToScroll: 1,
speed: 1000,
cssEase: 'ease-in-out',
initialSlide: 3,
beforeChange: (newIndex: number) => {
setCurrentId(newIndex);
},

appendDots: (dots: string) => (
<div
style={{
width: '100%',
display: 'flex',
justifyItems: 'center',
textAlign: 'center',
alignItems: 'center',
justifyContent: 'center',
padding: '1.6rem 0',
}}
>
<ul> {dots} </ul>
</div>
),
dotsClass: 'dots_custom',
};

return (
<>
<div className="banner-title">
<p className="title-date">{performData[currentId]?.performanceAt} </p>
<h1 className="title-name">{performData[currentId]?.title}</h1>
<p className="title-sub">{performData[currentId]?.subTitle}</p>
</div>
<div>
<Slider {...settings}>
{performData.map((item) => (
<img
className="card"
key={item.performanceId}
src={item.posterUrl}
></img>
))}
</Slider>
</div>
</>
);
};

export default TopCarousel;
2 changes: 2 additions & 0 deletions packages/design-system/src/styles/tokens/color.ts
Original file line number Diff line number Diff line change
@@ -26,4 +26,6 @@ export const color = {
// gradients
confeti_grad:
'var(--confeti_grad, linear-gradient(176deg, #B5F602 4.17%, #F5FFD8 95.15%))',

confeti_purple_grad: 'linear-gradient(180deg, #131433 -3.3%, #9747FF 89.71%)',
} as const;
1 change: 1 addition & 0 deletions packages/design-system/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"extends": "@confeti/typescript/react-library.json",
"compilerOptions": {
"typeRoots": ["node_modules/@types"],
"outDir": "dist"
},
"include": ["src"],
125 changes: 117 additions & 8 deletions pnpm-lock.yaml