mirror of
https://github.com/sqlchat/sqlchat.git
synced 2025-07-24 23:34:28 +08:00
feat: Use separate /setting route page instead of modal
Better extensibility as we may overlay additional modal in setting
This commit is contained in:
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -29,5 +29,6 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file"
|
||||
}
|
||||
},
|
||||
"i18n-ally.keystyle": "nested"
|
||||
}
|
||||
|
@ -12,6 +12,8 @@
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@headlessui/react": "^1.7.14",
|
||||
"@heroicons/react": "^2.0.17",
|
||||
"@mui/material": "^5.11.14",
|
||||
"@mui/styled-engine-sc": "^5.11.11",
|
||||
"@prisma/client": "4.13.0",
|
||||
|
26
pnpm-lock.yaml
generated
26
pnpm-lock.yaml
generated
@ -7,6 +7,12 @@ dependencies:
|
||||
'@emotion/styled':
|
||||
specifier: ^11.10.6
|
||||
version: 11.10.6(@emotion/react@11.10.6)(@types/react@18.0.28)(react@18.2.0)
|
||||
'@headlessui/react':
|
||||
specifier: ^1.7.14
|
||||
version: 1.7.14(react-dom@18.2.0)(react@18.2.0)
|
||||
'@heroicons/react':
|
||||
specifier: ^2.0.17
|
||||
version: 2.0.17(react@18.2.0)
|
||||
'@mui/material':
|
||||
specifier: ^5.11.14
|
||||
version: 5.11.14(@emotion/react@11.10.6)(@emotion/styled@11.10.6)(@types/react@18.0.28)(react-dom@18.2.0)(react@18.2.0)
|
||||
@ -645,6 +651,26 @@ packages:
|
||||
- '@types/react'
|
||||
dev: false
|
||||
|
||||
/@headlessui/react@1.7.14(react-dom@18.2.0)(react@18.2.0):
|
||||
resolution: {integrity: sha512-znzdq9PG8rkwcu9oQ2FwIy0ZFtP9Z7ycS+BAqJ3R5EIqC/0bJGvhT7193rFf+45i9nnPsYvCQVW4V/bB9Xc+gA==}
|
||||
engines: {node: '>=10'}
|
||||
peerDependencies:
|
||||
react: ^16 || ^17 || ^18
|
||||
react-dom: ^16 || ^17 || ^18
|
||||
dependencies:
|
||||
client-only: 0.0.1
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0(react@18.2.0)
|
||||
dev: false
|
||||
|
||||
/@heroicons/react@2.0.17(react@18.2.0):
|
||||
resolution: {integrity: sha512-90GMZktkA53YbNzHp6asVEDevUQCMtxWH+2UK2S8OpnLEu7qckTJPhNxNQG52xIR1WFTwFqtH6bt7a60ZNcLLA==}
|
||||
peerDependencies:
|
||||
react: '>= 16'
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
dev: false
|
||||
|
||||
/@humanwhocodes/config-array@0.9.5:
|
||||
resolution: {integrity: sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==}
|
||||
engines: {node: '>=10.10.0'}
|
||||
|
@ -2,25 +2,20 @@ import { Drawer } from "@mui/material";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useConnectionStore, useLayoutStore, ResponsiveWidth } from "@/store";
|
||||
import Link from "next/link";
|
||||
import Select from "./kit/Select";
|
||||
import Tooltip from "./kit/Tooltip";
|
||||
import Icon from "./Icon";
|
||||
import DarkModeSwitch from "./DarkModeSwitch";
|
||||
import SettingModal from "./SettingModal";
|
||||
import ConversationList from "./Sidebar/ConversationList";
|
||||
import ConnectionList from "./Sidebar/ConnectionList";
|
||||
|
||||
interface State {
|
||||
showSettingModal: boolean;
|
||||
}
|
||||
interface State {}
|
||||
|
||||
const ConnectionSidebar = () => {
|
||||
const { t } = useTranslation();
|
||||
const layoutStore = useLayoutStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
const [state, setState] = useState<State>({
|
||||
showSettingModal: false,
|
||||
});
|
||||
const [isRequestingDatabase, setIsRequestingDatabase] =
|
||||
useState<boolean>(false);
|
||||
const currentConnectionCtx = connectionStore.currentConnectionCtx;
|
||||
@ -60,13 +55,6 @@ const ConnectionSidebar = () => {
|
||||
}
|
||||
}, [currentConnectionCtx?.connection]);
|
||||
|
||||
const toggleSettingModal = (show = true) => {
|
||||
setState({
|
||||
...state,
|
||||
showSettingModal: show,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDatabaseNameSelect = async (databaseName: string) => {
|
||||
if (!currentConnectionCtx?.connection) {
|
||||
return;
|
||||
@ -101,13 +89,13 @@ const ConnectionSidebar = () => {
|
||||
<div className="w-full flex flex-col justify-end items-center">
|
||||
<DarkModeSwitch />
|
||||
<Tooltip title={t("common.setting")} side="right">
|
||||
<button
|
||||
<Link
|
||||
className=" w-10 h-10 p-1 rounded-full flex flex-row justify-center items-center hover:bg-gray-100 dark:hover:bg-zinc-700"
|
||||
data-tip={t("common.setting")}
|
||||
onClick={() => toggleSettingModal(true)}
|
||||
href="/setting"
|
||||
>
|
||||
<Icon.IoMdSettings className="text-gray-600 dark:text-gray-300 w-6 h-auto" />
|
||||
</button>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@ -179,10 +167,6 @@ const ConnectionSidebar = () => {
|
||||
</div>
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
{state.showSettingModal && (
|
||||
<SettingModal close={() => toggleSettingModal(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLocalStorage } from "react-use";
|
||||
import Link from "next/link";
|
||||
import Icon from "./Icon";
|
||||
import SettingModal from "./SettingModal";
|
||||
import { useState } from "react";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -15,7 +14,6 @@ const QuotaOverflowBanner = (props: Props) => {
|
||||
"hide-quota-overflow-banner",
|
||||
false
|
||||
);
|
||||
const [showSettingModal, setShowSettingModal] = useState(false);
|
||||
const show = !hideBanner;
|
||||
|
||||
return (
|
||||
@ -27,12 +25,9 @@ const QuotaOverflowBanner = (props: Props) => {
|
||||
>
|
||||
<div className="text-sm leading-6 pr-4 cursor-pointer">
|
||||
{t("banner.quota-overflow")}{" "}
|
||||
<button
|
||||
className="ml-1 underline hover:opacity-80"
|
||||
onClick={() => setShowSettingModal(true)}
|
||||
>
|
||||
<Link className="ml-1 underline hover:opacity-80" href="/setting">
|
||||
{t("banner.use-my-key")}
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<button
|
||||
className="absolute right-2 sm:right-4 opacity-60 hover:opacity-100"
|
||||
@ -41,10 +36,6 @@ const QuotaOverflowBanner = (props: Props) => {
|
||||
<Icon.BiX className="w-6 h-auto" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showSettingModal && (
|
||||
<SettingModal close={() => setShowSettingModal(false)} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -1,61 +0,0 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Modal from "./kit/Modal";
|
||||
import Icon from "./Icon";
|
||||
import WeChatQRCodeView from "./WeChatQRCodeView";
|
||||
import ClearDataButton from "./ClearDataButton";
|
||||
import LocaleSelector from "./LocaleSelector";
|
||||
import ThemeSelector from "./ThemeSelector";
|
||||
import OpenAIApiConfigView from "./OpenAIApiConfigView";
|
||||
|
||||
interface Props {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
const SettingModal = (props: Props) => {
|
||||
const { close } = props;
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal title={t("setting.self")} onClose={close}>
|
||||
<div className="w-full flex flex-col justify-start items-start space-y-3 pt-4">
|
||||
<div className="w-full flex flex-row justify-start items-start flex-wrap gap-2">
|
||||
<a
|
||||
href="https://discord.gg/z6kakemDjm"
|
||||
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 hover:underline hover:shadow"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon.BsDiscord className="w-4 h-auto mr-1" />
|
||||
{t("social.join-discord-channel")}
|
||||
</a>
|
||||
<WeChatQRCodeView />
|
||||
</div>
|
||||
|
||||
<h3 className="pl-4 text-sm text-gray-500">
|
||||
{t("setting.basic.self")}
|
||||
</h3>
|
||||
<div className="w-full border border-gray-200 dark:border-zinc-700 p-4 rounded-lg space-y-2">
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<span>{t("setting.basic.language")}</span>
|
||||
<LocaleSelector />
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<span>{t("setting.theme.self")}</span>
|
||||
<ThemeSelector />
|
||||
</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 dark:border-zinc-700 p-4 rounded-lg">
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<span>{t("setting.data.clear-all-data")}</span>
|
||||
<ClearDataButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingModal;
|
52
src/components/SettingView.tsx
Normal file
52
src/components/SettingView.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Icon from "./Icon";
|
||||
import WeChatQRCodeView from "./WeChatQRCodeView";
|
||||
import ClearDataButton from "./ClearDataButton";
|
||||
import LocaleSelector from "./LocaleSelector";
|
||||
import ThemeSelector from "./ThemeSelector";
|
||||
import OpenAIApiConfigView from "./OpenAIApiConfigView";
|
||||
|
||||
const SettingView = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-col justify-start items-start space-y-3 pt-4">
|
||||
<div className="w-full flex flex-row justify-start items-start flex-wrap gap-2">
|
||||
<a
|
||||
href="https://discord.gg/z6kakemDjm"
|
||||
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 hover:underline hover:shadow"
|
||||
target="_blank"
|
||||
>
|
||||
<Icon.BsDiscord className="w-4 h-auto mr-1" />
|
||||
{t("social.join-discord-channel")}
|
||||
</a>
|
||||
<WeChatQRCodeView />
|
||||
</div>
|
||||
|
||||
<h3 className="pl-4 text-sm text-gray-500">{t("setting.basic.self")}</h3>
|
||||
<div className="w-full border border-gray-200 dark:border-zinc-700 p-4 rounded-lg space-y-2">
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<span>{t("setting.basic.language")}</span>
|
||||
<LocaleSelector />
|
||||
</div>
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<span>{t("setting.theme.self")}</span>
|
||||
<ThemeSelector />
|
||||
</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 dark:border-zinc-700 p-4 rounded-lg">
|
||||
<div className="w-full flex flex-row justify-between items-center gap-2">
|
||||
<span>{t("setting.data.clear-all-data")}</span>
|
||||
<ClearDataButton />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingView;
|
@ -10,7 +10,6 @@ import CreateConnectionModal from "../CreateConnectionModal";
|
||||
|
||||
interface State {
|
||||
showCreateConnectionModal: boolean;
|
||||
showSettingModal: boolean;
|
||||
showUpdateConversationModal: boolean;
|
||||
}
|
||||
|
||||
@ -19,7 +18,6 @@ const ConnectionList = () => {
|
||||
const connectionStore = useConnectionStore();
|
||||
const [state, setState] = useState<State>({
|
||||
showCreateConnectionModal: false,
|
||||
showSettingModal: false,
|
||||
showUpdateConversationModal: false,
|
||||
});
|
||||
const [editConnectionModalContext, setEditConnectionModalContext] =
|
||||
|
195
src/pages/setting/index.tsx
Normal file
195
src/pages/setting/index.tsx
Normal file
@ -0,0 +1,195 @@
|
||||
import { NextPage } from "next";
|
||||
import { Fragment, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { Dialog, Transition } from "@headlessui/react";
|
||||
import {
|
||||
ArrowUturnLeftIcon,
|
||||
Bars3Icon,
|
||||
Cog6ToothIcon,
|
||||
XMarkIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import SettingView from "../../components/SettingView";
|
||||
|
||||
function classNames(...classes: string[]) {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
const SettingPage: NextPage = () => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const navigation = [
|
||||
{ name: "Back", href: "/", icon: ArrowUturnLeftIcon, current: false },
|
||||
{ name: "General", href: "/setting", icon: Cog6ToothIcon, current: true },
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{/*
|
||||
This example requires updating your template:
|
||||
|
||||
```
|
||||
<html class="h-full bg-white">
|
||||
<body class="h-full">
|
||||
```
|
||||
*/}
|
||||
<div>
|
||||
<Transition.Root show={sidebarOpen} as={Fragment}>
|
||||
<Dialog
|
||||
as="div"
|
||||
className="relative z-50 lg:hidden"
|
||||
onClose={setSidebarOpen}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition-opacity ease-linear duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="transition-opacity ease-linear duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="fixed inset-0 bg-gray-900/80" />
|
||||
</Transition.Child>
|
||||
|
||||
<div className="fixed inset-0 flex">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="transition ease-in-out duration-300 transform"
|
||||
enterFrom="-translate-x-full"
|
||||
enterTo="translate-x-0"
|
||||
leave="transition ease-in-out duration-300 transform"
|
||||
leaveFrom="translate-x-0"
|
||||
leaveTo="-translate-x-full"
|
||||
>
|
||||
<Dialog.Panel className="relative mr-16 flex w-full max-w-xs flex-1">
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter="ease-in-out duration-300"
|
||||
enterFrom="opacity-0"
|
||||
enterTo="opacity-100"
|
||||
leave="ease-in-out duration-300"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<div className="absolute left-full top-0 flex w-16 justify-center pt-5">
|
||||
<button
|
||||
type="button"
|
||||
className="-m-2.5 p-2.5"
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
>
|
||||
<span className="sr-only">Close sidebar</span>
|
||||
<XMarkIcon
|
||||
className="h-6 w-6 text-white"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</Transition.Child>
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto bg-white px-6 pb-2">
|
||||
<Link className="flex pt-4 shrink-0 items-center" href="/">
|
||||
<img className="w-auto" src="/chat-logo.webp" />
|
||||
</Link>
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:text-indigo-600 hover:bg-gray-50",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</Dialog.Panel>
|
||||
</Transition.Child>
|
||||
</div>
|
||||
</Dialog>
|
||||
</Transition.Root>
|
||||
|
||||
{/* Static sidebar for desktop */}
|
||||
<div className="hidden lg:fixed lg:inset-y-0 lg:z-50 lg:flex lg:w-72 lg:flex-col">
|
||||
{/* Sidebar component, swap this element with another sidebar if you like */}
|
||||
<div className="flex grow flex-col gap-y-5 overflow-y-auto border-r border-gray-200 bg-white px-6">
|
||||
<Link className="flex pt-4 shrink-0 items-center" href="/">
|
||||
<img className="" src="/chat-logo.webp" />
|
||||
</Link>
|
||||
<nav className="flex flex-1 flex-col">
|
||||
<ul role="list" className="flex flex-1 flex-col gap-y-7">
|
||||
<li>
|
||||
<ul role="list" className="-mx-2 space-y-1">
|
||||
{navigation.map((item) => (
|
||||
<li key={item.name}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className={classNames(
|
||||
item.current
|
||||
? "bg-gray-50 text-indigo-600"
|
||||
: "text-gray-700 hover:text-indigo-600 hover:bg-gray-50",
|
||||
"group flex gap-x-3 rounded-md p-2 text-sm leading-6 font-semibold"
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
className={classNames(
|
||||
item.current
|
||||
? "text-indigo-600"
|
||||
: "text-gray-400 group-hover:text-indigo-600",
|
||||
"h-6 w-6 shrink-0"
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{item.name}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="sticky top-0 z-40 flex items-center gap-x-6 bg-white px-4 py-4 shadow-sm sm:px-6 lg:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="-m-2.5 p-2.5 text-gray-700 lg:hidden"
|
||||
onClick={() => setSidebarOpen(true)}
|
||||
>
|
||||
<span className="sr-only">Open sidebar</span>
|
||||
<Bars3Icon className="h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<main className="py-10 lg:pl-72">
|
||||
<div className="px-4 sm:px-6 lg:px-8">
|
||||
<SettingView />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingPage;
|
Reference in New Issue
Block a user