diff --git a/apps/spectator/app/game/[id]/page.css.ts b/apps/spectator/app/game/[id]/page.css.ts index c457fff3..317bae3c 100644 --- a/apps/spectator/app/game/[id]/page.css.ts +++ b/apps/spectator/app/game/[id]/page.css.ts @@ -1,4 +1,4 @@ -import { theme } from '@hcc/styles'; +import { theme, rem } from '@hcc/styles'; import { style, styleVariants } from '@vanilla-extract/css'; const contentSection = style({ overflowY: 'auto', padding: '1.25rem' }); @@ -26,3 +26,34 @@ export const cheerTalk = styleVariants({ ...theme.textVariants.lg, }, }); + +const panelItemBase = style({ + ...theme.textVariants.default, + textAlign: 'center', + paddingBlock: rem(12), + color: theme.colors.gray[4], + borderBlock: `1px solid ${theme.colors.gray[2]}`, +}); + +export const panel = styleVariants({ + wrapper: { + position: 'relative', + }, + menu: { + display: 'grid', + gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', + width: '100%', + }, +}); + +export const item = styleVariants({ + active: [ + panelItemBase, + { + background: theme.colors.primary[1], + color: theme.colors.primary[3], + borderBottom: `1px solid ${theme.colors.primary[3]}`, + }, + ], + inactive: [panelItemBase], +}); diff --git a/apps/spectator/app/game/[id]/page.tsx b/apps/spectator/app/game/[id]/page.tsx index 907af6ab..c7d1274d 100644 --- a/apps/spectator/app/game/[id]/page.tsx +++ b/apps/spectator/app/game/[id]/page.tsx @@ -1,10 +1,11 @@ 'use client'; +import { Tabs } from '@hcc/ui'; + import Live from '@/app/_components/Live'; import AsyncBoundary from '@/components/AsyncBoundary'; import CheerTalkModal from '@/components/cheertalk/Modal/CheerTalkModal'; import Loader from '@/components/Loader'; -import Panel from '@/components/Panel'; import Banner from './_components/Banner'; import BannerFallback from './_components/Banner/Error'; @@ -16,13 +17,22 @@ import Lineup from './_components/Lineup'; import Timeline from './_components/Timeline'; import * as styles from './page.css'; -export default function Page({ params }: { params: { id: string } }) { - const options = [ - { label: '라인업' }, - { label: '타임라인' }, - { label: '경기영상' }, - ]; +const tabs = [ + { + label: '라인업', + renderer: (gameId: string) => , + }, + { + label: '타임라인', + renderer: (gameId: string) => , + }, + { + label: '경기영상', + renderer: (gameId: string) =>
{gameId} 경기영상
, + }, +]; +export default function Page({ params }: { params: { id: string } }) { return (
- - {({ selected }) => ( - <> - {selected === '라인업' && ( -
에러
} - loadingFallback={} - > - -
- )} - {selected === '타임라인' && ( -
에러
} - loadingFallback={} - > - -
- )} - {selected === '경기영상' && ( -
에러
} - loadingFallback={} - > -
-
- )} - - )} -
+ + + {tabs.map(tab => ( + styles.item[state]} + > + {tab.label} + + ))} + + {tabs.map(tab => ( + +
에러
} + loadingFallback={} + > + {tab.renderer(params.id)} +
+
+ ))} +
+ ); diff --git a/apps/spectator/components/Panel/Panel.css.ts b/apps/spectator/components/Panel/Panel.css.ts deleted file mode 100644 index e1b1506f..00000000 --- a/apps/spectator/components/Panel/Panel.css.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { rem, theme } from '@hcc/styles'; -import { style, styleVariants } from '@vanilla-extract/css'; - -const panelItemBase = style({ - ...theme.textVariants.default, - textAlign: 'center', - paddingBlock: rem(12), - color: theme.colors.gray[4], - borderBlock: `1px solid ${theme.colors.gray[2]}`, -}); - -export const panel = styleVariants({ - wrapper: { - position: 'relative', - }, - menu: { - display: 'grid', - gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', - width: '100%', - }, - item: [panelItemBase], - itemSelected: [ - panelItemBase, - { - background: theme.colors.primary[1], - color: theme.colors.primary[3], - borderBottom: `1px solid ${theme.colors.primary[3]}`, - }, - ], -}); diff --git a/apps/spectator/components/Panel/index.tsx b/apps/spectator/components/Panel/index.tsx deleted file mode 100644 index 82baf315..00000000 --- a/apps/spectator/components/Panel/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { MouseEvent, ReactNode, useState } from 'react'; - -import { Dropdown } from '@/components/Dropdown'; - -import { panel } from './Panel.css'; - -type PanelProps = { - options: Array<{ label: string }>; - children: ({ selected }: { selected: string }) => ReactNode; - defaultValue: string; -}; - -export default function Panel({ defaultValue, options, children }: PanelProps) { - const [selected, setSelected] = useState(defaultValue); - - const handleClickItem = (e: MouseEvent) => { - const selectedValue = (e.target as Element).textContent; - - if (!selectedValue) return; - if (selected === selectedValue) return; - - setSelected(selectedValue); - }; - - return ( -
- - - {options.map(option => ( - - {option.label} - - ))} - - {children({ selected })} - -
- ); -} diff --git a/docs/storybook/stories/Tabs.stories.tsx b/docs/storybook/stories/Tabs.stories.tsx new file mode 100644 index 00000000..a6f45942 --- /dev/null +++ b/docs/storybook/stories/Tabs.stories.tsx @@ -0,0 +1,35 @@ +import { Tabs } from '@hcc/ui'; +import { Meta, StoryObj } from '@storybook/react'; +import React from 'react'; + +const meta: Meta = { + title: '@hcc/Tabs', + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + render: () => { + return ( + + + A + B + + + +
저는 A입니다.
+
+ +
저는 B입니다.
+
+
+ ); + }, +}; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 687a4bf0..b7c05b65 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -3,5 +3,6 @@ export { default as Icon } from './icon'; export { default as Uploader } from './image-uploader'; export { default as Input } from './input'; export { default as Modal } from './modal'; +export { default as Tabs } from './tabs'; export { default as Toast } from './toast'; export { default as Tooltip } from './tooltip'; diff --git a/packages/ui/src/tabs/Content.tsx b/packages/ui/src/tabs/Content.tsx new file mode 100644 index 00000000..6c7dcd18 --- /dev/null +++ b/packages/ui/src/tabs/Content.tsx @@ -0,0 +1,15 @@ +import useTabs from './hooks'; + +type TabsContentProps = { + value: string; + className?: string; + children: React.ReactNode; +}; + +const TabsContent = ({ value, className, children }: TabsContentProps) => { + const { value: valueState } = useTabs(); + + return value === valueState &&
{children}
; +}; + +export default TabsContent; diff --git a/packages/ui/src/tabs/List.tsx b/packages/ui/src/tabs/List.tsx new file mode 100644 index 00000000..35ddb840 --- /dev/null +++ b/packages/ui/src/tabs/List.tsx @@ -0,0 +1,14 @@ +import { clsx } from 'clsx'; + +import * as styles from './Tabs.css'; + +type TabsListProps = { + className?: string; + children: React.ReactNode; +}; + +const TabsList = ({ className, children }: TabsListProps) => { + return
{children}
; +}; + +export default TabsList; diff --git a/packages/ui/src/tabs/Tabs.css.ts b/packages/ui/src/tabs/Tabs.css.ts new file mode 100644 index 00000000..4edc2dd7 --- /dev/null +++ b/packages/ui/src/tabs/Tabs.css.ts @@ -0,0 +1,14 @@ +import { style } from '@vanilla-extract/css'; + +export const root = style({ + display: 'flex', + flexDirection: 'column', + + width: '100%', +}); + +export const list = style({ + display: 'grid', + gridAutoFlow: 'column', + alignItems: 'center', +}); diff --git a/packages/ui/src/tabs/Trigger.tsx b/packages/ui/src/tabs/Trigger.tsx new file mode 100644 index 00000000..dfdfac99 --- /dev/null +++ b/packages/ui/src/tabs/Trigger.tsx @@ -0,0 +1,34 @@ +import useTabs from './hooks'; + +type TabsTriggerProps = { + value: string; + className?: (state: DataState) => string; + children: React.ReactNode; +}; + +type DataStateParams = { + value: string; + stateValue: string; +}; +type DataState = 'active' | 'inactive'; + +const getDataState = ({ value, stateValue }: DataStateParams) => { + return stateValue === value ? 'active' : 'inactive'; +}; + +const TabsTrigger = ({ value, className, children }: TabsTriggerProps) => { + const { value: stateValue, setValue } = useTabs(); + const activeState = getDataState({ value, stateValue }); + + return ( + + ); +}; + +export default TabsTrigger; diff --git a/packages/ui/src/tabs/hooks.ts b/packages/ui/src/tabs/hooks.ts new file mode 100644 index 00000000..ae7ac732 --- /dev/null +++ b/packages/ui/src/tabs/hooks.ts @@ -0,0 +1,15 @@ +import { useContext } from 'react'; + +import { TabsContext } from '.'; + +const useTabs = () => { + const context = useContext(TabsContext); + + if (!context) { + throw new Error('useTabs must be used within a TabsProvider'); + } + + return context; +}; + +export default useTabs; diff --git a/packages/ui/src/tabs/index.tsx b/packages/ui/src/tabs/index.tsx new file mode 100644 index 00000000..7ae05a67 --- /dev/null +++ b/packages/ui/src/tabs/index.tsx @@ -0,0 +1,39 @@ +import { clsx } from 'clsx'; +import { createContext, useState } from 'react'; + +import TabsContent from './Content'; +import TabsList from './List'; +import * as styles from './Tabs.css'; +import TabsTrigger from './Trigger'; + +type TabsContextType = { + value: string; + setValue: (value: string) => void; +}; + +export const TabsContext = createContext({ + value: '', + setValue: () => {}, +}); + +type TabsProps = { + defaultValue: string; + className?: string; + children: React.ReactNode; +}; + +const Tabs = ({ defaultValue, className, children }: TabsProps) => { + const [value, setValue] = useState(defaultValue); + + return ( + +
{children}
+
+ ); +}; + +Tabs.List = TabsList; +Tabs.Trigger = TabsTrigger; +Tabs.Content = TabsContent; + +export default Tabs;