build: format the code with prettier
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 是初始账户,不能删 */}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
</>
|
||||
)}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user