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
+12 -8
View File
@@ -20,7 +20,7 @@
"widget": "Widget",
"license": "License",
"about": "About",
"api_doc": "API Document",
"api_doc": "API Documentation",
"version": "Version"
},
"channel": {
@@ -47,7 +47,7 @@
"upload_desc": "Minimum size is 128x128, We recommend at least 512x512 for the server. Max size limited to 5M.",
"sign_up": {
"title": "Default Sign Up",
"desc": "Who can sign up this server.",
"desc": "Who can sign up to this server.",
"everyone": "Everyone",
"invite": "Invitation Link Only"
},
@@ -62,8 +62,8 @@
"desc": "Update the frontend url"
},
"lang": {
"title": "Language",
"desc": "Setting the language",
"title": "Languages",
"desc": "Set the system language",
"en": "English",
"tr": "Türkçe",
"zh": "中文",
@@ -71,14 +71,14 @@
},
"theme": {
"title": "Theme",
"desc": "Setting the theme",
"desc": "Choose your theme",
"auto": "Auto",
"dark": "Dark",
"light": "Light"
},
"online_status": {
"title": "Online Status",
"desc": "Toggle users' online status visible",
"desc": "If enabled, members will see the online/offline status of each other.",
"enable": "Enable",
"disable": "Disable"
},
@@ -90,7 +90,7 @@
},
"contact_verify": {
"title": "Contact Verification",
"desc": "Toggle contact verification",
"desc": "By default all server members can see each other. If you want server members to add and manage their own contacts, please enable the contact feature (admin will be in the contacts list by default).",
"enable": "Enable",
"disable": "Disable"
}
@@ -170,7 +170,7 @@
"use_official": "Use Official Configuration",
"custom": "Custom",
"enable": "Enable",
"token_url": "Token Url",
"token_url": "Token URL",
"project_id": "Project ID",
"private_key": "Private Key",
"client_email": "Client Email"
@@ -185,6 +185,10 @@
"how_to": "How to setup SMTP?",
"send_test_email": "Send Test Email"
},
"agora": {
"enable": "Enable",
"how_to": "How to setup Agora?"
},
"third_app": {
"key": "API Secure Key",
"update": "Update Secret",
+2 -2
View File
@@ -20,7 +20,7 @@
"widget": "Araçlar",
"license": "Lisans",
"about": "Hakkında",
"api_doc": "API Document",
"api_doc": "API Documentation",
"version": "Sürüm",
"faq": "SSS",
"feedback": "Geri Bildirim"
@@ -150,7 +150,7 @@
"use_official": "Resmi Yapılandırmayı Kullanın",
"custom": "Özel",
"enable": "Etkin",
"token_url": "Jeton Url",
"token_url": "Jeton URL",
"project_id": "Tasarı Kimliği",
"private_key": "Gizli Anahtar",
"client_email": "İstemci E-Posta'sı"
+5 -1
View File
@@ -84,7 +84,7 @@
},
"contact_verify": {
"title": "联系人验证",
"desc": "是否开启联系人验证",
"desc": "关闭:服务器内的所有成员能够互相看见;开启:通过搜索添加好友(管理员默认是所有人好友)",
"enable": "开启",
"disable": "关闭"
}
@@ -180,6 +180,10 @@
"how_to": "如何设置SMTP",
"send_test_email": "发送测试邮件"
},
"agora": {
"enable": "开启",
"how_to": "如何配置Agora"
},
"third_app": {
"key": "API 安全密钥",
"update": "更新安全密钥",
+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>
);
}
}