build: format the code with prettier

This commit is contained in:
Tristan Yang
2023-05-19 16:31:28 +08:00
parent 7afc132bbb
commit 5bd0183651
259 changed files with 7627 additions and 5607 deletions
+19 -13
View File
@@ -1,18 +1,18 @@
import { useEffect } from "react";
import Tippy from "@tippyjs/react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import Tippy from "@tippyjs/react";
import { hideAll } from "tippy.js";
import Input from "@/components/styled/Input";
import Toggle from "@/components/styled/Toggle";
import Button from "@/components/styled/Button";
import {
useGetThirdPartySecretQuery,
useUpdateThirdPartySecretMutation
} from "@/app/services/server";
import useConfig from "@/hooks/useConfig";
import { LoginConfig } from "@/types/server";
import { useTranslation } from "react-i18next";
import Button from "@/components/styled/Button";
import Input from "@/components/styled/Input";
import Toggle from "@/components/styled/Toggle";
import useConfig from "@/hooks/useConfig";
export default function APIConfig() {
const { t } = useTranslation("setting");
@@ -40,7 +40,10 @@ export default function APIConfig() {
checked={thirdParty}
/>
<div className="w-full flex flex-col items-start gap-2">
<label htmlFor="secret" className="text-sm text-gray-500 dark:text-gray-100"> {t("third_app.key")}:</label>
<label htmlFor="secret" className="text-sm text-gray-500 dark:text-gray-100">
{" "}
{t("third_app.key")}:
</label>
<Input disabled={!thirdParty} type="password" id="secret" value={updatedSecret || data} />
</div>
<Tippy
@@ -49,9 +52,7 @@ export default function APIConfig() {
trigger="click"
content={
<div className="p-3 rounded-lg border border-orange-400 border-solid flex flex-col gap-3 w-[250px] bg-white">
<div className="text-orange-500 text-xs">
{t("third_app.update_tip")}
</div>
<div className="text-orange-500 text-xs">{t("third_app.update_tip")}</div>
<div className="flex justify-end gap-3 w-full">
<Button onClick={() => hideAll()} className="cancel mini">
{ct("action.cancel")}
@@ -67,8 +68,13 @@ export default function APIConfig() {
</Tippy>
<div className="text-xs text-orange-400">
{t("third_app.key_tip")}
<a className="text-primary-500 font-bold" href="https://doc.voce.chat/login-with-other-account" target="_blank" rel="noopener noreferrer">
🔗 {t("third_app.how_to")}
<a
className="text-primary-500 font-bold"
href="https://doc.voce.chat/login-with-other-account"
target="_blank"
rel="noopener noreferrer"
>
🔗 {t("third_app.how_to")}
</a>
</div>
</div>
+79 -62
View File
@@ -1,69 +1,86 @@
// import React from 'react';
import { useTranslation } from 'react-i18next';
import { useAppSelector } from '../../app/store';
import IconCopy from '@/assets/icons/copy.svg';
import useCopy from '../../hooks/useCopy';
import { useTranslation } from "react-i18next";
import IconCopy from "@/assets/icons/copy.svg";
import { useAppSelector } from "../../app/store";
import useCopy from "../../hooks/useCopy";
// type Props = {}
const APIUrl = `${location.origin}/api/swagger`;
const APIDocument = () => {
const token = useAppSelector(store => store.authData.token);
const { copy } = useCopy();
const { t } = useTranslation("setting");
const handleCopy = () => {
copy(token);
};
return (
<section className="flex flex-col justify-start items-start gap-4">
<div className="font-semibold dark:text-white">
{t("api_doc.desc")}
const token = useAppSelector((store) => store.authData.token);
const { copy } = useCopy();
const { t } = useTranslation("setting");
const handleCopy = () => {
copy(token);
};
return (
<section className="flex flex-col justify-start items-start gap-4">
<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-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>
</div>
<div className="flex flex-col gap-2 w-full">
<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 ">{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-[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 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>
<div className="flex flex-col border border-solid border-green-500 bg-green-100 rounded-md p-2 w-fit break-words text-sm relative">
<p className="break-all md:max-w-4xl font-bold">
{token}
<IconCopy
onClick={handleCopy}
className="absolute right-2 bottom-2 cursor-pointer"
/>
</p>
</div>
<div className='flex flex-col gap-2 w-full'>
<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>
</div>
<div className='flex flex-col gap-2 w-full'>
<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 '>
{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-[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 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>
<div className='flex flex-col border border-solid border-green-500 bg-green-100 rounded-md p-2 w-fit break-words text-sm relative'>
<p className="break-all md:max-w-4xl font-bold">
{token}
<IconCopy onClick={handleCopy} className="absolute right-2 bottom-2 cursor-pointer" />
</p>
</div>
<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 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>
</div>
</div>
</section>
);
<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 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>
</div>
</div>
</section>
);
};
export default APIDocument;
export default APIDocument;
+104 -75
View File
@@ -1,82 +1,111 @@
// import Tippy from '@tippyjs/react';
import dayjs from 'dayjs';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useGetBotAPIKeysQuery } from '../../../app/services/user';
import IconDelete from '@/assets/icons/delete.svg';
import IconAdd from '@/assets/icons/add.svg';
import CreateAPIKeyModal from './CreateAPIKeyModal';
import DeleteAPIKeyModal from './DeleteAPIKeyModal';
import clsx from 'clsx';
import { useState } from "react";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import dayjs from "dayjs";
import IconAdd from "@/assets/icons/add.svg";
import IconDelete from "@/assets/icons/delete.svg";
import { useGetBotAPIKeysQuery } from "../../../app/services/user";
import CreateAPIKeyModal from "./CreateAPIKeyModal";
import DeleteAPIKeyModal from "./DeleteAPIKeyModal";
type Props = {
uid: number
}
type DeleteAPIKeyProps = { uid: number, kid: number }
uid: number;
};
type DeleteAPIKeyProps = { uid: number; kid: number };
const tdClass = "p-1 whitespace-nowrap text-xs text-gray-500 dark:text-gray-200 align-middle px-1";
const BotAPIKeys = ({ uid }: Props) => {
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const [currentUid, setCurrentUid] = useState<number | undefined>();
const [deleteApiKey, setDeleteApiKey] = useState<DeleteAPIKeyProps | undefined>();
const { data, refetch } = useGetBotAPIKeysQuery(uid);
const toggleCreateModal = (param?: number) => {
if (!param) refetch();
setCurrentUid(param);
};
const toggleDeleteModal = (param?: DeleteAPIKeyProps) => {
if (!param) refetch();
setDeleteApiKey(param);
};
if (!data) return null;
const colWidths = ["w-20", "w-[166px]", "w-36", "w-15", "w-10"];
return (
<div className='flex flex-col gap-2 items-start'>
<div className='border-t border-solid border-b border-gray-100 dark:border-gray-500 py-2 w-full'>
<table className="min-w-full table-fixed font-mono">
<thead >
<tr >
{[t("col_key_name"), t('col_key_value'), t('col_key_create_time'), t('col_key_last_used'), ""].map((title, idx) => <th key={title} scope="col" className={clsx(`text-xs text-gray-900 dark:text-gray-50 px-1 text-left pb-2`, colWidths[idx])}>
{title}
</th>)}
</tr>
</thead>
<tbody>
{data.length > 0 ? data.map(ak => {
const { id, name, key, created_at, last_used } = ak;
return <tr key={id} className="group" >
<td className={tdClass}>
{name}
</td>
<td className={`${tdClass} w-40`}>
{`${key.slice(0, 4)} ... ... ${key.slice(-6)}`}
</td>
<td className={tdClass}>
{dayjs(created_at).format("YYYY-MM-DD HH:mm:ss")}
</td>
<td className={tdClass}>
{last_used ? dayjs(last_used).format("YYYY-MM-DD HH:mm:ss") : "Unused"}
</td>
<td className={`${tdClass} invisible group-hover:visible`}>
<button onClick={toggleDeleteModal.bind(null, { kid: id, uid })}>
<IconDelete />
</button>
</td>
</tr>;
}) : <tr>
<td colSpan={4} className='text-center text-xs text-gray-400 py-2'>
{t("no_api_key")}
</td>
</tr>}
</tbody>
</table>
<button onClick={toggleCreateModal.bind(null, uid)} className="text-green-600 text-xs py-0.5 flex items-center gap-1 m-auto my-2 bg-green-50 rounded-full px-2 ">
<IconAdd className="!w-4 !h-4 fill-green-600" /> {t("add_api_key")}
</button>
</div>
{currentUid && <CreateAPIKeyModal uid={currentUid} closeModal={toggleCreateModal.bind(null, undefined)} />}
{deleteApiKey && <DeleteAPIKeyModal uid={deleteApiKey.uid} kid={deleteApiKey.kid} closeModal={toggleDeleteModal.bind(null, undefined)} />}
</div>
);
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const [currentUid, setCurrentUid] = useState<number | undefined>();
const [deleteApiKey, setDeleteApiKey] = useState<DeleteAPIKeyProps | undefined>();
const { data, refetch } = useGetBotAPIKeysQuery(uid);
const toggleCreateModal = (param?: number) => {
if (!param) refetch();
setCurrentUid(param);
};
const toggleDeleteModal = (param?: DeleteAPIKeyProps) => {
if (!param) refetch();
setDeleteApiKey(param);
};
if (!data) return null;
const colWidths = ["w-20", "w-[166px]", "w-36", "w-15", "w-10"];
return (
<div className="flex flex-col gap-2 items-start">
<div className="border-t border-solid border-b border-gray-100 dark:border-gray-500 py-2 w-full">
<table className="min-w-full table-fixed font-mono">
<thead>
<tr>
{[
t("col_key_name"),
t("col_key_value"),
t("col_key_create_time"),
t("col_key_last_used"),
""
].map((title, idx) => (
<th
key={title}
scope="col"
className={clsx(
`text-xs text-gray-900 dark:text-gray-50 px-1 text-left pb-2`,
colWidths[idx]
)}
>
{title}
</th>
))}
</tr>
</thead>
<tbody>
{data.length > 0 ? (
data.map((ak) => {
const { id, name, key, created_at, last_used } = ak;
return (
<tr key={id} className="group">
<td className={tdClass}>{name}</td>
<td className={`${tdClass} w-40`}>
{`${key.slice(0, 4)} ... ... ${key.slice(-6)}`}
</td>
<td className={tdClass}>{dayjs(created_at).format("YYYY-MM-DD HH:mm:ss")}</td>
<td className={tdClass}>
{last_used ? dayjs(last_used).format("YYYY-MM-DD HH:mm:ss") : "Unused"}
</td>
<td className={`${tdClass} invisible group-hover:visible`}>
<button onClick={toggleDeleteModal.bind(null, { kid: id, uid })}>
<IconDelete />
</button>
</td>
</tr>
);
})
) : (
<tr>
<td colSpan={4} className="text-center text-xs text-gray-400 py-2">
{t("no_api_key")}
</td>
</tr>
)}
</tbody>
</table>
<button
onClick={toggleCreateModal.bind(null, uid)}
className="text-green-600 text-xs py-0.5 flex items-center gap-1 m-auto my-2 bg-green-50 rounded-full px-2 "
>
<IconAdd className="!w-4 !h-4 fill-green-600" /> {t("add_api_key")}
</button>
</div>
{currentUid && (
<CreateAPIKeyModal uid={currentUid} closeModal={toggleCreateModal.bind(null, undefined)} />
)}
{deleteApiKey && (
<DeleteAPIKeyModal
uid={deleteApiKey.uid}
kid={deleteApiKey.kid}
closeModal={toggleDeleteModal.bind(null, undefined)}
/>
)}
</div>
);
};
export default BotAPIKeys;
export default BotAPIKeys;
@@ -1,88 +1,96 @@
import { useRef, useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useCreateBotAPIKeyMutation } from '../../../app/services/user';
import Modal from '../../../components/Modal';
import Button from '../../../components/styled/Button';
import Input from '../../../components/styled/Input';
import StyledModal from '../../../components/styled/Modal';
import useCopy from '../../../hooks/useCopy';
import { useEffect, useRef } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useCreateBotAPIKeyMutation } from "../../../app/services/user";
import Modal from "../../../components/Modal";
import Button from "../../../components/styled/Button";
import Input from "../../../components/styled/Input";
import StyledModal from "../../../components/styled/Modal";
import useCopy from "../../../hooks/useCopy";
type Props = {
uid: number,
closeModal: () => void
}
uid: number;
closeModal: () => void;
};
const CreateAPIKeyModal = ({ closeModal, uid }: Props) => {
const { copy } = useCopy();
const [createBotAPIKey, { error, isSuccess, isLoading, data = "" }] = useCreateBotAPIKeyMutation();
const formRef = useRef(null);
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
// const [input, setInput] = useState("");
const handleCreateBot = () => {
if (!formRef || !formRef.current) return;
const formEle = formRef.current as HTMLFormElement;
if (!formEle.checkValidity()) {
formEle.reportValidity();
return;
}
createBotAPIKey({
uid,
name: formEle.querySelector('input')?.value || ""
});
};
useEffect(() => {
if (error) {
switch (error.status) {
case 406:
toast.error("Invalid Webhook URL!");
break;
case 409:
toast.error("Name Already Exists!");
break;
default:
break;
}
}
}, [error]);
const handleCopy = () => {
copy(data);
toast.success("API Key Copied!");
closeModal();
};
const { copy } = useCopy();
const [createBotAPIKey, { error, isSuccess, isLoading, data = "" }] =
useCreateBotAPIKeyMutation();
const formRef = useRef(null);
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
// const [input, setInput] = useState("");
const handleCreateBot = () => {
if (!formRef || !formRef.current) return;
const formEle = formRef.current as HTMLFormElement;
if (!formEle.checkValidity()) {
formEle.reportValidity();
return;
}
createBotAPIKey({
uid,
name: formEle.querySelector("input")?.value || ""
});
};
useEffect(() => {
if (error) {
switch (error.status) {
case 406:
toast.error("Invalid Webhook URL!");
break;
case 409:
toast.error("Name Already Exists!");
break;
default:
break;
}
}
}, [error]);
const handleCopy = () => {
copy(data);
toast.success("API Key Copied!");
closeModal();
};
return (
<Modal id="modal-modal">
<StyledModal
title={t("create_key_title")}
description={t("create_key_desc")}
buttons={
isSuccess ? <Button onClick={handleCopy}>
{t("key_copy_and_close")}
</Button> : <>
<Button className="cancel" onClick={closeModal}>
{ct("action.cancel")}
</Button>
<Button onClick={handleCreateBot}>{isLoading ? "..." : ct("action.done")}</Button>
</>
}
>
{isSuccess ? <div className="flex flex-col gap-2 text-sm">
<div className='border-green-600 bg-green-200/50 rounded border border-solid p-2 max-w-md w-full whitespace-pre-wrap break-all' >
{data}
</div> <div className='text-red-400'>
{t("create_key_warning")}
</div>
</div>
: <form ref={formRef} className="w-full flex flex-col gap-2 items-center" action="/">
<div className="flex flex-col gap-1 w-full">
<label htmlFor={"name"} className="text-sm text-gray-500">Name</label>
<Input name={"name"} required placeholder='Please input API Key name'></Input>
</div>
</form>}
</StyledModal>
</Modal>
);
return (
<Modal id="modal-modal">
<StyledModal
title={t("create_key_title")}
description={t("create_key_desc")}
buttons={
isSuccess ? (
<Button onClick={handleCopy}>{t("key_copy_and_close")}</Button>
) : (
<>
<Button className="cancel" onClick={closeModal}>
{ct("action.cancel")}
</Button>
<Button onClick={handleCreateBot}>{isLoading ? "..." : ct("action.done")}</Button>
</>
)
}
>
{isSuccess ? (
<div className="flex flex-col gap-2 text-sm">
<div className="border-green-600 bg-green-200/50 rounded border border-solid p-2 max-w-md w-full whitespace-pre-wrap break-all">
{data}
</div>{" "}
<div className="text-red-400"> {t("create_key_warning")}</div>
</div>
) : (
<form ref={formRef} className="w-full flex flex-col gap-2 items-center" action="/">
<div className="flex flex-col gap-1 w-full">
<label htmlFor={"name"} className="text-sm text-gray-500">
Name
</label>
<Input name={"name"} required placeholder="Please input API Key name"></Input>
</div>
</form>
)}
</StyledModal>
</Modal>
);
};
export default CreateAPIKeyModal;
export default CreateAPIKeyModal;
+92 -88
View File
@@ -1,97 +1,101 @@
import { useRef, useState, useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useCreateUserMutation } from '../../../app/services/user';
import Modal from '../../../components/Modal';
import Button from '../../../components/styled/Button';
import Input from '../../../components/styled/Input';
import StyledModal from '../../../components/styled/Modal';
import { BASE_ORIGIN } from '../../../app/config';
import { useEffect, useRef, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { BASE_ORIGIN } from "../../../app/config";
import { useCreateUserMutation } from "../../../app/services/user";
import Modal from "../../../components/Modal";
import Button from "../../../components/styled/Button";
import Input from "../../../components/styled/Input";
import StyledModal from "../../../components/styled/Modal";
type Props = {
closeModal: () => void
}
closeModal: () => void;
};
type CreateDTO = {
name: string,
webhook_url?: string
}
name: string;
webhook_url?: string;
};
const CreateModal = ({ closeModal }: Props) => {
const [createUser, { isSuccess, isLoading, error }] = useCreateUserMutation();
const formRef = useRef(null);
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
// const [input, setInput] = useState("");
const handleCreateBot = () => {
if (!formRef || !formRef.current) return;
const formEle = formRef.current as HTMLFormElement;
if (!formEle.checkValidity()) {
formEle.reportValidity();
return;
const [createUser, { isSuccess, isLoading, error }] = useCreateUserMutation();
const formRef = useRef(null);
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
// const [input, setInput] = useState("");
const handleCreateBot = () => {
if (!formRef || !formRef.current) return;
const formEle = formRef.current as HTMLFormElement;
if (!formEle.checkValidity()) {
formEle.reportValidity();
return;
}
const myFormData = new FormData(formEle);
const formDataObj: CreateDTO = { name: "" };
myFormData.forEach((value, key) => {
if (value) {
formDataObj[key] = value;
}
});
const hostname = new URL(BASE_ORIGIN).hostname;
createUser({
is_bot: true,
is_admin: false,
gender: 1,
email: `bot_${new Date().getTime()}@${hostname}`,
password: "",
...formDataObj
});
};
useEffect(() => {
if (error) {
switch (error.status) {
case 406:
toast.error("Invalid Webhook URL!");
break;
}
const myFormData = new FormData(formEle);
const formDataObj: CreateDTO = { name: "" };
myFormData.forEach((value, key) => {
if (value) {
formDataObj[key] = value;
}
});
const hostname = new URL(BASE_ORIGIN).hostname;
createUser({
is_bot: true,
is_admin: false,
gender: 1,
email: `bot_${new Date().getTime()}@${hostname}`,
password: "",
...formDataObj
});
};
useEffect(() => {
if (error) {
switch (error.status) {
case 406:
toast.error("Invalid Webhook URL!");
break;
default:
break;
}
}
}, [error]);
useEffect(() => {
if (isSuccess) {
toast.success("Create Bot Successfully!");
closeModal();
}
}, [isSuccess]);
default:
break;
}
return (
<Modal id="modal-modal">
<StyledModal
title={t("create_title")}
description={t("create_desc")}
buttons={
<>
<Button className="cancel" onClick={closeModal}>
{ct("action.cancel")}
</Button>
<Button onClick={handleCreateBot}>{isLoading ? "Creating" : ct("action.done")}</Button>
</>
}
}, [error]);
useEffect(() => {
if (isSuccess) {
toast.success("Create Bot Successfully!");
closeModal();
}
}, [isSuccess]);
return (
<Modal id="modal-modal">
<StyledModal
title={t("create_title")}
description={t("create_desc")}
buttons={
<>
<Button className="cancel" onClick={closeModal}>
{ct("action.cancel")}
</Button>
<Button onClick={handleCreateBot}>{isLoading ? "Creating" : ct("action.done")}</Button>
</>
}
>
<form ref={formRef} className="w-full flex flex-col gap-2" action="/">
<div className="flex flex-col items-start gap-1 w-full">
<label htmlFor={"name"} className="text-sm text-gray-500">Name</label>
<Input name={"name"} required placeholder='Please input bot name'></Input>
</div>
<div className="flex flex-col items-start gap-1 w-full">
<label htmlFor={"webhook_url"} className="text-sm text-gray-500">Webhook URL (Optional)</label>
<Input name={"webhook_url"} type="url" placeholder='Please input webhook url'></Input>
</div>
</form>
</StyledModal>
</Modal>
);
>
<form ref={formRef} className="w-full flex flex-col gap-2" action="/">
<div className="flex flex-col items-start gap-1 w-full">
<label htmlFor={"name"} className="text-sm text-gray-500">
Name
</label>
<Input name={"name"} required placeholder="Please input bot name"></Input>
</div>
<div className="flex flex-col items-start gap-1 w-full">
<label htmlFor={"webhook_url"} className="text-sm text-gray-500">
Webhook URL (Optional)
</label>
<Input name={"webhook_url"} type="url" placeholder="Please input webhook url"></Input>
</div>
</form>
</StyledModal>
</Modal>
);
};
export default CreateModal;
export default CreateModal;
@@ -1,48 +1,50 @@
import { useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useLazyDeleteBotAPIKeyQuery } from '../../../app/services/user';
import Modal from '../../../components/Modal';
import Button from '../../../components/styled/Button';
import StyledModal from '../../../components/styled/Modal';
import { useEffect } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useLazyDeleteBotAPIKeyQuery } from "../../../app/services/user";
import Modal from "../../../components/Modal";
import Button from "../../../components/styled/Button";
import StyledModal from "../../../components/styled/Modal";
type Props = {
uid: number,
kid: number,
closeModal: () => void
}
uid: number;
kid: number;
closeModal: () => void;
};
const DeleteAPIKeyModal = ({ closeModal, uid, kid }: Props) => {
const [deleteKey, { isSuccess, isLoading }] = useLazyDeleteBotAPIKeyQuery();
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
// const [input, setInput] = useState("");
const handleDeleteBot = () => {
deleteKey({ uid, kid });
};
useEffect(() => {
if (isSuccess) {
toast.success(ct("tip.delete"));
closeModal();
}
}, [isSuccess]);
const [deleteKey, { isSuccess, isLoading }] = useLazyDeleteBotAPIKeyQuery();
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
// const [input, setInput] = useState("");
const handleDeleteBot = () => {
deleteKey({ uid, kid });
};
useEffect(() => {
if (isSuccess) {
toast.success(ct("tip.delete"));
closeModal();
}
}, [isSuccess]);
return (
<Modal id="modal-modal">
<StyledModal
title={`${t("delete_key_title")} ${name}`}
description={t("delete_key_desc")}
buttons={
<>
<Button className="cancel" onClick={closeModal}>
{ct("action.cancel")}
</Button>
<Button className='danger' onClick={handleDeleteBot}>{isLoading ? "Deleting" : ct("action.done")}</Button>
</>
}
>
</StyledModal>
</Modal>
);
return (
<Modal id="modal-modal">
<StyledModal
title={`${t("delete_key_title")} ${name}`}
description={t("delete_key_desc")}
buttons={
<>
<Button className="cancel" onClick={closeModal}>
{ct("action.cancel")}
</Button>
<Button className="danger" onClick={handleDeleteBot}>
{isLoading ? "Deleting" : ct("action.done")}
</Button>
</>
}
></StyledModal>
</Modal>
);
};
export default DeleteAPIKeyModal;
export default DeleteAPIKeyModal;
+44 -42
View File
@@ -1,48 +1,50 @@
import { useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useLazyDeleteUserQuery } from '../../../app/services/user';
import Modal from '../../../components/Modal';
import Button from '../../../components/styled/Button';
import StyledModal from '../../../components/styled/Modal';
import { useEffect } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useLazyDeleteUserQuery } from "../../../app/services/user";
import Modal from "../../../components/Modal";
import Button from "../../../components/styled/Button";
import StyledModal from "../../../components/styled/Modal";
type Props = {
uid: number,
name: string,
closeModal: () => void
}
uid: number;
name: string;
closeModal: () => void;
};
const DeleteModal = ({ closeModal, uid, name }: Props) => {
const [deleteUser, { isSuccess, isLoading }] = useLazyDeleteUserQuery();
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
// const [input, setInput] = useState("");
const handleDeleteBot = () => {
deleteUser(uid);
};
useEffect(() => {
if (isSuccess) {
toast.success(ct("tip.delete"));
closeModal();
}
}, [isSuccess]);
const [deleteUser, { isSuccess, isLoading }] = useLazyDeleteUserQuery();
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
// const [input, setInput] = useState("");
const handleDeleteBot = () => {
deleteUser(uid);
};
useEffect(() => {
if (isSuccess) {
toast.success(ct("tip.delete"));
closeModal();
}
}, [isSuccess]);
return (
<Modal id="modal-modal">
<StyledModal
title={`${t("delete_title")} ${name}`}
description={t("delete_desc")}
buttons={
<>
<Button className="cancel" onClick={closeModal.bind(null, undefined)}>
{ct("action.cancel")}
</Button>
<Button className='danger' onClick={handleDeleteBot}>{isLoading ? "Deleting" : ct("action.done")}</Button>
</>
}
>
</StyledModal>
</Modal>
);
return (
<Modal id="modal-modal">
<StyledModal
title={`${t("delete_title")} ${name}`}
description={t("delete_desc")}
buttons={
<>
<Button className="cancel" onClick={closeModal.bind(null, undefined)}>
{ct("action.cancel")}
</Button>
<Button className="danger" onClick={handleDeleteBot}>
{isLoading ? "Deleting" : ct("action.done")}
</Button>
</>
}
></StyledModal>
</Modal>
);
};
export default DeleteModal;
export default DeleteModal;
+110 -78
View File
@@ -1,86 +1,118 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef, useState } from "react";
// import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import Modal from '../../../components/Modal';
import Button from '../../../components/styled/Button';
import Textarea from '../../../components/styled/Textarea';
import StyledModal from '../../../components/styled/Modal';
import { useLazyGetBotRelatedChannelsQuery, useSendMessageByBotMutation } from '../../../app/services/server';
import clsx from 'clsx';
import { MessageTypes } from '../../../app/config';
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import { MessageTypes } from "../../../app/config";
import {
useLazyGetBotRelatedChannelsQuery,
useSendMessageByBotMutation
} from "../../../app/services/server";
import Modal from "../../../components/Modal";
import Button from "../../../components/styled/Button";
import StyledModal from "../../../components/styled/Modal";
import Textarea from "../../../components/styled/Textarea";
type Props = {
closeModal: () => void
}
closeModal: () => void;
};
const TestAPIKeyModal = ({ closeModal }: Props) => {
const [currCid, setCurrCid] = useState<number | null>(null);
const [msgType, setMsgType] = useState("text");
const [getChannels, { data }] = useLazyGetBotRelatedChannelsQuery();
const [sendMessage] = useSendMessageByBotMutation();
const inputRef = useRef<HTMLTextAreaElement | undefined>();
const msgInputRef = useRef<HTMLTextAreaElement | undefined>();
const [key, setKey] = useState("");
// const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
const handleSetKey = () => {
const input = inputRef?.current;
if (input && input.value) {
setKey(input.value);
}
};
const handleSetChannel = (cid: number) => {
setCurrCid(cid);
};
const handleSetMsgType = (type: string) => {
setMsgType(type);
};
const handleSend = () => {
const input = msgInputRef?.current;
if (input && input.value && currCid) {
sendMessage({ cid: currCid, api_key: key, type: msgType, content: input.value });
}
};
useEffect(() => {
if (key) {
getChannels({ api_key: key });
}
}, [key]);
const [currCid, setCurrCid] = useState<number | null>(null);
const [msgType, setMsgType] = useState("text");
const [getChannels, { data }] = useLazyGetBotRelatedChannelsQuery();
const [sendMessage] = useSendMessageByBotMutation();
const inputRef = useRef<HTMLTextAreaElement | undefined>();
const msgInputRef = useRef<HTMLTextAreaElement | undefined>();
const [key, setKey] = useState("");
// const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
const handleSetKey = () => {
const input = inputRef?.current;
if (input && input.value) {
setKey(input.value);
}
};
const handleSetChannel = (cid: number) => {
setCurrCid(cid);
};
const handleSetMsgType = (type: string) => {
setMsgType(type);
};
const handleSend = () => {
const input = msgInputRef?.current;
if (input && input.value && currCid) {
sendMessage({ cid: currCid, api_key: key, type: msgType, content: input.value });
}
};
useEffect(() => {
if (key) {
getChannels({ api_key: key });
}
}, [key]);
return (
<Modal id="modal-modal">
<StyledModal
title={key ? "" : `Input API Key`}
buttons={
<>
<Button className="cancel" onClick={closeModal}>
{ct("action.cancel")}
</Button>
<Button onClick={handleSetKey}>{ct("action.done")}</Button>
</>
}
>
{key ? (
data ? (
<ul className="divide-y-2">
{data.map(({ gid, name, is_public }) => {
return (
<li
key={gid}
className={clsx(
"py-1 px-2 text-gray-500 cursor-pointer md:hover:bg-slate-50",
gid == currCid ? "bg-slate-100" : ""
)}
onClick={handleSetChannel.bind(null, gid)}
>
# {name} {!is_public ? "🔒" : ""}
</li>
);
})}
</ul>
) : null
) : (
<Textarea rows={6} ref={inputRef} placeholder="Input API Key First" />
)}
return (
<Modal id="modal-modal">
<StyledModal
title={key ? "" : `Input API Key`}
buttons={
<>
<Button className="cancel" onClick={closeModal}>
{ct("action.cancel")}
</Button>
<Button onClick={handleSetKey} >{ct("action.done")}</Button>
</>
}
>
{key ? (data ? <ul className="divide-y-2">
{data.map(({ gid, name, is_public }) => {
return <li key={gid} className={clsx("py-1 px-2 text-gray-500 cursor-pointer md:hover:bg-slate-50", gid == currCid ? 'bg-slate-100' : "")} onClick={handleSetChannel.bind(null, gid)}>
# {name} {!is_public ? "🔒" : ""}
</li>;
})}
</ul> : null)
: <Textarea rows={6} ref={inputRef} placeholder='Input API Key First' />}
{currCid ? <div className='mt-4 flex flex-col items-start gap-2'>
<Textarea ref={msgInputRef} placeholder='Input Something...' />
<ul className='flex gap-1'>
{Object.entries(MessageTypes).map(([key, value]) => {
return <li onClick={handleSetMsgType.bind(null, key)} className={clsx("py-1 px-2 text-gray-500 cursor-pointer md:hover:bg-slate-50", msgType == key ? 'bg-slate-100' : "")} key={key}>{value}</li>;
})}
</ul>
<Button className='mini' onClick={handleSend} >Send</Button>
</div> : null}
</StyledModal>
</Modal>
);
{currCid ? (
<div className="mt-4 flex flex-col items-start gap-2">
<Textarea ref={msgInputRef} placeholder="Input Something..." />
<ul className="flex gap-1">
{Object.entries(MessageTypes).map(([key, value]) => {
return (
<li
onClick={handleSetMsgType.bind(null, key)}
className={clsx(
"py-1 px-2 text-gray-500 cursor-pointer md:hover:bg-slate-50",
msgType == key ? "bg-slate-100" : ""
)}
key={key}
>
{value}
</li>
);
})}
</ul>
<Button className="mini" onClick={handleSend}>
Send
</Button>
</div>
) : null}
</StyledModal>
</Modal>
);
};
export default TestAPIKeyModal;
export default TestAPIKeyModal;
+118 -91
View File
@@ -1,95 +1,122 @@
import clsx from 'clsx';
import { useState, useEffect, useRef } from 'react';
import { toast } from 'react-hot-toast';
import { useEffect, useRef, useState } from "react";
import { toast } from "react-hot-toast";
import { Orbit } from "@uiball/loaders";
import { useGetUserByAdminQuery, useUpdateUserMutation } from '../../../app/services/user';
import IconEdit from '@/assets/icons/edit.svg';
import IconSave from '@/assets/icons/save.svg';
import IconCancel from '@/assets/icons/close.circle.svg';
import clsx from "clsx";
import IconCancel from "@/assets/icons/close.circle.svg";
import IconEdit from "@/assets/icons/edit.svg";
import IconSave from "@/assets/icons/save.svg";
import { useGetUserByAdminQuery, useUpdateUserMutation } from "../../../app/services/user";
type Props = {
uid: number
}
const WebhookEdit = ({ uid }: Props) => {
const formRef = useRef<HTMLFormElement>(null);
const [editable, setEditable] = useState(false);
const [url, setUrl] = useState("");
const { data, isSuccess, refetch } = useGetUserByAdminQuery(uid);
const [updateUser, { isSuccess: updateSuccess, isLoading: isUpdating }] = useUpdateUserMutation();
useEffect(() => {
if (isSuccess && data) {
setUrl(data.webhook_url || "");
}
}, [data, isSuccess]);
useEffect(() => {
if (updateSuccess) {
refetch();
}
}, [updateSuccess]);
const handleEdit = async () => {
if (editable && formRef) {
const form = formRef.current;
// 检查格式
if (!form?.checkValidity()) {
form?.reportValidity();
return;
}
// 保存编辑
const webhook_url = new FormData(form).get("webhook") as string;
const resp = await updateUser({ id: uid, webhook_url });
// console.log("ressssss", resp);
if ("error" in resp) {
switch (resp.error.status) {
case 406:
toast.error("Not Valid URL!");
break;
default:
break;
}
return;
}
}
setEditable(prev => !prev);
};
const handleEditable = () => {
setEditable(true);
};
const handleCancelEdit = () => {
setEditable(false);
const form = formRef.current;
if (form) {
const input = form.querySelector("input");
input!.value = data?.webhook_url || "";
}
};
return (
<div>
{(url || editable || updateSuccess) ?
<div className="flex gap-2">
<form action="/" ref={formRef} onSubmit={(evt) => {
evt.preventDefault();
handleEdit();
}}>
<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 ?
<IconSave className="stroke-gray-500 !w-5 !h-5" />
: <IconEdit className="fill-gray-500 !w-5 !h-5" />}
</button>
{editable && !isUpdating && <button type='button' disabled={isUpdating} onClick={handleCancelEdit}>
<IconCancel className="!w-5 !h-5 fill-gray-500" />
</button>}
</div>
:
<button type='button' className="rounded-full bg-primary-50 text-green-600 text-xs py-0.5 px-2" onClick={handleEditable}>
Set Webhook
</button>}
</div>
);
uid: number;
};
export default WebhookEdit;
const WebhookEdit = ({ uid }: Props) => {
const formRef = useRef<HTMLFormElement>(null);
const [editable, setEditable] = useState(false);
const [url, setUrl] = useState("");
const { data, isSuccess, refetch } = useGetUserByAdminQuery(uid);
const [updateUser, { isSuccess: updateSuccess, isLoading: isUpdating }] = useUpdateUserMutation();
useEffect(() => {
if (isSuccess && data) {
setUrl(data.webhook_url || "");
}
}, [data, isSuccess]);
useEffect(() => {
if (updateSuccess) {
refetch();
}
}, [updateSuccess]);
const handleEdit = async () => {
if (editable && formRef) {
const form = formRef.current;
// 检查格式
if (!form?.checkValidity()) {
form?.reportValidity();
return;
}
// 保存编辑
const webhook_url = new FormData(form).get("webhook") as string;
const resp = await updateUser({ id: uid, webhook_url });
// console.log("ressssss", resp);
if ("error" in resp) {
switch (resp.error.status) {
case 406:
toast.error("Not Valid URL!");
break;
default:
break;
}
return;
}
}
setEditable((prev) => !prev);
};
const handleEditable = () => {
setEditable(true);
};
const handleCancelEdit = () => {
setEditable(false);
const form = formRef.current;
if (form) {
const input = form.querySelector("input");
input!.value = data?.webhook_url || "";
}
};
return (
<div>
{url || editable || updateSuccess ? (
<div className="flex gap-2">
<form
action="/"
ref={formRef}
onSubmit={(evt) => {
evt.preventDefault();
handleEdit();
}}
>
<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 ? (
<IconSave className="stroke-gray-500 !w-5 !h-5" />
) : (
<IconEdit className="fill-gray-500 !w-5 !h-5" />
)}
</button>
{editable && !isUpdating && (
<button type="button" disabled={isUpdating} onClick={handleCancelEdit}>
<IconCancel className="!w-5 !h-5 fill-gray-500" />
</button>
)}
</div>
) : (
<button
type="button"
className="rounded-full bg-primary-50 text-green-600 text-xs py-0.5 px-2"
onClick={handleEditable}
>
Set Webhook
</button>
)}
</div>
);
};
export default WebhookEdit;
+68 -63
View File
@@ -1,69 +1,74 @@
import { useRef, useState, useEffect, ChangeEvent } from 'react';
import { toast } from 'react-hot-toast';
import { useTranslation } from 'react-i18next';
import { useUpdateUserMutation } from '../../../app/services/user';
import Modal from '../../../components/Modal';
import Button from '../../../components/styled/Button';
import Input from '../../../components/styled/Input';
import StyledModal from '../../../components/styled/Modal';
import { ChangeEvent, useEffect, useRef, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useUpdateUserMutation } from "../../../app/services/user";
import Modal from "../../../components/Modal";
import Button from "../../../components/styled/Button";
import Input from "../../../components/styled/Input";
import StyledModal from "../../../components/styled/Modal";
type Props = {
uid: number,
webhook?: string,
closeModal: () => void
}
uid: number;
webhook?: string;
closeModal: () => void;
};
const WebhookModal = ({ uid, webhook, closeModal }: Props) => {
const [url, setUrl] = useState(webhook);
const [updateUser, { isSuccess, isLoading }] = useUpdateUserMutation();
const formRef = useRef(null);
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
// const [input, setInput] = useState("");
const handleUrlChange = (evt: ChangeEvent<HTMLInputElement>) => {
setUrl(evt.target.value);
};
const handleUpdateWebhook = () => {
if (!formRef || !formRef.current) return;
const formEle = formRef.current as HTMLFormElement;
if (!formEle.checkValidity()) {
formEle.reportValidity();
return;
}
const myFormData = new FormData(formEle);
const webhook_url = myFormData.get("webhook")?.toString() || "";
updateUser({
id: uid,
webhook_url
});
};
useEffect(() => {
if (isSuccess) {
toast.success(ct("tip.update"));
closeModal();
}
}, [isSuccess]);
const [url, setUrl] = useState(webhook);
const [updateUser, { isSuccess, isLoading }] = useUpdateUserMutation();
const formRef = useRef(null);
const { t } = useTranslation("setting", { keyPrefix: "bot" });
const { t: ct } = useTranslation();
// const [input, setInput] = useState("");
const handleUrlChange = (evt: ChangeEvent<HTMLInputElement>) => {
setUrl(evt.target.value);
};
const handleUpdateWebhook = () => {
if (!formRef || !formRef.current) return;
const formEle = formRef.current as HTMLFormElement;
if (!formEle.checkValidity()) {
formEle.reportValidity();
return;
}
const myFormData = new FormData(formEle);
const webhook_url = myFormData.get("webhook")?.toString() || "";
updateUser({
id: uid,
webhook_url
});
};
useEffect(() => {
if (isSuccess) {
toast.success(ct("tip.update"));
closeModal();
}
}, [isSuccess]);
return (
<Modal id="modal-modal">
<StyledModal
title={t("webhook_title")}
description={t("webhook_desc")}
buttons={
<>
<Button className="cancel" onClick={closeModal.bind(null, undefined)}>
{ct("action.cancel")}
</Button>
<Button disabled={!url} onClick={handleUpdateWebhook}>{isLoading ? "Updating" : ct("action.done")}</Button>
</>
}
>
<form ref={formRef} className="w-full flex flex-col gap-2" action="/">
<label htmlFor={"webhook"} className="text-sm text-gray-500">Webhook URL</label>
<Input name={"webhook"} value={url} onChange={handleUrlChange} type="url"></Input>
</form>
</StyledModal>
</Modal>
);
return (
<Modal id="modal-modal">
<StyledModal
title={t("webhook_title")}
description={t("webhook_desc")}
buttons={
<>
<Button className="cancel" onClick={closeModal.bind(null, undefined)}>
{ct("action.cancel")}
</Button>
<Button disabled={!url} onClick={handleUpdateWebhook}>
{isLoading ? "Updating" : ct("action.done")}
</Button>
</>
}
>
<form ref={formRef} className="w-full flex flex-col gap-2" action="/">
<label htmlFor={"webhook"} className="text-sm text-gray-500">
Webhook URL
</label>
<Input name={"webhook"} value={url} onChange={handleUrlChange} type="url"></Input>
</form>
</StyledModal>
</Modal>
);
};
export default WebhookModal;
export default WebhookModal;
+4 -5
View File
@@ -1,5 +1,5 @@
import { useEffect } from "react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useUpdateAvatarByAdminMutation } from "@/app/services/user";
@@ -7,12 +7,11 @@ import { useAppSelector } from "@/app/store";
import AvatarUploader from "@/components/AvatarUploader";
import Button from "@/components/styled/Button";
import IconDelete from "@/assets/icons/delete.svg";
import BotAPIKeys from "./BotAPIKeys";
import CreateModal from "./CreateModal";
import DeleteModal from "./DeleteModal";
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) => {
@@ -1,18 +1,19 @@
import { ChangeEvent, FC, useState } from "react";
import toast from "react-hot-toast";
import * as linkify from 'linkifyjs';
import Modal from "@/components/Modal";
import StyledModal from "@/components/styled/Modal";
import Button from "@/components/styled/Button";
import StyledRadio from "@/components/styled/Radio";
import Input from "@/components/styled/Input";
import { useGetLicensePaymentUrlMutation } from "@/app/services/server";
import { getLicensePriceList } from "@/app/config";
import { useTranslation } from "react-i18next";
import dayjs from "dayjs";
import { PriceSubscriptionDuration, PriceType } from "@/types/common";
import Tippy from "@tippyjs/react";
import dayjs from "dayjs";
import * as linkify from "linkifyjs";
import { getLicensePriceList } from "@/app/config";
import { useGetLicensePaymentUrlMutation } from "@/app/services/server";
import { PriceSubscriptionDuration, PriceType } from "@/types/common";
import Modal from "@/components/Modal";
import Button from "@/components/styled/Button";
import Input from "@/components/styled/Input";
import StyledModal from "@/components/styled/Modal";
import StyledRadio from "@/components/styled/Radio";
// import { LicenseMetadata, RenewLicense } from "@/types/server";
interface Props {
@@ -36,7 +37,7 @@ const getExpireDay = (sub_dur: PriceSubscriptionDuration) => {
break;
default:
break;
};
}
return res.format("YYYY-MM-DD");
};
const LicensePriceList = getLicensePriceList();
@@ -47,14 +48,21 @@ const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
const [host, setHost] = useState(location.hostname);
const [popUpVisible, setPopUpVisible] = useState(false);
const [selectPrice, setSelectPrice] = useState(
`${LicensePriceList[0].pid}|${LicensePriceList[0].limit}|${LicensePriceList[0].type}|${LicensePriceList[0].sub_dur || ""}`
`${LicensePriceList[0].pid}|${LicensePriceList[0].limit}|${LicensePriceList[0].type}|${
LicensePriceList[0].sub_dur || ""
}`
);
const handleRenew = async () => {
if (!linkify.test(host)) {
toast.error("Invalid Host");
return;
}
const [priceId, user_limit, type, sub_dur = "month"] = selectPrice.split("|") as [string, string, PriceType, PriceSubscriptionDuration];
const [priceId, user_limit, type, sub_dur = "month"] = selectPrice.split("|") as [
string,
string,
PriceType,
PriceSubscriptionDuration
];
const metadata = {
user_limit: Number(user_limit),
expire: type == "subscription" ? getExpireDay(sub_dur) : getExpireDay("year"),
@@ -87,7 +95,7 @@ const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
setHost(evt.target.value);
};
const togglePopUpVisible = () => {
setPopUpVisible(prev => !prev);
setPopUpVisible((prev) => !prev);
};
const handleTalk = () => {
window.open("https://calendly.com/hansu", "_blank");
@@ -105,46 +113,65 @@ const LicensePriceListModal: FC<Props> = ({ closeModal }) => {
<Button onClick={closeModal} className="ghost">
{ct("action.cancel")}
</Button>
{isBooking ? <Button onClick={handleTalk}>
Booking a meeting!
</Button> : <Tippy
visible={popUpVisible}
interactive
placement="top-end"
offset={[0, -40]}
trigger="click"
content={
<div className="p-3 rounded-lg border border-solid border-gray-200 dark:border-gray-900 flex flex-col items-start gap-3 w-[380px] bg-white dark:bg-gray-800 shadow shadow-gray-200 dark:shadow-gray-900 drop-shadow-xl">
<div className="text-gray-500 text-sm">
{t("license.tip_domain")}
</div>
<Input value={host} onChange={handleUpdateHost} />
<div className="flex justify-between items-center w-full mt-4">
<span className="text-xs text-orange-500"> {t("license.tip_port")}</span>
<div className="flex gap-3">
<Button className="mini cancel" onClick={togglePopUpVisible}>
{ct("action.cancel")}
</Button>
<Button className="mini" disabled={isLoading || isSuccess} onClick={handleRenew}>
{isLoading ? "Initialize Payment URL" : isSuccess ? "Redirecting" : t("license.tip_confirm")}
</Button>
{isBooking ? (
<Button onClick={handleTalk}>Booking a meeting!</Button>
) : (
<Tippy
visible={popUpVisible}
interactive
placement="top-end"
offset={[0, -40]}
trigger="click"
content={
<div className="p-3 rounded-lg border border-solid border-gray-200 dark:border-gray-900 flex flex-col items-start gap-3 w-[380px] bg-white dark:bg-gray-800 shadow shadow-gray-200 dark:shadow-gray-900 drop-shadow-xl">
<div className="text-gray-500 text-sm">{t("license.tip_domain")}</div>
<Input value={host} onChange={handleUpdateHost} />
<div className="flex justify-between items-center w-full mt-4">
<span className="text-xs text-orange-500"> {t("license.tip_port")}</span>
<div className="flex gap-3">
<Button className="mini cancel" onClick={togglePopUpVisible}>
{ct("action.cancel")}
</Button>
<Button
className="mini"
disabled={isLoading || isSuccess}
onClick={handleRenew}
>
{isLoading
? "Initialize Payment URL"
: isSuccess
? "Redirecting"
: t("license.tip_confirm")}
</Button>
</div>
</div>
</div>
</div>
}
>
<button onClick={togglePopUpVisible} className="text-sm text-white bg-primary-400 break-keep shadow rounded-lg px-3.5 py-2.5 md:hover:bg-primary-500 active:bg-primary-500 disabled:bg-gray-300"> {t("license.renew")}</button>
{/* <Button >
}
>
<button
onClick={togglePopUpVisible}
className="text-sm text-white bg-primary-400 break-keep shadow rounded-lg px-3.5 py-2.5 md:hover:bg-primary-500 active:bg-primary-500 disabled:bg-gray-300"
>
{" "}
{t("license.renew")}
</button>
{/* <Button >
{t("license.renew")}
</Button> */}
</Tippy>}
</Tippy>
)}
</>
}
>
<StyledRadio
options={LicensePriceList.map(({ title, desc, price }) => `${title} ${desc ? `[${desc}]` : ""}${price ? `[${price}]` : ""}`)}
values={LicensePriceList.map(({ pid, limit, type = "payment", sub_dur = "month" }) => `${pid}|${limit}|${type}|${sub_dur}`)}
options={LicensePriceList.map(
({ title, desc, price }) =>
`${title} ${desc ? `[${desc}]` : ""}${price ? `[${price}]` : ""}`
)}
values={LicensePriceList.map(
({ pid, limit, type = "payment", sub_dur = "month" }) =>
`${pid}|${limit}|${type}|${sub_dur}`
)}
value={selectPrice}
onChange={(v) => {
handlePriceSelect(v);
@@ -1,16 +1,17 @@
import { ChangeEvent, FC, useState, useEffect } from "react";
import { ChangeEvent, FC, useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import Modal from "@/components/Modal";
import StyledModal from "@/components/styled/Modal";
import Button from "@/components/styled/Button";
import StyledModal from "@/components/styled/Modal";
import Textarea from "@/components/styled/Textarea";
import { useTranslation } from "react-i18next";
interface Props {
closeModal: () => void;
updateLicense: (param: string) => Promise<any>;
updated: boolean;
updating: boolean
updating: boolean;
// domain: string;
}
@@ -43,13 +44,18 @@ const UpdateLicenseModal: FC<Props> = ({ closeModal, updateLicense, updating, up
<Button onClick={closeModal} className="ghost">
{ct("action.cancel")}
</Button>
<Button disabled={updating || updated || !value} onClick={handleRenew} >
<Button disabled={updating || updated || !value} onClick={handleRenew}>
{updating ? "Updating" : updated ? "Update Successfully" : t("license.update")}
</Button>
</>
}
>
<Textarea rows={18} placeholder={t("license.update_placeholder")} value={value} onChange={handleLicenseUpdate} />
<Textarea
rows={18}
placeholder={t("license.update_placeholder")}
value={value}
onChange={handleLicenseUpdate}
/>
</StyledModal>
</Modal>
);
+74 -34
View File
@@ -1,28 +1,40 @@
import { useState, HTMLAttributes } from "react";
import { HTMLAttributes, useState } from "react";
import { useTranslation } from "react-i18next";
import clsx from "clsx";
import dayjs from "dayjs";
import Button from "@/components/styled/Button";
import useLicense from "@/hooks/useLicense";
import LicensePriceListModal from "./LicensePriceListModal";
import clsx from "clsx";
import { useTranslation } from "react-i18next";
import UpdateLicenseModal from "./UpdateLicenseModal";
// import Tippy from "@tippyjs/react";
interface ItemProps extends HTMLAttributes<HTMLSpanElement> { label: string, data?: string | number | string[], foldable?: boolean }
interface ItemProps extends HTMLAttributes<HTMLSpanElement> {
label: string;
data?: string | number | string[];
foldable?: boolean;
}
const Item = ({ label, data, foldable = false, ...rest }: ItemProps) => {
const infoClass = clsx("font-bold w-full cursor-pointer dark:text-green-500", foldable ? "truncate" : "whitespace-pre-wrap break-all");
const infoClass = clsx(
"font-bold w-full cursor-pointer dark:text-green-500",
foldable ? "truncate" : "whitespace-pre-wrap break-all"
);
if (!data) return null;
return <div className="whitespace-nowrap flex flex-col items-start justify-start text-lg">
<span className="text-sm text-green-500">{label}</span>
{Array.isArray(data) ? <ul className={infoClass}>
{data.map((d) => {
return <li key={d}>{d}</li>;
})}
</ul> : <span className={infoClass} {...rest}>
{data}
</span>}
</div>;
return (
<div className="whitespace-nowrap flex flex-col items-start justify-start text-lg">
<span className="text-sm text-green-500">{label}</span>
{Array.isArray(data) ? (
<ul className={infoClass}>
{data.map((d) => {
return <li key={d}>{d}</li>;
})}
</ul>
) : (
<span className={infoClass} {...rest}>
{data}
</span>
)}
</div>
);
};
export default function License() {
const { t, i18n } = useTranslation("setting");
@@ -38,43 +50,71 @@ export default function License() {
setUpdateVisible((prev) => !prev);
};
const handleLicenseValueToggle = () => {
setBase58Fold(pre => !pre);
setBase58Fold((pre) => !pre);
};
return (
<>
<div className="max-w-3xl flex flex-col justify-start items-start gap-4">
<div className={clsx('relative w-full p-3 rounded border-solid border flex flex-col gap-4 shadow', reachLimit ? "border-red-600 bg-red-200/50" : "border-green-600 bg-green-100 dark:bg-green-900")}>
<div
className={clsx(
"relative w-full p-3 rounded border-solid border flex flex-col gap-4 shadow",
reachLimit
? "border-red-600 bg-red-200/50"
: "border-green-600 bg-green-100 dark:bg-green-900"
)}
>
<Item label={t("license.signed")} data={licenseInfo?.sign ? "Yes" : "Not Yet"} />
<Item label={t("license.domain")} data={licenseInfo?.domains} />
<Item label={t("license.user_limit")} data={(licenseInfo?.user_limit ?? 0) >= 999999 ? "No Limit" : licenseInfo?.user_limit} />
<Item label={t("license.expire")} data={dayjs(licenseInfo?.expired_at).format("YYYY-MM-DD h:mm:ss A")} />
<Item label={t("license.create")} data={dayjs(licenseInfo?.created_at).format("YYYY-MM-DD h:mm:ss A")} />
<Item label={t("license.value")} data={licenseInfo?.base58} foldable={base58Fold} title={base58Fold ? "Click to see full text" : "Click to fold text"}
onClick={handleLicenseValueToggle} />
<Item
label={t("license.user_limit")}
data={(licenseInfo?.user_limit ?? 0) >= 999999 ? "No Limit" : licenseInfo?.user_limit}
/>
<Item
label={t("license.expire")}
data={dayjs(licenseInfo?.expired_at).format("YYYY-MM-DD h:mm:ss A")}
/>
<Item
label={t("license.create")}
data={dayjs(licenseInfo?.created_at).format("YYYY-MM-DD h:mm:ss A")}
/>
<Item
label={t("license.value")}
data={licenseInfo?.base58}
foldable={base58Fold}
title={base58Fold ? "Click to see full text" : "Click to fold text"}
onClick={handleLicenseValueToggle}
/>
</div>
<div className="flex gap-2">
<Button onClick={toggleModalVisible}>{t("license.renew")}</Button>
<Button onClick={toggleUpdateModalVisible} className="ghost">{t("license.update")}</Button>
<Button onClick={toggleUpdateModalVisible} className="ghost">
{t("license.update")}
</Button>
</div>
<div className="flex flex-col gap-4 bg-primary-500 text-white rounded drop-shadow-xl p-5">
<h2 className="text-2xl font-bold">{t("license.tip.title")} 🎁</h2>
<p className="flex flex-col">
<span>
{t("license.tip.user_test")}
</span>
<span>{t("license.tip.user_test")}</span>
<span>
{t("license.tip.contact")}
{i18n.language.startsWith("zh") ? "Privoce" : <a className="underline text-lg text-green-200" href="https://calendly.com/hansu/han-meeting" target="_blank" rel="noopener noreferrer">https://calendly.com/hansu/han-meeting</a>}
{i18n.language.startsWith("zh") ? (
"Privoce"
) : (
<a
className="underline text-lg text-green-200"
href="https://calendly.com/hansu/han-meeting"
target="_blank"
rel="noopener noreferrer"
>
https://calendly.com/hansu/han-meeting
</a>
)}
</span>
</p>
</div>
</div>
{modalVisible && (
<LicensePriceListModal
closeModal={toggleModalVisible}
/>
)}
{modalVisible && <LicensePriceListModal closeModal={toggleModalVisible} />}
{updateVisible && (
<UpdateLicenseModal
updated={upserted}
+14 -7
View File
@@ -1,12 +1,12 @@
import { ChangeEvent, FC, useEffect, useState } from "react";
import toast from "react-hot-toast";
import Modal from "@/components/Modal";
import StyledModal from "@/components/styled/Modal";
import Button from "@/components/styled/Button";
import Checkbox from "@/components/styled/Checkbox";
import useLogout from "@/hooks/useLogout";
import { useTranslation } from "react-i18next";
import Modal from "@/components/Modal";
import Button from "@/components/styled/Button";
import Checkbox from "@/components/styled/Checkbox";
import StyledModal from "@/components/styled/Modal";
import useLogout from "@/hooks/useLogout";
interface Props {
closeModal: () => void;
@@ -41,7 +41,9 @@ const LogoutConfirmModal: FC<Props> = ({ closeModal }) => {
description={t("logout.desc")}
buttons={
<>
<Button className="cancel" onClick={closeModal}>{ct("action.cancel")}</Button>
<Button className="cancel" onClick={closeModal}>
{ct("action.cancel")}
</Button>
<Button onClick={handleLogout} className="danger">
{exiting ? "Logging out" : ct("action.logout")}
</Button>
@@ -52,7 +54,12 @@ const LogoutConfirmModal: FC<Props> = ({ closeModal }) => {
<label htmlFor="clear_cb" className="cursor-pointer text-orange-500 mr-3">
{t("logout.clear_local")}
</label>
<Checkbox className=" cursor-pointer" name="clear_cb" checked={clearLocal} onChange={handleCheck} />
<Checkbox
className=" cursor-pointer"
name="clear_cb"
checked={clearLocal}
onChange={handleCheck}
/>
</div>
</StyledModal>
</Modal>
+5 -6
View File
@@ -1,13 +1,14 @@
import { useEffect, useState, MouseEvent } from "react";
import { MouseEvent, useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useUpdateAvatarMutation } from "@/app/services/user";
import { useAppSelector } from "@/app/store";
import AvatarUploader from "@/components/AvatarUploader";
import Button from "@/components/styled/Button";
import ProfileBasicEditModal from "./ProfileBasicEditModal";
import RemoveAccountConfirmModal from "./RemoveAccountConfirmModal";
import UpdatePasswordModal from "./UpdatePasswordModal";
import { useAppSelector } from "@/app/store";
import { useTranslation } from "react-i18next";
type EditField = "name" | "email" | "";
export default function MyAccount() {
@@ -91,9 +92,7 @@ export default function MyAccount() {
<span className="text-xs uppercase font-semibold">{t("password")}</span>
<span className="text-sm">*********</span>
</div>
<Button onClick={togglePasswordModal}>
{ct("action.edit")}
</Button>
<Button onClick={togglePasswordModal}>{ct("action.edit")}</Button>
</div>
</div>
{/* uid 1 是初始账户,不能删 */}
+38 -33
View File
@@ -1,40 +1,45 @@
// import React from 'react'
import { useTranslation } from 'react-i18next';
import { useGetSystemCommonQuery, useUpdateSystemCommonMutation } from '../../../app/services/server';
import { useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { useEffect } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import StyledRadio from "@/components/styled/Radio";
import { useAppSelector } from '../../../app/store';
import SettingBlock from './SettingBlock';
import { ChatLayout } from '../../../types/server';
import {
useGetSystemCommonQuery,
useUpdateSystemCommonMutation
} from "../../../app/services/server";
import { useAppSelector } from "../../../app/store";
import { ChatLayout } from "../../../types/server";
import SettingBlock from "./SettingBlock";
// type Props = {}
const Index = () => {
const currStatus = useAppSelector(store => store.server.chat_layout_mode ?? "Left");
const { t } = useTranslation("setting", { keyPrefix: "overview.chat_layout" });
const { t: ct } = useTranslation();
const { refetch } = useGetSystemCommonQuery();
const [updateSetting, { isSuccess }] = useUpdateSystemCommonMutation();
useEffect(() => {
if (isSuccess) {
refetch();
toast.success(ct("tip.update"));
}
}, [isSuccess]);
const handleChange = (newVal: ChatLayout) => {
updateSetting({ chat_layout_mode: newVal });
};
// if (!loadSuccess) return null;
return (
<SettingBlock title={t("title")} desc={t('desc')} >
<StyledRadio
options={[t("left"), t("self_right")]}
values={["Left", "SelfRight"]}
value={currStatus}
onChange={handleChange}
/>
</SettingBlock>
);
const currStatus = useAppSelector((store) => store.server.chat_layout_mode ?? "Left");
const { t } = useTranslation("setting", { keyPrefix: "overview.chat_layout" });
const { t: ct } = useTranslation();
const { refetch } = useGetSystemCommonQuery();
const [updateSetting, { isSuccess }] = useUpdateSystemCommonMutation();
useEffect(() => {
if (isSuccess) {
refetch();
toast.success(ct("tip.update"));
}
}, [isSuccess]);
const handleChange = (newVal: ChatLayout) => {
updateSetting({ chat_layout_mode: newVal });
};
// if (!loadSuccess) return null;
return (
<SettingBlock title={t("title")} desc={t("desc")}>
<StyledRadio
options={[t("left"), t("self_right")]}
values={["Left", "SelfRight"]}
value={currStatus}
onChange={handleChange}
/>
</SettingBlock>
);
};
export default Index;
export default Index;
@@ -1,39 +1,44 @@
// import React from 'react'
import { useTranslation } from 'react-i18next';
import { useGetSystemCommonQuery, useUpdateSystemCommonMutation } from '../../../app/services/server';
import { useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { useEffect } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import StyledRadio from "@/components/styled/Radio";
import { useAppSelector } from '../../../app/store';
import SettingBlock from './SettingBlock';
import {
useGetSystemCommonQuery,
useUpdateSystemCommonMutation
} from "../../../app/services/server";
import { useAppSelector } from "../../../app/store";
import SettingBlock from "./SettingBlock";
// type Props = {}
const Index = () => {
const currStatus = useAppSelector(store => !!store.server.contact_verification_enable);
const { t } = useTranslation("setting", { keyPrefix: "overview.contact_verify" });
const { t: ct } = useTranslation();
const { refetch } = useGetSystemCommonQuery();
const [updateSetting, { isSuccess }] = useUpdateSystemCommonMutation();
useEffect(() => {
if (isSuccess) {
refetch();
toast.success(ct("tip.update"));
}
}, [isSuccess]);
const handleToggle = () => {
updateSetting({ contact_verification_enable: !currStatus });
};
// if (!loadSuccess) return null;
return (
<SettingBlock title={t("title")} desc={t('desc')} >
<StyledRadio
options={[t("enable"), t("disable")]}
values={["true", "false"]}
value={`${currStatus}`}
onChange={handleToggle}
/>
</SettingBlock>
);
const currStatus = useAppSelector((store) => !!store.server.contact_verification_enable);
const { t } = useTranslation("setting", { keyPrefix: "overview.contact_verify" });
const { t: ct } = useTranslation();
const { refetch } = useGetSystemCommonQuery();
const [updateSetting, { isSuccess }] = useUpdateSystemCommonMutation();
useEffect(() => {
if (isSuccess) {
refetch();
toast.success(ct("tip.update"));
}
}, [isSuccess]);
const handleToggle = () => {
updateSetting({ contact_verification_enable: !currStatus });
};
// if (!loadSuccess) return null;
return (
<SettingBlock title={t("title")} desc={t("desc")}>
<StyledRadio
options={[t("enable"), t("disable")]}
values={["true", "false"]}
value={`${currStatus}`}
onChange={handleToggle}
/>
</SettingBlock>
);
};
export default Index;
export default Index;
+34 -33
View File
@@ -1,39 +1,40 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import Radio from '../../../components/styled/Radio';
import { Theme } from '../../../types/common';
import SettingBlock from './SettingBlock';
import { useState } from "react";
import { useTranslation } from "react-i18next";
import Radio from "../../../components/styled/Radio";
import { Theme } from "../../../types/common";
import SettingBlock from "./SettingBlock";
// type Props = {}
const DarkMode = () => {
const [theme, setTheme] = useState<Theme>(localStorage.theme || "auto");
const { t } = useTranslation("setting");
const handleThemeToggle = (v: Theme) => {
setTheme(v);
localStorage.theme = v;
// reset
document.documentElement.classList.remove("dark");
document.documentElement.classList.remove("light");
if (v !== "auto") {
document.documentElement.classList.add(v);
} else {
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.documentElement.classList.add(isDark ? "dark" : 'light');
}
};
return (
<SettingBlock title={t("overview.theme.title")} desc={t("overview.theme.desc")}>
<Radio
options={[t("overview.theme.auto"), t("overview.theme.dark"), t("overview.theme.light")]}
values={['auto', 'dark', 'light']}
value={theme}
onChange={(v) => {
handleThemeToggle(v);
}}
/>
</SettingBlock>
);
const [theme, setTheme] = useState<Theme>(localStorage.theme || "auto");
const { t } = useTranslation("setting");
const handleThemeToggle = (v: Theme) => {
setTheme(v);
localStorage.theme = v;
// reset
document.documentElement.classList.remove("dark");
document.documentElement.classList.remove("light");
if (v !== "auto") {
document.documentElement.classList.add(v);
} else {
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
document.documentElement.classList.add(isDark ? "dark" : "light");
}
};
return (
<SettingBlock title={t("overview.theme.title")} desc={t("overview.theme.desc")}>
<Radio
options={[t("overview.theme.auto"), t("overview.theme.dark"), t("overview.theme.light")]}
values={["auto", "dark", "light"]}
value={theme}
onChange={(v) => {
handleThemeToggle(v);
}}
/>
</SettingBlock>
);
};
export default DarkMode;
export default DarkMode;
+43 -38
View File
@@ -1,45 +1,50 @@
// import React from 'react'
import { useTranslation } from 'react-i18next';
import { toast } from 'react-hot-toast';
import { useGetFrontendUrlQuery, useUpdateFrontendUrlMutation } from '../../../app/services/server';
import StyledInput from "@/components/styled/Input";
import { ChangeEvent, useEffect, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import StyledButton from "@/components/styled/Button";
import { ChangeEvent, useState, useEffect } from 'react';
import SettingBlock from './SettingBlock';
import StyledInput from "@/components/styled/Input";
import { useGetFrontendUrlQuery, useUpdateFrontendUrlMutation } from "../../../app/services/server";
import SettingBlock from "./SettingBlock";
// type Props = {}
const Index = () => {
const { data, isSuccess: getUrlSuccess } = useGetFrontendUrlQuery();
const [url, setUrl] = useState(location.origin);
const { t } = useTranslation("setting");
const { t: ct } = useTranslation();
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
// update
setUrl(evt.target.value);
};
const handleUpdate = () => {
updateUrl(url);
};
const [updateUrl, { isLoading, isSuccess }] = useUpdateFrontendUrlMutation();
useEffect(() => {
if (isSuccess) {
toast.success(ct("tip.update"));
}
}, [isSuccess]);
useEffect(() => {
if (getUrlSuccess && data) {
setUrl(data);
}
}, [getUrlSuccess, data]);
// if(!fetch)
return (
<SettingBlock title={t("overview.url.title")} desc={t("overview.url.desc")}>
<div className="flex items-center gap-4 mt-2">
<StyledInput placeholder='frontend url' value={url} onChange={handleChange} />
<StyledButton disabled={!url || isLoading} className='small' onClick={handleUpdate}> {ct("action.update")}</StyledButton>
</div>
</SettingBlock>
);
const { data, isSuccess: getUrlSuccess } = useGetFrontendUrlQuery();
const [url, setUrl] = useState(location.origin);
const { t } = useTranslation("setting");
const { t: ct } = useTranslation();
const handleChange = (evt: ChangeEvent<HTMLInputElement>) => {
// update
setUrl(evt.target.value);
};
const handleUpdate = () => {
updateUrl(url);
};
const [updateUrl, { isLoading, isSuccess }] = useUpdateFrontendUrlMutation();
useEffect(() => {
if (isSuccess) {
toast.success(ct("tip.update"));
}
}, [isSuccess]);
useEffect(() => {
if (getUrlSuccess && data) {
setUrl(data);
}
}, [getUrlSuccess, data]);
// if(!fetch)
return (
<SettingBlock title={t("overview.url.title")} desc={t("overview.url.desc")}>
<div className="flex items-center gap-4 mt-2">
<StyledInput placeholder="frontend url" value={url} onChange={handleChange} />
<StyledButton disabled={!url || isLoading} className="small" onClick={handleUpdate}>
{" "}
{ct("action.update")}
</StyledButton>
</div>
</SettingBlock>
);
};
export default Index;
export default Index;
+26 -20
View File
@@ -1,29 +1,35 @@
// import React from 'react'
import { useTranslation } from 'react-i18next';
import { useTranslation } from "react-i18next";
import StyledRadio from "@/components/styled/Radio";
import SettingBlock from './SettingBlock';
import SettingBlock from "./SettingBlock";
// type Props = {}
const Index = () => {
const { t, i18n } = useTranslation("setting");
const handleGuestToggle = (v: "zh" | "en") => {
i18n.changeLanguage(v);
};
return (
<SettingBlock title={t("overview.lang.title")} desc={t("overview.lang.desc")}>
<StyledRadio
options={[t("overview.lang.en"), t("overview.lang.zh"), t("overview.lang.jp"), t("overview.lang.tr")]}
values={["en", "zh", "jp", "tr"]}
value={i18n.language.split("-")[0]}
onChange={(v) => {
console.log("wtff", v);
const { t, i18n } = useTranslation("setting");
const handleGuestToggle = (v: "zh" | "en") => {
i18n.changeLanguage(v);
};
return (
<SettingBlock title={t("overview.lang.title")} desc={t("overview.lang.desc")}>
<StyledRadio
options={[
t("overview.lang.en"),
t("overview.lang.zh"),
t("overview.lang.jp"),
t("overview.lang.tr")
]}
values={["en", "zh", "jp", "tr"]}
value={i18n.language.split("-")[0]}
onChange={(v) => {
console.log("wtff", v);
handleGuestToggle(v);
}}
/>
</SettingBlock>
);
handleGuestToggle(v);
}}
/>
</SettingBlock>
);
};
export default Index;
export default Index;
+37 -32
View File
@@ -1,39 +1,44 @@
// import React from 'react'
import { useTranslation } from 'react-i18next';
import { useGetSystemCommonQuery, useUpdateSystemCommonMutation } from '../../../app/services/server';
import { useEffect } from 'react';
import { toast } from 'react-hot-toast';
import { useEffect } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import StyledRadio from "@/components/styled/Radio";
import { useAppSelector } from '../../../app/store';
import SettingBlock from './SettingBlock';
import {
useGetSystemCommonQuery,
useUpdateSystemCommonMutation
} from "../../../app/services/server";
import { useAppSelector } from "../../../app/store";
import SettingBlock from "./SettingBlock";
// type Props = {}
const Index = () => {
const currStatus = useAppSelector(store => !!store.server.show_user_online_status);
const { t } = useTranslation("setting", { keyPrefix: "overview.online_status" });
const { t: ct } = useTranslation();
const { refetch } = useGetSystemCommonQuery();
const [updateSetting, { isSuccess }] = useUpdateSystemCommonMutation();
useEffect(() => {
if (isSuccess) {
refetch();
toast.success(ct("tip.update"));
}
}, [isSuccess]);
const handleToggle = () => {
updateSetting({ show_user_online_status: !currStatus });
};
// if (!loadSuccess) return null;
return (
<SettingBlock title={t("title")} desc={t('desc')} >
<StyledRadio
options={[t("enable"), t("disable")]}
values={["true", "false"]}
value={`${currStatus}`}
onChange={handleToggle}
/>
</SettingBlock>
);
const currStatus = useAppSelector((store) => !!store.server.show_user_online_status);
const { t } = useTranslation("setting", { keyPrefix: "overview.online_status" });
const { t: ct } = useTranslation();
const { refetch } = useGetSystemCommonQuery();
const [updateSetting, { isSuccess }] = useUpdateSystemCommonMutation();
useEffect(() => {
if (isSuccess) {
refetch();
toast.success(ct("tip.update"));
}
}, [isSuccess]);
const handleToggle = () => {
updateSetting({ show_user_online_status: !currStatus });
};
// if (!loadSuccess) return null;
return (
<SettingBlock title={t("title")} desc={t("desc")}>
<StyledRadio
options={[t("enable"), t("disable")]}
values={["true", "false"]}
value={`${currStatus}`}
onChange={handleToggle}
/>
</SettingBlock>
);
};
export default Index;
export default Index;
+4 -3
View File
@@ -1,13 +1,14 @@
import { ChangeEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useUpdateLogoMutation, useUpdateServerMutation } from "@/app/services/server";
import { useAppSelector } from "@/app/store";
import LogoUploader from "@/components/AvatarUploader";
import SaveTip from "@/components/SaveTip";
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";
const Index = () => {
const { t } = useTranslation("setting");
+15 -17
View File
@@ -1,21 +1,19 @@
import React from 'react';
import React from "react";
type Props = {
title: string,
desc: string,
children: React.ReactNode
}
const SettingBlock = ({ title, desc, children }: Props) => {
return (
<div className="text-sm">
<p className="text-gray-600 dark:text-gray-100 font-semibold">{title}</p>
<p className="flex justify-between w-full text-gray-400 mb-2 text-xs">
{desc}
</p>
{children}
</div>
);
title: string;
desc: string;
children: React.ReactNode;
};
export default SettingBlock;
const SettingBlock = ({ title, desc, children }: Props) => {
return (
<div className="text-sm">
<p className="text-gray-600 dark:text-gray-100 font-semibold">{title}</p>
<p className="flex justify-between w-full text-gray-400 mb-2 text-xs">{desc}</p>
{children}
</div>
);
};
export default SettingBlock;
+9 -10
View File
@@ -1,18 +1,19 @@
// import { useState, useEffect, ChangeEvent } from "react";
import { useTranslation } from "react-i18next";
import StyledRadio from "@/components/styled/Radio";
import { useAppSelector } from "@/app/store";
import { LoginConfig, WhoCanSignUp } from "@/types/server";
import useConfig from "@/hooks/useConfig";
import Server from './Server';
import Language from './Language';
import FrontendURL from "./FrontendURL";
import DarkMode from "./DarkMode";
import ServerVersionChecker from "@/components/ServerVersionChecker";
import OnlineStatus from "./OnlineStatus";
import StyledRadio from "@/components/styled/Radio";
import useConfig from "@/hooks/useConfig";
import ChatLayout from "./ChatLayout";
import SettingBlock from "./SettingBlock";
import ContactVerification from "./ContactVerification";
import DarkMode from "./DarkMode";
import FrontendURL from "./FrontendURL";
import Language from "./Language";
import OnlineStatus from "./OnlineStatus";
import Server from "./Server";
import SettingBlock from "./SettingBlock";
export default function Overview() {
const { t } = useTranslation("setting");
@@ -78,8 +79,6 @@ export default function Overview() {
<ServerVersionChecker version="0.3.7" empty={true}>
<ContactVerification />
</ServerVersionChecker>
</>
)}
+26 -20
View File
@@ -1,29 +1,35 @@
// import React from 'react'
import { useTranslation } from 'react-i18next';
import { useTranslation } from "react-i18next";
import StyledRadio from "@/components/styled/Radio";
import SettingBlock from './SettingBlock';
import SettingBlock from "./SettingBlock";
// type Props = {}
const Index = () => {
const { t, i18n } = useTranslation("setting");
const handleGuestToggle = (v: "zh" | "en") => {
i18n.changeLanguage(v);
};
return (
<SettingBlock title={t("overview.lang.title")} desc={t("overview.lang.desc")}>
<StyledRadio
options={[t("overview.lang.en"), t("overview.lang.zh"), t("overview.lang.jp"), t("overview.lang.tr")]}
values={["en", "zh", "jp", "tr"]}
value={i18n.language.split("-")[0]}
onChange={(v) => {
console.log("wtff", v);
const { t, i18n } = useTranslation("setting");
const handleGuestToggle = (v: "zh" | "en") => {
i18n.changeLanguage(v);
};
return (
<SettingBlock title={t("overview.lang.title")} desc={t("overview.lang.desc")}>
<StyledRadio
options={[
t("overview.lang.en"),
t("overview.lang.zh"),
t("overview.lang.jp"),
t("overview.lang.tr")
]}
values={["en", "zh", "jp", "tr"]}
value={i18n.language.split("-")[0]}
onChange={(v) => {
console.log("wtff", v);
handleGuestToggle(v);
}}
/>
</SettingBlock>
);
handleGuestToggle(v);
}}
/>
</SettingBlock>
);
};
export default Index;
export default Index;
+4 -3
View File
@@ -1,13 +1,14 @@
import { ChangeEvent, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useUpdateLogoMutation, useUpdateServerMutation } from "@/app/services/server";
import { useAppSelector } from "@/app/store";
import LogoUploader from "@/components/AvatarUploader";
import SaveTip from "@/components/SaveTip";
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";
const Index = () => {
const { t } = useTranslation("setting");
+9 -8
View File
@@ -1,12 +1,12 @@
import { ChangeEvent, FC, useEffect, useState, useRef } from "react";
import { ChangeEvent, FC, useEffect, useRef, useState } from "react";
import toast from "react-hot-toast";
import Input from "@/components/styled/Input";
import { useUpdateInfoMutation } from "@/app/services/user";
import StyledModal from "@/components/styled/Modal";
import Button from "@/components/styled/Button";
import Modal from "@/components/Modal";
import { useTranslation } from "react-i18next";
import { useUpdateInfoMutation } from "@/app/services/user";
import Modal from "@/components/Modal";
import Button from "@/components/styled/Button";
import Input from "@/components/styled/Input";
import StyledModal from "@/components/styled/Modal";
interface Props {
label?: string;
@@ -40,7 +40,6 @@ const ProfileBasicEditModal: FC<Props> = ({
if (!formEle.checkValidity()) {
formEle.reportValidity();
return;
}
update({ [valueKey]: input });
};
@@ -66,7 +65,9 @@ const ProfileBasicEditModal: FC<Props> = ({
}
>
<form ref={formRef} className="flex flex-col gap-2 w-full" action="/">
<label htmlFor={valueKey} className="text-sm text-left text-gray-500">{label}</label>
<label htmlFor={valueKey} className="text-sm text-left text-gray-500">
{label}
</label>
<Input name={valueKey} value={input} onChange={handleChange} type={type} required></Input>
</form>
</StyledModal>
@@ -1,11 +1,12 @@
import { FC } from "react";
import toast from "react-hot-toast";
import Modal from "@/components/Modal";
import StyledModal from "@/components/styled/Modal";
import Button from "@/components/styled/Button";
import { useLazyDeleteCurrentAccountQuery } from "@/app/services/auth";
import { useTranslation } from "react-i18next";
import { useLazyDeleteCurrentAccountQuery } from "@/app/services/auth";
import Modal from "@/components/Modal";
import Button from "@/components/styled/Button";
import StyledModal from "@/components/styled/Modal";
interface Props {
closeModal: () => void;
}
+15 -9
View File
@@ -1,12 +1,12 @@
import { ChangeEvent, useEffect, useState, FC } from "react";
import { ChangeEvent, FC, useEffect, useState } from "react";
import toast from "react-hot-toast";
import Input from "@/components/styled/Input";
import { useUpdatePasswordMutation, useGetCredentialsQuery } from "@/app/services/auth";
import StyledModal from "@/components/styled/Modal";
import Button from "@/components/styled/Button";
import Modal from "@/components/Modal";
import { useTranslation } from "react-i18next";
import { useGetCredentialsQuery, useUpdatePasswordMutation } from "@/app/services/auth";
import Modal from "@/components/Modal";
import Button from "@/components/styled/Button";
import Input from "@/components/styled/Input";
import StyledModal from "@/components/styled/Modal";
interface Props {
closeModal: () => void;
@@ -80,7 +80,9 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
>
{data?.password && (
<div className={inputClass}>
<label className={labelClass} htmlFor={"current"}>{t("current_pwd")}</label>
<label className={labelClass} htmlFor={"current"}>
{t("current_pwd")}
</label>
<Input
type="password"
id="current"
@@ -92,7 +94,9 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
</div>
)}
<div className={inputClass}>
<label className={labelClass} htmlFor={"newPassword"}>{t("new_pwd")}</label>
<label className={labelClass} htmlFor={"newPassword"}>
{t("new_pwd")}
</label>
<Input
type="password"
name={"newPassword"}
@@ -102,7 +106,9 @@ const ProfileBasicEditModal: FC<Props> = ({ closeModal }) => {
></Input>
</div>
<div className={inputClass}>
<label className={labelClass} htmlFor={"confirmPassword"}>{t("confirm_new_pwd")}</label>
<label className={labelClass} htmlFor={"confirmPassword"}>
{t("confirm_new_pwd")}
</label>
<Input
onBlur={handleCompare}
type="password"
+2 -1
View File
@@ -3,11 +3,12 @@
import { useTranslation } from "react-i18next";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { vscDarkPlus } from "react-syntax-highlighter/dist/esm/styles/prism";
import IconQuestion from "@/assets/icons/question.svg";
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,
+2 -2
View File
@@ -1,12 +1,12 @@
import { ChangeEvent } from "react";
import { useTranslation } from "react-i18next";
import { AgoraConfig } from "@/types/server";
import SaveTip from "@/components/SaveTip";
import Input from "@/components/styled/Input";
import Label from "@/components/styled/Label";
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() {
+7 -7
View File
@@ -1,13 +1,13 @@
import { ChangeEvent } from "react";
import Input from "@/components/styled/Input";
import Textarea from "@/components/styled/Textarea";
import Label from "@/components/styled/Label";
import SaveTip from "@/components/SaveTip";
import useConfig from "@/hooks/useConfig";
import StyledRadio from "@/components/styled/Radio";
import { useTranslation } from "react-i18next";
import { FirebaseConfig } from "@/types/server";
import { useTranslation } from "react-i18next";
import SaveTip from "@/components/SaveTip";
import Input from "@/components/styled/Input";
import Label from "@/components/styled/Label";
import StyledRadio from "@/components/styled/Radio";
import Textarea from "@/components/styled/Textarea";
import useConfig from "@/hooks/useConfig";
interface IOptions {
disable: string;
+14 -20
View File
@@ -1,25 +1,19 @@
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>
);
link: string;
text: string;
};
export default HowToTip;
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;
@@ -1,11 +1,12 @@
import { ChangeEvent, FC, useState } from "react";
import { useTranslation } from "react-i18next";
import Select, { Option } from "@/components/styled/Select";
import Button from "@/components/styled/Button";
import Input from "@/components/styled/Input";
import Select, { Option } from "@/components/styled/Select";
import Toggle from "@/components/styled/Toggle";
import options from "./items.json";
import IconMinus from "@/assets/icons/minus.circle.svg";
import options from "./items.json";
interface Issuer {
domain: string;
+11 -10
View File
@@ -1,17 +1,18 @@
import { ChangeEvent } from "react";
import toast from "react-hot-toast";
import Toggle from "@/components/styled/Toggle";
import Label from "@/components/styled/Label";
import Input from "@/components/styled/Input";
import SaveTip from "@/components/SaveTip";
import useConfig from "@/hooks/useConfig";
import Tooltip from "./Tooltip";
import IssuerList from "./IssuerList";
import useGoogleAuthConfig from "@/hooks/useGoogleAuthConfig";
import useGithubAuthConfig from "@/hooks/useGithubAuthConfig";
import { LoginConfig } from "@/types/server";
import { useTranslation } from "react-i18next";
import { LoginConfig } from "@/types/server";
import SaveTip from "@/components/SaveTip";
import Input from "@/components/styled/Input";
import Label from "@/components/styled/Label";
import Toggle from "@/components/styled/Toggle";
import useConfig from "@/hooks/useConfig";
import useGithubAuthConfig from "@/hooks/useGithubAuthConfig";
import useGoogleAuthConfig from "@/hooks/useGoogleAuthConfig";
import IssuerList from "./IssuerList";
import Tooltip from "./Tooltip";
export default function Logins() {
const { t } = useTranslation("setting", { keyPrefix: "login" });
const { t: ct } = useTranslation();
+7 -7
View File
@@ -1,15 +1,15 @@
import { useState, useEffect, ChangeEvent } from "react";
import { ChangeEvent, useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { useSendTestEmailMutation } from "@/app/services/server";
import useConfig from "@/hooks/useConfig";
import Input from "@/components/styled/Input";
import Button from "@/components/styled/Button";
import Toggle from "@/components/styled/Toggle";
import Label from "@/components/styled/Label";
import SaveTip from "@/components/SaveTip";
import { SMTPConfig } from "@/types/server";
import SaveTip from "@/components/SaveTip";
import Button from "@/components/styled/Button";
import Input from "@/components/styled/Input";
import Label from "@/components/styled/Label";
import Toggle from "@/components/styled/Toggle";
import useConfig from "@/hooks/useConfig";
import HowToTip from "./HowToTip";
export default function ConfigSMTP() {
+10 -8
View File
@@ -1,16 +1,18 @@
import Tippy from "@tippyjs/react";
import { roundArrow } from "tippy.js";
import "tippy.js/dist/svg-arrow.css";
import IconQuestion from "@/assets/icons/question.svg";
// import { useTranslation } from "react-i18next";
import { PropsWithChildren } from "react";
import { Trans } from 'react-i18next';
import { Trans } from "react-i18next";
import IconQuestion from "@/assets/icons/question.svg";
const Link = ({ to, children }: PropsWithChildren<{ to: string }>) => {
return <a href={to} className="text-primary-500" target="_blank" rel="noreferrer" >
{children}
</a>;
return (
<a href={to} className="text-primary-500" target="_blank" rel="noreferrer">
{children}
</a>
);
};
export default function Tooltip({ link = "#" }) {
return (
@@ -21,7 +23,7 @@ export default function Tooltip({ link = "#" }) {
placement="bottom"
content={
<div className="py-2 px-3 bg-gray-800 text-xs text-white rounded-lg">
<Trans ns="setting" i18nKey={"login.more_details"} >
<Trans ns="setting" i18nKey={"login.more_details"}>
<Link to={link} />
</Trans>
</div>
+2 -2
View File
@@ -3,8 +3,8 @@ import { useTranslation } from "react-i18next";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import StyledSettingContainer from "@/components/StyledSettingContainer";
import useNavs from "./navs";
import LogoutConfirmModal from "./LogoutConfirmModal";
import useNavs from "./navs";
let pageFrom: string = "";
@@ -13,7 +13,7 @@ export default function Setting() {
const [searchParams] = useSearchParams();
const navs = useNavs();
const flattenNaves = navs.map(({ items }) => items).flat();
const { nav: navKey } = useParams();;
const { nav: navKey } = useParams();
const [logoutConfirm, setLogoutConfirm] = useState(false);
const navigateTo = useNavigate();
pageFrom = pageFrom ? pageFrom : searchParams.get("f") || "/";
+27 -22
View File
@@ -1,19 +1,20 @@
import { useTranslation } from "react-i18next";
import { useAppSelector } from "@/app/store";
import ManageMembers from "@/components/ManageMembers";
import ServerVersionChecker from "@/components/ServerVersionChecker";
import Version from "@/components/Version";
import APIConfig from "./APIConfig";
import APIDocument from "./APIDocument";
import BotConfig from "./BotConfig";
import ConfigAgora from "./config/Agora";
import ConfigFirebase from "./config/Firebase";
import Logins from "./config/Logins";
import ConfigSMTP from "./config/SMTP";
import License from "./License";
import MyAccount from "./MyAccount";
import Overview from "./Overview";
import Logins from "./config/Logins";
import ConfigFirebase from "./config/Firebase";
import ConfigSMTP from "./config/SMTP";
import APIConfig from "./APIConfig";
import License from "./License";
import Widget from "./Widget";
import BotConfig from "./BotConfig";
import APIDocument from "./APIDocument";
import ManageMembers from "@/components/ManageMembers";
import Version from "@/components/Version";
import ConfigAgora from "./config/Agora";
import { useAppSelector } from "@/app/store";
import ServerVersionChecker from "@/components/ServerVersionChecker";
const navs = [
{
@@ -31,7 +32,7 @@ const navs = [
name: "members",
component: <ManageMembers />,
admin: true
},
}
]
},
{
@@ -39,9 +40,11 @@ const navs = [
items: [
{
name: "bot",
component: <ServerVersionChecker version="0.3.2">
<BotConfig />
</ServerVersionChecker>,
component: (
<ServerVersionChecker version="0.3.2">
<BotConfig />
</ServerVersionChecker>
),
admin: true
},
{
@@ -50,9 +53,11 @@ const navs = [
},
{
name: "agora",
component: <ServerVersionChecker version="0.3.5">
<ConfigAgora />
</ServerVersionChecker>
component: (
<ServerVersionChecker version="0.3.5">
<ConfigAgora />
</ServerVersionChecker>
)
},
{
name: "smtp",
@@ -87,7 +92,7 @@ const navs = [
{
name: "version",
component: <Version />
},
}
]
}
];
@@ -97,13 +102,13 @@ const useNavs = () => {
const { loginUser, upgraded } = useAppSelector((store) => {
return { loginUser: store.authData.user, upgraded: store.server.upgraded };
});
const transformedNavs = navs.map(n => {
const transformedNavs = navs.map((n) => {
const { name, items, ...rest } = n;
return {
name,
// @ts-ignore
title: t(`nav.${name}`),
items: items.map(item => {
items: items.map((item) => {
const { name, ...rest } = item;
return {
name,