🎉 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,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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,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'),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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,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,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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user