🎉 chore(init): project initialization

This commit is contained in:
web@ppanel
2024-11-14 01:22:43 +07:00
commit 829edfa824
479 changed files with 61413 additions and 0 deletions
+42
View File
@@ -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';
}
+56
View File
@@ -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>
);
}
+49
View File
@@ -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>
);
}
+28
View File
@@ -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'),
}}
/>
);
}
+51
View File
@@ -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>
);
}
+80
View File
@@ -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>
);
}
+32
View File
@@ -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>
);
}
+67
View File
@@ -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>
);
}
}