feat: select language

This commit is contained in:
Tristan Yang
2025-06-05 08:42:54 +08:00
parent e7a024e49d
commit 6f0038d4ea
8 changed files with 103 additions and 37 deletions
+34
View File
@@ -0,0 +1,34 @@
import Tippy from "@tippyjs/react";
import { useState } from "react";
import LanguageList, { LangMap, LanguageType } from "../routes/setting/Overview/Language";
import StyledButton from "./styled/Button";
import { useTranslation } from "react-i18next";
import ArrowDown from "@/assets/icons/arrow.down.svg";
import { cn } from "../utils";
type Props = {};
const SelectLanguage = ({}: Props) => {
const { i18n } = useTranslation();
const [visible, setVisible] = useState(false);
const langKey = i18n.language as LanguageType;
const [lang, setLang] = useState(LangMap[langKey]);
return (
<Tippy
onShown={setVisible.bind(null, true)}
onHidden={setVisible.bind(null, false)}
interactive
placement="bottom-end"
trigger="click"
content={<LanguageList context="aside" callback={setLang} />}
>
<StyledButton className="small ghost fixed right-3 top-3 inline-flex gap-1 !w-fit">
<span>{lang}</span>{" "}
<ArrowDown
className={cn("dark:stroke-gray-100 transition-all", visible ? "rotate-180" : "")}
/>{" "}
</StyledButton>
</Tippy>
);
};
export default SelectLanguage;
+1
View File
@@ -8,6 +8,7 @@ type Props = {
};
const SettingBlock = ({ toggler, title, desc, children }: Props) => {
if (!title) return <div className="min-w-56">{children}</div>;
return (
<div className="text-sm w-full">
<div className="flex justify-between mb-2">
+6 -5
View File
@@ -1,4 +1,3 @@
import { ChangeEvent, FormEvent, useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
@@ -17,10 +16,11 @@ import MagicLinkLogin from "./MagicLinkLogin";
import SignUpLink from "./SignUpLink";
import SocialLoginButtons from "./SocialLoginButtons";
import { shallowEqual } from "react-redux";
import SelectLanguage from "../../components/Language";
const defaultInput = {
email: "",
password: ""
password: "",
};
export default function LoginPage() {
const { name: serverName, logo } = useAppSelector((store) => store.server, shallowEqual);
@@ -44,7 +44,7 @@ export default function LoginPage() {
login({
code,
state,
type: "oidc"
type: "oidc",
});
}
// magic link
@@ -54,7 +54,7 @@ export default function LoginPage() {
// login
login({
magic_token,
type: "magiclink"
type: "magiclink",
});
} else {
// reg with magic link and set name only
@@ -104,7 +104,7 @@ export default function LoginPage() {
}
login({
...input,
type: "password"
type: "password",
});
};
@@ -199,6 +199,7 @@ export default function LoginPage() {
</div>
{whoCanSignUp === "EveryOne" && <SignUpLink />}
</div>
<SelectLanguage />
</div>
);
}
+3 -1
View File
@@ -10,6 +10,7 @@ import InviteLink from "./steps/invite-link";
import ServerName from "./steps/server-name";
import WelcomePage from "./steps/welcome-page";
import WhoCanSignUp from "./steps/who-can-sign-up";
import SelectLanguage from "../../components/Language";
const Navigator = () => {
const { activeStep, goToStep } = useWizard();
@@ -52,7 +53,7 @@ export default function OnboardingPage() {
const [serverName, setServerName] = useState("");
return (
<>
<title>{t("onboarding.title") || ""}</title>
<title>{t("onboarding.title") || ""}</title>
<div className="h-screen bg-neutral-100 dark:bg-neutral-900 overflow-y-auto">
<Wizard header={<Navigator />}>
<WelcomePage />
@@ -64,6 +65,7 @@ export default function OnboardingPage() {
<DonePage serverName={serverName} />
</Wizard>
</div>
<SelectLanguage />
</>
);
}
+15 -11
View File
@@ -3,6 +3,7 @@ import { Outlet, useOutletContext, useSearchParams } from "react-router-dom";
import { useCheckMagicTokenValidMutation } from "@/app/services/auth";
import ExpiredTip from "./ExpiredTip";
import SelectLanguage from "../../components/Language";
type ContextType = { token: string };
export default function RegContainer() {
@@ -23,19 +24,22 @@ export default function RegContainer() {
}, [tokenIsValid, magic_token]);
if (checkingToken) return <div className="dark:text-gray-100">Checking Magic Link...</div>;
return (
<div className="flex-center h-screen overflow-x-hidden overflow-y-auto dark:bg-gray-700">
<div className="py-8 px-10 shadow-md rounded-xl max-h-[95vh] overflow-y-auto overflow-x-hidden">
{magic_token ? (
tokenIsValid ? (
<Outlet context={{ token }} />
<>
<div className="flex-center h-screen overflow-x-hidden overflow-y-auto dark:bg-gray-700">
<div className="py-8 px-10 shadow-md rounded-xl max-h-[95vh] overflow-y-auto overflow-x-hidden">
{magic_token ? (
tokenIsValid ? (
<Outlet context={{ token }} />
) : (
<ExpiredTip />
)
) : (
<ExpiredTip />
)
) : (
<Outlet context={{ token }} />
)}
<Outlet context={{ token }} />
)}
</div>
</div>
</div>
<SelectLanguage />
</>
);
}
+12 -8
View File
@@ -4,6 +4,7 @@ import { useNavigate, useParams } from "react-router-dom";
import { useSendLoginMagicLinkMutation } from "@/app/services/auth";
import SentTip from "./SentTip";
import SelectLanguage from "../../components/Language";
export default function SendMagicLinkPage() {
const { email = "" } = useParams();
@@ -46,14 +47,17 @@ export default function SendMagicLinkPage() {
};
return (
<div className="flex-center h-screen dark:bg-gray-800">
<div className="py-8 px-10 shadow-md rounded-xl bg-white dark:bg-gray-700">
{isSuccess ? (
<SentTip email={email} handleBack={handlePwdPath} />
) : isLoading ? (
<div className="">Sending...</div>
) : null}
<>
<SelectLanguage />
<div className="flex-center h-screen dark:bg-gray-800">
<div className="py-8 px-10 shadow-md rounded-xl bg-white dark:bg-gray-700">
{isSuccess ? (
<SentTip email={email} handleBack={handlePwdPath} />
) : isLoading ? (
<div className="">Sending...</div>
) : null}
</div>
</div>
</div>
</>
);
}
+16 -6
View File
@@ -5,8 +5,8 @@ import SettingBlock from "@/components/SettingBlock";
import StyledRadio from "@/components/styled/Radio";
// type Props = {}
type LanguageType = "en" | "zh" | "jp" | "tr" | "pt" | "es" | "fr" | "ru";
const LangMap: Record<LanguageType, string> = {
export type LanguageType = "en" | "zh" | "jp" | "tr" | "pt" | "es" | "fr" | "ru";
export const LangMap: Record<LanguageType, string> = {
en: "English",
zh: "中文",
tr: "Türkçe",
@@ -14,15 +14,24 @@ const LangMap: Record<LanguageType, string> = {
pt: "Portuguese",
fr: "Français",
es: "Español",
ru: "Русский"
ru: "Русский",
};
const Index = () => {
const LanguageList = ({
context = "overview",
callback,
}: {
context?: "overview" | "aside";
callback?: (_v: string) => void;
}) => {
const { t, i18n } = useTranslation("setting");
const handleGuestToggle = (v: LanguageType) => {
i18n.changeLanguage(v);
};
return (
<SettingBlock title={t("overview.lang.title")} desc={t("overview.lang.desc")}>
<SettingBlock
title={context == "overview" ? t("overview.lang.title") : ""}
desc={t("overview.lang.desc")}
>
<StyledRadio
options={Object.values(LangMap)}
values={Object.keys(LangMap)}
@@ -30,10 +39,11 @@ const Index = () => {
onChange={(v) => {
const _v = v as LanguageType;
handleGuestToggle(_v);
callback?.(LangMap[_v]);
}}
/>
</SettingBlock>
);
};
export default Index;
export default LanguageList;
+16 -6
View File
@@ -5,8 +5,8 @@ import SettingBlock from "@/components/SettingBlock";
import StyledRadio from "@/components/styled/Radio";
// type Props = {}
type LanguageType = "en" | "zh" | "jp" | "tr" | "pt" | "es" | "fr" | "ru";
const LangMap: Record<LanguageType, string> = {
export type LanguageType = "en" | "zh" | "jp" | "tr" | "pt" | "es" | "fr" | "ru";
export const LangMap: Record<LanguageType, string> = {
en: "English",
zh: "中文",
tr: "Türkçe",
@@ -14,15 +14,24 @@ const LangMap: Record<LanguageType, string> = {
pt: "Portuguese",
fr: "Français",
es: "Español",
ru: "Русский"
ru: "Русский",
};
const Index = () => {
const LanguageList = ({
context = "overview",
callback,
}: {
context?: "overview" | "aside";
callback?: (_v: string) => void;
}) => {
const { t, i18n } = useTranslation("setting");
const handleGuestToggle = (v: LanguageType) => {
i18n.changeLanguage(v);
};
return (
<SettingBlock title={t("overview.lang.title")} desc={t("overview.lang.desc")}>
<SettingBlock
title={context == "overview" ? t("overview.lang.title") : ""}
desc={t("overview.lang.desc")}
>
<StyledRadio
options={Object.values(LangMap)}
values={Object.keys(LangMap)}
@@ -30,10 +39,11 @@ const Index = () => {
onChange={(v) => {
const _v = v as LanguageType;
handleGuestToggle(_v);
callback?.(LangMap[_v]);
}}
/>
</SettingBlock>
);
};
export default Index;
export default LanguageList;