chore: update UIs

This commit is contained in:
Tristan Yang
2023-05-19 15:16:45 +08:00
parent 85f01cf05b
commit 50387e726a
22 changed files with 608 additions and 481 deletions
+2 -2
View File
@@ -69,7 +69,7 @@ const AutoDeleteMessages = ({ id, type = "channel" }: Props) => {
<section className="max-w-[512px] h-full relative">
<div className="text-sm">
<h2 className='dark:text-white' >{t("title")}</h2>
<p className="text-gray-500 dark:text-gray-400">{t("desc")}</p>
<p className="text-gray-400">{t("desc")}</p>
</div>
<div className="mt-4">
<StyledRadio
@@ -83,7 +83,7 @@ const AutoDeleteMessages = ({ id, type = "channel" }: Props) => {
{showClear && <>
<div className="text-sm mt-8">
<h2 className='dark:text-white' >{t("clear_title")}</h2>
<p className="text-gray-500 dark:text-gray-400">{t("clear_desc")}</p>
<p className="text-gray-400">{t("clear_desc")}</p>
</div>
<div className="mt-4">
<StyledButton className="danger" onClick={handleClear}>{t("clear")}</StyledButton>
+2 -2
View File
@@ -19,8 +19,8 @@ const ManageMembers: FC<Props> = ({ cid }) => {
<section className="flex flex-col w-full">
{loginUser?.is_admin && <InviteLink />}
<div className="flex flex-col mb-10">
<h4 className="font-bold text-gray-600 dark:text-white">{t("manage_members")}</h4>
<p className="text-gray-500 dark:text-gray-100 text-xs">
<h4 className="font-bold text-gray-700 dark:text-white">{t("manage_members")}</h4>
<p className="text-gray-400 dark:text-gray-100 text-xs">
{t("manage_tip")}
</p>
</div>
+1 -1
View File
@@ -65,7 +65,7 @@ export default function URLPreview({ url = "" }) {
)}
<div className="flex flex-col">
<h3 className="text-sm text-gray-900 dark:text-gray-100">{title}</h3>
<p className={`hidden md:block text-xs text-gray-500 dark:text-gray-400 w-[288px] truncate`}>{description}</p>
<p className={`hidden md:block text-xs text-gray-500 w-[288px] truncate`}>{description}</p>
<span className={`text-[10px] text-gray-500 w-[288px] truncate`}>{url}</span>
</div>
</a>
+1 -1
View File
@@ -95,7 +95,7 @@ const DeviceList = ({ type, visible, setVisible, devices }: {
console.log("device selected", title, selected);
if (list.length == 0) return null;
return <div key={title} className="w-full flex flex-col items-start gap-2 pt-3">
<p className="text-gray-500 dark:text-gray-400 text-xs">{title}</p>
<p className="text-gray-500 text-xs">{title}</p>
<ul className="w-full flex flex-col gap-4">
{list.map(({ deviceId, kind, label }) => {
return (
+37 -9
View File
@@ -38,31 +38,59 @@ const Input: FC<Props> = ({ type = "text", prefix = "", className = "", ...rest
const isNone = className.includes("none");
// const noInner=!className.includes("inner");
const isPwd = type == "password";
const inputClass = clsx(`w-full bg-white dark:bg-gray-800 text-sm text-gray-800 dark:text-gray-200 p-2 outline-none
const inputClass = clsx(
`w-full text-sm text-gray-800 dark:text-gray-200 p-2 outline-none
bg-inherit
disabled:text-gray-500 disabled:bg-gray-100
dark:disabled:text-gray-500
dark:disabled:bg-gray-800/50
dark:disabled:border-gray-600
placeholder:text-gray-400`,
// noInner && 'rounded border border-solid border-gray-200 shadow',
isLarge && 'py-3',
isLarge && "py-3",
isNone && "!border-none bg-transparent shadow-none",
isPwd && "pr-[30px]"
);
return type == "password" ? (
<div className={`w-full relative flex overflow-hidden rounded border border-solid border-gray-200 dark:border-gray-400 shadow-sm ${className}`}>
<input type={inputType} autoComplete={inputType == "password" ? "current-password" : "on"} className={`${inputClass} ${className}`} {...rest} />
<div className="absolute top-1/2 right-2.5 -translate-y-1/2 cursor-pointer" onClick={togglePasswordVisible}>
{inputType == "password" ? <IconEyeClose className="fill-gray-500" /> : <IconEyeOpen className="fill-gray-500" />}
<div
className={`w-full relative flex overflow-hidden rounded border border-solid border-gray-200 dark:border-gray-400 shadow-sm ${className}`}
>
<input
type={inputType}
autoComplete={inputType == "password" ? "current-password" : "on"}
className={`${inputClass} ${className}`}
{...rest}
/>
<div
className="absolute top-1/2 right-2.5 -translate-y-1/2 cursor-pointer"
onClick={togglePasswordVisible}
>
{inputType == "password" ? (
<IconEyeClose className="fill-gray-500" />
) : (
<IconEyeOpen className="fill-gray-500" />
)}
</div>
</div>
) : prefix ? (
<div className={`w-full relative flex overflow-hidden rounded border border-solid border-gray-200 dark:border-gray-400 shadow-sm ${className}`}>
{typeof prefix === "string" ? <span className="px-4 py-2 text-sm text-gray-500 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 shadow-[rgb(0_0_0_/_10%)_-1px_0px_0px_inset]">{prefix}</span> : <span className="flex-center p-2 bg-gray-100 dark:bg-gray-800 ">{prefix}</span>}
<div
className={`w-full relative flex overflow-hidden rounded border border-solid border-gray-200 dark:border-gray-400 shadow-sm bg-white dark:bg-gray-800 ${className}`}
>
{typeof prefix === "string" ? (
<span className="px-4 py-2 text-sm text-gray-500 dark:text-gray-300 bg-gray-100 dark:bg-gray-800 shadow-[rgb(0_0_0_/_10%)_-1px_0px_0px_inset]">
{prefix}
</span>
) : (
<span className="flex-center p-2 bg-transparent">{prefix}</span>
)}
<input className={`${inputClass} ${className}`} type={type} {...rest} />
</div>
) : (
<input type={inputType} className={`${inputClass} rounded border border-solid border-gray-200 dark:border-gray-400 shadow-sm ${className}`} {...rest} />
<input
type={inputType}
className={`${inputClass} rounded border border-solid border-gray-200 dark:border-gray-400 shadow-sm ${className}`}
{...rest}
/>
);
};
+10 -5
View File
@@ -1,9 +1,14 @@
import { LabelHTMLAttributes, ReactNode } from "react";
type Props = LabelHTMLAttributes<HTMLLabelElement> & { children?: ReactNode }
const StyledLabel = ({ children, className = '', ...rest }: Props) => {
return <label className={`text-gray-500 text-sm ${className}`} {...rest}>
{children}
</label>;
type Props = LabelHTMLAttributes<HTMLLabelElement> & { children?: ReactNode };
const StyledLabel = ({ children, className = "", ...rest }: Props) => {
return (
<label
className={`text-gray-800 dark:text-gray-100 text-sm font-semibold ${className}`}
{...rest}
>
{children}
</label>
);
};
export default StyledLabel;
+2 -2
View File
@@ -152,12 +152,12 @@ const Session: FC<IProps> = ({
</i>
{!is_public && <IconLock className="dark:fill-gray-400" />}
</span>
<span className={clsx("text-xs text-gray-500 dark:text-gray-400 max-w-[80px] truncate font-semibold")}>
<span className={clsx("text-xs text-gray-500 max-w-[80px] truncate font-semibold")}>
{fromNowTime(previewMsg.created_at)}
</span>
</div>
<div className="flex items-center justify-between">
<span className={clsx("text-xs text-gray-500 dark:text-gray-400 truncate", unreads > 0 ? `w-36` : ``)}>{renderPreviewMessage(previewMsg)}</span>
<span className={clsx("text-xs text-gray-500 truncate", unreads > 0 ? `w-36` : ``)}>{renderPreviewMessage(previewMsg)}</span>
{(unreads > 0 && !isCurrentPath) ? <strong className={clsx(`text-white px-1.5 py-[3px] font-bold text-[10px] leading-[10px] rounded-[10px]`, muted ? "bg-black/10 dark:bg-gray-500" : "bg-primary-400")}>
{unreads > 99 ? "99+" : unreads}
</strong> : (muted && <IconMute className="w-3 h-3 fill-black/10 dark:fill-gray-500" />)}
+13 -13
View File
@@ -14,33 +14,33 @@ const APIDocument = () => {
};
return (
<section className="flex flex-col justify-start items-start gap-4">
<div className="text-gray-500 dark:text-gray-100">
<div className="font-semibold dark:text-white">
{t("api_doc.desc")}
</div>
<div className='flex flex-col gap-2 w-full'>
<h2 className='text-gray-700 dark:text-white text-xl font-bold'>
<h2 className='text-gray-700 dark:text-white text-lg font-semibold flex gap-1'>
{t("api_doc.access")}
<a href={APIUrl} target="_blank" rel="noopener noreferrer" className='underline text-primary-600'>
{APIUrl}
</a>
</h2>
<a href={APIUrl} target="_blank" rel="noopener noreferrer" className='underline text-primary-600'>
{APIUrl}
</a>
</div>
<div className='flex flex-col gap-2 w-full'>
<h2 className='text-gray-700 dark:text-white text-xl font-bold'>
<h2 className='text-gray-700 dark:text-white text-lg font-semibold'>
{t("api_doc.use_method")}
</h2>
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-2">
<h3 className='text-gray-700 dark:text-white text-lg'>
👉 {t("api_doc.step_1")}
<h3 className='text-gray-700 dark:text-white '>
{t("api_doc.step_1")}
</h3>
{/* <div className="flex flex-col gap-1"> */}
<img className='border border-solid rounded-md border-gray-300 shadow-lg md:w-[85%]' src="https://s.voce.chat/web_client/assets/img/api.doc.step1.png" alt="step 1" />
<img className='border border-solid rounded-md border-gray-300 shadow-lg md:w-[50%]' src="https://s.voce.chat/web_client/assets/img/api.doc.step1.png" alt="step 1" />
{/* </div> */}
</div>
<div className="flex flex-col gap-2">
<h3 className='text-gray-700 dark:text-white text-lg flex flex-col items-start gap-2'>
👉 {t("api_doc.step_2")} <span className='text-gray-500 dark:text-white text-sm'>
<h3 className='text-gray-700 dark:text-white flex flex-col items-start gap-2'>
{t("api_doc.step_2")} <span className='text-gray-400 dark:text-white text-xs'>
({t("api_doc.step_2_desc")})
</span>
</h3>
@@ -53,8 +53,8 @@ const APIDocument = () => {
<img className='border border-solid rounded-md border-gray-300 shadow-lg md:w-[85%]' src="https://s.voce.chat/web_client/assets/img/api.doc.step2.jpg" alt="step 2" />
</div>
<div className="flex flex-col gap-2">
<h3 className='text-gray-700 dark:text-white text-lg flex items-center gap-2'>
👉 {t("api_doc.last")}
<h3 className='text-gray-700 dark:text-white flex items-center gap-2'>
{t("api_doc.last")}
</h3>
<img className='border border-solid rounded-md border-gray-300 shadow-lg md:w-[85%]' src="https://s.voce.chat/web_client/assets/img/api.doc.step3.png" alt="step 3" />
</div>
+1 -1
View File
@@ -72,7 +72,7 @@ const WebhookEdit = ({ uid }: Props) => {
evt.preventDefault();
handleEdit();
}}>
<input readOnly={!editable} required autoFocus type="url" name='webhook' defaultValue={url} className={clsx("text-sm text-gray-500 dark:text-gray-100 dark:bg-slate-900 px-2 py-1", editable ? "ring-1 ring-gray-500 bg-gray-50" : "bg-transparent")} />
<input readOnly={!editable} required autoFocus type="url" name='webhook' defaultValue={url} className={clsx("text-sm text-gray-400 dark:text-gray-100 dark:bg-slate-900 px-2 py-1", editable ? "ring-1 ring-gray-500 bg-gray-50" : "bg-transparent")} />
</form>
<button type='button' disabled={isUpdating} onClick={handleEdit}>
{isUpdating ? <Orbit size={16} /> : editable ?
+95 -63
View File
@@ -1,39 +1,39 @@
import { useEffect } from "react";
// import dayjs from "dayjs";
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useUpdateAvatarByAdminMutation } from '../../../app/services/user';
import { useAppSelector } from '../../../app/store';
import AvatarUploader from '../../../components/AvatarUploader';
import Button from '../../../components/styled/Button';
import IconDelete from '@/assets/icons/delete.svg';
// import IconTest from '@/assets/icons/test.svg';
import CreateModal from './CreateModal';
import WebhookEdit from './WebhookEdit';
import WebhookModal from './WebhookModal';
// import TestAPIKeyModal from './TestAPIKeyModal';
import DeleteModal from './DeleteModal';
import BotAPIKeys from './BotAPIKeys';
import { toast } from 'react-hot-toast';
import { useState } from "react";
import { useTranslation } from "react-i18next";
type TipProps = { title: string, desc: string };
import { useUpdateAvatarByAdminMutation } from "@/app/services/user";
import { useAppSelector } from "@/app/store";
import AvatarUploader from "@/components/AvatarUploader";
import Button from "@/components/styled/Button";
import IconDelete from "@/assets/icons/delete.svg";
import CreateModal from "./CreateModal";
import WebhookEdit from "./WebhookEdit";
import WebhookModal from "./WebhookModal";
import DeleteModal from "./DeleteModal";
import BotAPIKeys from "./BotAPIKeys";
import { toast } from "react-hot-toast";
type TipProps = { title: string; desc: string };
const Tip = ({ title, desc }: TipProps) => {
return <div className="flex flex-col text-sm">
<h2 className="dark:text-white">{title}</h2>
<p className="text-gray-500 dark:text-gray-400">{desc}</p>
</div>;
return (
<div className="flex flex-col text-sm">
<h2 className="font-semibold dark:text-white">{title}</h2>
<p className="text-gray-400 text-xs">{desc}</p>
</div>
);
};
const tdClass = "p-6 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100 align-top";
type WebhookParams = { webhook?: string, uid: number };
type DeleteParams = { name: string, uid: number };
const tdClass =
"p-6 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100 align-top";
type WebhookParams = { webhook?: string; uid: number };
type DeleteParams = { name: string; uid: number };
export default function BotConfig() {
// const [testAPIKeyModalVisible, setTestAPIKeyModalVisible] = useState(false);
const [updateAvatar, { isSuccess: updateAvatarSuccess }] = useUpdateAvatarByAdminMutation();
const [createModalVisible, setCreateModalVisible] = useState(false);
const [currWebhookParams, setCurrWebhookParams] = useState<WebhookParams | undefined>(undefined);
const [currDeleteParams, setCurrDeleteParams] = useState<DeleteParams | undefined>(undefined);
const bots = useAppSelector(store => Object.values(store.users.byId).filter(u => !!u.is_bot));
const bots = useAppSelector((store) => Object.values(store.users.byId).filter((u) => !!u.is_bot));
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
@@ -41,7 +41,7 @@ export default function BotConfig() {
// setTestAPIKeyModalVisible(prev => !prev);
// };
const toggleCreateModalVisible = () => {
setCreateModalVisible(prev => !prev);
setCreateModalVisible((prev) => !prev);
};
const toggleWebhookModalVisible = (obj?: WebhookParams) => {
console.log("webhook modal", obj);
@@ -65,65 +65,97 @@ export default function BotConfig() {
<Tip title={t("webhook_tip_title")} desc={t("webhook_tip_desc")} />
</div>
<div className="flex flex-col gap-1">
<h2 className='font-semibold flex gap-4 items-center dark:text-white'>
<h2 className="font-semibold flex gap-4 items-center dark:text-white">
{t("manage")}
<a href="https://doc.voce.chat/bot-and-webhook" target="_blank" className="text-sm text-blue-400 underline-offset-1 underline" rel="noopener noreferrer">
<a
href="https://doc.voce.chat/bot-and-webhook"
target="_blank"
className="text-sm text-blue-400 underline-offset-1 underline"
rel="noopener noreferrer"
>
🔗 {t("how_to_use")}
</a>
</h2>
<p className='text-gray-500 dark:text-gray-400 text-xs'>{t("manage_desc")}</p>
<p className="text-gray-400 text-xs">{t("manage_desc")}</p>
</div>
<div className="w-full md:w-fit overflow-auto md:overflow-hidden">
<table className="min-w-full table-auto">
<thead className="border-b dark:border-b-gray-500 bg-gray-50 dark:bg-gray-600">
<tr>
{[t("col_avatar"), t('col_name'), t('col_api_key'), t('col_webhook'), t('col_opt')].map(title => <th key={title} scope="col" className="text-sm font-bold text-gray-900 dark:text-gray-100 px-6 py-4 text-left">
{title}
</th>)}
{[
t("col_avatar"),
t("col_name"),
t("col_api_key"),
t("col_webhook"),
t("col_opt")
].map((title) => (
<th
key={title}
scope="col"
className="text-sm font-bold text-gray-900 dark:text-gray-100 px-6 py-4 text-left"
>
{title}
</th>
))}
</tr>
</thead>
<tbody>
{bots.map(bot => {
{bots.map((bot) => {
const { uid, name, avatar } = bot;
return <tr key={uid} className="bg-white dark:bg-gray-800 border-b dark:border-b-gray-500 transition duration-300 ease-in-out md:hover:bg-gray-100 dark:md:hover:bg-transparent">
<td className="px-4 py-2">
<AvatarUploader uid={uid} url={avatar} uploadImage={updateAvatar} name={name} size={56} />
</td>
<td className={`${tdClass}`}>
<div>
{name}
</div>
<div className='text-xs text-gray-500'>
#{uid}
</div>
</td>
<td className={`${tdClass} py-2`}>
<BotAPIKeys uid={uid} />
</td>
<td className={tdClass}>
<WebhookEdit uid={uid} />
</td>
<td className={tdClass}>
<button type='button' onClick={toggleDeleteModalVisible.bind(null, { uid, name })} >
<IconDelete className="hover:opacity-80" />
</button>
{/* <Button className='mini' onClick={toggleWebhookModalVisible.bind(null, { webhook: webhook_url, uid })} >Set Webhook</Button> */}
</td>
</tr>;
return (
<tr
key={uid}
className="bg-white dark:bg-gray-800 border-b dark:border-b-gray-500 transition duration-300 ease-in-out md:hover:bg-gray-100 dark:md:hover:bg-transparent"
>
<td className="px-4 py-2">
<AvatarUploader
uid={uid}
url={avatar}
uploadImage={updateAvatar}
name={name}
size={56}
/>
</td>
<td className={`${tdClass}`}>
<div>{name}</div>
<div className="text-xs text-gray-500">#{uid}</div>
</td>
<td className={`${tdClass} py-2`}>
<BotAPIKeys uid={uid} />
</td>
<td className={tdClass}>
<WebhookEdit uid={uid} />
</td>
<td className={tdClass}>
<button
type="button"
onClick={toggleDeleteModalVisible.bind(null, { uid, name })}
>
<IconDelete className="hover:opacity-80" />
</button>
{/* <Button className='mini' onClick={toggleWebhookModalVisible.bind(null, { webhook: webhook_url, uid })} >Set Webhook</Button> */}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
{/* <Button onClick={toggleCreateModalVisible} className="ghost small">{ct("action.add")}</Button> */}
<div className="flex gap-4">
<Button onClick={toggleCreateModalVisible} className="small">{ct("action.add")}</Button>
<Button onClick={toggleCreateModalVisible} className="small">
{ct("action.add")}
</Button>
{/* {bots.length > 0 && <Button onClick={toggleTestAPIKeyModalVisible} className="ghost small stroke-slate-200 fill-gray-200"> Test API Key</Button>} */}
</div>
</div>
{createModalVisible && <CreateModal closeModal={toggleCreateModalVisible} />}
{currWebhookParams && <WebhookModal closeModal={toggleWebhookModalVisible} {...currWebhookParams} />}
{currDeleteParams && <DeleteModal closeModal={toggleDeleteModalVisible} {...currDeleteParams} />}
{currWebhookParams && (
<WebhookModal closeModal={toggleWebhookModalVisible} {...currWebhookParams} />
)}
{currDeleteParams && (
<DeleteModal closeModal={toggleDeleteModalVisible} {...currDeleteParams} />
)}
{/* {testAPIKeyModalVisible && <TestAPIKeyModal closeModal={toggleTestAPIKeyModalVisible} />} */}
</>
);
@@ -126,7 +126,7 @@ const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
{ct("action.cancel")}
</Button>
<Button className="mini" disabled={isLoading || isSuccess} onClick={handleRenew}>
{isLoading ? "Initialize Payment Url" : isSuccess ? "Redirecting" : t("license.tip_confirm")}
{isLoading ? "Initialize Payment URL" : isSuccess ? "Redirecting" : t("license.tip_confirm")}
</Button>
</div>
</div>
+101 -101
View File
@@ -1,113 +1,113 @@
import { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
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 { useUpdateLogoMutation, useUpdateServerMutation } from "@/app/services/server";
import LogoUploader from "@/components/AvatarUploader";
import Input from "@/components/styled/Input";
import Label from "@/components/styled/Label";
import Textarea from "@/components/styled/Textarea";
import SaveTip from '../../../components/SaveTip';
import { useAppSelector } from '../../../app/store';
import SaveTip from "@/components/SaveTip";
import { useAppSelector } from "@/app/store";
const Index = () => {
const { t } = useTranslation("setting");
const { t: ct } = useTranslation();
const { loginUser, server } = useAppSelector((store) => {
return { loginUser: store.authData.user, server: store.server };
const { t } = useTranslation("setting");
const { t: ct } = useTranslation();
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 [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(ct("tip.update"));
}
}, [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="flex gap-4">
<div className="w-24 h-24">
<LogoUploader
disabled={!isAdmin}
url={uploadSuccess ? `${logo}?t=${+new Date()}` : logo}
name={name}
uploadImage={uploadLogo}
/>
</div>
{isAdmin && (
<div className="flex flex-col justify-between items-start">
<div className="text-sm text-gray-600 dark:text-gray-100">
{t("overview.upload_desc")}
</div>
</div>
)}
const handleReset = () => {
setServerValues(server);
};
useEffect(() => {
if (server) {
setServerValues(server);
}
}, [server]);
useEffect(() => {
if (uploadSuccess) {
toast.success(ct("tip.update"));
}
}, [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="flex gap-4">
<div className="w-24 h-24">
<LogoUploader
disabled={!isAdmin}
url={uploadSuccess ? `${logo}?t=${+new Date()}` : logo}
name={name}
uploadImage={uploadLogo}
/>
</div>
{isAdmin && (
<div className="flex flex-col justify-between items-start">
<div className="text-sm text-gray-600 dark:text-gray-100">
{t("overview.upload_desc")}
</div>
<div className="flex flex-col items-start gap-6 mb-3">
<div className="w-full flex flex-col items-start gap-2">
<Label className='dark:text-gray-100' 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="w-full flex flex-col items-start gap-2">
<Label className='dark:text-gray-100' 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} />}
</>
);
</div>
)}
</div>
<div className="flex flex-col items-start gap-6 mb-3">
<div className="w-full flex flex-col items-start gap-2">
<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="w-full flex flex-col items-start gap-2">
<Label htmlFor="desc">{t("overview.desc")}</Label>
<Textarea
disabled={!isAdmin}
data-type="description"
onChange={handleChange}
value={description ?? ""}
rows={4}
name="desc"
id="desc"
placeholder="Tell the world a bit about this server"
/>
</div>
</div>
{changed && <SaveTip saveHandler={handleUpdateServer} resetHandler={handleReset} />}
</>
);
};
export default Index;
export default Index;
+101 -101
View File
@@ -1,113 +1,113 @@
import { ChangeEvent, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
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 { useUpdateLogoMutation, useUpdateServerMutation } from "@/app/services/server";
import LogoUploader from "@/components/AvatarUploader";
import Input from "@/components/styled/Input";
import Label from "@/components/styled/Label";
import Textarea from "@/components/styled/Textarea";
import SaveTip from '../../../components/SaveTip';
import { useAppSelector } from '../../../app/store';
import SaveTip from "@/components/SaveTip";
import { useAppSelector } from "@/app/store";
const Index = () => {
const { t } = useTranslation("setting");
const { t: ct } = useTranslation();
const { loginUser, server } = useAppSelector((store) => {
return { loginUser: store.authData.user, server: store.server };
const { t } = useTranslation("setting");
const { t: ct } = useTranslation();
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 [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(ct("tip.update"));
}
}, [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="flex gap-4">
<div className="w-24 h-24">
<LogoUploader
disabled={!isAdmin}
url={uploadSuccess ? `${logo}?t=${+new Date()}` : logo}
name={name}
uploadImage={uploadLogo}
/>
</div>
{isAdmin && (
<div className="flex flex-col justify-between items-start">
<div className="text-sm text-gray-600 dark:text-gray-100">
{t("overview.upload_desc")}
</div>
</div>
)}
const handleReset = () => {
setServerValues(server);
};
useEffect(() => {
if (server) {
setServerValues(server);
}
}, [server]);
useEffect(() => {
if (uploadSuccess) {
toast.success(ct("tip.update"));
}
}, [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="flex gap-4">
<div className="w-24 h-24">
<LogoUploader
disabled={!isAdmin}
url={uploadSuccess ? `${logo}?t=${+new Date()}` : logo}
name={name}
uploadImage={uploadLogo}
/>
</div>
{isAdmin && (
<div className="flex flex-col justify-between items-start">
<div className="text-sm text-gray-600 dark:text-gray-100">
{t("overview.upload_desc")}
</div>
<div className="flex flex-col items-start gap-6 mb-3">
<div className="w-full flex flex-col items-start gap-2">
<Label className='dark:text-gray-100' 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="w-full flex flex-col items-start gap-2">
<Label className='dark:text-gray-100' 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} />}
</>
);
</div>
)}
</div>
<div className="flex flex-col items-start gap-6 mb-3">
<div className="w-full flex flex-col items-start gap-2">
<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="w-full flex flex-col items-start gap-2">
<Label htmlFor="desc">{t("overview.desc")}</Label>
<Textarea
disabled={!isAdmin}
data-type="description"
onChange={handleChange}
value={description ?? ""}
rows={4}
name="desc"
id="desc"
placeholder="Tell the world a bit about this server"
/>
</div>
</div>
{changed && <SaveTip saveHandler={handleUpdateServer} resetHandler={handleReset} />}
</>
);
};
export default Index;
export default Index;
+102 -68
View File
@@ -1,30 +1,39 @@
// 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';
import { useAppSelector } from '../../app/store';
import Button from '../../components/styled/Button';
import Input from '../../components/styled/Input';
import useCopy from '../../hooks/useCopy';
import IconQuestion from '@/assets/icons/question.svg';
import { useTranslation } from "react-i18next";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import { useAppSelector } from "../../app/store";
import Button from "../../components/styled/Button";
import Input from "../../components/styled/Input";
import useCopy from "../../hooks/useCopy";
import IconQuestion from "@/assets/icons/question.svg";
const Row = ({ paramKey, paramDefault, remarks }: { paramKey: string, paramDefault: string | number, remarks: string }) => {
return <tr className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-50 border-b transition duration-300 ease-in-out md:hover:bg-gray-100 dark:md:hover:bg-gray-900">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
{paramKey}
</td>
<td className="text-sm font-light px-6 py-4 whitespace-nowrap">
{paramKey == "theme-color" ? <span style={{ color: paramDefault as string }}> {paramDefault}</span> : paramDefault}
</td>
<td className="text-sm font-light px-6 py-4 whitespace-nowrap">
{remarks}
</td>
</tr>;
const Row = ({
paramKey,
paramDefault,
remarks
}: {
paramKey: string;
paramDefault: string | number;
remarks: string;
}) => {
return (
<tr className="bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-50 border-b transition duration-300 ease-in-out md:hover:bg-gray-100 dark:md:hover:bg-gray-900">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">{paramKey}</td>
<td className="text-sm font-light px-6 py-4 whitespace-nowrap">
{paramKey == "theme-color" ? (
<span style={{ color: paramDefault as string }}> {paramDefault}</span>
) : (
paramDefault
)}
</td>
<td className="text-sm font-light px-6 py-4 whitespace-nowrap">{remarks}</td>
</tr>
);
};
export default function Widget() {
const loginUid = useAppSelector(store => store.authData.user?.uid);
const loginUid = useAppSelector((store) => store.authData.user?.uid);
const widgetLink = `${location.origin}/widget.html?host=${loginUid}`;
const { t } = useTranslation("setting", { keyPrefix: "widget" });
const { t: wt } = useTranslation("widget");
@@ -36,63 +45,79 @@ export default function Widget() {
// const disableBtn = !reachLimit;
return (
<div className="flex flex-col justify-start items-start">
<div className="text-gray-600 dark:text-gray-100">
{t('tip')}
</div>
<label htmlFor="code" className="text-gray-500 dark:text-white text-sm mt-5">
{t('code')}:
<div className="font-semibold dark:text-white">{t("tip")}</div>
<label htmlFor="code" className="font-semibold dark:text-white">
{t("code")}:
</label>
<SyntaxHighlighter id="code" language="html" style={vscDarkPlus} className="rounded">
{`<!-- ${t('code_comment')} -->\n<script \n data-host-id="${loginUid}" \n data-theme-color="#1fe1f9" \n data-close-width="48" \n data-close-height="48" \n data-open-width="380" \n data-open-height="680" \n data-welcome="Your custom welcome text" \n src="${location.origin}/widget.js" \n async \n></script>`}
{`<!-- ${t(
"code_comment"
)} -->\n<script \n data-host-id="${loginUid}" \n data-theme-color="#1fe1f9" \n data-close-width="48" \n data-close-height="48" \n data-open-width="380" \n data-open-height="680" \n data-welcome="Your custom welcome text" \n src="${
location.origin
}/widget.js" \n async \n></script>`}
</SyntaxHighlighter>
<div className="text-gray-500 dark:text-white text-sm mt-5 mb-2">
{t('config')}:
</div>
<div className="font-semibold dark:text-white mt-5 mb-2">{t("config")}:</div>
<div className="w-full md:w-[700px] border border-solid border-gray-300 dark:border-gray-400 rounded overflow-auto md:overflow-hidden">
<table className="min-w-full table-auto">
<thead className="border-b bg-gray-50 dark:bg-gray-500">
<tr>
{[t('param_key'), t('default_value'), t('remark')].map(title => <th key={title} scope="col" className="text-sm font-bold text-gray-900 dark:text-white px-6 py-4 text-left">
{title}
</th>)}
{[t("param_key"), t("default_value"), t("remark")].map((title) => (
<th
key={title}
scope="col"
className="text-sm font-bold text-gray-900 dark:text-white px-6 py-4 text-left whitespace-nowrap"
>
{title}
</th>
))}
</tr>
</thead>
<tbody>
{[{
paramKey: "host-id",
paramDefault: 1,
remarks: t("param_host")
}, {
paramKey: "theme-color",
paramDefault: "#1fe1f9",
remarks: t("param_theme_color")
}, {
paramKey: "close-width",
paramDefault: `48(px)`,
remarks: t("param_close_width")
}, {
paramKey: "close-height",
paramDefault: `48(px)`,
remarks: t("param_close_height")
}, {
paramKey: "open-width",
paramDefault: `380(px)`,
remarks: t("param_open_width")
}, {
paramKey: "open-height",
paramDefault: `680(px)`,
remarks: t("param_open_height")
}, {
paramKey: "welcome",
paramDefault: wt("welcome"),
remarks: t("param_welcome")
}
].map(row => <Row key={row.paramKey} {...row} />)}
{[
{
paramKey: "host-id",
paramDefault: 1,
remarks: t("param_host")
},
{
paramKey: "theme-color",
paramDefault: "#1fe1f9",
remarks: t("param_theme_color")
},
{
paramKey: "close-width",
paramDefault: `48(px)`,
remarks: t("param_close_width")
},
{
paramKey: "close-height",
paramDefault: `48(px)`,
remarks: t("param_close_height")
},
{
paramKey: "open-width",
paramDefault: `380(px)`,
remarks: t("param_open_width")
},
{
paramKey: "open-height",
paramDefault: `680(px)`,
remarks: t("param_open_height")
},
{
paramKey: "welcome",
paramDefault: wt("welcome"),
remarks: t("param_welcome")
}
].map((row) => (
<Row key={row.paramKey} {...row} />
))}
</tbody>
<tfoot className="border-t border-solid border-gray-200 dark:border-gray-50 dark:bg-gray-500" >
<tfoot className="border-t border-solid border-gray-200 dark:border-gray-50 dark:bg-gray-500">
<tr>
<td colSpan={3} className="text-gray-400 dark:text-white px-5 py-3 text-sm">
* All the parameters are optional, and prefixed by <i className="bg-gray-700 text-white px-1">data-</i>
* All the parameters are optional, and prefixed by{" "}
<i className="bg-gray-700 text-white px-1">data-</i>
</td>
</tr>
</tfoot>
@@ -100,13 +125,22 @@ export default function Widget() {
</div>
<div className="mt-8">
<p className="text-sm mb-2 text-gray-500 dark:text-gray-50 flex flex-col md:flex-row gap-4">
{t("share_link")} <a className="text-primary-500 flex gap-1 items-center" href="https://doc.voce.chat/widget" target="_blank" rel="noopener noreferrer">
{t("share_link")}{" "}
<a
className="text-primary-500 flex gap-1 items-center"
href="https://doc.voce.chat/widget"
target="_blank"
rel="noopener noreferrer"
>
<IconQuestion /> {t("widget_faq")}
</a>
</p>
<div className="w-full md:w-96 mb-3 relative">
<Input readOnly className={"large !pr-16"} value={widgetLink} />
<Button onClick={copyLink} className="ghost small border_less absolute right-1 top-1/2 -translate-y-1/2 dark:!text-primary-400">
<Button
onClick={copyLink}
className="ghost small border_less absolute right-1 top-1/2 -translate-y-1/2 dark:!text-primary-400"
>
{copied ? "Copied" : ct("action.copy")}
</Button>
</div>
+9 -2
View File
@@ -1,4 +1,5 @@
import { ChangeEvent } from "react";
import { useTranslation } from "react-i18next";
import Input from "@/components/styled/Input";
import Label from "@/components/styled/Label";
@@ -6,8 +7,10 @@ import Toggle from "@/components/styled/Toggle";
import SaveTip from "@/components/SaveTip";
import useConfig from "@/hooks/useConfig";
import { AgoraConfig } from "@/types/server";
import HowToTip from "./HowToTip";
export default function ConfigAgora() {
const { t } = useTranslation("setting", { keyPrefix: "agora" });
const { changed, reset, values, setValues, toggleEnable, updateConfig } = useConfig("agora");
const handleUpdate = () => {
// const { token_url, description } = values;
@@ -41,11 +44,14 @@ export default function ConfigAgora() {
<div className="setting-container">
<div className="inputs">
<div className="input row">
<Label>Enable</Label>
<Label className="flex items-center gap-2">
Enable
<HowToTip link="https://doc.voce.chat/setting/setting-agora" text={t("how_to")} />
</Label>
<Toggle onClick={toggleEnable} checked={enabled}></Toggle>
</div>
<div className="input">
<Label htmlFor="url">Agora Url</Label>
<Label htmlFor="url">Agora URL</Label>
<Input
disabled={!enabled}
data-type="url"
@@ -116,6 +122,7 @@ export default function ConfigAgora() {
/>
</div>
</div>
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={reset} />}
{/* <button onClick={handleUpdate} className="btn">update</button> */}
</div>
+62 -67
View File
@@ -10,9 +10,9 @@ import { FirebaseConfig } from "@/types/server";
import { useTranslation } from "react-i18next";
interface IOptions {
disable: string,
official: string,
custom: string
disable: string;
official: string;
custom: string;
}
export default function ConfigFirebase() {
const { values, setValues, updateConfig, changed, reset } = useConfig("firebase");
@@ -24,12 +24,8 @@ export default function ConfigFirebase() {
};
let select: keyof IOptions | "" = "";
if (values) {
const {
use_official,
enabled = false
} = values as FirebaseConfig;
const { use_official, enabled = false } = values as FirebaseConfig;
select = enabled ? (use_official ? "official" : "custom") : "disable";
}
const handleUpdate = () => {
// const { token_url, description } = values;
@@ -46,17 +42,20 @@ export default function ConfigFirebase() {
const handleChangeSelect = (v: keyof IOptions) => {
let tmp = null;
switch (v) {
case "custom": {
tmp = { ...values, enabled: true, use_official: false, };
}
case "custom":
{
tmp = { ...values, enabled: true, use_official: false };
}
break;
case "official": {
tmp = { ...values, enabled: true, use_official: true };
}
case "official":
{
tmp = { ...values, enabled: true, use_official: true };
}
break;
case "disable": {
tmp = { ...values, enabled: false };
}
case "disable":
{
tmp = { ...values, enabled: false };
}
break;
default:
break;
@@ -70,12 +69,7 @@ export default function ConfigFirebase() {
};
if (!values) return null;
const {
token_url,
project_id,
private_key,
client_email,
} = values as FirebaseConfig;
const { token_url, project_id, private_key, client_email } = values as FirebaseConfig;
return (
<div className="setting-container max-md:w-full max-md:h-auto">
<StyledRadio
@@ -84,50 +78,51 @@ export default function ConfigFirebase() {
value={select}
onChange={handleChangeSelect}
/>
{<fieldset className="inputs" disabled={select !== "custom"}>
<div className="input">
<Label htmlFor="name" className="dark:text-gray-200">{t("firebase.token_url")}</Label>
<Input
data-type="token_url"
onChange={handleChange}
value={token_url}
name="token_url"
placeholder={t("firebase.token_url")}
/>
</div>
<div className="input">
<Label htmlFor="desc" className="dark:text-gray-200">{t("firebase.project_id")}</Label>
<Input
data-type="project_id"
onChange={handleChange}
value={project_id}
name="project_id"
placeholder={t("firebase.project_id")}
/>
</div>
<div className="input">
<Label htmlFor="desc" className="dark:text-gray-200">{t("firebase.private_key")}</Label>
<Textarea
rows={10}
spellCheck={false}
data-type="private_key"
onChange={handleChange}
value={private_key}
name="private_key"
placeholder={t("firebase.private_key")}
/>
</div>
<div className="input">
<Label htmlFor="desc" className="dark:text-gray-200">{t("firebase.client_email")}</Label>
<Input
data-type="client_email"
onChange={handleChange}
value={client_email}
name="client_email"
placeholder={t("firebase.client_email")}
/>
</div>
</fieldset>
{
<fieldset className="inputs" disabled={select !== "custom"}>
<div className="input">
<Label htmlFor="name">{t("firebase.token_url")}</Label>
<Input
data-type="token_url"
onChange={handleChange}
value={token_url}
name="token_url"
placeholder={t("firebase.token_url")}
/>
</div>
<div className="input">
<Label htmlFor="project_id">{t("firebase.project_id")}</Label>
<Input
data-type="project_id"
onChange={handleChange}
value={project_id}
name="project_id"
placeholder={t("firebase.project_id")}
/>
</div>
<div className="input">
<Label htmlFor="private_key">{t("firebase.private_key")}</Label>
<Textarea
rows={10}
spellCheck={false}
data-type="private_key"
onChange={handleChange}
value={private_key}
name="private_key"
placeholder={t("firebase.private_key")}
/>
</div>
<div className="input">
<Label htmlFor="client_email">{t("firebase.client_email")}</Label>
<Input
data-type="client_email"
onChange={handleChange}
value={client_email}
name="client_email"
placeholder={t("firebase.client_email")}
/>
</div>
</fieldset>
}
{changed && <SaveTip saveHandler={handleUpdate} resetHandler={handleReset} />}
</div>
+25
View File
@@ -0,0 +1,25 @@
import React from 'react';
import IconQuestion from "@/assets/icons/question.svg";
type Props = {
link: string,
text: string
}
const HowToTip = ({ link, text }: Props) => {
return (
<div className="tip">
<IconQuestion className="dark:fill-gray-300" />
<a
href={link}
target="_blank"
className="link"
rel="noreferrer"
>
{text}
</a>
</div>
);
};
export default HowToTip;
+12 -12
View File
@@ -76,9 +76,9 @@ export default function Logins() {
<div className="row">
<div className="title">
<div className="txt">
<Label className="dark:text-gray-200">{t("password")}</Label>
<Label>{t("password")}</Label>
</div>
<span className="desc dark:!text-gray-400" >{t("password_desc")}</span>
<span className="desc dark:!text-gray-400">{t("password_desc")}</span>
</div>
<Toggle
onClick={handleToggle.bind(null, { password: !password })}
@@ -90,9 +90,9 @@ export default function Logins() {
<div className="row">
<div className="title">
<div className="txt">
<Label className="dark:text-gray-200">{t("magic_link")}</Label>
<Label>{t("magic_link")}</Label>
</div>
<span className="desc dark:!text-gray-400" >{t("magic_link_desc")}</span>
<span className="desc dark:!text-gray-400">{t("magic_link_desc")}</span>
</div>
<Toggle
onClick={handleToggle.bind(null, { magic_link: !magic_link })}
@@ -104,10 +104,10 @@ export default function Logins() {
<div className="row">
<div className="title">
<div className="txt">
<Label className="dark:text-gray-200">{t("google")}</Label>
<Label>{t("google")}</Label>
<Tooltip link="https://doc.voce.chat/setting/third_login/login-google" />
</div>
<span className="desc dark:!text-gray-400" >{t("google_desc")}</span>
<span className="desc dark:!text-gray-400">{t("google_desc")}</span>
</div>
<Toggle
onClick={handleToggle.bind(null, { google: !google })}
@@ -127,10 +127,10 @@ export default function Logins() {
<div className="row">
<div className="title">
<div className="txt">
<Label className="dark:text-gray-200">{t("github")}</Label>
<Label>{t("github")}</Label>
<Tooltip link="https://doc.voce.chat/setting/third_login/login-github" />
</div>
<span className="desc dark:!text-gray-400" >{t("github_desc")}</span>
<span className="desc dark:!text-gray-400">{t("github_desc")}</span>
</div>
<Toggle
onClick={handleToggle.bind(null, { github: !github })}
@@ -158,10 +158,10 @@ export default function Logins() {
<div className="row">
<div className="title">
<div className="txt">
<Label className="dark:text-gray-200">{t("metamask")}</Label>
<Label>{t("metamask")}</Label>
<Tooltip link="https://doc.voce.chat/setting/third_login/login-metamask" />
</div>
<span className="desc dark:!text-gray-400" >{t("metamask_desc")}</span>
<span className="desc dark:!text-gray-400">{t("metamask_desc")}</span>
</div>
<Toggle
onClick={handleToggle.bind(null, { metamask: !metamask })}
@@ -173,10 +173,10 @@ export default function Logins() {
<div className="row">
<div className="title">
<div className="txt">
<Label className="dark:text-gray-200" htmlFor="desc">{t("oidc")}</Label>
<Label>{t("oidc")}</Label>
<Tooltip link="https://doc.voce.chat/setting/third_login/login-webid" />
</div>
<span className="desc dark:!text-gray-400" >{t("oidc_desc")}</span>
<span className="desc dark:!text-gray-400">{t("oidc_desc")}</span>
</div>
</div>
<div className="row">
+12 -19
View File
@@ -3,7 +3,6 @@ import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useSendTestEmailMutation } from "@/app/services/server";
import IconQuestion from "@/assets/icons/question.svg";
import useConfig from "@/hooks/useConfig";
import Input from "@/components/styled/Input";
import Button from "@/components/styled/Button";
@@ -11,6 +10,7 @@ import Toggle from "@/components/styled/Toggle";
import Label from "@/components/styled/Label";
import SaveTip from "@/components/SaveTip";
import { SMTPConfig } from "@/types/server";
import HowToTip from "./HowToTip";
export default function ConfigSMTP() {
const { t } = useTranslation("setting", { keyPrefix: "smtp" });
@@ -56,11 +56,14 @@ export default function ConfigSMTP() {
<div className="setting-container max-md:w-full">
<div className="inputs">
<div className="input row">
<Label className="dark:text-gray-200">{t("enable")}</Label>
<Label className="flex items-center gap-2">
{t("enable")}{" "}
<HowToTip link="https://doc.voce.chat/setting/smtp/smtp-gmail" text={t("how_to")} />
</Label>
<Toggle onClick={toggleEnable} checked={enabled}></Toggle>
</div>
<div className="input">
<Label className="dark:text-gray-200" htmlFor="name">{t("host")}</Label>
<Label htmlFor="host">{t("host")}</Label>
<Input
disabled={!enabled}
data-type="host"
@@ -71,7 +74,7 @@ export default function ConfigSMTP() {
/>
</div>
<div className="input">
<Label className="dark:text-gray-200" htmlFor="desc">{t("port")}</Label>
<Label htmlFor="port">{t("port")}</Label>
<Input
disabled={!enabled}
type={"number"}
@@ -83,7 +86,7 @@ export default function ConfigSMTP() {
/>
</div>
<div className="input">
<Label className="dark:text-gray-200" htmlFor="desc">{t("from")}</Label>
<Label htmlFor="from">{t("from")}</Label>
<Input
disabled={!enabled}
data-type="from"
@@ -94,7 +97,7 @@ export default function ConfigSMTP() {
/>
</div>
<div className="input">
<Label className="dark:text-gray-200" htmlFor="desc">{t("username")}</Label>
<Label htmlFor="username">{t("username")}</Label>
<Input
disabled={!enabled}
data-type="username"
@@ -105,7 +108,7 @@ export default function ConfigSMTP() {
/>
</div>
<div className="input">
<Label className="dark:text-gray-200" htmlFor="desc">{t("password")}</Label>
<Label htmlFor="password">{t("password")}</Label>
<Input
type={"password"}
disabled={!enabled}
@@ -117,17 +120,7 @@ export default function ConfigSMTP() {
/>
</div>
</div>
<div className="tip">
<IconQuestion className="dark:fill-gray-300" />
<a
href="https://doc.voce.chat/setting/smtp/smtp-gmail"
target="_blank"
className="link"
rel="noreferrer"
>
{t("how_to")}
</a>
</div>
<div className="flex gap-4 whitespace-nowrap mt-6">
<Input
type={"email"}
@@ -145,4 +138,4 @@ export default function ConfigSMTP() {
{/* <button onClick={handleUpdate} className="btn">update</button> */}
</div>
);
}
}