feat: switch language on the panel (#1882)
* feat: switch language on the panel move nameMap to i18n.ts add Switch Language i18n add switch language in ConnectButton.tsx close https://github.com/Crossbell-Box/xLog/issues/1881 * feat: Add auto placement option to Menu component to looks like submenu adjust dom in connectButton * feat: Add support for nested menus in Menu component --------- Co-authored-by: pengyunfei <pengyunfei@360.cn>
This commit is contained in:
parent
3a3f6c19cd
commit
2f76c6b420
|
|
@ -1,6 +1,6 @@
|
|||
"use client"
|
||||
|
||||
import { useTranslations } from "next-intl"
|
||||
import { useLocale, useTranslations } from "next-intl"
|
||||
import React, { useEffect, useState } from "react"
|
||||
|
||||
import {
|
||||
|
|
@ -32,6 +32,7 @@ import { BellAlertIcon } from "@heroicons/react/24/solid"
|
|||
import { Avatar } from "~/components/ui/Avatar"
|
||||
import { Button, type Variant, type VariantColor } from "~/components/ui/Button"
|
||||
import { Menu } from "~/components/ui/Menu"
|
||||
import { nameMap } from "~/i18n"
|
||||
import { SITE_URL } from "~/lib/env"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
|
@ -39,6 +40,8 @@ import { cn } from "~/lib/utils"
|
|||
type HeaderLinkType = {
|
||||
icon?: React.ReactNode
|
||||
label: string | JSX.Element
|
||||
isSubmenu?: boolean
|
||||
subMenu?: HeaderLinkType[]
|
||||
} & (
|
||||
| {
|
||||
href: string
|
||||
|
|
@ -77,6 +80,7 @@ export const ConnectButton = ({
|
|||
sizeDecrease = "xs"
|
||||
}
|
||||
|
||||
const locale = useLocale()
|
||||
const [ssrReady, account] = useAccountState(({ ssrReady, computed }) => [
|
||||
ssrReady,
|
||||
computed.account,
|
||||
|
|
@ -95,23 +99,17 @@ export const ConnectButton = ({
|
|||
const selectCharactersModal = useSelectCharactersModal()
|
||||
const walletMintNewCharacterModal = useWalletMintNewCharacterModal()
|
||||
|
||||
const showNotificationModal = useShowNotificationModal()
|
||||
const { isAllRead } = useNotifications()
|
||||
|
||||
const [InsufficientBalance, setInsufficientBalance] = useState<boolean>(false)
|
||||
const t = useTranslations()
|
||||
|
||||
useEffect(() => {
|
||||
if (balance) {
|
||||
if (
|
||||
BigInt(balance.value.toString()) >
|
||||
BigInt("1" + "0".repeat(balance.decimals - 2))
|
||||
) {
|
||||
setInsufficientBalance(false)
|
||||
} else {
|
||||
setInsufficientBalance(true)
|
||||
}
|
||||
}
|
||||
if (!balance) return
|
||||
|
||||
const balanceValue =
|
||||
BigInt(balance.value.toString()) <=
|
||||
BigInt("1" + "0".repeat(balance.decimals - 2))
|
||||
|
||||
setInsufficientBalance(balanceValue)
|
||||
}, [balance])
|
||||
|
||||
const dropdownLinks: HeaderLinkType[] = [
|
||||
|
|
@ -141,6 +139,21 @@ export const ConnectButton = ({
|
|||
copyLabel()
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: "i-mingcute-translate-2-line",
|
||||
isSubmenu: true,
|
||||
label: t("Switch Language") || "",
|
||||
subMenu: Object.keys(nameMap).map((lo) => ({
|
||||
label: nameMap[lo],
|
||||
onClick: () => {
|
||||
document.cookie = `NEXT_LOCALE=${lo};`
|
||||
window.location.reload()
|
||||
},
|
||||
})),
|
||||
onClick: (e) => {
|
||||
e.preventDefault()
|
||||
},
|
||||
},
|
||||
...(account?.type === "wallet"
|
||||
? [
|
||||
{
|
||||
|
|
@ -217,182 +230,133 @@ export const ConnectButton = ({
|
|||
href: `${SITE_URL}/dashboard/${account?.character?.handle}/editor?type=portfolio`,
|
||||
},
|
||||
]
|
||||
// TODO: maybe test this style effect
|
||||
if (!ssrReady) return <div aria-hidden></div>
|
||||
|
||||
if (!account)
|
||||
return (
|
||||
<div>
|
||||
<Button
|
||||
className="text-accent"
|
||||
onClick={openConnectModal}
|
||||
style={{ height: avatarSize + "px" }}
|
||||
variant={variant || "primary"}
|
||||
variantColor={variantColor}
|
||||
>
|
||||
{t("Connect")}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
{...(!ssrReady && {
|
||||
"aria-hidden": true,
|
||||
style: {
|
||||
opacity: 0,
|
||||
pointerEvents: "none",
|
||||
userSelect: "none",
|
||||
},
|
||||
})}
|
||||
>
|
||||
{(() => {
|
||||
if (!account) {
|
||||
return (
|
||||
<Button
|
||||
className="text-accent"
|
||||
onClick={openConnectModal}
|
||||
style={{ height: avatarSize + "px" }}
|
||||
variant={variant || "primary"}
|
||||
variantColor={variantColor}
|
||||
<div>
|
||||
<div
|
||||
className="relative flex items-center -mr-2"
|
||||
style={{ height: avatarSize + "px" }}
|
||||
>
|
||||
{!hideNotification && <Notification />}
|
||||
<Menu
|
||||
placement="bottom"
|
||||
target={
|
||||
<PlusCircleIcon
|
||||
className={`${
|
||||
size === "base" ? "size-6 ml-2" : "size-5 ml-1"
|
||||
} text-zinc-500 cursor-pointer hidden sm:block`}
|
||||
/>
|
||||
}
|
||||
dropdown={
|
||||
<div
|
||||
className={`min-w-[140px] ${
|
||||
size === "base" ? "text-base" : "text-sm"
|
||||
}`}
|
||||
>
|
||||
{t("Connect")}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className="relative flex items-center -mr-2"
|
||||
style={{ height: avatarSize + "px" }}
|
||||
>
|
||||
{!hideNotification && (
|
||||
<>
|
||||
{isAllRead ? (
|
||||
<BellIcon
|
||||
className={`${
|
||||
size === "base" ? "size-6" : "size-5"
|
||||
} text-zinc-500 cursor-pointer sm:hover:animate-buzz-out`}
|
||||
onClick={showNotificationModal}
|
||||
/>
|
||||
) : (
|
||||
<BellAlertIcon
|
||||
className={`${
|
||||
size === "base" ? "size-6" : "size-5"
|
||||
} text-accent cursor-pointer sm:hover:animate-buzz-out`}
|
||||
onClick={showNotificationModal}
|
||||
/>
|
||||
)}
|
||||
<Menu
|
||||
placement="bottom"
|
||||
target={
|
||||
<PlusCircleIcon
|
||||
className={`${
|
||||
size === "base" ? "size-6 ml-2" : "size-5 ml-1"
|
||||
} text-zinc-500 cursor-pointer hidden sm:block`}
|
||||
/>
|
||||
}
|
||||
dropdown={
|
||||
<div
|
||||
className={`min-w-[140px] ${
|
||||
size === "base" ? "text-base" : "text-sm"
|
||||
}`}
|
||||
{addDropdownLinks.map((link, i) => (
|
||||
<ConnectMenuItem link={link} size={size} key={i} />
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
></Menu>
|
||||
<div className="h-full w-[2px] py-1 ml-2">
|
||||
<div className="size-full bg-zinc-200 rounded-full"></div>
|
||||
</div>
|
||||
<Menu
|
||||
placement="bottom-end"
|
||||
target={
|
||||
<button
|
||||
className="flex items-center hover:bg-hover transition-colors py-1 px-2 rounded-lg ml-1 focus-visible:outline focus-visible:outline-accent focus-visible:outline-offset-1"
|
||||
type="button"
|
||||
aria-label="connector"
|
||||
>
|
||||
<Avatar
|
||||
cid={account.character?.characterId}
|
||||
className="align-middle"
|
||||
images={account.character?.metadata?.content?.avatars || []}
|
||||
name={account.character?.metadata?.content?.name}
|
||||
size={avatarSize}
|
||||
/>
|
||||
{!hideName && (
|
||||
<>
|
||||
<div
|
||||
className={`flex-1 flex-col min-w-0 ml-2 max-w-[100px] ${
|
||||
mobileSimplification ? "hidden sm:flex" : "flex"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`text-left leading-none font-medium truncate ${
|
||||
InsufficientBalance ? "text-red-600" : "text-gray-600"
|
||||
} ${size === "base" ? "text-base" : "text-sm"}`}
|
||||
style={{ marginBottom: "0.15rem" }}
|
||||
>
|
||||
{addDropdownLinks.map((link, i) => (
|
||||
<Menu.Item
|
||||
key={i}
|
||||
icon={<i className={cn(link.icon, "text-base")} />}
|
||||
className={`${
|
||||
size === "base" ? "pl-5 pr-6 h-11" : "pl-4 pr-5 h-9"
|
||||
} whitespace-nowrap`}
|
||||
{...("href" in link
|
||||
? {
|
||||
type: "link",
|
||||
href: link.href,
|
||||
}
|
||||
: {
|
||||
type: "button",
|
||||
onClick: link.onClick,
|
||||
})}
|
||||
>
|
||||
{link.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="h-full w-[2px] py-1 ml-2">
|
||||
<div className="size-full bg-zinc-200 rounded-full"></div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Menu
|
||||
placement="bottom-end"
|
||||
target={
|
||||
<button
|
||||
className="flex items-center hover:bg-hover transition-colors py-1 px-2 rounded-lg ml-1 focus-visible:outline focus-visible:outline-accent focus-visible:outline-offset-1"
|
||||
type="button"
|
||||
aria-label="connector"
|
||||
>
|
||||
<Avatar
|
||||
cid={account.character?.characterId}
|
||||
className="align-middle"
|
||||
images={account.character?.metadata?.content?.avatars || []}
|
||||
name={account.character?.metadata?.content?.name}
|
||||
size={avatarSize}
|
||||
/>
|
||||
{!hideName && (
|
||||
<>
|
||||
<div
|
||||
className={`flex-1 flex-col min-w-0 ml-2 max-w-[100px] ${
|
||||
mobileSimplification ? "hidden sm:flex" : "flex"
|
||||
{account.character?.metadata?.content?.name ||
|
||||
getAccountDisplayName(account)}
|
||||
</span>
|
||||
{account.character?.handle && (
|
||||
<span
|
||||
className={`text-left leading-none ${
|
||||
sizeDecrease === "sm" ? "text-sm" : "text-xs"
|
||||
} truncate ${
|
||||
InsufficientBalance ? "text-red-400" : "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`text-left leading-none font-medium truncate ${
|
||||
InsufficientBalance
|
||||
? "text-red-600"
|
||||
: "text-gray-600"
|
||||
} ${size === "base" ? "text-base" : "text-sm"}`}
|
||||
style={{ marginBottom: "0.15rem" }}
|
||||
>
|
||||
{account.character?.metadata?.content?.name ||
|
||||
getAccountDisplayName(account)}
|
||||
</span>
|
||||
{account.character?.handle && (
|
||||
<span
|
||||
className={`text-left leading-none ${
|
||||
sizeDecrease === "sm" ? "text-sm" : "text-xs"
|
||||
} truncate ${
|
||||
InsufficientBalance
|
||||
? "text-red-400"
|
||||
: "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{"@" + account.character?.handle ||
|
||||
getAccountDisplayName(account)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<i className="i-mingcute-down-line text-xl ml-[2px]" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
dropdown={
|
||||
<div
|
||||
className={`min-w-[140px] ${
|
||||
size === "base" ? "text-base" : "text-sm"
|
||||
}`}
|
||||
>
|
||||
{dropdownLinks.map((link, i) => (
|
||||
<Menu.Item
|
||||
{"@" + account.character?.handle ||
|
||||
getAccountDisplayName(account)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<i className="i-mingcute-down-line text-xl ml-[2px]" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
dropdown={
|
||||
<div
|
||||
className={`min-w-[140px] ${
|
||||
size === "base" ? "text-base" : "text-sm"
|
||||
}`}
|
||||
>
|
||||
{dropdownLinks.map((link, i) => {
|
||||
if (link.isSubmenu) {
|
||||
return (
|
||||
<Menu.SubMenu
|
||||
key={i}
|
||||
icon={<i className={cn(link.icon, "text-base")} />}
|
||||
className={`${
|
||||
size === "base" ? "pl-5 pr-6 h-11" : "pl-4 pr-5 h-9"
|
||||
} whitespace-nowrap`}
|
||||
{...("href" in link
|
||||
? {
|
||||
type: "link",
|
||||
href: link.href,
|
||||
}
|
||||
: {
|
||||
type: "button",
|
||||
onClick: link.onClick,
|
||||
})}
|
||||
icon={
|
||||
<i className={cn(link.icon, "text-base mr-2 size-4")} />
|
||||
}
|
||||
dropdown={link.subMenu?.map((subLink, j) => (
|
||||
<ConnectMenuItem link={subLink} size={size} key={j} />
|
||||
))}
|
||||
>
|
||||
{link.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
<span>{link.label}</span>
|
||||
</Menu.SubMenu>
|
||||
)
|
||||
} else {
|
||||
return <ConnectMenuItem link={link} size={size} key={i} />
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -421,3 +385,52 @@ function getAccountDisplayName(account: GeneralAccount | null) {
|
|||
|
||||
return value.slice(0, 5) + "..." + value.slice(-4)
|
||||
}
|
||||
// maybe we could be create a file for this component
|
||||
function Notification() {
|
||||
const showNotificationModal = useShowNotificationModal()
|
||||
const { isAllRead } = useNotifications()
|
||||
|
||||
if (isAllRead)
|
||||
return (
|
||||
<BellIcon
|
||||
className="size-6 text-zinc-500 cursor-pointer sm:hover:animate-buzz-out"
|
||||
onClick={showNotificationModal}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<BellAlertIcon
|
||||
className="size-6 text-accent cursor-pointer sm:hover:animate-buzz-out"
|
||||
onClick={showNotificationModal}
|
||||
/>
|
||||
)
|
||||
}
|
||||
// the all menuItem in this file
|
||||
// it can just be used in this file, because it's props link is the headerLinkType
|
||||
function ConnectMenuItem({
|
||||
link,
|
||||
size,
|
||||
}: {
|
||||
link: HeaderLinkType
|
||||
size: "base" | "sm"
|
||||
}) {
|
||||
return (
|
||||
<Menu.Item
|
||||
icon={<i className={cn(link.icon, "text-base")} />}
|
||||
className={`${
|
||||
size === "base" ? "pl-5 pr-6 h-11" : "pl-4 pr-5 h-9"
|
||||
} whitespace-nowrap`}
|
||||
{...("href" in link
|
||||
? {
|
||||
type: "link",
|
||||
href: link.href,
|
||||
}
|
||||
: {
|
||||
type: "button",
|
||||
onClick: link.onClick,
|
||||
})}
|
||||
>
|
||||
{link.label}
|
||||
</Menu.Item>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,20 +2,13 @@
|
|||
|
||||
import { useLocale } from "next-intl"
|
||||
|
||||
import { locales } from "~/i18n"
|
||||
import { locales, nameMap } from "~/i18n"
|
||||
|
||||
import { Menu } from "../ui/Menu"
|
||||
|
||||
export function LanguageSwitch() {
|
||||
const locale = useLocale()
|
||||
|
||||
const nameMap: Record<string, string> = {
|
||||
en: "English",
|
||||
zh: "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
ja: "日本語",
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
placement="top"
|
||||
|
|
|
|||
|
|
@ -2,7 +2,13 @@ import { AnimatePresence, m } from "framer-motion"
|
|||
import Link from "next/link"
|
||||
import React, { Fragment } from "react"
|
||||
|
||||
import { autoUpdate, Placement, shift, useFloating } from "@floating-ui/react"
|
||||
import {
|
||||
autoPlacement,
|
||||
autoUpdate,
|
||||
Placement,
|
||||
shift,
|
||||
useFloating,
|
||||
} from "@floating-ui/react"
|
||||
import { Menu as HeadlessUiMenu } from "@headlessui/react"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
|
@ -17,16 +23,21 @@ export function Menu({
|
|||
target,
|
||||
dropdown,
|
||||
placement = "bottom-start",
|
||||
enableAutoPlacement = false,
|
||||
allowedPlacements = ["bottom-start"],
|
||||
}: React.PropsWithChildren<{
|
||||
target: JSX.Element
|
||||
dropdown: React.ReactNode
|
||||
placement?: Placement
|
||||
enableAutoPlacement?: boolean
|
||||
allowedPlacements?: Placement[]
|
||||
}>) {
|
||||
const { refs, floatingStyles } = useFloating({
|
||||
placement,
|
||||
middleware: [
|
||||
// Prevent overflowing viewport
|
||||
shift({ padding: 20 }),
|
||||
enableAutoPlacement ? autoPlacement({ allowedPlacements }) : undefined,
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
})
|
||||
|
|
@ -145,3 +156,40 @@ Menu.Item = function MenuItem({
|
|||
</HeadlessUiMenu.Item>
|
||||
)
|
||||
}
|
||||
|
||||
// TODO: Add support for nested menus
|
||||
// this is a hack submenu use the div and <Menu /> component
|
||||
// about why i'm not use the headlessui Menu.Item, because it's not support nested menu,
|
||||
// and it will make the autoPlacement not work properly.
|
||||
// problem that's will happen is the styles maybe not sync with the <Menu.Item /> component
|
||||
|
||||
Menu.SubMenu = function MenuSubMenu({
|
||||
icon,
|
||||
children,
|
||||
dropdown,
|
||||
}: React.PropsWithChildren<{
|
||||
icon: React.ReactNode
|
||||
dropdown: React.ReactNode
|
||||
}>) {
|
||||
return (
|
||||
<Menu
|
||||
placement="right-start"
|
||||
enableAutoPlacement
|
||||
allowedPlacements={["left-start", "right-start"]}
|
||||
target={
|
||||
<div
|
||||
className="w-full px-3 flex items-center flex-nowrap
|
||||
pl-5 pr-6 h-11 whitespace-nowrap
|
||||
hover:bg-hover
|
||||
cursor-pointer select-none
|
||||
"
|
||||
aria-hidden
|
||||
>
|
||||
{icon}
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
dropdown={dropdown}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ export const languageNames: Record<(typeof locales)[number], string> = {
|
|||
zh: "Chinese (Simplified)",
|
||||
"zh-TW": "Chinese (Traditional)",
|
||||
}
|
||||
export const nameMap: Record<string, string> = {
|
||||
en: "English",
|
||||
zh: "简体中文",
|
||||
"zh-TW": "繁體中文",
|
||||
ja: "日本語",
|
||||
}
|
||||
export const defaultLocale = "en"
|
||||
|
||||
export default getRequestConfig(async ({ locale }) => ({
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@
|
|||
"Dashboard": "ダッシュボード",
|
||||
"Copied!": "コピーしました!",
|
||||
"Operator Sign": "オペレーター署名",
|
||||
"Switch Language": "言語を切り替える",
|
||||
"Switch Characters": "キャラクターを切り替える",
|
||||
"Upgrade to Wallet": "ウォレットにアップグレード",
|
||||
"Disconnect": "中断する",
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@
|
|||
"Dashboard": "主控臺",
|
||||
"Copied!": "拷貝完成",
|
||||
"Operator Sign": "簽名授權",
|
||||
"Switch Language": "切換語言",
|
||||
"Switch Characters": "切換身份",
|
||||
"Upgrade to Wallet": "升級為錢包認證",
|
||||
"Disconnect": "中斷連線",
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@
|
|||
"Dashboard": "仪表盘",
|
||||
"Copied!": "已复制!",
|
||||
"Operator Sign": "签名授权",
|
||||
"Switch Language": "切换语言",
|
||||
"Switch Characters": "切换角色",
|
||||
"Upgrade to Wallet": "升级为钱包",
|
||||
"Disconnect": "断开连接",
|
||||
|
|
|
|||
Loading…
Reference in New Issue