feat: implement Dialog kit component

This commit is contained in:
Steven
2023-04-11 23:07:18 +08:00
parent 42a4194d10
commit bcd7bece81
10 changed files with 308 additions and 289 deletions

View File

@ -13,6 +13,7 @@
"@emotion/styled": "^11.10.6", "@emotion/styled": "^11.10.6",
"@mui/material": "^5.11.14", "@mui/material": "^5.11.14",
"@mui/styled-engine-sc": "^5.11.11", "@mui/styled-engine-sc": "^5.11.11",
"@radix-ui/react-dialog": "^1.0.3",
"@radix-ui/react-select": "^1.2.1", "@radix-ui/react-select": "^1.2.1",
"@radix-ui/react-tooltip": "^1.0.5", "@radix-ui/react-tooltip": "^1.0.5",
"@vercel/analytics": "^0.1.11", "@vercel/analytics": "^0.1.11",

30
pnpm-lock.yaml generated
View File

@ -13,6 +13,9 @@ dependencies:
'@mui/styled-engine-sc': '@mui/styled-engine-sc':
specifier: ^5.11.11 specifier: ^5.11.11
version: 5.11.11(styled-components@5.3.9) version: 5.11.11(styled-components@5.3.9)
'@radix-ui/react-dialog':
specifier: ^1.0.3
version: 1.0.3(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-select': '@radix-ui/react-select':
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0) version: 1.2.1(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
@ -876,6 +879,33 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/@radix-ui/react-dialog@1.0.3(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-owNhq36kNPqC2/a+zJRioPg6HHnTn5B/sh/NjTY8r4W9g1L5VJlrzZIVcBr7R9Mg8iLjVmh6MGgMlfoVf/WO/A==}
peerDependencies:
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
dependencies:
'@babel/runtime': 7.21.0
'@radix-ui/primitive': 1.0.0
'@radix-ui/react-compose-refs': 1.0.0(react@18.2.0)
'@radix-ui/react-context': 1.0.0(react@18.2.0)
'@radix-ui/react-dismissable-layer': 1.0.3(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-focus-guards': 1.0.0(react@18.2.0)
'@radix-ui/react-focus-scope': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-id': 1.0.0(react@18.2.0)
'@radix-ui/react-portal': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-presence': 1.0.0(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-slot': 1.0.1(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.0(react@18.2.0)
aria-hidden: 1.2.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-remove-scroll: 2.5.5(@types/react@18.0.28)(react@18.2.0)
transitivePeerDependencies:
- '@types/react'
dev: false
/@radix-ui/react-direction@1.0.0(react@18.2.0): /@radix-ui/react-direction@1.0.0(react@18.2.0):
resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==} resolution: {integrity: sha512-2HV05lGUgYcA6xgLQ4BKPDmtL+QbIZYH5fCOTAOOcJ5O0QbWS3i9lKaurLzliYUDhORI2Qr3pyjhJh44lKA3rQ==}
peerDependencies: peerDependencies:

View File

@ -1,5 +1,5 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Icon from "./Icon"; import Dialog from "./kit/Dialog";
export interface ActionConfirmModalProps { export interface ActionConfirmModalProps {
title: string; title: string;
@ -14,31 +14,25 @@ const ActionConfirmModal = (props: ActionConfirmModalProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className="modal modal-middle modal-open"> <Dialog title={title} onClose={close}>
<div className="modal-box relative"> <div className="w-full flex flex-col justify-start items-start mt-2">
<h3 className="font-bold text-lg">{title}</h3> <p className="text-gray-500">{content}</p>
<button className="btn btn-sm btn-circle absolute right-4 top-4" onClick={close}>
<Icon.IoMdClose className="w-5 h-auto" />
</button>
<div className="w-full flex flex-col justify-start items-start space-y-3 pt-4">
<p className="text-gray-500">{content}</p>
</div>
<div className="modal-action">
<button className="btn btn-outline" onClick={close}>
{t("common.close")}
</button>
<button
className={`btn ${confirmButtonStyle}`}
onClick={() => {
confirm();
close();
}}
>
{t("common.confirm")}
</button>
</div>
</div> </div>
</div> <div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
<button className="btn btn-outline" onClick={close}>
{t("common.close")}
</button>
<button
className={`btn ${confirmButtonStyle}`}
onClick={() => {
confirm();
close();
}}
>
{t("common.confirm")}
</button>
</div>
</Dialog>
); );
}; };

View File

@ -1,5 +1,4 @@
import { useState } from "react"; import { useState } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ClearDataConfirmModal from "./ClearDataConfirmModal"; import ClearDataConfirmModal from "./ClearDataConfirmModal";
@ -13,8 +12,7 @@ const ClearDataButton = () => {
{t("common.clear")} {t("common.clear")}
</button> </button>
{showClearDataConfirmModal && {showClearDataConfirmModal && <ClearDataConfirmModal close={() => setShowClearDataConfirmModal(false)} />}
createPortal(<ClearDataConfirmModal close={() => setShowClearDataConfirmModal(false)} />, document.body)}
</> </>
); );
}; };

View File

@ -1,6 +1,6 @@
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Icon from "./Icon"; import Dialog from "./kit/Dialog";
interface Props { interface Props {
close: () => void; close: () => void;
@ -20,25 +20,19 @@ const ClearDataConfirmModal = (props: Props) => {
}; };
return ( return (
<div className="modal modal-middle modal-open"> <Dialog title="Clear all data" onClose={close}>
<div className="modal-box relative"> <div className="w-full flex flex-col justify-start items-start mt-2">
<h3 className="font-bold text-lg">Clear all data</h3> <p className="text-gray-500">SQL Chat saves all your data in your local browser. Are you sure to clear all of them?</p>
<button className="btn btn-sm btn-circle absolute right-4 top-4" onClick={close}>
<Icon.IoMdClose className="w-5 h-auto" />
</button>
<div className="w-full flex flex-col justify-start items-start space-y-3 pt-4">
<p className="text-gray-500">SQL Chat saves all your data in your local browser. Are you sure to clear all of them?</p>
</div>
<div className="modal-action">
<button className="btn btn-outline" onClick={close}>
{t("common.close")}
</button>
<button className="btn btn-error" onClick={handleClearData}>
{t("common.clear")}
</button>
</div>
</div> </div>
</div> <div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
<button className="btn btn-outline" onClick={close}>
{t("common.close")}
</button>
<button className="btn btn-error" onClick={handleClearData}>
{t("common.clear")}
</button>
</div>
</Dialog>
); );
}; };

View File

@ -1,7 +1,6 @@
import { Drawer } from "@mui/material"; import { Drawer } from "@mui/material";
import { head } from "lodash-es"; import { head } from "lodash-es";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useConversationStore, useConnectionStore, useLayoutStore, ResponsiveWidth } from "@/store"; import { useConversationStore, useConnectionStore, useLayoutStore, ResponsiveWidth } from "@/store";
import { Conversation, Connection } from "@/types"; import { Conversation, Connection } from "@/types";
@ -290,26 +289,18 @@ const ConnectionSidebar = () => {
</div> </div>
</Drawer> </Drawer>
{createPortal( {state.showCreateConnectionModal && (
<CreateConnectionModal <CreateConnectionModal connection={editConnectionModalContext} close={() => toggleCreateConnectionModal(false)} />
show={state.showCreateConnectionModal}
connection={editConnectionModalContext}
close={() => toggleCreateConnectionModal(false)}
/>,
document.body
)} )}
{createPortal(<SettingModal show={state.showSettingModal} close={() => toggleSettingModal(false)} />, document.body)} {state.showSettingModal && <SettingModal close={() => toggleSettingModal(false)} />}
{state.showEditConversationTitleModal && {state.showEditConversationTitleModal && editConversationTitleModalContext && (
editConversationTitleModalContext && <EditConversationTitleModal
createPortal( close={() => toggleEditConversationTitleModal(false)}
<EditConversationTitleModal conversation={editConversationTitleModalContext}
close={() => toggleEditConversationTitleModal(false)} />
conversation={editConversationTitleModalContext} )}
/>,
document.body
)}
</> </>
); );
}; };

View File

@ -1,18 +1,17 @@
import { cloneDeep, head } from "lodash-es"; import { cloneDeep, head } from "lodash-es";
import { ChangeEvent, useEffect, useState } from "react"; import { ChangeEvent, useEffect, useState } from "react";
import { createPortal } from "react-dom";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import TextareaAutosize from "react-textarea-autosize"; import TextareaAutosize from "react-textarea-autosize";
import { useConnectionStore } from "@/store"; import { useConnectionStore } from "@/store";
import { Connection, Engine, ResponseObject } from "@/types"; import { Connection, Engine, ResponseObject } from "@/types";
import Select from "./kit/Select"; import Select from "./kit/Select";
import TextField from "./kit/TextField";
import Dialog from "./kit/Dialog";
import Icon from "./Icon"; import Icon from "./Icon";
import DataStorageBanner from "./DataStorageBanner"; import DataStorageBanner from "./DataStorageBanner";
import ActionConfirmModal from "./ActionConfirmModal"; import ActionConfirmModal from "./ActionConfirmModal";
import TextField from "./kit/TextField";
interface Props { interface Props {
show: boolean;
connection?: Connection; connection?: Connection;
close: () => void; close: () => void;
} }
@ -47,7 +46,7 @@ const defaultConnection: Connection = {
}; };
const CreateConnectionModal = (props: Props) => { const CreateConnectionModal = (props: Props) => {
const { show, connection: editConnection, close } = props; const { connection: editConnection, close } = props;
const connectionStore = useConnectionStore(); const connectionStore = useConnectionStore();
const [connection, setConnection] = useState<Connection>(defaultConnection); const [connection, setConnection] = useState<Connection>(defaultConnection);
const [showDeleteConnectionModal, setShowDeleteConnectionModal] = useState(false); const [showDeleteConnectionModal, setShowDeleteConnectionModal] = useState(false);
@ -59,23 +58,21 @@ const CreateConnectionModal = (props: Props) => {
const allowSave = connection.host !== "" && connection.username !== ""; const allowSave = connection.host !== "" && connection.username !== "";
useEffect(() => { useEffect(() => {
if (show) { const connection = isEditing ? editConnection : defaultConnection;
const connection = isEditing ? editConnection : defaultConnection; setConnection(connection);
setConnection(connection); if (connection.ssl) {
if (connection.ssl) { if (connection.ssl.ca && connection.ssl.cert && connection.ssl.key) {
if (connection.ssl.ca && connection.ssl.cert && connection.ssl.key) { setSSLType("full");
setSSLType("full");
} else {
setSSLType("ca-only");
}
} else { } else {
setSSLType("none"); setSSLType("ca-only");
} }
setSelectedSSLField("ca"); } else {
setIsRequesting(false); setSSLType("none");
setShowDeleteConnectionModal(false);
} }
}, [show]); setSelectedSSLField("ca");
setIsRequesting(false);
setShowDeleteConnectionModal(false);
}, []);
useEffect(() => { useEffect(() => {
let ssl = undefined; let ssl = undefined;
@ -209,164 +206,156 @@ const CreateConnectionModal = (props: Props) => {
return ( return (
<> <>
<div className={`modal modal-middle ${show && "modal-open"}`}> <Dialog title={isEditing ? "Edit Connection" : "Create Connection"} onClose={close}>
<div className="modal-box relative"> <div className="w-full flex flex-col justify-start items-start space-y-3 pt-4">
<h3 className="font-bold text-lg">{isEditing ? "Edit Connection" : "Create Connection"}</h3> <DataStorageBanner className="rounded-lg bg-white border py-2 !justify-start" alwaysShow={true} />
<button className="btn btn-sm btn-circle absolute right-4 top-4" onClick={close}> <div className="w-full flex flex-col">
<Icon.IoMdClose className="w-5 h-auto" /> <label className="block text-sm font-medium text-gray-700 mb-1">Database Type</label>
</button> <Select
<div className="w-full flex flex-col justify-start items-start space-y-3 pt-4"> className="w-full"
<DataStorageBanner className="rounded-lg bg-white border py-2 !justify-start" alwaysShow={true} /> value={connection.engineType}
<div className="w-full flex flex-col"> itemList={[
<label className="block text-sm font-medium text-gray-700 mb-1">Database Type</label> { value: Engine.MySQL, label: "MySQL" },
<Select { value: Engine.PostgreSQL, label: "PostgreSQL" },
className="w-full" ]}
value={connection.engineType} onValueChange={(value) => setPartialConnection({ engineType: value as Engine })}
itemList={[ />
{ value: Engine.MySQL, label: "MySQL" },
{ value: Engine.PostgreSQL, label: "PostgreSQL" },
]}
onValueChange={(value) => setPartialConnection({ engineType: value as Engine })}
/>
</div>
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">Host</label>
<TextField placeholder="Connect host" value={connection.host} onChange={(value) => setPartialConnection({ host: value })} />
</div>
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">Port</label>
<TextField placeholder="Connect port" value={connection.port} onChange={(value) => setPartialConnection({ port: value })} />
</div>
{showDatabaseField && (
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">Database Name</label>
<TextField
placeholder="Connect database"
value={connection.database || ""}
onChange={(value) => setPartialConnection({ database: value })}
/>
</div>
)}
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">Username</label>
<TextField
placeholder="Connect username"
value={connection.username || ""}
onChange={(value) => setPartialConnection({ username: value })}
/>
</div>
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
<TextField
placeholder="Connect password"
value={connection.password || ""}
onChange={(value) => setPartialConnection({ password: value })}
/>
</div>
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">SSL</label>
<div className="w-full flex flex-row justify-start items-start flex-wrap">
{SSLTypeOptions.map((option) => (
<label key={option.value} className="w-auto flex flex-row justify-start items-center cursor-pointer mr-3 mb-2">
<input
type="radio"
className="radio w-4 h-4 mr-1"
value={option.value}
checked={sslType === option.value}
onChange={(e) => setSSLType(e.target.value as SSLType)}
/>
<span className="text-sm">{option.label}</span>
</label>
))}
</div>
{sslType !== "none" && (
<>
<div className="text-sm space-x-3 mb-2">
<span
className={`leading-6 pb-1 border-b-2 border-transparent cursor-pointer opacity-60 hover:opacity-80 ${
selectedSSLField === "ca" && "!border-indigo-600 !opacity-100"
} `}
onClick={() => setSelectedSSLField("ca")}
>
CA Certificate
</span>
{sslType === "full" && (
<>
<span
className={`leading-6 pb-1 border-b-2 border-transparent cursor-pointer opacity-60 hover:opacity-80 ${
selectedSSLField === "key" && "!border-indigo-600 !opacity-100"
}`}
onClick={() => setSelectedSSLField("key")}
>
Client Key
</span>
<span
className={`leading-6 pb-1 border-b-2 border-transparent cursor-pointer opacity-60 hover:opacity-80 ${
selectedSSLField === "cert" && "!border-indigo-600 !opacity-100"
}`}
onClick={() => setSelectedSSLField("cert")}
>
Client Certificate
</span>
</>
)}
</div>
<div className="w-full h-auto relative">
<TextareaAutosize
className="w-full border resize-none rounded-lg text-sm p-3"
minRows={3}
maxRows={3}
value={(connection.ssl && connection.ssl[selectedSSLField]) ?? ""}
onChange={handleSSLValueChange}
/>
<div
className={`${
connection.ssl && connection.ssl[selectedSSLField] && "hidden"
} absolute top-3 left-4 text-gray-400 text-sm leading-6 pointer-events-none`}
>
<span className="">Input or </span>
<label className="pointer-events-auto border border-dashed px-2 py-1 rounded-lg cursor-pointer hover:border-gray-600 hover:text-gray-600">
upload file
<input className="hidden" type="file" onChange={handleSSLFileInputChange} />
</label>
</div>
</div>
</>
)}
</div>
</div> </div>
<div className="modal-action w-full flex flex-row justify-between items-center space-x-2"> <div className="w-full flex flex-col">
<div> <label className="block text-sm font-medium text-gray-700 mb-1">Host</label>
{isEditing && ( <TextField placeholder="Connect host" value={connection.host} onChange={(value) => setPartialConnection({ host: value })} />
<button className="btn btn-ghost" onClick={() => setShowDeleteConnectionModal(true)}> </div>
Delete <div className="w-full flex flex-col">
</button> <label className="block text-sm font-medium text-gray-700 mb-1">Port</label>
)} <TextField placeholder="Connect port" value={connection.port} onChange={(value) => setPartialConnection({ port: value })} />
</div>
{showDatabaseField && (
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">Database Name</label>
<TextField
placeholder="Connect database"
value={connection.database || ""}
onChange={(value) => setPartialConnection({ database: value })}
/>
</div> </div>
<div className="space-x-2 flex flex-row justify-center"> )}
<button className="btn btn-outline" onClick={close}> <div className="w-full flex flex-col">
Close <label className="block text-sm font-medium text-gray-700 mb-1">Username</label>
</button> <TextField
<button className="btn" disabled={isRequesting || !allowSave} onClick={handleCreateConnection}> placeholder="Connect username"
{isRequesting && <Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" />} value={connection.username || ""}
Save onChange={(value) => setPartialConnection({ username: value })}
</button> />
</div>
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
<TextField
placeholder="Connect password"
value={connection.password || ""}
onChange={(value) => setPartialConnection({ password: value })}
/>
</div>
<div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">SSL</label>
<div className="w-full flex flex-row justify-start items-start flex-wrap">
{SSLTypeOptions.map((option) => (
<label key={option.value} className="w-auto flex flex-row justify-start items-center cursor-pointer mr-3 mb-2">
<input
type="radio"
className="radio w-4 h-4 mr-1"
value={option.value}
checked={sslType === option.value}
onChange={(e) => setSSLType(e.target.value as SSLType)}
/>
<span className="text-sm">{option.label}</span>
</label>
))}
</div> </div>
{sslType !== "none" && (
<>
<div className="text-sm space-x-3 mb-2">
<span
className={`leading-6 pb-1 border-b-2 border-transparent cursor-pointer opacity-60 hover:opacity-80 ${
selectedSSLField === "ca" && "!border-indigo-600 !opacity-100"
} `}
onClick={() => setSelectedSSLField("ca")}
>
CA Certificate
</span>
{sslType === "full" && (
<>
<span
className={`leading-6 pb-1 border-b-2 border-transparent cursor-pointer opacity-60 hover:opacity-80 ${
selectedSSLField === "key" && "!border-indigo-600 !opacity-100"
}`}
onClick={() => setSelectedSSLField("key")}
>
Client Key
</span>
<span
className={`leading-6 pb-1 border-b-2 border-transparent cursor-pointer opacity-60 hover:opacity-80 ${
selectedSSLField === "cert" && "!border-indigo-600 !opacity-100"
}`}
onClick={() => setSelectedSSLField("cert")}
>
Client Certificate
</span>
</>
)}
</div>
<div className="w-full h-auto relative">
<TextareaAutosize
className="w-full border resize-none rounded-lg text-sm p-3"
minRows={3}
maxRows={3}
value={(connection.ssl && connection.ssl[selectedSSLField]) ?? ""}
onChange={handleSSLValueChange}
/>
<div
className={`${
connection.ssl && connection.ssl[selectedSSLField] && "hidden"
} absolute top-3 left-4 text-gray-400 text-sm leading-6 pointer-events-none`}
>
<span className="">Input or </span>
<label className="pointer-events-auto border border-dashed px-2 py-1 rounded-lg cursor-pointer hover:border-gray-600 hover:text-gray-600">
upload file
<input className="hidden" type="file" onChange={handleSSLFileInputChange} />
</label>
</div>
</div>
</>
)}
</div> </div>
</div> </div>
</div> <div className="modal-action w-full flex flex-row justify-between items-center space-x-2">
<div>
{isEditing && (
<button className="btn btn-ghost" onClick={() => setShowDeleteConnectionModal(true)}>
Delete
</button>
)}
</div>
<div className="space-x-2 flex flex-row justify-center">
<button className="btn btn-outline" onClick={close}>
Close
</button>
<button className="btn" disabled={isRequesting || !allowSave} onClick={handleCreateConnection}>
{isRequesting && <Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" />}
Save
</button>
</div>
</div>
</Dialog>
{showDeleteConnectionModal && {showDeleteConnectionModal && (
createPortal( <ActionConfirmModal
<ActionConfirmModal title="Delete Connection"
title="Delete Connection" content="Are you sure you want to delete this connection?"
content="Are you sure you want to delete this connection?" confirmButtonStyle="btn-error"
confirmButtonStyle="btn-error" close={() => setShowDeleteConnectionModal(false)}
close={() => setShowDeleteConnectionModal(false)} confirm={() => handleDeleteConnection()}
confirm={() => handleDeleteConnection()} />
/>, )}
document.body
)}
</> </>
); );
}; };

View File

@ -3,8 +3,8 @@ import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useConversationStore } from "@/store"; import { useConversationStore } from "@/store";
import { Conversation } from "@/types"; import { Conversation } from "@/types";
import Icon from "./Icon";
import TextField from "./kit/TextField"; import TextField from "./kit/TextField";
import Dialog from "./kit/Dialog";
interface Props { interface Props {
conversation: Conversation; conversation: Conversation;
@ -32,25 +32,19 @@ const EditConversationTitleModal = (props: Props) => {
}; };
return ( return (
<div className="modal modal-middle modal-open"> <Dialog title={t("conversation.edit-title")} onClose={close}>
<div className="modal-box relative"> <div className="w-full flex flex-col justify-start items-start mt-2">
<h3 className="font-bold text-lg">{t("conversation.edit-title")}</h3> <TextField placeholder={t("conversation.conversation-title") || ""} value={title} onChange={(value) => setTitle(value)} />
<button className="btn btn-sm btn-circle absolute right-4 top-4" onClick={close}>
<Icon.IoMdClose className="w-5 h-auto" />
</button>
<div className="w-full flex flex-col justify-start items-start space-y-3 pt-4">
<TextField placeholder={t("conversation.conversation-title") || ""} value={title} onChange={(value) => setTitle(value)} />
</div>
<div className="modal-action">
<button className="btn btn-outline" onClick={close}>
{t("common.close")}
</button>
<button className="btn" disabled={!allowSave} onClick={handleSaveEdit}>
{t("common.save")}
</button>
</div>
</div> </div>
</div> <div className="w-full flex flex-row justify-end items-center mt-4 space-x-2">
<button className="btn btn-outline" onClick={close}>
{t("common.close")}
</button>
<button className="btn" disabled={!allowSave} onClick={handleSaveEdit}>
{t("common.save")}
</button>
</div>
</Dialog>
); );
}; };

View File

@ -1,4 +1,5 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Dialog from "./kit/Dialog";
import Icon from "./Icon"; import Icon from "./Icon";
import WeChatQRCodeView from "./WeChatQRCodeView"; import WeChatQRCodeView from "./WeChatQRCodeView";
import ClearDataButton from "./ClearDataButton"; import ClearDataButton from "./ClearDataButton";
@ -6,54 +7,47 @@ import LocaleSelector from "./LocaleSelector";
import OpenAIApiConfigView from "./OpenAIApiConfigView"; import OpenAIApiConfigView from "./OpenAIApiConfigView";
interface Props { interface Props {
show: boolean;
close: () => void; close: () => void;
} }
const SettingModal = (props: Props) => { const SettingModal = (props: Props) => {
const { show, close } = props; const { close } = props;
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
<div className={`modal modal-middle ${show && "modal-open"}`}> <Dialog title={t("setting.self")} onClose={close}>
<div className="modal-box relative"> <div className="w-full flex flex-col justify-start items-start space-y-3 pt-4">
<h3 className="font-bold text-lg">{t("setting.self")}</h3> <div className="w-full flex flex-row justify-start items-start flex-wrap">
<button className="btn btn-sm btn-circle absolute right-4 top-4" onClick={close}> <a
<Icon.IoMdClose className="w-5 h-auto" /> href="https://discord.gg/z6kakemDjm"
</button> className="w-auto px-4 py-2 rounded-full bg-indigo-600 text-white text-sm font-medium flex flex-row justify-center items-center mr-2 mb-2 hover:underline hover:shadow"
<div className="w-full flex flex-col justify-start items-start space-y-3 pt-4"> target="_blank"
<div className="w-full flex flex-row justify-start items-start flex-wrap"> >
<a <Icon.BsDiscord className="w-4 h-auto mr-1" />
href="https://discord.gg/z6kakemDjm" {t("social.join-discord-channel")}
className="w-auto px-4 py-2 rounded-full bg-indigo-600 text-white text-sm font-medium flex flex-row justify-center items-center mr-2 mb-2 hover:underline hover:shadow" </a>
target="_blank" <WeChatQRCodeView />
> </div>
<Icon.BsDiscord className="w-4 h-auto mr-1" />
{t("social.join-discord-channel")} <h3 className="pl-4 text-sm text-gray-500">{t("setting.basic.self")}</h3>
</a> <div className="w-full border border-gray-200 p-4 rounded-lg">
<WeChatQRCodeView /> <div className="w-full flex flex-row justify-between items-center gap-2">
<span>{t("setting.basic.language")}</span>
<LocaleSelector />
</div> </div>
</div>
<h3 className="pl-4 text-sm text-gray-500">{t("setting.basic.self")}</h3> <OpenAIApiConfigView />
<div className="w-full border border-gray-200 p-4 rounded-lg">
<div className="w-full flex flex-row justify-between items-center gap-2">
<span>{t("setting.basic.language")}</span>
<LocaleSelector />
</div>
</div>
<OpenAIApiConfigView /> <h3 className="pl-4 text-sm text-gray-500">{t("setting.data.self")}</h3>
<div className="w-full border border-red-200 p-4 rounded-lg">
<h3 className="pl-4 text-sm text-gray-500">{t("setting.data.self")}</h3> <div className="w-full flex flex-row justify-between items-center gap-2">
<div className="w-full border border-red-200 p-4 rounded-lg"> <span>{t("setting.data.clear-all-data")}</span>
<div className="w-full flex flex-row justify-between items-center gap-2"> <ClearDataButton />
<span>{t("setting.data.clear-all-data")}</span>
<ClearDataButton />
</div>
</div> </div>
</div> </div>
</div> </div>
</div> </Dialog>
); );
}; };

View File

@ -0,0 +1,34 @@
import * as DialogUI from "@radix-ui/react-dialog";
import React, { ReactNode } from "react";
import Icon from "../Icon";
interface Props {
title: string;
children: ReactNode;
onClose: () => void;
}
const Dialog = (props: Props) => {
const { children, title, onClose } = props;
return (
<DialogUI.Root open={true}>
<DialogUI.Portal>
<DialogUI.Overlay className="fixed inset-0 bg-black bg-opacity-60 z-[9999]" />
<DialogUI.Content className="bg-white rounded-xl p-4 px-5 fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] z-[9999]">
<DialogUI.Title className="text-lg text-black font-medium mb-2">{title}</DialogUI.Title>
<DialogUI.Close
className="absolute top-3 right-3 outline-none w-8 h-8 p-1 bg-zinc-600 rounded-full text-white hover:opacity-80"
aria-label="Close"
onClick={onClose}
>
<Icon.IoClose className="w-full h-auto" />
</DialogUI.Close>
<div className="w-full flex flex-col justify-start items-start">{children}</div>
</DialogUI.Content>
</DialogUI.Portal>
</DialogUI.Root>
);
};
export default Dialog;