🎉 feat: initialization
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { Separator } from "@workspace/ui/components/separator";
|
||||
import { Icon } from "@workspace/ui/composed/icon";
|
||||
import { Fragment, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGlobalStore } from "@/stores/global";
|
||||
|
||||
interface CustomData {
|
||||
community?: {
|
||||
discord?: string;
|
||||
facebook?: string;
|
||||
github?: string;
|
||||
instagram?: string;
|
||||
linkedin?: string;
|
||||
telegram?: string;
|
||||
twitter?: string;
|
||||
};
|
||||
contacts?: {
|
||||
address?: string;
|
||||
email?: string;
|
||||
telephone?: string;
|
||||
};
|
||||
website?: string;
|
||||
}
|
||||
|
||||
export default function Footer() {
|
||||
const { t } = useTranslation("components");
|
||||
const { common } = useGlobalStore();
|
||||
const { site } = common;
|
||||
|
||||
const customData = useMemo<CustomData>(() => {
|
||||
try {
|
||||
return JSON.parse(site.custom_data || "{}");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}, [site.custom_data]);
|
||||
|
||||
const links = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: "email",
|
||||
icon: "uil:envelope",
|
||||
href: customData.contacts?.email
|
||||
? `mailto:${customData.contacts.email}`
|
||||
: undefined,
|
||||
},
|
||||
{
|
||||
name: "telegram",
|
||||
icon: "uil:telegram",
|
||||
href: customData.community?.telegram,
|
||||
},
|
||||
{
|
||||
name: "twitter",
|
||||
icon: "uil:twitter",
|
||||
href: customData.community?.twitter,
|
||||
},
|
||||
{
|
||||
name: "discord",
|
||||
icon: "uil:discord",
|
||||
href: customData.community?.discord,
|
||||
},
|
||||
{
|
||||
name: "instagram",
|
||||
icon: "uil:instagram",
|
||||
href: customData.community?.instagram,
|
||||
},
|
||||
{
|
||||
name: "linkedin",
|
||||
icon: "uil:linkedin",
|
||||
href: customData.community?.linkedin,
|
||||
},
|
||||
{
|
||||
name: "github",
|
||||
icon: "uil:github",
|
||||
href: customData.community?.github,
|
||||
},
|
||||
{
|
||||
name: "facebook",
|
||||
icon: "uil:facebook",
|
||||
href: customData.community?.facebook,
|
||||
},
|
||||
],
|
||||
[customData]
|
||||
);
|
||||
return (
|
||||
<footer>
|
||||
<Separator className="my-14" />
|
||||
<div className="container mb-14 flex flex-wrap justify-between gap-4 text-muted-foreground 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" />}
|
||||
<a
|
||||
aria-label={t(
|
||||
`footer.social.${item.name}`,
|
||||
`Visit our ${item.name}`
|
||||
)}
|
||||
href={item.href}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon className="size-5 text-foreground" icon={item.icon} />
|
||||
</a>
|
||||
</Fragment>
|
||||
))}
|
||||
</nav>
|
||||
<div>
|
||||
<strong className="text-foreground">{site.site_name}</strong> ©{" "}
|
||||
{t("footer.copyright", "All rights reserved")}.
|
||||
<div>
|
||||
<a className="underline" href="/tos">
|
||||
{t("footer.tos", "Terms of Service")}
|
||||
</a>
|
||||
<a className="ml-2 underline" href="/privacy-policy">
|
||||
{t("footer.privacyPolicy", "Privacy Policy")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { buttonVariants } from "@workspace/ui/components/button";
|
||||
import { LanguageSwitch } from "@workspace/ui/composed/language-switch";
|
||||
import { ThemeSwitch } from "@workspace/ui/composed/theme-switch";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGlobalStore } from "@/stores/global";
|
||||
import { UserNav } from "./user-nav";
|
||||
|
||||
export default function Header() {
|
||||
const { t } = useTranslation("components");
|
||||
|
||||
const { common, user } = useGlobalStore();
|
||||
const { site } = common;
|
||||
const Logo = (
|
||||
<Link className="flex items-center gap-2 font-bold text-lg" to="/">
|
||||
{site.site_logo && (
|
||||
<img alt="logo" height={36} src={site.site_logo} width={36} />
|
||||
)}
|
||||
<span className="">{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 font-medium text-lg 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
|
||||
className={buttonVariants({
|
||||
size: "sm",
|
||||
})}
|
||||
to="/auth"
|
||||
>
|
||||
{t("loginRegister", "Login / Register")}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface NavItem {
|
||||
title: string;
|
||||
url: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface NavGroup {
|
||||
title: string;
|
||||
url?: string;
|
||||
icon?: string;
|
||||
items?: NavItem[];
|
||||
}
|
||||
|
||||
export function useNavs() {
|
||||
const { t } = useTranslation("components");
|
||||
|
||||
const navs: NavGroup[] = [
|
||||
{
|
||||
title: t("menu.dashboard", "Dashboard"),
|
||||
url: "/dashboard",
|
||||
icon: "uil:dashboard",
|
||||
},
|
||||
{
|
||||
title: t("menu.personal", "Personal"),
|
||||
items: [
|
||||
{
|
||||
title: t("menu.profile", "User Detail"),
|
||||
url: "/profile",
|
||||
icon: "uil:user",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("menu.server", "Server Management"),
|
||||
items: [
|
||||
{
|
||||
url: "/subscribe",
|
||||
icon: "uil:shop",
|
||||
title: t("menu.subscribe", "Subscribe"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("menu.finance", "Commerce"),
|
||||
items: [
|
||||
{
|
||||
url: "/order",
|
||||
icon: "uil:notes",
|
||||
title: t("menu.order", "Order Management"),
|
||||
},
|
||||
{
|
||||
url: "/wallet",
|
||||
icon: "uil:wallet",
|
||||
title: t("menu.wallet", "Balance"),
|
||||
},
|
||||
{
|
||||
url: "/affiliate",
|
||||
icon: "uil:users-alt",
|
||||
title: t("menu.affiliate", "Commission"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("menu.help", "Users & Support"),
|
||||
items: [
|
||||
{
|
||||
url: "/document",
|
||||
icon: "uil:book-alt",
|
||||
title: t("menu.document", "Document Management"),
|
||||
},
|
||||
{
|
||||
url: "/announcement",
|
||||
icon: "uil:megaphone",
|
||||
title: t("menu.announcement", "Announcement Management"),
|
||||
},
|
||||
{
|
||||
url: "/ticket",
|
||||
icon: "uil:message",
|
||||
title: t("menu.ticket", "Ticket Management"),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return navs;
|
||||
}
|
||||
|
||||
export function useFindNavByUrl(url: string) {
|
||||
const navs = useNavs();
|
||||
|
||||
for (const nav of navs) {
|
||||
if (nav.url && nav.url === url) {
|
||||
return [nav];
|
||||
}
|
||||
if (nav.items) {
|
||||
const current = nav.items.find((item) => item.url === url);
|
||||
if (current) {
|
||||
return [nav, current];
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function useNavItems() {
|
||||
const { t } = useTranslation("components");
|
||||
|
||||
return [
|
||||
{
|
||||
url: "/profile",
|
||||
icon: "uil:user",
|
||||
title: t("menu.profile", "User Detail"),
|
||||
},
|
||||
{
|
||||
url: "/subscribe",
|
||||
icon: "uil:shop",
|
||||
title: t("menu.subscribe", "Subscribe"),
|
||||
},
|
||||
{
|
||||
url: "/order",
|
||||
icon: "uil:notes",
|
||||
title: t("menu.order", "Order Management"),
|
||||
},
|
||||
{
|
||||
url: "/wallet",
|
||||
icon: "uil:wallet",
|
||||
title: t("menu.wallet", "Balance"),
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
"use client";
|
||||
|
||||
import { useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@workspace/ui/components/avatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@workspace/ui/components/dropdown-menu";
|
||||
import { Icon } from "@workspace/ui/composed/icon";
|
||||
import { removeCookie } from "@workspace/ui/lib/cookies";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useNavs } from "@/layout/navs";
|
||||
import { useGlobalStore } from "@/stores/global";
|
||||
|
||||
export function UserNav() {
|
||||
const { t } = useTranslation("components");
|
||||
const { user, setUser } = useGlobalStore();
|
||||
const navigate = useNavigate();
|
||||
const navs = useNavs();
|
||||
|
||||
const handleLogout = () => {
|
||||
removeCookie("Authorization");
|
||||
setUser(undefined);
|
||||
navigate({ to: "/" });
|
||||
};
|
||||
|
||||
if (user) {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="flex cursor-pointer items-center gap-2 rounded-full border bg-background px-2 py-1.5 transition-colors duration-200 hover:bg-accent">
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage
|
||||
alt={user?.avatar ?? ""}
|
||||
className="object-cover"
|
||||
src={user?.auth_methods?.[0]?.auth_identifier ?? ""}
|
||||
/>
|
||||
<AvatarFallback className="bg-gradient-to-br from-primary/90 to-primary font-medium text-background">
|
||||
{user?.auth_methods?.[0]?.auth_identifier
|
||||
.toUpperCase()
|
||||
.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="max-w-[40px] truncate text-sm sm:max-w-[100px]">
|
||||
{user?.auth_methods?.[0]?.auth_identifier.split("@")[0]}
|
||||
</span>
|
||||
<Icon
|
||||
className="size-4 text-muted-foreground"
|
||||
icon="mdi:chevron-down"
|
||||
/>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-64" forceMount>
|
||||
<div className="flex items-center justify-start gap-2 p-2">
|
||||
<Avatar className="h-10 w-10">
|
||||
<AvatarImage
|
||||
alt={user?.avatar ?? ""}
|
||||
className="object-cover"
|
||||
src={user?.avatar ?? ""}
|
||||
/>
|
||||
<AvatarFallback className="bg-gradient-to-br from-primary/90 to-primary text-background">
|
||||
{user?.auth_methods?.[0]?.auth_identifier
|
||||
.toUpperCase()
|
||||
.charAt(0)}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex flex-col space-y-0.5">
|
||||
<p className="font-medium text-sm leading-none">
|
||||
{user?.auth_methods?.[0]?.auth_identifier.split("@")[0]}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{user?.auth_methods?.[0]?.auth_identifier}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownMenuSeparator />
|
||||
{navs.map((nav) => (
|
||||
<DropdownMenuGroup key={nav.title}>
|
||||
{(nav.items || [nav]).map((item) => (
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center gap-2 py-2"
|
||||
key={item.title}
|
||||
onClick={() => {
|
||||
navigate({ to: item.url });
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
className="size-4 flex-none text-muted-foreground"
|
||||
icon={item.icon as string}
|
||||
/>
|
||||
<span className="flex-grow truncate">{item.title}</span>
|
||||
<Icon
|
||||
className="size-4 text-muted-foreground opacity-50"
|
||||
icon="lucide:chevron-right"
|
||||
/>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
))}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
className="flex cursor-pointer items-center gap-2 py-2 text-destructive focus:text-destructive"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<Icon className="size-4 flex-none" icon="uil:exit" />
|
||||
<span className="flex-grow">{t("menu.logout", "Logout")}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user