chore: update UIs
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user