Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"recharts": "^2.12.5",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"use-intl": "^3.20.0",
"zod": "^3.22.4"
},
"devDependencies": {
Expand Down
63 changes: 63 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions src/components/language-provider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createContext, useContext, useState } from 'react'
import { IntlProvider } from 'use-intl'
import translations from '../translations'

export type Language = 'en' | 'zh'

type LanguageProviderProps = {
children: React.ReactNode
defaultLanguage?: Language
storageKey?: string
}

type LanguageProviderState = {
language: Language
setLanguage: (lang: Language) => void
}

const initialState: LanguageProviderState = {
language: 'en',
setLanguage: () => null,
}

const LanguageProviderContext =
createContext<LanguageProviderState>(initialState)

export function LanguageProvider({
children,
defaultLanguage = 'en',
storageKey = 'vite-ui-language',
...props
}: LanguageProviderProps) {
const [language, setLanguage] = useState<Language>(
() => (localStorage.getItem(storageKey) as Language) || defaultLanguage
)

const value = {
language,
setLanguage: (lang: Language) => {
localStorage.setItem(storageKey, lang)
setLanguage(lang)
},
}

return (
<LanguageProviderContext.Provider {...props} value={value}>
<IntlProvider locale={language} messages={translations[language]}>
{children}
</IntlProvider>
</LanguageProviderContext.Provider>
)
}

// eslint-disable-next-line react-refresh/only-export-components
export const useLanguage = () => {
const context = useContext(LanguageProviderContext)

if (context === undefined)
throw new Error('useLanguage must be used within a LanguageProvider')

return context
}
49 changes: 49 additions & 0 deletions src/components/language-switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Language, useLanguage } from './language-provider'
import { IconCheck } from '@tabler/icons-react'
import { cn } from '@/lib/utils'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { Button } from './custom/button'

export default function LanguageSwitch() {
const { language, setLanguage } = useLanguage()

const languageText = new Map<string, string>([
['en', 'English'],
['zh', '简体中文'],
])

const renderDropdownItem = () => {
return Array.from(languageText).map(([key, value]) => (
<DropdownMenuItem key={key} onClick={() => setLanguage(key as Language)}>
{value}{' '}
<IconCheck
size={14}
className={cn('ml-auto', language !== key && 'hidden')}
/>
</DropdownMenuItem>
))
}

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant='outline'
size='default'
className='scale-95 rounded-full'
>
{languageText.get(language)}
<span className='sr-only'>Toggle language</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
{renderDropdownItem()}
</DropdownMenuContent>
</DropdownMenu>
)
}
9 changes: 6 additions & 3 deletions src/components/nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
import { cn } from '@/lib/utils'
import useCheckActiveNav from '@/hooks/use-check-active-nav'
import { SideLink } from '@/data/sidelinks'
import { useTranslations } from 'use-intl'

interface NavProps extends React.HTMLAttributes<HTMLDivElement> {
isCollapsed: boolean
Expand Down Expand Up @@ -89,6 +90,7 @@ function NavLink({
subLink = false,
}: NavLinkProps) {
const { checkActiveNav } = useCheckActiveNav()
const t = useTranslations()
return (
<Link
to={href}
Expand All @@ -104,7 +106,7 @@ function NavLink({
aria-current={checkActiveNav(href) ? 'page' : undefined}
>
<div className='mr-2'>{icon}</div>
{title}
{t(title)}
{label && (
<div className='ml-2 rounded-lg bg-primary px-1 text-[0.625rem] text-primary-foreground'>
{label}
Expand All @@ -116,6 +118,7 @@ function NavLink({

function NavLinkDropdown({ title, icon, label, sub, closeNav }: NavLinkProps) {
const { checkActiveNav } = useCheckActiveNav()
const t = useTranslations()

/* Open collapsible by default
* if one of child element is active */
Expand All @@ -130,7 +133,7 @@ function NavLinkDropdown({ title, icon, label, sub, closeNav }: NavLinkProps) {
)}
>
<div className='mr-2'>{icon}</div>
{title}
{t(title)}
{label && (
<div className='ml-2 rounded-lg bg-primary px-1 text-[0.625rem] text-primary-foreground'>
{label}
Expand Down Expand Up @@ -201,7 +204,7 @@ function NavLinkIconDropdown({ title, icon, label, sub }: NavLinkProps) {
<Button
variant={isChildActive ? 'secondary' : 'ghost'}
size='icon'
className='h-12 w-12'
className='w-12 h-12'
>
{icon}
</Button>
Expand Down
4 changes: 3 additions & 1 deletion src/components/search.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Input } from '@/components/ui/input'
import { useTranslations } from 'use-intl'

export function Search() {
const t = useTranslations('dashboard')
return (
<div>
<Input
type='search'
placeholder='Search...'
placeholder={t('search')}
className='md:w-[100px] lg:w-[300px]'
/>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/components/sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default function Sidebar({
onClick={() => setIsCollapsed((prev) => !prev)}
size='icon'
variant='outline'
className='absolute -right-5 top-1/2 z-50 hidden rounded-full md:inline-flex'
className='absolute z-50 hidden rounded-full -right-5 top-1/2 md:inline-flex'
>
<IconChevronsLeft
stroke={1.5}
Expand Down
6 changes: 4 additions & 2 deletions src/components/top-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from '@/components/ui/dropdown-menu'
import { Button } from './custom/button'
import { IconMenu } from '@tabler/icons-react'
import { useTranslations } from 'use-intl'

interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
links: {
Expand All @@ -18,6 +19,7 @@ interface TopNavProps extends React.HTMLAttributes<HTMLElement> {
}

export function TopNav({ className, links, ...props }: TopNavProps) {
const t = useTranslations()
return (
<>
<div className='md:hidden'>
Expand All @@ -34,7 +36,7 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
to={href}
className={!isActive ? 'text-muted-foreground' : ''}
>
{title}
{t(title)}
</Link>
</DropdownMenuItem>
))}
Expand All @@ -55,7 +57,7 @@ export function TopNav({ className, links, ...props }: TopNavProps) {
to={href}
className={`text-sm font-medium transition-colors hover:text-primary ${isActive ? '' : 'text-muted-foreground'}`}
>
{title}
{t(title)}
</Link>
))}
</nav>
Expand Down
16 changes: 9 additions & 7 deletions src/components/user-nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ import {
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useTranslations } from 'use-intl'

export function UserNav() {
const t = useTranslations('userNav');
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost' className='relative h-8 w-8 rounded-full'>
<Avatar className='h-8 w-8'>
<Button variant='ghost' className='relative w-8 h-8 rounded-full'>
<Avatar className='w-8 h-8'>
<AvatarImage src='/avatars/01.png' alt='@shadcn' />
<AvatarFallback>SN</AvatarFallback>
</Avatar>
Expand All @@ -34,22 +36,22 @@ export function UserNav() {
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
Profile
{t('profile')}
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Billing
{t('billing')}
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
{t('settings')}
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>New Team</DropdownMenuItem>
<DropdownMenuItem>{t("new_team")}</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
Log out
{t('log_out')}
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
Expand Down
Loading
Loading