d3ro-voice/apps/web/src/components/layout/sidebar.tsx
Yun Chan 035d0a76f5 feat(release): ship the current feature set as 1.2.0
Version 1.1.0 was published on 2026-09-15, so its tag is closed. Every commit
since then, the update-feed rework, dictionary import and export, the extra
push transports, and entitlement gating, needs its own immutable version.

Product version, Android version code, iOS build number, and all package and
store surfaces move to 1.2.0 / 1020001, with Korean and English store notes for
the new version code. The download centers read that version and its release
date from one place instead of repeating them inline.
2026-09-16 23:49:50 +09:00

219 lines
6.6 KiB
TypeScript

'use client'
// apps/web/src/components/layout/sidebar.tsx
// D3RO VOICE 대시보드 좌측 사이드바 — 정본 네비게이션
import { usePathname, useRouter } from 'next/navigation'
import {
Box,
FormControl,
InputLabel,
List,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
MenuItem,
Select,
type SelectChangeEvent
} from '@mui/material'
import DashboardIcon from '@mui/icons-material/Dashboard'
import MicIcon from '@mui/icons-material/Mic'
import HistoryIcon from '@mui/icons-material/History'
import ChatIcon from '@mui/icons-material/Chat'
import BookIcon from '@mui/icons-material/Book'
import AutoAwesomeIcon from '@mui/icons-material/AutoAwesome'
import PaymentIcon from '@mui/icons-material/Payment'
import LogoutIcon from '@mui/icons-material/Logout'
import PaletteIcon from '@mui/icons-material/Palette'
import type { ThemeMode } from '@d3ro/core/types'
import { PhosphorText } from '@d3ro/ui/components/ds'
import { d3roPalette } from '@d3ro/ui/theme'
import { useI18n } from '@d3ro/i18n'
import { getSupabaseBrowserClient } from '@/lib/supabase-browser'
import { AVAILABLE_MODES, useThemeMode } from '@/components/providers/theme-mode-context'
interface NavItem {
key: string
path: string
label: string
icon: React.ReactElement
}
export function Sidebar(): React.ReactElement {
const router = useRouter()
const pathname = usePathname()
const { t } = useI18n()
const { mode, setMode } = useThemeMode()
const handleThemeChange = (event: SelectChangeEvent<ThemeMode>): void => {
setMode(event.target.value as ThemeMode)
}
const items: NavItem[] = [
{
key: 'dashboard',
path: '/dashboard',
label: t('nav.dashboard') ?? '대시보드',
icon: <DashboardIcon />
},
{
key: 'record',
path: '/record',
label: t('nav.record') ?? '실시간 녹음',
icon: <MicIcon />
},
{
key: 'history',
path: '/history',
label: t('nav.history') ?? '전사 히스토리',
icon: <HistoryIcon />
},
{
key: 'chat',
path: '/chat',
label: t('nav.chat') ?? '음성 대화',
icon: <ChatIcon />
},
{
key: 'dictionary',
path: '/dictionary',
label: t('nav.dictionary') ?? '커스텀 사전',
icon: <BookIcon />
},
{
key: 'commands',
path: '/commands',
label: t('nav.commands') ?? '명령어',
icon: <AutoAwesomeIcon />
},
{
key: 'billing',
path: '/billing',
label: t('nav.billing') ?? '구독 & 업그레이드',
icon: <PaymentIcon />
}
]
async function handleLogout(): Promise<void> {
const supabase = getSupabaseBrowserClient()
await supabase.auth.signOut()
router.replace('/login')
}
return (
<Box
sx={{
width: 240,
minHeight: '100dvh',
bgcolor: d3roPalette.bg.sidebar,
borderRight: `1px solid ${d3roPalette.border.default}`,
display: 'flex',
flexDirection: 'column'
}}
>
<Box sx={{ p: 3, borderBottom: `1px solid ${d3roPalette.border.default}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Box sx={{ display: 'flex', gap: 0.75 }}>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: 'var(--d3-accent-main)',
boxShadow: '0 0 8px var(--d3-accent-main)'
}}
/>
<Box
sx={{
width: 8,
height: 8,
borderRadius: '50%',
bgcolor: 'var(--d3-tag-green)',
boxShadow: '0 0 6px var(--d3-tag-green)'
}}
/>
</Box>
<PhosphorText variant="heading" sx={{ letterSpacing: '0.1em' }}>
D3RO VOICE
</PhosphorText>
</Box>
<Box sx={{ fontSize: '10px', fontFamily: 'ui-monospace, monospace', color: d3roPalette.text.muted }}>
v1.2.0
</Box>
</Box>
<List sx={{ flex: 1, py: 2 }}>
{items.map((item) => {
const active = pathname === item.path || pathname.startsWith(`${item.path}/`)
return (
<ListItem key={item.key} disablePadding>
<ListItemButton
selected={active}
onClick={() => router.push(item.path)}
sx={{
py: 1.25,
'&.Mui-selected': {
bgcolor: d3roPalette.bg.inset,
borderLeft: `3px solid ${d3roPalette.accent.main}`
}
}}
>
<ListItemIcon sx={{ minWidth: 36, color: active ? d3roPalette.accent.main : d3roPalette.text.label }}>
{item.icon}
</ListItemIcon>
<ListItemText
primary={item.label}
primaryTypographyProps={{
fontSize: 13,
fontWeight: active ? 600 : 400,
color: active ? d3roPalette.text.primary : d3roPalette.text.secondary
}}
/>
</ListItemButton>
</ListItem>
)
})}
</List>
<Box sx={{ px: 2, py: 1.5, borderTop: `1px solid ${d3roPalette.border.default}` }}>
<FormControl fullWidth size="small">
<InputLabel
id="theme-mode-label"
sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}
>
<PaletteIcon fontSize="inherit" />
{t('theme.label') ?? 'Theme'}
</InputLabel>
<Select<ThemeMode>
labelId="theme-mode-label"
id="theme-mode-select"
value={mode}
onChange={handleThemeChange}
label={t('theme.label') ?? 'Theme'}
>
{AVAILABLE_MODES.map((m) => (
<MenuItem key={m} value={m}>
{t(`theme.${m}`) ?? m}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
<List sx={{ borderTop: `1px solid ${d3roPalette.border.default}` }}>
<ListItem disablePadding>
<ListItemButton onClick={() => void handleLogout()}>
<ListItemIcon sx={{ minWidth: 36, color: d3roPalette.text.label }}>
<LogoutIcon />
</ListItemIcon>
<ListItemText
primary={t('nav.logout') ?? '로그아웃'}
primaryTypographyProps={{ fontSize: 13, color: d3roPalette.text.secondary }}
/>
</ListItemButton>
</ListItem>
</List>
</Box>
)
}