🎉 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';
}
+77
View File
@@ -0,0 +1,77 @@
'use client';
import {
NEXT_PUBLIC_DISCORD_LINK,
NEXT_PUBLIC_EMAIL,
NEXT_PUBLIC_GITHUB_LINK,
NEXT_PUBLIC_INSTAGRAM_LINK,
NEXT_PUBLIC_LINKEDIN_LINK,
NEXT_PUBLIC_TELEGRAM_LINK,
NEXT_PUBLIC_TWITTER_LINK,
} from '@/config/constants';
import useGlobalStore from '@/config/use-global';
import { Icon } from '@iconify/react/dist/iconify.js';
import { Separator } from '@shadcn/ui/separator';
import { useTranslations } from 'next-intl';
import Link from 'next/link';
import { Fragment } from 'react';
const Links = [
{
icon: 'uil:envelope',
href: NEXT_PUBLIC_EMAIL ?? `mailto:${NEXT_PUBLIC_EMAIL}`,
},
{
icon: 'uil:telegram',
href: NEXT_PUBLIC_TELEGRAM_LINK,
},
{
icon: 'uil:twitter',
href: NEXT_PUBLIC_TWITTER_LINK,
},
{
icon: 'uil:discord',
href: NEXT_PUBLIC_DISCORD_LINK,
},
{
icon: 'uil:instagram',
href: NEXT_PUBLIC_INSTAGRAM_LINK,
},
{
icon: 'uil:linkedin',
href: NEXT_PUBLIC_LINKEDIN_LINK,
},
{
icon: 'uil:github',
href: NEXT_PUBLIC_GITHUB_LINK,
},
];
export default function Footer() {
const { common } = useGlobalStore();
const { site } = common;
const t = useTranslations('auth');
return (
<footer>
<Separator className='my-14' />
<div className='text-muted-foreground container mb-14 flex flex-wrap justify-between gap-4 text-sm'>
<nav className='flex flex-wrap items-center gap-2'>
{Links.filter((item) => item.href).map((item, index) => (
<Fragment key={index}>
{index !== 0 && <Separator orientation='vertical' />}
<Link href={item.href!}>
<Icon icon={item.icon} className='text-foreground size-5' />
</Link>
</Fragment>
))}
</nav>
<div>
<strong className='text-foreground'>{site.site_name}</strong> © All rights reserved.
<Link href='/tos' className='ml-2 underline'>
{t('tos')}
</Link>
</div>
</div>
</footer>
);
}
+45
View File
@@ -0,0 +1,45 @@
'use client';
import useGlobalStore from '@/config/use-global';
import { buttonVariants } from '@shadcn/ui/button';
import Image from 'next/legacy/image';
import Link from 'next/link';
import LanguageSwitch from './language-switch';
// import { MobileSidebar } from './mobile-sidebar';
import ThemeSwitch from './theme-switch';
import { UserNav } from './user-nav';
export default function Header() {
const { common, user } = useGlobalStore();
const { site } = common;
const Logo = (
<Link href='/' className='flex items-center gap-2 text-lg font-bold'>
{site.site_logo && <Image src={site.site_logo} width={48} height={48} alt='logo' />}
<span>{site.site_name}</span>
</Link>
);
return (
<header className='sticky top-0 z-50 border-b backdrop-blur-md'>
<div className='container flex h-16 items-center justify-between'>
<nav className='flex-col gap-6 text-lg font-medium md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6'>
{Logo}
</nav>
<div className='flex flex-1 items-center justify-end gap-2'>
<LanguageSwitch />
<ThemeSwitch />
<UserNav />
{!user && (
<Link
href='/auth'
className={buttonVariants({
size: 'sm',
})}
>
/
</Link>
)}
</div>
</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>
);
}
+24
View File
@@ -0,0 +1,24 @@
'use client';
import { ProList as _ProList, ProListProps } from '@repo/ui/pro-list';
import { useTranslations } from 'next-intl';
export { type ProListActions, type ProListProps } from '@repo/ui/pro-list';
export function ProList<TData, TValue extends Record<string, unknown>>(
props: ProListProps<TData, TValue>,
) {
const t = useTranslations('common.list');
return (
<_ProList
{...props}
texts={{
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>
);
}
+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>
);
}
+78
View File
@@ -0,0 +1,78 @@
'use client';
import { navs } from '@/config/navs';
import useGlobalStore from '@/config/use-global';
import { Logout } from '@/utils/common';
import { Icon } from '@iconify/react';
import { Avatar, AvatarFallback, AvatarImage } from '@shadcn/ui/avatar';
import { Button } from '@shadcn/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@shadcn/ui/dropdown-menu';
import { useTranslations } from 'next-intl';
import { useRouter } from 'next/navigation';
export function UserNav() {
const t = useTranslations('menu');
const { user, setUser } = useGlobalStore();
const router = useRouter();
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-muted-foreground text-xs leading-none'>ID: {user?.id}</p>
<p className='text-sm font-medium leading-none'>{user?.email}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
{navs.map((nav) => (
<DropdownMenuGroup key={nav.title}>
{/* {nav.items && <DropdownMenuLabel>{t(nav.title)}</DropdownMenuLabel>} */}
{(nav.items || [nav]).map((item) => (
<DropdownMenuItem
key={item.title}
onClick={() => {
router.push(`${item.url}`);
}}
>
<Icon className='mr-2 size-4 flex-none' icon={item.icon!} />
<span className='truncate'>{t(item.title)}</span>
</DropdownMenuItem>
))}
</DropdownMenuGroup>
))}
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
Logout();
setUser();
}}
>
<Icon className='mr-2 size-4 flex-none' icon='uil:exit' />
{t('logout')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}
}