feat: i18n

This commit is contained in:
Tristan Yang
2022-11-23 19:38:45 +08:00
parent 9b3dc2f740
commit 48ffdcc1b5
97 changed files with 1470 additions and 652 deletions
+4 -2
View File
@@ -4,6 +4,7 @@ import usePinMessage from "../../../common/hook/usePinMessage";
import PreviewMessage from "../../../common/component/Message/PreviewMessage";
import IconSurprise from "../../../assets/icons/emoji.surprise.svg";
import IconClose from "../../../assets/icons/close.svg";
import { useTranslation } from "react-i18next";
const Styled = styled.div`
padding: 16px;
background: #f9fafb;
@@ -86,6 +87,7 @@ type Props = {
id: number;
};
const PinList: FC<Props> = ({ id }: Props) => {
const { t } = useTranslation("chat");
const { pins, unpinMessage, canPin } = usePinMessage(id);
const handleUnpin = (evt: FormEvent<HTMLButtonElement>) => {
const { mid } = evt.currentTarget.dataset;
@@ -95,12 +97,12 @@ const PinList: FC<Props> = ({ id }: Props) => {
const noPins = pins.length == 0;
return (
<Styled>
<h4 className="head">Pinned Message({pins.length})</h4>
<h4 className="head">{t("pinned_msg")}({pins.length})</h4>
{noPins ? (
<div className="none">
<IconSurprise />
<div className="tip">This channel doesnt have any pinned message yet.</div>
<div className="tip">{t("pin_empty_tip")}</div>
</div>
) : (
<ul className="list">
+9 -7
View File
@@ -28,11 +28,13 @@ import InviteModal from "../../../common/component/InviteModal";
import LoadMore from "../LoadMore";
import { useAppSelector } from "../../../app/store";
import { AgoraConfig } from "../../../types/server";
import { useTranslation } from "react-i18next";
type Props = {
cid?: number;
dropFiles?: File[];
};
function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
const { t } = useTranslation("chat");
const { values: agoraConfig } = useConfig("agora");
const {
list: msgIds,
@@ -105,7 +107,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
</Tooltip>
</li>
)}
<Tooltip tip="Pin" placement="left">
<Tooltip tip={t("pin")} placement="left">
<Tippy
placement="left-start"
popperOptions={{ strategy: "fixed" }}
@@ -119,7 +121,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
</li>
</Tippy>
</Tooltip>
<Tooltip tip="Favorite" placement="left">
<Tooltip tip={t("fav")} placement="left">
<Tippy
placement="left-start"
popperOptions={{ strategy: "fixed" }}
@@ -137,7 +139,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
className={`tool ${membersVisible ? "active" : ""}`}
onClick={toggleMembersVisible}
>
<Tooltip tip="Channel Members" placement="left">
<Tooltip tip={t("channel_members")} placement="left">
<IconPeople />
</Tooltip>
</li>
@@ -160,7 +162,7 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
{addVisible && (
<div className="add" onClick={toggleAddVisible}>
<img className="icon" src={addIcon} />
<div className="txt">Add members</div>
<div className="txt">{t("add_channel_members")}</div>
</div>
)}
{memberIds.map((uid: number) => {
@@ -186,12 +188,12 @@ function ChannelChat({ cid = 0, dropFiles = [] }: Props) {
<LoadMore pullUp={pullUp} />
) : (
<div className="info">
<h2 className="title">Welcome to #{name} !</h2>
<p className="desc">This is the start of the #{name} channel. </p>
<h2 className="title">{t("welcome_channel", { name })}</h2>
<p className="desc">{t("welcome_desc", { name })} </p>
{loginUser?.is_admin && (
<NavLink to={`/setting/channel/${cid}?f=${pathname}`} className="edit">
<EditIcon className="icon" />
Edit Channel
{t("edit_channel")}
</NavLink>
)}
</div>
+6 -4
View File
@@ -15,12 +15,14 @@ import StyledLink from "./styled";
import ChannelIcon from "../../../common/component/ChannelIcon";
import { getUnreadCount } from "../utils";
import { useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next";
interface IProps {
id: number;
setFiles: (files: File[]) => void;
toggleRemoveConfirm: (id: number) => void;
}
const NavItem: FC<IProps> = ({ id, setFiles, toggleRemoveConfirm }) => {
const { t } = useTranslation();
const { pathname } = useLocation();
const [inviteModalVisible, setInviteModalVisible] = useState(false);
const navigate = useNavigate();
@@ -118,20 +120,20 @@ const NavItem: FC<IProps> = ({ id, setFiles, toggleRemoveConfirm }) => {
hideMenu={hideContextMenu}
items={[
{
title: "Mark As Read",
title: t("action.mark_read"),
underline: true,
handler: handleReadAll
},
{
title: muted ? "Unmute" : "Mute",
title: muted ? t("action.unmute") : t("action.mute"),
handler: handleMute
},
{
title: "Invite People",
title: t("action.invite_people"),
handler: toggleInviteModalVisible
},
{
title: "Delete Channel",
title: t("channel.delete", { ns: "setting" }),
danger: true,
handler: toggleRemoveConfirm.bind(null, id)
}
+5 -2
View File
@@ -17,6 +17,8 @@ import User from "../../../common/component/User";
import { ContentTypes } from "../../../app/config";
import { useAppSelector } from "../../../app/store";
import { ArchiveMessage } from "../../../types/resource";
import { useTranslation } from "react-i18next";
interface IProps {
uid: number;
mid?: number;
@@ -24,6 +26,7 @@ interface IProps {
setFiles: (files: File[]) => void;
}
const NavItem: FC<IProps> = ({ uid, mid = 0, unreads, setFiles }) => {
const { t } = useTranslation();
const [previewMsg, setPreviewMsg] = useState<ArchiveMessage>();
const { messages: normalizedMessages, normalizeMessage } = useNormalizeMessage();
const dispatch = useDispatch();
@@ -93,11 +96,11 @@ const NavItem: FC<IProps> = ({ uid, mid = 0, unreads, setFiles }) => {
hideMenu={hideContextMenu}
items={[
{
title: "Mark As Read",
title: t("action.mark_read"),
handler: handleReadAll
},
{
title: "Hide Session",
title: t("action.hide_session"),
danger: true,
handler: handleRemoveSession
}
+4 -2
View File
@@ -4,6 +4,7 @@ import FavoredMessage from "../../common/component/Message/FavoredMessage";
import IconSurprise from "../../assets/icons/emoji.surprise.svg";
import IconRemove from "../../assets/icons/close.svg";
import useFavMessage from "../../common/hook/useFavMessage";
import { useTranslation } from "react-i18next";
const Styled = styled.div`
padding: 16px;
@@ -84,6 +85,7 @@ const Styled = styled.div`
`;
type Props = { cid?: number; uid?: number };
const FavList: FC<Props> = ({ cid = null, uid = null }) => {
const { t } = useTranslation("chat");
const { favorites, removeFavorite } = useFavMessage({ cid, uid });
const handleRemove = (evt: MouseEvent<HTMLButtonElement>) => {
const { id = "" } = evt.currentTarget.dataset;
@@ -93,11 +95,11 @@ const FavList: FC<Props> = ({ cid = null, uid = null }) => {
const noFavs = favorites.length == 0;
return (
<Styled>
<h4 className="head">Saved Message({favorites.length})</h4>
<h4 className="head">{t('fav_msg')}({favorites.length})</h4>
{noFavs ? (
<div className="none">
<IconSurprise />
<div className="tip">This channel doesnt have any saved message yet.</div>
<div className="tip">{t("fav_empty_tip")}</div>
</div>
) : (
<ul className="list">
+5 -3
View File
@@ -1,4 +1,5 @@
// import React from "react";
import { useTranslation } from "react-i18next";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
@@ -32,6 +33,7 @@ const Styled = styled.div`
// type Props = {};
const GuestBlankPlaceholder = () => {
const { t } = useTranslation("auth");
const dispatch = useDispatch();
const { clearLocalData } = useLogout();
const navigateTo = useNavigate();
@@ -43,10 +45,10 @@ const GuestBlankPlaceholder = () => {
};
return (
<Styled>
<h2 className="title">Welcome to {serverName} server</h2>
<h2 className="title">{t("welcome", { name: serverName })}</h2>
<div className="details">
<span className="desc">Please sign in to send a message</span>
<Button onClick={handleSignIn} className="small">{`Sign In`}</Button>
<span className="desc">{t("guest_login_tip")}</span>
<Button onClick={handleSignIn} className="small">{t("sign_in")}</Button>
</div>
</Styled>
);
+3 -1
View File
@@ -11,6 +11,7 @@ import { useAppSelector } from "../../../app/store";
import LoginTip from "./LoginTip";
import useLicense from "../../../common/hook/useLicense";
import LicenseUpgradeTip from "./LicenseOutdatedTip";
import { useTranslation } from "react-i18next";
interface Props {
readonly?: boolean;
@@ -33,6 +34,7 @@ const Layout: FC<Props> = ({
context = "channel",
to
}) => {
const { t } = useTranslation('chat');
const { reachLimit } = useLicense();
const { addStageFile } = useUploadFile({ context, id: to });
const messagesContainer = useRef<HTMLDivElement>(null);
@@ -129,7 +131,7 @@ const Layout: FC<Props> = ({
>
<div className={`box ${isActive ? "animate__animated animate__bounceIn" : ""}`}>
<div className="inner">
<h4 className="head">{`Send to ${ChatPrefixes[context]}${name}`}</h4>
<h4 className="head">{`${t("send_to")} ${ChatPrefixes[context]}${name}`}</h4>
<span className="intro">Photos accept jpg, png, max size limit to 10M.</span>
</div>
</div>
+41 -38
View File
@@ -8,6 +8,8 @@ import { removeUserSession } from "../../../app/slices/message.user";
import ContextMenu, { Item } from "../../../common/component/ContextMenu";
import useUserOperation from "../../../common/hook/useUserOperation";
import { useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next";
import { t } from "i18next";
type Props = {
context: "user" | "channel";
id: number;
@@ -28,6 +30,7 @@ const SessionContextMenu: FC<Props> = ({
setInviteChannelId,
children
}) => {
const { t } = useTranslation();
const { canCopyEmail, copyEmail, canDeleteChannel } = useUserOperation({
uid: context == "user" ? id : undefined,
cid: context == "channel" ? id : undefined
@@ -71,45 +74,45 @@ const SessionContextMenu: FC<Props> = ({
const items =
context == "user"
? [
{
title: "Mark As Read",
handler: handleReadAll
},
canCopyEmail && {
title: "Copy Email",
handler: copyEmail
},
{
title: "Hide Session",
danger: true,
handler: handleRemoveSession
}
]
{
title: t("action.mark_read"),
handler: handleReadAll
},
canCopyEmail && {
title: t("action.copy_email"),
handler: copyEmail
},
{
title: t("action.hide_session"),
danger: true,
handler: handleRemoveSession
}
]
: [
{
title: "Settings",
underline: true,
handler: handleChannelSetting
},
{
title: "Mark As Read",
// underline: true
handler: handleReadAll
},
{
title: channelMuted ? "Unmute" : "Mute",
handler: handleChannelMute
},
{
title: "Invite People",
handler: setInviteChannelId.bind(null, id)
},
canDeleteChannel && {
title: "Delete Channel",
danger: true,
handler: deleteChannel.bind(null, id)
}
];
{
title: t("setting"),
underline: true,
handler: handleChannelSetting
},
{
title: t("action.mark_read"),
// underline: true
handler: handleReadAll
},
{
title: channelMuted ? t("action.unmute") : t("action.mute"),
handler: handleChannelMute
},
{
title: t("action.invite_people"),
handler: setInviteChannelId.bind(null, id)
},
canDeleteChannel && {
title: t("action.delete_channel"),
danger: true,
handler: deleteChannel.bind(null, id)
}
];
return (
<Tippy
interactive
+25 -22
View File
@@ -10,32 +10,35 @@ import IconChannel from "../../assets/icons/channel.svg";
import { ContentTypes } from "../../app/config";
import { useAppSelector } from "../../app/store";
import { Favorite } from "../../app/slices/favorites";
const Filters = [
{
icon: <IconUnknown className="icon" />,
title: "All Items",
filter: ""
},
{
icon: <IconImage className="icon" />,
title: "Images",
filter: "image"
},
{
icon: <IconVideo className="icon" />,
title: "Videos",
filter: "video"
},
{
icon: <IconAudio className="icon" />,
title: "Audios",
filter: "audio"
}
];
import { useTranslation } from "react-i18next";
type filter = "audio" | "video" | "image" | "";
function FavsPage() {
const { t } = useTranslation("fav");
const [filter, setFilter] = useState<filter>("");
const [favs, setFavs] = useState<Favorite[]>([]);
const Filters = [
{
icon: <IconUnknown className="icon" />,
title: t("all_items"),
filter: ""
},
{
icon: <IconImage className="icon" />,
title: t("image"),
filter: "image"
},
{
icon: <IconVideo className="icon" />,
title: t("video"),
filter: "video"
},
{
icon: <IconAudio className="icon" />,
title: t("audio"),
filter: "audio"
}
];
const { favorites, channelData, userData } = useAppSelector((store) => {
return {
favorites: store.favorites,
+6 -4
View File
@@ -15,8 +15,10 @@ import UserIcon from "../../assets/icons/user.svg";
import FavIcon from "../../assets/icons/bookmark.svg";
import FolderIcon from "../../assets/icons/folder.svg";
import { useAppSelector } from "../../app/store";
import { useTranslation } from "react-i18next";
// const routes = ["/setting", "/setting/channel/:cid"];
function HomePage() {
const { t } = useTranslation();
const isHomePath = useMatch(`/`);
const isChatHomePath = useMatch(`/chat`);
const { pathname } = useLocation();
@@ -66,22 +68,22 @@ function HomePage() {
}}
to={chatNav}
>
<Tooltip tip="Chat">
<Tooltip tip={t("chat")}>
<ChatIcon />
</Tooltip>
</NavLink>
<NavLink className="link" to={userNav}>
<Tooltip tip="Members">
<Tooltip tip={t("members")}>
<UserIcon />
</Tooltip>
</NavLink>
<NavLink className="link" to={"/favs"}>
<Tooltip tip="Saved Items">
<Tooltip tip={t("favs")}>
<FavIcon />
</Tooltip>
</NavLink>
<NavLink className="link" to={"/files"}>
<Tooltip tip="Files">
<Tooltip tip={t("files")}>
<FolderIcon />
</Tooltip>
</NavLink>
+1 -1
View File
@@ -21,6 +21,7 @@ const InvitePage = lazy(() => import("./invite"));
const SettingChannelPage = lazy(() => import("./settingChannel"));
const SettingPage = lazy(() => import("./setting"));
const ResourceManagement = lazy(() => import("./resources"));
const GuestLogin = lazy(() => import("./guest"));
import RequireAuth from "../common/component/RequireAuth";
import RequireNoAuth from "../common/component/RequireNoAuth";
import Meta from "../common/component/Meta";
@@ -28,7 +29,6 @@ import HomePage from "./home";
import ChatPage from "./chat";
import Loading from "../common/component/Loading";
import store, { useAppSelector } from "../app/store";
import GuestLogin from "./guest";
let toastId: string;
const PageRoutes = () => {
const {
+3 -1
View File
@@ -6,6 +6,7 @@ import metamaskSvg from "../../assets/icons/metamask.svg?url";
import { StyledSocialButton } from "./styled";
import Onboarding from "@metamask/onboarding";
import { LoginCredential } from "../../types/auth";
import { useTranslation } from "react-i18next";
// import toast from "react-hot-toast";
export default function MetamaskLoginButton({
@@ -13,6 +14,7 @@ export default function MetamaskLoginButton({
}: {
login: (params: LoginCredential) => void;
}) {
const { t } = useTranslation("auth");
const [requesting, setRequesting] = useState(false);
const [accounts, setAccounts] = useState([]);
const [getNonce] = useLazyGetMetamaskNonceQuery();
@@ -91,7 +93,7 @@ export default function MetamaskLoginButton({
return (
<StyledSocialButton disabled={requesting} onClick={handleMetamaskLogin}>
<img className="icon" src={metamaskSvg} alt="meta mask icon" />
Sign in with MetaMask
{t("login.metamask")}
</StyledSocialButton>
);
}
+3 -1
View File
@@ -7,6 +7,7 @@ import { StyledSocialButton } from "./styled";
import StyledButton from "../../common/component/styled/Button";
import OidcLoginEntry from "./OidcLoginEntry";
import { OIDCConfig } from "../../types/auth";
import { useTranslation } from "react-i18next";
const StyledOidcLoginModal = styled(StyledModal)`
text-align: center;
@@ -31,6 +32,7 @@ interface IProps {
issuers?: OIDCConfig[];
}
const OidcLoginButton: FC<IProps> = ({ issuers }) => {
const { t } = useTranslation("auth");
const [modal, setModal] = useState(false);
if (!issuers) return null;
return (
@@ -40,7 +42,7 @@ const OidcLoginButton: FC<IProps> = ({ issuers }) => {
setModal(true);
}}
>
Sign in with OIDC
{t("login.oidc")}
</StyledSocialButton>
{modal && (
<Modal id="modal-modal">
+4 -2
View File
@@ -1,7 +1,9 @@
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
export default function MagicLinkLogin() {
const { t } = useTranslation("auth");
const navigate = useNavigate();
const handleSignUp = () => {
navigate("/register");
@@ -9,8 +11,8 @@ export default function MagicLinkLogin() {
return (
<div className="flex gap-1 mt-7 text-sm text-[#667085] justify-center">
<span>Dont have an account?</span>
<a className="text-[#22d3ee]" onClick={handleSignUp}>Sign up</a>
<span>{t("login.no_account")}</span>
<a className="text-[#22d3ee] cursor-pointer" onClick={handleSignUp}>{t("sign_up")}</a>
</div>
);
}
+7 -5
View File
@@ -17,8 +17,10 @@ import useGithubAuthConfig from "../../common/hook/useGithubAuthConfig";
import GoogleLoginButton from "../../common/component/GoogleLoginButton";
import GithubLoginButton from "../../common/component/GithubLoginButton";
import { FetchBaseQueryError } from "@reduxjs/toolkit/dist/query";
import { useTranslation } from "react-i18next";
export default function LoginPage() {
const { t } = useTranslation("auth");
const { data: enableSMTP } = useGetSMTPStatusQuery();
const [login, { isSuccess, isLoading, error }] = useLoginMutation();
const { clientId } = useGoogleAuthConfig();
@@ -128,8 +130,8 @@ export default function LoginPage() {
<div className="form">
<div className="tips">
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="logo" />
<h2 className="title">Login to VoceChat</h2>
<span className="desc">Please enter your details.</span>
<h2 className="title">{t("login.title")}</h2>
<span className="desc">{t("login.desc")}</span>
</div>
<form onSubmit={handleLogin}>
<Input
@@ -137,7 +139,7 @@ export default function LoginPage() {
name="email"
value={email}
required
placeholder="Enter your email"
placeholder={t("placeholder_email") || ""}
data-type="email"
onChange={handleInput}
/>
@@ -149,10 +151,10 @@ export default function LoginPage() {
required
data-type="password"
onChange={handleInput}
placeholder="Enter your password"
placeholder={t("placeholder_pwd") || ""}
/>
<Button type="submit" disabled={isLoading}>
{isLoading ? "Signing" : `Sign in`}
{isLoading ? "Signing" : t("sign_in")}
</Button>
</form>
{hasDivider && <hr className="or" />}
+8 -6
View File
@@ -11,6 +11,7 @@ import useGithubAuthConfig from "../../common/hook/useGithubAuthConfig";
import useGoogleAuthConfig from "../../common/hook/useGoogleAuthConfig";
import GoogleLoginButton from "../../common/component/GoogleLoginButton";
import GithubLoginButton from "../../common/component/GithubLoginButton";
import { useTranslation } from "react-i18next";
interface AuthForm {
email: string;
@@ -19,6 +20,7 @@ interface AuthForm {
}
export default function Reg() {
const { t } = useTranslation("auth");
const [sendRegMagicLink, { isLoading: signingUp, data, isSuccess }] =
useSendRegMagicLinkMutation();
const [checkEmail, { isLoading: checkingEmail }] = useLazyCheckEmailQuery();
@@ -107,8 +109,8 @@ export default function Reg() {
<>
<div className="tips">
<img src={`${BASE_URL}/resource/organization/logo`} alt="logo" className="logo" />
<h2 className="title">Sign Up to VoceChat</h2>
<span className="desc">Please enter your details.</span>
<h2 className="title">{t("reg.title")}</h2>
<span className="desc">{t("reg.desc")}</span>
</div>
<form onSubmit={handleReg} autoSave={"false"} autoComplete={"true"}>
@@ -117,7 +119,7 @@ export default function Reg() {
name="email"
value={email}
required
placeholder="Enter email"
placeholder={t("placeholder_email") || ""}
data-type="email"
onChange={handleInput}
/>
@@ -129,7 +131,7 @@ export default function Reg() {
required
data-type="password"
onChange={handleInput}
placeholder="Enter password"
placeholder={t("placeholder_pwd") || ""}
/>
<Input
required
@@ -139,10 +141,10 @@ export default function Reg() {
value={confirmPassword}
data-type="confirmPassword"
onChange={handleInput}
placeholder="Confirm Password"
placeholder={t("placeholder_confirm_pwd") || ""}
></Input>
<Button type="submit" disabled={isLoading}>
{isLoading ? "Signing Up" : `Sign Up`}
{isLoading ? "Signing Up" : t("sign_up")}
</Button>
</form>
<hr className="or" />
+4 -2
View File
@@ -1,6 +1,8 @@
import { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
export default function SignInLink({ token }: { token?: string }) {
const { t } = useTranslation("auth");
const handleSignIn = () => {
location.href = "/#/login";
};
@@ -14,8 +16,8 @@ export default function SignInLink({ token }: { token?: string }) {
return (
<div className="flex gap-1 mt-7 text-sm text-[#667085] justify-center">
<span>Have an account?</span>
<a onClick={handleSignIn} className="text-[#22d3ee]">Sign In</a>
<span>{t("reg.have_account")}</span>
<a onClick={handleSignIn} className="text-[#22d3ee] cursor-pointer">{t("sign_in")}</a>
</div>
);
}
+6 -4
View File
@@ -8,6 +8,7 @@ import FilterChannel from "./Channel";
import FilterType, { FileTypes } from "./Type";
import ArrowDown from "../../../assets/icons/arrow.down.svg";
import { useAppSelector } from "../../../app/store";
import { useTranslation } from "react-i18next";
const Styled = styled.div`
/* padding: 20px 0; */
@@ -44,6 +45,7 @@ const Styled = styled.div`
`;
export default function Filter({ filter, updateFilter }) {
const { t } = useTranslation("file");
const [filtersVisible, setFiltersVisible] = useState({
channel: false,
date: false,
@@ -93,7 +95,7 @@ export default function Filter({ filter, updateFilter }) {
{from && (
<Avatar className="avatar" name={userMap[from].name} url={userMap[from].avatar} />
)}
<span className="txt">From {from && userMap[from].name}</span>
<span className="txt">{t("from")} {from && userMap[from].name}</span>
<ArrowDown className="arrow" />
</div>
</Tippy>
@@ -108,7 +110,7 @@ export default function Filter({ filter, updateFilter }) {
className={`filter ${channel && "selected"}`}
onClick={toggleFilterVisible.bind(null, { channel: true })}
>
<span className="txt">{channel ? `In ${channelMap[channel].name}` : `Channel`}</span>
<span className="txt">{channel ? `In ${channelMap[channel].name}` : t("channel")}</span>
<ArrowDown className="arrow" />
</div>
</Tippy>
@@ -123,7 +125,7 @@ export default function Filter({ filter, updateFilter }) {
className={`filter ${type && "selected"}`}
onClick={toggleFilterVisible.bind(null, { type: true })}
>
<span className="txt">{type ? FileTypes[type].title : "Type"}</span>
<span className="txt">{type ? FileTypes[type].title : t("type")}</span>
<ArrowDown className="arrow" />
</div>
</Tippy>
@@ -138,7 +140,7 @@ export default function Filter({ filter, updateFilter }) {
className={`filter ${date && "selected"}`}
onClick={toggleFilterVisible.bind(null, { date: true })}
>
<span className="txt">{date ? Dates[date].title : "Date"}</span>
<span className="txt">{date ? Dates[date].title : t("date")}</span>
<ArrowDown className="arrow" />
</div>
</Tippy>
+3 -1
View File
@@ -1,4 +1,5 @@
import { ChangeEvent, FC } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";
import iconSearch from "../../assets/icons/search.svg?url";
@@ -32,6 +33,7 @@ interface Props {
}
const Search: FC<Props> = ({ value = "", updateSearchValue = null, embed = false }) => {
const { t } = useTranslation();
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
if (updateSearchValue) {
updateSearchValue(evt.target.value);
@@ -40,7 +42,7 @@ const Search: FC<Props> = ({ value = "", updateSearchValue = null, embed = false
return (
<Styled className={embed ? "embed" : ""}>
<input value={value} onChange={handleChange} className="search" placeholder="Search..." />
<input value={value} onChange={handleChange} className="search" placeholder={`${t("action.search")}...`} />
</Styled>
);
};
+8 -7
View File
@@ -12,6 +12,7 @@ import {
} from "../../app/services/server";
import useConfig from "../../common/hook/useConfig";
import { LoginConfig } from "../../types/server";
import { useTranslation } from "react-i18next";
const StyledConfirm = styled.div`
padding: 12px;
@@ -61,6 +62,7 @@ const Styled = styled.div`
`;
export default function APIConfig() {
const { t } = useTranslation(["setting", "common"]);
const { updateConfig, values } = useConfig("login");
const { data } = useGetThirdPartySecretQuery();
const [updateSecret, { data: updatedSecret, isSuccess, isLoading }] =
@@ -83,7 +85,7 @@ export default function APIConfig() {
data-checked={thirdParty}
/>
<div className="input">
<label htmlFor="secret">API Secure Key:</label>
<label htmlFor="secret"> {t("third_app.key")}:</label>
<Input disabled={!thirdParty} type="password" id="secret" value={updatedSecret || data} />
</div>
<Tippy
@@ -93,24 +95,23 @@ export default function APIConfig() {
content={
<StyledConfirm>
<div className="tip">
Are you sure to update API secret? Previous secret will be invalided
{t("third_app.update_tip")}
</div>
<div className="btns">
<Button onClick={() => hideAll()} className="cancel small">
Cancel
{t("action.cancel", { ns: "common" })}
</Button>
<Button disabled={isLoading} className="small danger" onClick={() => updateSecret()}>
Yes
{t("action.yes", { ns: "common" })}
</Button>
</div>
</StyledConfirm>
}
>
<Button disabled={!thirdParty}>Update Secret</Button>
<Button disabled={!thirdParty}> {t("third_app.update")}</Button>
</Tippy>
<div className="tip">
Tip: The security key agreed between the server and the third-party app is used to encrypt
the communication data.
{t("third_app.key_tip")}
</div>
</Styled>
);
@@ -1,9 +1,10 @@
import { useState, HTMLAttributes } from "react";
import dayjs from "dayjs";
import Button from "../../common/component/styled/Button";
import useLicense from "../../common/hook/useLicense";
import Button from "../../../common/component/styled/Button";
import useLicense from "../../../common/hook/useLicense";
import LicensePriceListModal from "./LicensePriceListModal";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
interface ItemProps extends HTMLAttributes<HTMLSpanElement> { label: string, data?: string | number | string[], foldable?: boolean }
const Item = ({ label, data, foldable = false, ...rest }: ItemProps) => {
@@ -22,6 +23,7 @@ const Item = ({ label, data, foldable = false, ...rest }: ItemProps) => {
};
export default function License() {
const { t } = useTranslation("setting");
const { license: licenseInfo, reachLimit } = useLicense();
const [modalVisible, setModalVisible] = useState(false);
const [base58Fold, setBase58Fold] = useState(true);
@@ -38,25 +40,25 @@ export default function License() {
<>
<div className="max-w-3xl flex flex-col justify-start items-start gap-4">
<div className={clsx('relative w-full p-3 rounded border-solid border flex flex-col gap-4 shadow', reachLimit ? "border-red-600 bg-red-200/50" : "border-green-600 bg-green-200/50")}>
<Item label="Signed" data={licenseInfo?.sign ? "Yes" : "Not Yet"} />
<Item label="Domains" data={licenseInfo?.domains} />
<Item label="User Limit" data={licenseInfo?.user_limit == 99999 ? "No Limit" : licenseInfo?.user_limit} />
<Item label="Expired At" data={dayjs(licenseInfo?.expired_at).format("YYYY-MM-DD h:mm:ss A")} />
<Item label="Created At" data={dayjs(licenseInfo?.created_at).format("YYYY-MM-DD h:mm:ss A")} />
<Item label="License Value" data={licenseInfo?.base58} foldable={base58Fold} title={base58Fold ? "Click to see full text" : "Click to fold text"}
<Item label={t("license.signed")} data={licenseInfo?.sign ? "Yes" : "Not Yet"} />
<Item label={t("license.domain")} data={licenseInfo?.domains} />
<Item label={t("license.user_limit")} data={licenseInfo?.user_limit == 99999 ? "No Limit" : licenseInfo?.user_limit} />
<Item label={t("license.expire")} data={dayjs(licenseInfo?.expired_at).format("YYYY-MM-DD h:mm:ss A")} />
<Item label={t("license.create")} data={dayjs(licenseInfo?.created_at).format("YYYY-MM-DD h:mm:ss A")} />
<Item label={t("license.value")} data={licenseInfo?.base58} foldable={base58Fold} title={base58Fold ? "Click to see full text" : "Click to fold text"}
onClick={handleLicenseValueToggle} />
</div>
<Button onClick={handleRenewLicense}>Renew License</Button>
<Button onClick={handleRenewLicense}>{t("license.renew")}</Button>
<div className="flex flex-col gap-4 bg-primary-500 text-white rounded drop-shadow-xl p-5">
<h2 className="text-2xl font-bold">A chance to get a free license upgrade! 🎁</h2>
<h2 className="text-2xl font-bold">{t("license.tip.title")} 🎁</h2>
<p className="text-base flex flex-col"><span>
Getting a free license upgrade by joining our <em className="font-bold">User Test Session</em>
{t("license.tip.user_test")}
</span>
<span>
Book a time here: <a className="underline text-lg text-green-200" href="https://calendly.com/hansu/han-meeting" target="_blank" rel="noopener noreferrer">https://calendly.com/hansu/han-meeting</a>
{t("license.tip.booking")} <a className="underline text-lg text-green-200" href="https://calendly.com/hansu/han-meeting" target="_blank" rel="noopener noreferrer">https://calendly.com/hansu/han-meeting</a>
</span>
<span>
Or, add WeChat for more information: <em className="text-lg text-green-200">yanggc_2013</em>
{t("license.tip.wechat")}<em className="text-lg text-green-200">yanggc_2013</em>
</span>
</p>
</div>
@@ -1,12 +1,13 @@
import { FC, useState } from "react";
import toast from "react-hot-toast";
import Modal from "../../common/component/Modal";
import StyledModal from "../../common/component/styled/Modal";
import Button from "../../common/component/styled/Button";
import StyledRadio from "../../common/component/styled/Radio";
import Modal from "../../../common/component/Modal";
import StyledModal from "../../../common/component/styled/Modal";
import Button from "../../../common/component/styled/Button";
import StyledRadio from "../../../common/component/styled/Radio";
import { useGetLicensePaymentUrlMutation } from "../../app/services/server";
import { LicensePriceList } from "../../app/config";
import { useGetLicensePaymentUrlMutation } from "../../../app/services/server";
import { LicensePriceList } from "../../../app/config";
import { useTranslation } from "react-i18next";
// import { LicenseMetadata, RenewLicense } from "../../types/server";
interface Props {
@@ -15,6 +16,7 @@ interface Props {
}
const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
const { t } = useTranslation(["setting", "common"]);
const [getUrl, { isLoading, isSuccess }] = useGetLicensePaymentUrlMutation();
const [selectPrice, setSelectPrice] = useState(
`${LicensePriceList[0].pid}|${LicensePriceList[0].limit}`
@@ -46,15 +48,15 @@ const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
return (
<Modal id="modal-modal">
<StyledModal
title="Renew License"
description="Please select the price"
title={t("license.renew") || ""}
description={t("license.renew_select") || ""}
buttons={
<>
<Button onClick={closeModal} className="ghost">
Cancel
{t("action.cancel", { ns: "common" })}
</Button>
<Button disabled={isLoading || isSuccess} onClick={handleRenew} className="danger">
{isLoading ? "Initialize Payment Url" : isSuccess ? "Redirecting" : "Renew"}
{isLoading ? "Initialize Payment Url" : isSuccess ? "Redirecting" : t("license.renew")}
</Button>
</>
}
+4 -2
View File
@@ -6,6 +6,7 @@ import StyledModal from "../../common/component/styled/Modal";
import Button from "../../common/component/styled/Button";
import Checkbox from "../../common/component/styled/Checkbox";
import useLogout from "../../common/hook/useLogout";
import { useTranslation } from "react-i18next";
const StyledConfirm = styled(StyledModal)`
.clear {
@@ -32,6 +33,7 @@ interface Props {
}
const LogoutConfirmModal: FC<Props> = ({ closeModal }) => {
const { t } = useTranslation();
const [clearLocal, setClearLocal] = useState(false);
const { logout, exited, exiting, clearLocalData } = useLogout();
const handleLogout = () => {
@@ -58,9 +60,9 @@ const LogoutConfirmModal: FC<Props> = ({ closeModal }) => {
description="Are you sure want to log out this account?"
buttons={
<>
<Button onClick={closeModal}>Cancel</Button>
<Button onClick={closeModal}>{t("action.cancel")}</Button>
<Button onClick={handleLogout} className="danger">
{exiting ? "Logging out" : "Log Out"}
{exiting ? "Logging out" : t("action.logout")}
</Button>
</>
}
+21 -23
View File
@@ -7,6 +7,7 @@ import ProfileBasicEditModal from "./ProfileBasicEditModal";
import RemoveAccountConfirmModal from "./RemoveAccountConfirmModal";
import UpdatePasswordModal from "./UpdatePasswordModal";
import { useAppSelector } from "../../app/store";
import { useTranslation } from "react-i18next";
const StyledWrapper = styled.div`
display: flex;
@@ -99,24 +100,25 @@ const StyledWrapper = styled.div`
border-radius: 8px;
}
`;
const EditModalInfo = {
name: {
label: "Username",
title: "Change your username",
intro: "Enter a new username."
},
email: {
label: "Email",
title: "Change your email",
intro: "Enter a new email."
}
};
type EditFields = "name" | "email";
export default function MyAccount() {
const { t } = useTranslation(["member", "common"]);
const [passwordModal, setPasswordModal] = useState(false);
const [editModal, setEditModal] = useState<EditFields | null>(null);
const [removeConfirmVisible, setRemoveConfirmVisible] = useState(false);
const [uploadAvatar, { isSuccess: uploadSuccess }] = useUpdateAvatarMutation();
const EditModalInfo = {
name: {
label: t("username"),
title: t("change_name"),
intro: t("change_name_desc")
},
email: {
label: t("email"),
title: t("change_email"),
intro: t("change_email_desc")
}
};
const loginUser = useAppSelector((store) => {
return store.users.byId[store.authData.user?.uid || 0];
@@ -156,43 +158,39 @@ export default function MyAccount() {
</div>
<div className="row">
<div className="info">
<span className="label">username</span>
<span className="label">{t("username")}</span>
<span className="txt">
{name} <span className="gray"> #{uid}</span>
</span>
</div>
<button data-edit="name" onClick={handleBasicEdit} className="btn">
Edit
{t("action.edit", { ns: "common" })}
</button>
</div>
<div className="row">
<div className="info">
<span className="label">email</span>
<span className="label">{t("email")}</span>
<span className="txt">{email}</span>
</div>
<button data-edit="email" onClick={handleBasicEdit} className="btn">
Edit
{t("action.edit", { ns: "common" })}
</button>
</div>
<div className="row">
<div className="info">
<span className="label">password</span>
<span className="label">{t("password")}</span>
<span className="txt">*********</span>
</div>
<button onClick={togglePasswordModal} className="btn">
Edit
{t("action.edit", { ns: "common" })}
</button>
</div>
</div>
{/* uid 1 是初始账户,不能删 */}
{uid != 1 && (
<div className="danger">
<h4 className="head">Account Removal</h4>
<div className="desc">
Disabling your account means you can recover it at any time after taking this action.
</div>
<button className="btn" onClick={toggleRemoveAccountModalVisible}>
Delete Account
{t("delete_account")}
</button>
</div>
)}
-229
View File
@@ -1,229 +0,0 @@
import { useState, useEffect, ChangeEvent } from "react";
import styled from "styled-components";
import toast from "react-hot-toast";
import { useUpdateServerMutation, useUpdateLogoMutation } from "../../app/services/server";
import LogoUploader from "../../common/component/AvatarUploader";
import Input from "../../common/component/styled/Input";
import Label from "../../common/component/styled/Label";
import Textarea from "../../common/component/styled/Textarea";
import SaveTip from "../../common/component/SaveTip";
import StyledRadio from "../../common/component/styled/Radio";
import { useAppSelector } from "../../app/store";
import { LoginConfig, WhoCanSignUp } from "../../types/server";
import useConfig from "../../common/hook/useConfig";
const StyledWrapper = styled.div`
position: relative;
width: 512px;
height: 100%;
display: flex;
flex-direction: column;
gap: 24px;
.logo {
display: flex;
gap: 16px;
.preview {
width: 96px;
height: 96px;
}
.upload {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
.tip {
font-weight: normal;
font-size: 14px;
line-height: 20px;
color: #374151;
}
.btn {
padding: 8px 14px;
font-weight: 500;
font-size: 14px;
line-height: 20px;
color: #1fe1f9;
background: #ecfeff;
border: 1px solid #ecfeff;
box-sizing: border-box;
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.05);
border-radius: 8px;
}
}
}
.inputs {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
margin-bottom: 64px;
.input {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
> .setting {
font-size: 14px;
line-height: 20px;
> .label {
font-weight: 500;
}
> .tip {
font-weight: 400;
color: #667085;
display: flex;
width: 100%;
justify-content: space-between;
}
> form {
margin-top: 16px;
width: 512px;
}
}
`;
export default function Overview() {
const { loginUser, server } = useAppSelector((store) => {
return { loginUser: store.authData.user, server: store.server };
});
const [changed, setChanged] = useState(false);
const [serverValues, setServerValues] = useState<typeof server>(server);
const { values: loginConfig, updateConfig: updateLoginConfig } = useConfig("login");
const [updateServer] = useUpdateServerMutation();
const [uploadLogo, { isSuccess: uploadSuccess }] = useUpdateLogoMutation();
const handleUpdateServer = () => {
const { name, description } = serverValues;
updateServer({ name, description });
};
const handleUpdateWhoCanSignUp = (value: WhoCanSignUp) => {
updateLoginConfig({ ...loginConfig, who_can_sign_up: value });
};
const handleChange = (evt: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const newValue = evt.target.value;
const { type = "" } = evt.target.dataset;
setServerValues((prev) => {
return { ...prev, [type]: newValue };
});
};
const handleReset = () => {
setServerValues(server);
};
const handleGuestToggle = (v: "true" | "false") => {
const guest = v === "true";
updateLoginConfig({ ...loginConfig, guest });
};
useEffect(() => {
if (uploadSuccess) {
toast.success("Update logo successfully!");
}
}, [uploadSuccess]);
useEffect(() => {
if (server) {
setServerValues(server);
}
}, [server]);
useEffect(() => {
if (server && serverValues) {
const { name, description } = serverValues;
const { name: oName, description: oDescription } = server;
if (oName !== name || oDescription !== description) {
setChanged(true);
} else {
setChanged(false);
}
}
}, [server, serverValues]);
if (!serverValues || !loginConfig) return null;
const { name, description, logo } = serverValues;
const { who_can_sign_up: whoCanSignUp, guest } = loginConfig as LoginConfig;
const isAdmin = loginUser?.is_admin;
return (
<StyledWrapper>
<div className="logo">
<div className="preview">
<LogoUploader
disabled={!isAdmin}
url={uploadSuccess ? `${logo}?t=${+new Date()}` : logo}
name={name}
uploadImage={uploadLogo}
/>
</div>
{isAdmin && (
<div className="upload">
<div className="tip">
Minimum size is 128x128, We recommend at least 512x512 for the server. Max size
limited to 5M.
</div>
{/* <button className="btn">Upload Image</button> */}
</div>
)}
</div>
<div className="inputs">
<div className="input">
<Label htmlFor="name">Server Name</Label>
<Input
disabled={!isAdmin}
data-type="name"
onChange={handleChange}
value={name}
name="name"
id="name"
placeholder="Server Name"
/>
</div>
<div className="input">
<Label htmlFor="desc">Server Description</Label>
<Textarea
disabled={!isAdmin}
data-type="description"
onChange={handleChange}
value={description ?? ""}
rows={4}
name="name"
id="name"
placeholder="Tell the world a bit about this server"
/>
</div>
</div>
{isAdmin && (
<>
<div className="setting">
<p className="label">Default Sign Up</p>
<p className="tip">Who can sign up this server.</p>
<StyledRadio
options={["Everyone", "Invitation Link Only"]}
values={["EveryOne", "InvitationOnly"]}
value={whoCanSignUp}
onChange={(v: WhoCanSignUp) => {
handleUpdateWhoCanSignUp(v);
}}
/>
</div>
<div className="setting">
<p className="label">Guest Mode</p>
<p className="tip">
<span className="txt">
If enabled, visitors will see public channels on this server.
</span>
</p>
<StyledRadio
options={["Enabled", "Disabled"]}
values={["true", "false"]}
value={String(guest)}
onChange={(v) => {
console.log("wtff", v);
handleGuestToggle(v);
}}
/>
</div>
</>
)}
{changed && <SaveTip saveHandler={handleUpdateServer} resetHandler={handleReset} />}
</StyledWrapper>
);
}
+138
View File
@@ -0,0 +1,138 @@
// import { useState, useEffect, ChangeEvent } from "react";
import styled from "styled-components";
import { useTranslation } from "react-i18next";
import StyledRadio from "../../../common/component/styled/Radio";
import { useAppSelector } from "../../../app/store";
import { LoginConfig, WhoCanSignUp } from "../../../types/server";
import useConfig from "../../../common/hook/useConfig";
import Server from './server';
import Language from './language';
const StyledWrapper = styled.div`
position: relative;
width: 512px;
height: 100%;
display: flex;
flex-direction: column;
gap: 24px;
.logo {
display: flex;
gap: 16px;
.preview {
width: 96px;
height: 96px;
}
.upload {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-start;
.tip {
font-weight: normal;
font-size: 14px;
line-height: 20px;
color: #374151;
}
}
}
.inputs {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 24px;
margin-bottom: 64px;
.input {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 8px;
}
}
> .setting {
font-size: 14px;
line-height: 20px;
> .label {
font-weight: 500;
}
> .tip {
font-weight: 400;
color: #667085;
display: flex;
width: 100%;
justify-content: space-between;
}
> form {
margin-top: 16px;
width: 512px;
}
}
`;
export default function Overview() {
const { t } = useTranslation("setting");
const { loginUser } = useAppSelector((store) => {
return { loginUser: store.authData.user };
});
const { values: loginConfig, updateConfig: updateLoginConfig } = useConfig("login");
const handleUpdateWhoCanSignUp = (value: WhoCanSignUp) => {
updateLoginConfig({ ...loginConfig, who_can_sign_up: value });
};
const handleGuestToggle = (v: "true" | "false") => {
const guest = v === "true";
updateLoginConfig({ ...loginConfig, guest });
};
if (!loginConfig) return null;
const { who_can_sign_up: whoCanSignUp, guest } = loginConfig as LoginConfig;
const isAdmin = loginUser?.is_admin;
return (
<StyledWrapper>
<Server />
{isAdmin && (
<>
<div className="setting">
<p className="label">{t("overview.sign_up.title")}</p>
<p className="tip">{t("overview.sign_up.desc")}</p>
<StyledRadio
options={[t("overview.sign_up.everyone"), t("overview.sign_up.invite")]}
values={["EveryOne", "InvitationOnly"]}
value={whoCanSignUp}
onChange={(v: WhoCanSignUp) => {
handleUpdateWhoCanSignUp(v);
}}
/>
</div>
<div className="setting">
<p className="label">{t("overview.guest_mode.title")}</p>
<p className="tip">
<span className="txt">
{t("overview.guest_mode.desc")}
</span>
</p>
<StyledRadio
options={[t("overview.guest_mode.enable"), t("overview.guest_mode.disable")]}
values={["true", "false"]}
value={String(guest)}
onChange={(v) => {
console.log("wtff", v);
handleGuestToggle(v);
}}
/>
</div>
</>
)}
<Language />
</StyledWrapper>
);
}
+36
View File
@@ -0,0 +1,36 @@
// import React from 'react'
import dayjs from 'dayjs';
import { useTranslation } from 'react-i18next';
import StyledRadio from "../../../common/component/styled/Radio";
// type Props = {}
const Index = () => {
const { t, i18n } = useTranslation("setting");
const handleGuestToggle = (v: "zh" | "en") => {
i18n.changeLanguage(v);
dayjs.locale(v === "zh" ? "zh-cn" : "en");
};
return (
<div className="setting">
<p className="label">{t("overview.lang.title")}</p>
<p className="tip">
<span className="txt">
{t("overview.lang.desc")}
</span>
</p>
<StyledRadio
options={[t("overview.lang.en"), t("overview.lang.zh")]}
values={["en", "zh"]}
value={i18n.language.split("-")[0]}
onChange={(v) => {
console.log("wtff", v);
handleGuestToggle(v);
}}
/>
</div>
);
};
export default Index;
+114
View File
@@ -0,0 +1,114 @@
import { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import toast from "react-hot-toast";
import { useUpdateLogoMutation, useUpdateServerMutation } from '../../../app/services/server';
import LogoUploader from "../../../common/component/AvatarUploader";
import Input from "../../../common/component/styled/Input";
import Label from "../../../common/component/styled/Label";
import Textarea from "../../../common/component/styled/Textarea";
import SaveTip from '../../../common/component/SaveTip';
import { useAppSelector } from '../../../app/store';
const Index = () => {
const { t } = useTranslation("setting");
const { loginUser, server } = useAppSelector((store) => {
return { loginUser: store.authData.user, server: store.server };
});
const [uploadLogo, { isSuccess: uploadSuccess }] = useUpdateLogoMutation();
const [updateServer] = useUpdateServerMutation();
const [changed, setChanged] = useState(false);
const [serverValues, setServerValues] = useState<typeof server>(server);
const handleChange = (evt: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
const newValue = evt.target.value;
const { type = "" } = evt.target.dataset;
setServerValues((prev) => {
return { ...prev, [type]: newValue };
});
};
const handleUpdateServer = () => {
const { name, description } = serverValues;
updateServer({ name, description });
};
const handleReset = () => {
setServerValues(server);
};
useEffect(() => {
if (server) {
setServerValues(server);
}
}, [server]);
useEffect(() => {
if (uploadSuccess) {
toast.success("Update logo successfully!");
}
}, [uploadSuccess]);
useEffect(() => {
if (server && serverValues) {
const { name, description } = serverValues;
const { name: oName, description: oDescription } = server;
if (oName !== name || oDescription !== description) {
setChanged(true);
} else {
setChanged(false);
}
}
}, [server, serverValues]);
const { name, description, logo } = serverValues;
const isAdmin = loginUser?.is_admin;
if (!loginUser || !serverValues) return null;
return (
<>
<div className="logo">
<div className="preview">
<LogoUploader
disabled={!isAdmin}
url={uploadSuccess ? `${logo}?t=${+new Date()}` : logo}
name={name}
uploadImage={uploadLogo}
/>
</div>
{isAdmin && (
<div className="upload">
<div className="tip">
{t("overview.upload_desc")}
</div>
</div>
)}
</div>
<div className="inputs">
<div className="input">
<Label htmlFor="name">{t("overview.name")}</Label>
<Input
disabled={!isAdmin}
data-type="name"
onChange={handleChange}
value={name}
name="name"
id="name"
placeholder="Server Name"
/>
</div>
<div className="input">
<Label htmlFor="desc">{t("overview.desc")}</Label>
<Textarea
disabled={!isAdmin}
data-type="description"
onChange={handleChange}
value={description ?? ""}
rows={4}
name="name"
id="name"
placeholder="Tell the world a bit about this server"
/>
</div>
</div>
{changed && <SaveTip saveHandler={handleUpdateServer} resetHandler={handleReset} />}
</>
);
};
export default Index;
+4 -2
View File
@@ -6,6 +6,7 @@ import { useUpdateInfoMutation } from "../../app/services/user";
import StyledModal from "../../common/component/styled/Modal";
import Button from "../../common/component/styled/Button";
import Modal from "../../common/component/Modal";
import { useTranslation } from "react-i18next";
const StyledEdit = styled(StyledModal)`
.input {
@@ -40,6 +41,7 @@ const ProfileBasicEditModal: FC<Props> = ({
intro = "Enter a new username and your existing password.",
closeModal
}) => {
const { t } = useTranslation();
const [input, setInput] = useState(value);
const [update, { isLoading, isSuccess }] = useUpdateInfoMutation();
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
@@ -63,9 +65,9 @@ const ProfileBasicEditModal: FC<Props> = ({
buttons={
<>
<Button className="cancel" onClick={closeModal}>
Cancel
{t("action.cancel")}
</Button>
<Button onClick={handleUpdate}>{isLoading ? "Updating" : `Done`}</Button>
<Button onClick={handleUpdate}>{isLoading ? "Updating" : t("action.done")}</Button>
</>
}
>
@@ -4,12 +4,14 @@ import Modal from "../../common/component/Modal";
import StyledModal from "../../common/component/styled/Modal";
import Button from "../../common/component/styled/Button";
import { useLazyDeleteCurrentAccountQuery } from "../../app/services/auth";
import { useTranslation } from "react-i18next";
interface Props {
closeModal: () => void;
}
const RemoveConfirmModal: FC<Props> = ({ closeModal }) => {
const { t } = useTranslation("member");
const [removeCurrentAccount, { isLoading }] = useLazyDeleteCurrentAccountQuery();
const handleRemove = async () => {
try {
@@ -22,13 +24,13 @@ const RemoveConfirmModal: FC<Props> = ({ closeModal }) => {
return (
<Modal id="modal-modal">
<StyledModal
title="Remove Account"
description="Are you sure remove this account?"
title={t("remove_account") || ""}
description={t("remove_account_desc") || ""}
buttons={
<>
<Button onClick={closeModal}>Cancel</Button>
<Button onClick={closeModal}>{t("action.cancel", { ns: "common" })}</Button>
<Button disabled={isLoading} onClick={handleRemove} className="danger">
Remove
{t("remove")}
</Button>
</>
}
+9 -7
View File
@@ -6,6 +6,7 @@ import { useUpdatePasswordMutation, useGetCredentialsQuery } from "../../app/ser
import StyledModal from "../../common/component/styled/Modal";
import Button from "../../common/component/styled/Button";
import Modal from "../../common/component/Modal";
import { useTranslation } from "react-i18next";
const StyledEdit = styled(StyledModal)`
.input {
@@ -34,6 +35,7 @@ interface BaseForm {
}
const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
const { t } = useTranslation("member");
const { data } = useGetCredentialsQuery();
const [input, setInput] = useState<BaseForm>({
current: "",
@@ -75,22 +77,22 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
return (
<Modal id="modal-modal">
<StyledEdit
title={"Change your password"}
description={"Enter current password and new password."}
title={t("change_pwd") || ""}
description={t("change_pwd_desc") || ""}
buttons={
<>
<Button className="cancel" onClick={closeModal}>
Cancel
{t("action.cancel", { ns: "common" })}
</Button>
<Button disabled={disableBtn} onClick={handleUpdate}>
{isLoading ? "Updating" : `Update`}
{isLoading ? "Updating" : t("action.update", { ns: "common" })}
</Button>
</>
}
>
{data?.password && (
<div className="input">
<label htmlFor={"current"}>Current Password</label>
<label htmlFor={"current"}>{t("current_pwd")}</label>
<Input
type="password"
id="current"
@@ -102,7 +104,7 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
</div>
)}
<div className="input">
<label htmlFor={"newPassword"}>New Password</label>
<label htmlFor={"newPassword"}>{t("new_pwd")}</label>
<Input
type="password"
name={"newPassword"}
@@ -112,7 +114,7 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
></Input>
</div>
<div className="input">
<label htmlFor={"confirmPassword"}>Confirm New Password</label>
<label htmlFor={"confirmPassword"}>{t("confirm_new_pwd")}</label>
<Input
onBlur={handleCompare}
type="password"
+7 -5
View File
@@ -1,5 +1,6 @@
// import { useState, MouseEvent } from "react";
// import dayjs from "dayjs";
import { useTranslation } from 'react-i18next';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';
@@ -17,26 +18,27 @@ const Row = ({ paramKey, paramDefault, remarks }: { paramKey: string, paramDefau
</tr>;
};
export default function Widget() {
const { t } = useTranslation("setting", { keyPrefix: "widget" });
// const disableBtn = !reachLimit;
return (
<div className="flex flex-col justify-start items-start">
<div className="text-gray-600 ">
Extending VoceChat by embedding the vocechat widget SDK!
{t('tip')}
</div>
<label htmlFor="code" className="text-gray-500 text-sm mt-5">
Code Example:
{t('code')}:
</label>
<SyntaxHighlighter id="code" language="html" style={vscDarkPlus} className="rounded">
{`<!-- put this code snippet into your html file -->\n<script \n data-host-id="4" \n data-close-width="48" \n data-close-height="48" \n data-open-width="380" \n data-open-height="680" \n src="${location.origin}/widget.js" \n async \n/>`}
{`<!-- ${t('code_comment')} -->\n<script \n data-host-id="4" \n data-close-width="48" \n data-close-height="48" \n data-open-width="380" \n data-open-height="680" \n src="${location.origin}/widget.js" \n async \n/>`}
</SyntaxHighlighter>
<div className="text-gray-500 text-sm mt-5 mb-2">
Configuration Description:
{t('config')}:
</div>
<div className="w-[700px] border border-solid border-gray-300 rounded overflow-hidden">
<table className="min-w-full table-auto">
<thead className="border-b bg-gray-50">
<tr>
{["Parameter Key", "Default Value", "Remarks"].map(title => <th key={title} scope="col" className="text-sm font-bold text-gray-900 px-6 py-4 text-left">
{[t('param_key'), t('default_value'), t('remark')].map(title => <th key={title} scope="col" className="text-sm font-bold text-gray-900 px-6 py-4 text-left">
{title}
</th>)}
</tr>
+11 -9
View File
@@ -7,8 +7,10 @@ import Label from "../../../common/component/styled/Label";
import SaveTip from "../../../common/component/SaveTip";
import useConfig from "../../../common/hook/useConfig";
import { FirebaseConfig } from "../../../types/server";
import { useTranslation } from "react-i18next";
export default function ConfigFirebase() {
const { t } = useTranslation("setting");
const { values, toggleEnable, updateConfig, setValues, reset, changed } = useConfig("firebase");
const handleUpdate = () => {
// const { token_url, description } = values;
@@ -34,33 +36,33 @@ export default function ConfigFirebase() {
<StyledContainer>
<div className="inputs">
<div className="input row">
<Label>Enable</Label>
<Label>{t("firebase.enable")}</Label>
<Toggle onClick={toggleEnable} data-checked={enabled}></Toggle>
</div>
<div className="input">
<Label htmlFor="name">Token Url</Label>
<Label htmlFor="name">{t("firebase.token_url")}</Label>
<Input
disabled={!enabled}
data-type="token_url"
onChange={handleChange}
value={token_url}
name="token_url"
placeholder="Token URL"
placeholder={t("firebase.token_url") || ""}
/>
</div>
<div className="input">
<Label htmlFor="desc">Project ID</Label>
<Label htmlFor="desc">{t("firebase.project_id")}</Label>
<Input
disabled={!enabled}
data-type="project_id"
onChange={handleChange}
value={project_id}
name="project_id"
placeholder="Project ID"
placeholder={t("firebase.project_id") || ""}
/>
</div>
<div className="input">
<Label htmlFor="desc">Private Key</Label>
<Label htmlFor="desc">{t("firebase.private_key")}</Label>
<Textarea
rows={10}
spellCheck={false}
@@ -69,18 +71,18 @@ export default function ConfigFirebase() {
onChange={handleChange}
value={private_key}
name="private_key"
placeholder="Private key"
placeholder={t("firebase.private_key") || ""}
/>
</div>
<div className="input">
<Label htmlFor="desc">Client Email</Label>
<Label htmlFor="desc">{t("firebase.client_email")}</Label>
<Input
disabled={!enabled}
data-type="client_email"
onChange={handleChange}
value={client_email}
name="client_email"
placeholder="Client Email address"
placeholder={t("firebase.client_email") || ""}
/>
</div>
</div>
@@ -6,6 +6,7 @@ import Toggle from "../../../../common/component/styled/Toggle";
import options from "./items.json";
import Styled from "./styled";
import IconMinus from "../../../../assets/icons/minus.circle.svg";
import { useTranslation } from "react-i18next";
interface Issuer {
domain: string;
@@ -19,6 +20,7 @@ interface Props {
}
const IssuerList: FC<Props> = ({ issuers = [], onChange }) => {
const { t } = useTranslation();
const [select, setSelect] = useState<Partial<Option> | null>(null);
const [newDomain, setNewDomain] = useState("");
@@ -111,7 +113,7 @@ const IssuerList: FC<Props> = ({ issuers = [], onChange }) => {
setNewDomain("");
}}
>
Add
{t("action.add")}
</Button>
</div>
</li>
+14 -12
View File
@@ -11,8 +11,10 @@ import IssuerList from "./IssuerList";
import useGoogleAuthConfig from "../../../common/hook/useGoogleAuthConfig";
import useGithubAuthConfig from "../../../common/hook/useGithubAuthConfig";
import { LoginConfig } from "../../../types/server";
import { useTranslation } from "react-i18next";
export default function Logins() {
const { t } = useTranslation("setting", { keyPrefix: "login" });
const {
changed: clientIdChanged,
clientId,
@@ -74,9 +76,9 @@ export default function Logins() {
<div className="row">
<div className="title">
<div className="txt">
<Label>Password</Label>
<Label>{t("password")}</Label>
</div>
<span className="desc">Allows members login with password.</span>
<span className="desc">{t("password_desc")}</span>
</div>
<Toggle
onClick={handleToggle.bind(null, { password: !password })}
@@ -88,9 +90,9 @@ export default function Logins() {
<div className="row">
<div className="title">
<div className="txt">
<Label>Magic Link</Label>
<Label>{t("magic_link")}</Label>
</div>
<span className="desc">Allows members login with Magic Link.</span>
<span className="desc">{t("magic_link_desc")}</span>
</div>
<Toggle
onClick={handleToggle.bind(null, { magic_link: !magic_link })}
@@ -102,10 +104,10 @@ export default function Logins() {
<div className="row">
<div className="title">
<div className="txt">
<Label>Google</Label>
<Label>{t("google")}</Label>
<Tooltip link="https://doc.voce.chat/setting/third_login/login-google" />
</div>
<span className="desc">Allows members login with Google.</span>
<span className="desc">{t("google_desc")}</span>
</div>
<Toggle
onClick={handleToggle.bind(null, { google: !google })}
@@ -125,10 +127,10 @@ export default function Logins() {
<div className="row">
<div className="title">
<div className="txt">
<Label>Github</Label>
<Label>{t("github")}</Label>
<Tooltip link="https://doc.voce.chat/setting/third_login/login-github" />
</div>
<span className="desc">Allows members login with Github.</span>
<span className="desc">{t("github_desc")}</span>
</div>
<Toggle
onClick={handleToggle.bind(null, { github: !github })}
@@ -156,10 +158,10 @@ export default function Logins() {
<div className="row">
<div className="title">
<div className="txt">
<Label>Metamask</Label>
<Label>{t("metamask")}</Label>
<Tooltip link="https://doc.voce.chat/setting/third_login/login-metamask" />
</div>
<span className="desc">Allows members login with Metamask.</span>
<span className="desc">{t("metamask_desc")}</span>
</div>
<Toggle
onClick={handleToggle.bind(null, { metamask: !metamask })}
@@ -171,10 +173,10 @@ export default function Logins() {
<div className="row">
<div className="title">
<div className="txt">
<Label htmlFor="desc">OIDC</Label>
<Label htmlFor="desc">{t("oidc")}</Label>
<Tooltip link="https://doc.voce.chat/setting/third_login/login-webid" />
</div>
<span className="desc">Save my login details for next time.</span>
<span className="desc">{t("oidc_desc")}</span>
</div>
</div>
<div className="row">
+10 -8
View File
@@ -17,8 +17,10 @@ import Label from "../../../common/component/styled/Label";
import SaveTip from "../../../common/component/SaveTip";
import toast from "react-hot-toast";
import { SMTPConfig } from "../../../types/server";
import { useTranslation } from "react-i18next";
export default function ConfigSMTP() {
const { t } = useTranslation("setting", { keyPrefix: "smtp" });
const [testEmail, setTestEmail] = useState("");
const [sendTestEmail, { isSuccess, isError }] = useSendTestEmailMutation();
const { reset, updateConfig, values, setValues, changed, toggleEnable } = useConfig("smtp");
@@ -61,11 +63,11 @@ export default function ConfigSMTP() {
<StyledContainer>
<div className="inputs">
<div className="input row">
<Label>Enable</Label>
<Label>{t("enable")}</Label>
<Toggle onClick={toggleEnable} data-checked={enabled}></Toggle>
</div>
<div className="input">
<Label htmlFor="name">Host</Label>
<Label htmlFor="name">{t("host")}</Label>
<Input
disabled={!enabled}
data-type="host"
@@ -76,7 +78,7 @@ export default function ConfigSMTP() {
/>
</div>
<div className="input">
<Label htmlFor="desc">Port</Label>
<Label htmlFor="desc">{t("port")}</Label>
<Input
disabled={!enabled}
type={"number"}
@@ -88,7 +90,7 @@ export default function ConfigSMTP() {
/>
</div>
<div className="input">
<Label htmlFor="desc">From</Label>
<Label htmlFor="desc">{t("from")}</Label>
<Input
disabled={!enabled}
data-type="from"
@@ -99,7 +101,7 @@ export default function ConfigSMTP() {
/>
</div>
<div className="input">
<Label htmlFor="desc">Username</Label>
<Label htmlFor="desc">{t("username")}</Label>
<Input
disabled={!enabled}
data-type="username"
@@ -110,7 +112,7 @@ export default function ConfigSMTP() {
/>
</div>
<div className="input">
<Label htmlFor="desc">Password</Label>
<Label htmlFor="desc">{t("password")}</Label>
<Input
type={"password"}
disabled={!enabled}
@@ -130,7 +132,7 @@ export default function ConfigSMTP() {
className="link"
rel="noreferrer"
>
How to set up SMTP?
{t("how_to")}
</a>
</div>
<StyledTest>
@@ -143,7 +145,7 @@ export default function ConfigSMTP() {
placeholder="test@email.com"
/>
<Button disabled={!enabled || !testEmail} onClick={handleTestClick}>
Send Test Email
{t("send_test_email")}
</Button>
</StyledTest>
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={reset} />}
+13 -20
View File
@@ -1,22 +1,17 @@
import Tippy from "@tippyjs/react";
import { roundArrow } from "tippy.js";
import "tippy.js/dist/svg-arrow.css";
import styled from "styled-components";
import IconQuestion from "../../../assets/icons/question.svg";
// import { useTranslation } from "react-i18next";
import { PropsWithChildren } from "react";
import { Trans } from 'react-i18next';
const StyledContent = styled.div`
padding: 8px 12px;
background: #101828;
border-radius: 8px;
font-weight: 500;
font-size: 12px;
line-height: 18px;
color: #ffffff;
a {
color: #55c7ec;
}
`;
const Link = ({ to, children }: PropsWithChildren<{ to: string }>) => {
return <a href={to} className="text-primary-500" target="_blank" rel="noreferrer" >
{children}
</a>;
};
export default function Tooltip({ link = "#" }) {
return (
<Tippy
@@ -25,13 +20,11 @@ export default function Tooltip({ link = "#" }) {
arrow={roundArrow}
placement="bottom"
content={
<StyledContent>
Need more detail? See our{" "}
<a target={"doc"} href={link}>
doc
</a>
.
</StyledContent>
<div className="py-2 px-3 bg-gray-800 text-xs text-white rounded-lg">
<Trans ns="setting" i18nKey={"login.more_details"} >
<Link to={link} />
</Trans>
</div>
}
>
<IconQuestion className="icon" />
+4 -2
View File
@@ -3,10 +3,12 @@ import { useNavigate, useSearchParams } from "react-router-dom";
import StyledSettingContainer from "../../common/component/StyledSettingContainer";
import useNavs from "./navs";
import LogoutConfirmModal from "./LogoutConfirmModal";
import { useTranslation } from "react-i18next";
let pageFrom: string = "";
export default function Setting() {
const { t } = useTranslation();
const [searchParams] = useSearchParams();
const navs = useNavs();
const flattenNaves = navs.map(({ items }) => items).flat();
@@ -30,9 +32,9 @@ export default function Setting() {
<StyledSettingContainer
nav={currNav}
closeModal={close}
title="Settings"
title={t("setting") || ""}
navs={navs}
dangers={[{ title: "Log Out", handler: toggleLogoutConfirm }]}
dangers={[{ title: t("action.logout"), handler: toggleLogoutConfirm }]}
>
{currNav.component}
</StyledSettingContainer>
+35 -36
View File
@@ -4,108 +4,107 @@ import Logins from "./config/Logins";
import ConfigFirebase from "./config/Firebase";
import ConfigSMTP from "./config/SMTP";
import APIConfig from "./APIConfig";
import License from "./License";
import License from "./License/License";
import Widget from "./Widget";
import ManageMembers from "../../common/component/ManageMembers";
import FAQ from "../../common/component/FAQ";
import ConfigAgora from "./config/Agora";
// import ConfigAgora from "./config/Agora";
import { useAppSelector } from "../../app/store";
import { useTranslation } from "react-i18next";
const navs = [
{
title: "General",
title: "general",
items: [
{
name: "overview",
title: "Overview",
component: <Overview />
},
{
name: "my_account",
component: <MyAccount />
},
{
name: "members",
title: "Members",
component: <ManageMembers />,
admin: true
}
},
]
},
{
title: "User",
items: [
{
name: "my_account",
title: "My Account",
component: <MyAccount />
}
]
},
{
title: "Configuration",
title: "config",
items: [
{
name: "firebase",
title: "Firebase",
component: <ConfigFirebase />
},
{
name: "agora",
title: "Agora",
component: <ConfigAgora />
},
// {
// name: "agora",
// component: <ConfigAgora />
// },
{
name: "smtp",
title: "SMTP",
component: <ConfigSMTP />
},
{
name: "social_login",
title: "Login Methods",
name: "login_method",
component: <Logins />
},
{
name: "api",
title: "Third-party APP",
name: "third_app",
component: <APIConfig />
},
{
name: "widget",
title: "Widget",
component: <Widget />
},
{
name: "license",
title: "License",
component: <License />
}
],
admin: true
},
{
title: "About",
title: "about",
items: [
{
name: "faq",
title: "FAQ",
component: <FAQ />
},
{
name: "terms",
title: "Terms & Privacy",
component: "Terms & Privacy"
},
{
name: "feedback",
title: "Feedback",
component: "feedback"
component: "Email: han@privoce.com\nWechat: yanggc_2013"
}
]
}
];
const useNavs = () => {
const { t } = useTranslation("setting");
const loginUser = useAppSelector((store) => {
return store.authData.user;
});
return navs.filter((nav) => {
const transformedNavs = navs.map(n => {
const { title, items, ...rest } = n;
return {
title: t(`nav.${title}`),
items: items.map(item => {
const { name, ...rest } = item;
return {
name,
title: t(`nav.${name}`),
...rest
};
}),
...rest
};
});
return transformedNavs.filter((nav) => {
if (loginUser?.is_admin) {
return true;
} else {
@@ -5,6 +5,7 @@ import Modal from "../../common/component/Modal";
import { useLazyRemoveChannelQuery } from "../../app/services/channel";
import StyledModal from "../../common/component/styled/Modal";
import Button from "../../common/component/styled/Button";
import { useTranslation } from "react-i18next";
interface Props {
id: number;
@@ -12,6 +13,7 @@ interface Props {
}
const DeleteConfirmModal: FC<Props> = ({ id, closeModal }) => {
const { t } = useTranslation("setting");
const navigateTo = useNavigate();
const [deleteChannel, { isLoading, isSuccess }] = useLazyRemoveChannelQuery();
const handleDelete = () => {
@@ -31,15 +33,15 @@ const DeleteConfirmModal: FC<Props> = ({ id, closeModal }) => {
<Modal id="modal-modal">
<StyledModal
className="compact"
title="Delete Channel"
description="Are you sure want to delete this channel?"
title={t("channel.delete") || ""}
description={t("channel.delete_desc") || ""}
buttons={
<>
<Button onClick={closeModal.bind(null, undefined)} className="cancel">
Cancel
{t("action.cancel", { ns: "common" })}
</Button>
<Button onClick={handleDelete} className="danger">
{isLoading ? "Deleting" : `Delete`}
{isLoading ? "Deleting" : t("action.remove", { ns: "common" })}
</Button>
</>
}
+8 -7
View File
@@ -16,6 +16,7 @@ import SaveTip from "../../common/component/SaveTip";
import channelIcon from "../../assets/icons/channel.svg?url";
import { useAppSelector } from "../../app/store";
import { Channel } from "../../types/channel";
import { useTranslation } from "react-i18next";
const StyledWrapper = styled.div`
position: relative;
@@ -48,6 +49,7 @@ const StyledWrapper = styled.div`
}
`;
export default function Overview({ id = 0 }) {
const { t } = useTranslation("setting", { keyPrefix: "channel" });
const { loginUser, channel } = useAppSelector((store) => {
return {
loginUser: store.authData.user,
@@ -122,7 +124,7 @@ export default function Overview({ id = 0 }) {
<AvatarUploader type="channel" url={channel?.icon} name={name} uploadImage={updateIcon} />
<div className="inputs">
<div className="input">
<Label htmlFor="name">Channel Name</Label>
<Label htmlFor="name">{t("name")}</Label>
<Input
disabled={readOnly}
className="name"
@@ -131,11 +133,11 @@ export default function Overview({ id = 0 }) {
value={name}
name="name"
id="name"
placeholder="Channel Name"
placeholder={t("name") || ""}
/>
</div>
<div className="input">
<Label htmlFor="desc">Channel Topic</Label>
<Label htmlFor="desc">{t("topic")}</Label>
<Textarea
disabled={readOnly}
data-type="description"
@@ -144,13 +146,12 @@ export default function Overview({ id = 0 }) {
rows={4}
name="name"
id="name"
placeholder="Let everyone know how to use this channel."
/>
placeholder={t("topic_placeholder") || ""} />
</div>
{!readOnly && <div className="input">
<Label htmlFor="desc">Channel Visibility</Label>
<Label htmlFor="desc">{t("visibility")}</Label>
<Radio
options={["Public", "Private"]}
options={[t("public"), t("private")]}
values={["true", "false"]}
value={String(channel.is_public)}
onChange={(v: string) => {
+4 -2
View File
@@ -5,10 +5,12 @@ import StyledSettingContainer from "../../common/component/StyledSettingContaine
import DeleteConfirmModal from "./DeleteConfirmModal";
import useNavs from "./navs";
import { useAppSelector } from "../../app/store";
import { useTranslation } from "react-i18next";
let from: string = "";
export default function ChannelSetting() {
const { t } = useTranslation("setting");
const { cid = 0 } = useParams();
const { loginUser, channel } = useAppSelector((store) => {
return {
@@ -52,11 +54,11 @@ export default function ChannelSetting() {
navs={navs}
dangers={[
canLeave && {
title: "Leave Channel",
title: t("channel.leave"),
handler: toggleLeaveConfirm
},
canDelete && {
title: "Delete Channel",
title: t("channel.delete"),
handler: toggleDeleteConfirm
}
]}
+5 -3
View File
@@ -1,6 +1,7 @@
import Overview from "./Overview";
import ManageMembers from "../../common/component/ManageMembers";
import { ReactNode } from "react";
import { useTranslation } from "react-i18next";
export interface NavItem {
name: string;
@@ -15,18 +16,19 @@ export interface Nav {
}
const useNavs = (cid: number): Nav[] => {
const { t } = useTranslation("setting");
return [
{
title: "General",
title: t("nav.general"),
items: [
{
name: "overview",
title: "Overview",
title: t("nav.overview"),
component: <Overview id={cid} />
},
{
name: "members",
title: "Members",
title: t("nav.members"),
component: <ManageMembers cid={cid} />
}
]