🎉 chore(init): project initialization
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
'use client';
|
||||
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { formatBytes, unitConversion } from '@repo/ui/utils';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
type DisplayType = 'currency' | 'traffic' | 'number';
|
||||
|
||||
interface DisplayProps<T> {
|
||||
value?: T;
|
||||
unlimited?: boolean;
|
||||
type?: DisplayType;
|
||||
}
|
||||
|
||||
export function Display<T extends number | undefined | null>({
|
||||
value = 0,
|
||||
unlimited = false,
|
||||
type = 'number',
|
||||
}: DisplayProps<T>): string {
|
||||
const t = useTranslations('common');
|
||||
const { common } = useGlobalStore();
|
||||
const { currency } = common;
|
||||
|
||||
if (type === 'currency') {
|
||||
const formattedValue = `${currency?.currency_symbol ?? ''}${unitConversion('centsToDollars', value as number)?.toFixed(2) ?? '0.00'}`;
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
if (['traffic', 'number'].includes(type) && unlimited && !value) {
|
||||
return t('unlimited');
|
||||
}
|
||||
|
||||
if (type === 'traffic') {
|
||||
return value ? formatBytes(value as number) : '0';
|
||||
}
|
||||
|
||||
if (type === 'number') {
|
||||
return value ? value.toString() : '0';
|
||||
}
|
||||
|
||||
return '0';
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
import { findNavByUrl } from '@/config/navs';
|
||||
import {
|
||||
Breadcrumb,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
} from '@shadcn/ui/breadcrumb';
|
||||
import { Separator } from '@shadcn/ui/separator';
|
||||
import { SidebarTrigger } from '@shadcn/ui/sidebar';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { Fragment, useMemo } from 'react';
|
||||
import LanguageSwitch from './language-switch';
|
||||
import ThemeSwitch from './theme-switch';
|
||||
import { UserNav } from './user-nav';
|
||||
|
||||
export function Header() {
|
||||
const t = useTranslations('menu');
|
||||
const pathname = usePathname();
|
||||
const items = useMemo(() => findNavByUrl(pathname), [pathname]);
|
||||
return (
|
||||
<header className='bg-background sticky top-0 z-50 flex h-14 shrink-0 items-center gap-2'>
|
||||
<div className='flex flex-1 items-center gap-2 px-3'>
|
||||
<SidebarTrigger />
|
||||
<Separator orientation='vertical' className='mr-2 h-4' />
|
||||
<Breadcrumb>
|
||||
<BreadcrumbList>
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<Fragment key={item?.title}>
|
||||
{index !== items.length - 1 && (
|
||||
<BreadcrumbItem>
|
||||
<BreadcrumbLink href={item?.url || '/dashboard'}>
|
||||
{t(item?.title)}
|
||||
</BreadcrumbLink>
|
||||
</BreadcrumbItem>
|
||||
)}
|
||||
{index < items.length - 1 && <BreadcrumbSeparator />}
|
||||
{index === items.length - 1 && <BreadcrumbPage>{t(item?.title)}</BreadcrumbPage>}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div className='flex items-center gap-2 px-3'>
|
||||
<LanguageSwitch />
|
||||
<ThemeSwitch />
|
||||
<UserNav />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client';
|
||||
|
||||
import { locales } from '@/config/constants';
|
||||
import { setLocale } from '@/utils/common';
|
||||
import { Icon } from '@iconify/react';
|
||||
import { getCountry } from '@repo/ui/utils';
|
||||
import { Button } from '@shadcn/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@shadcn/ui/dropdown-menu';
|
||||
import { useLocale, useTranslations } from 'next-intl';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function LanguageSwitch() {
|
||||
const locale = useLocale();
|
||||
const country = getCountry(locale);
|
||||
const t = useTranslations('language');
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='ghost' size='icon'>
|
||||
<Icon icon={`flagpack:${country?.alpha2.toLowerCase()}`} className='!size-5' />
|
||||
<span className='sr-only'>Switch Language</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
{locales.map(getCountry).map((item) => (
|
||||
<DropdownMenuItem
|
||||
key={`${item?.lang}-${item?.alpha2}`}
|
||||
onClick={() => {
|
||||
setLocale(`${item?.lang}-${item?.alpha2}`);
|
||||
router.refresh();
|
||||
}}
|
||||
>
|
||||
<div className='flex items-center gap-1'>
|
||||
<Icon icon={`flagpack:${item?.alpha2.toLowerCase()}`} />
|
||||
{t(`${item?.lang}-${item?.alpha2}`)}
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
import { ProTable as _ProTable, ProTableProps } from '@repo/ui/pro-table';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export { type ProTableActions } from '@repo/ui/pro-table';
|
||||
export function ProTable<TData, TValue extends Record<string, unknown>>(
|
||||
props: ProTableProps<TData, TValue>,
|
||||
) {
|
||||
const t = useTranslations('common.table');
|
||||
return (
|
||||
<_ProTable
|
||||
{...props}
|
||||
texts={{
|
||||
actions: t('actions'),
|
||||
asc: t('asc'),
|
||||
desc: t('desc'),
|
||||
hide: t('hide'),
|
||||
selectedRowsText(total) {
|
||||
return t('selectedItems', { total });
|
||||
},
|
||||
textPageOf(current, total) {
|
||||
return t('pageInfo', { current, total });
|
||||
},
|
||||
textRowsPerPage: t('rowsPerPage'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import useGlobalStore, { GlobalStore } from '@/config/use-global';
|
||||
import { Logout } from '@/utils/common';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryStreamedHydration } from '@tanstack/react-query-next-experimental';
|
||||
import { ThemeProvider as NextThemesProvider } from 'next-themes';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
export default function Providers({
|
||||
children,
|
||||
common,
|
||||
user,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
common: Partial<GlobalStore['common']>;
|
||||
user: GlobalStore['user'];
|
||||
}) {
|
||||
const [queryClient] = useState(
|
||||
() =>
|
||||
new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 5 * 1000,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const { setCommon, setUser } = useGlobalStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setUser(user);
|
||||
} else {
|
||||
Logout();
|
||||
}
|
||||
}, [setUser, user]);
|
||||
|
||||
useEffect(() => {
|
||||
setCommon(common);
|
||||
}, [setCommon, common]);
|
||||
|
||||
return (
|
||||
<NextThemesProvider attribute='class' defaultTheme='system' enableSystem>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ReactQueryStreamedHydration>{children}</ReactQueryStreamedHydration>
|
||||
</QueryClientProvider>
|
||||
</NextThemesProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
'use client';
|
||||
import { navs } from '@/config/navs';
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { Icon } from '@iconify/react';
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from '@shadcn/ui/sidebar';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Image from 'next/legacy/image';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
|
||||
export function SidebarLeft({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const { common } = useGlobalStore();
|
||||
const { site } = common;
|
||||
const t = useTranslations('menu');
|
||||
const pathname = usePathname();
|
||||
return (
|
||||
<Sidebar className='border-r-0' collapsible='icon' {...props}>
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton size='lg' asChild>
|
||||
<Link href='/'>
|
||||
<div className='flex aspect-square size-8 items-center justify-center rounded-lg'>
|
||||
<Image
|
||||
src={site.site_logo || '/favicon.svg'}
|
||||
alt='logo'
|
||||
width={48}
|
||||
height={48}
|
||||
className='size-full'
|
||||
/>
|
||||
</div>
|
||||
<div className='grid flex-1 text-left text-sm leading-tight'>
|
||||
<span className='truncate font-semibold'>{site.site_name}</span>
|
||||
<span className='truncate text-xs'>{site.site_desc}</span>
|
||||
</div>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarMenu>
|
||||
{navs.map((nav) => (
|
||||
<SidebarGroup key={nav.title}>
|
||||
{nav.items && <SidebarGroupLabel>{t(nav.title)}</SidebarGroupLabel>}
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
{(nav.items || [nav]).map((item) => (
|
||||
<SidebarMenuItem key={item.title}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
tooltip={t(item.title)}
|
||||
isActive={item.url === pathname}
|
||||
>
|
||||
<Link href={item.url}>
|
||||
{item.icon && <Icon icon={item.icon} />}
|
||||
<span>{t(item.title)}</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
))}
|
||||
</SidebarMenu>
|
||||
</SidebarContent>
|
||||
</Sidebar>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
'use client';
|
||||
|
||||
import { MoonIcon, SunIcon } from '@repo/ui/lotties';
|
||||
import { Button } from '@shadcn/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@shadcn/ui/dropdown-menu';
|
||||
import { useTheme } from 'next-themes';
|
||||
|
||||
export default function ThemeSwitch() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant='ghost' size='icon'>
|
||||
<MoonIcon className='size-8 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' />
|
||||
<SunIcon className='roate-90 absolute size-8 scale-0 transition-all dark:rotate-0 dark:scale-100' />
|
||||
<span className='sr-only'>Toggle theme</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align='end'>
|
||||
<DropdownMenuItem onClick={() => setTheme('light')}>Light</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('dark')}>Dark</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => setTheme('system')}>System</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
'use client';
|
||||
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { Logout } from '@/utils/common';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@shadcn/ui/avatar';
|
||||
import { Button } from '@shadcn/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from '@shadcn/ui/dropdown-menu';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export function UserNav() {
|
||||
const t = useTranslations('auth');
|
||||
const { user } = useGlobalStore();
|
||||
|
||||
if (user) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size='icon' variant='default'>
|
||||
<Avatar className='size-8'>
|
||||
<AvatarImage alt={user?.avatar ?? ''} src={user?.avatar ?? ''} />
|
||||
<AvatarFallback className='rounded-none bg-transparent'>
|
||||
{user?.email?.[0]?.toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent forceMount align='end' className='w-56'>
|
||||
<DropdownMenuLabel className='font-normal'>
|
||||
<div className='flex flex-col space-y-1'>
|
||||
<p className='text-sm font-medium leading-none'>{user?.email}</p>
|
||||
{/* <p className='text-xs leading-none text-muted-foreground'>ID: {user?.id}</p> */}
|
||||
</div>
|
||||
</DropdownMenuLabel>
|
||||
{/* <DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
Profile
|
||||
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Billing
|
||||
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>
|
||||
Settings
|
||||
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem>New Team</DropdownMenuItem>
|
||||
</DropdownMenuGroup> */}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={Logout}>
|
||||
{t('logout')}
|
||||
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user