Browse Source

Merge pull request #959 from Yidadaa/0420-mask

refactor: close #643 use react router
Yifei Zhang 1 year ago
parent
commit
a62bca442e

+ 29 - 10
app/components/chat-list.tsx

@@ -10,6 +10,8 @@ import {
 import { useChatStore } from "../store";
 
 import Locale from "../locales";
+import { Link, useNavigate } from "react-router-dom";
+import { Path } from "../constant";
 
 export function ChatItem(props: {
   onClick?: () => void;
@@ -20,6 +22,7 @@ export function ChatItem(props: {
   selected: boolean;
   id: number;
   index: number;
+  narrow?: boolean;
 }) {
   return (
     <Draggable draggableId={`${props.id}`} index={props.index}>
@@ -33,13 +36,20 @@ export function ChatItem(props: {
           {...provided.draggableProps}
           {...provided.dragHandleProps}
         >
-          <div className={styles["chat-item-title"]}>{props.title}</div>
-          <div className={styles["chat-item-info"]}>
-            <div className={styles["chat-item-count"]}>
-              {Locale.ChatItem.ChatItemCount(props.count)}
-            </div>
-            <div className={styles["chat-item-date"]}>{props.time}</div>
-          </div>
+          {props.narrow ? (
+            <div className={styles["chat-item-narrow"]}>{props.count}</div>
+          ) : (
+            <>
+              <div className={styles["chat-item-title"]}>{props.title}</div>
+              <div className={styles["chat-item-info"]}>
+                <div className={styles["chat-item-count"]}>
+                  {Locale.ChatItem.ChatItemCount(props.count)}
+                </div>
+                <div className={styles["chat-item-date"]}>{props.time}</div>
+              </div>
+            </>
+          )}
+
           <div className={styles["chat-item-delete"]} onClick={props.onDelete}>
             <DeleteIcon />
           </div>
@@ -49,7 +59,7 @@ export function ChatItem(props: {
   );
 }
 
-export function ChatList() {
+export function ChatList(props: { narrow?: boolean }) {
   const [sessions, selectedIndex, selectSession, removeSession, moveSession] =
     useChatStore((state) => [
       state.sessions,
@@ -59,6 +69,7 @@ export function ChatList() {
       state.moveSession,
     ]);
   const chatStore = useChatStore();
+  const navigate = useNavigate();
 
   const onDragEnd: OnDragEndResponder = (result) => {
     const { destination, source } = result;
@@ -94,8 +105,16 @@ export function ChatList() {
                 id={item.id}
                 index={i}
                 selected={i === selectedIndex}
-                onClick={() => selectSession(i)}
-                onDelete={() => chatStore.deleteSession(i)}
+                onClick={() => {
+                  navigate(Path.Chat);
+                  selectSession(i);
+                }}
+                onDelete={() => {
+                  if (!props.narrow || confirm(Locale.Home.DeleteChat)) {
+                    chatStore.deleteSession(i);
+                  }
+                }}
+                narrow={props.narrow}
               />
             ))}
             {provided.placeholder}

+ 7 - 7
app/components/chat.tsx

@@ -54,6 +54,8 @@ import styles from "./home.module.scss";
 import chatStyle from "./chat.module.scss";
 
 import { Input, Modal, showModal } from "./ui-lib";
+import { useNavigate } from "react-router-dom";
+import { Path } from "../constant";
 
 const Markdown = dynamic(
   async () => memo((await import("./markdown")).Markdown),
@@ -418,10 +420,7 @@ export function ChatActions(props: {
   );
 }
 
-export function Chat(props: {
-  showSideBar?: () => void;
-  sideBarShowing?: boolean;
-}) {
+export function Chat() {
   type RenderMessage = Message & { preview?: boolean };
 
   const chatStore = useChatStore();
@@ -439,6 +438,7 @@ export function Chat(props: {
   const { scrollRef, setAutoScroll, scrollToBottom } = useScrollToBottom();
   const [hitBottom, setHitBottom] = useState(false);
   const isMobileScreen = useMobileScreen();
+  const navigate = useNavigate();
 
   const onChatBodyScroll = (e: HTMLElement) => {
     const isTouchBottom = e.scrollTop + e.clientHeight >= e.scrollHeight - 20;
@@ -641,7 +641,7 @@ export function Chat(props: {
 
   // Auto focus
   useEffect(() => {
-    if (props.sideBarShowing && isMobileScreen) return;
+    if (isMobileScreen) return;
     inputRef.current?.focus();
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, []);
@@ -666,7 +666,7 @@ export function Chat(props: {
               icon={<ReturnIcon />}
               bordered
               title={Locale.Chat.Actions.ChatList}
-              onClick={props?.showSideBar}
+              onClick={() => navigate(Path.Home)}
             />
           </div>
           <div className={styles["window-action-button"]}>
@@ -830,7 +830,7 @@ export function Chat(props: {
               setAutoScroll(false);
               setTimeout(() => setPromptHints([]), 500);
             }}
-            autoFocus={!props?.sideBarShowing}
+            autoFocus
             rows={inputRows}
           />
           <IconButton

+ 68 - 1
app/components/home.module.scss

@@ -50,7 +50,7 @@
   flex-direction: column;
   box-shadow: inset -2px 0px 2px 0px rgb(0, 0, 0, 0.05);
   position: relative;
-  transition: width ease 0.1s;
+  transition: width ease 0.05s;
 }
 
 .sidebar-drag {
@@ -126,11 +126,13 @@
 .sidebar-title {
   font-size: 20px;
   font-weight: bold;
+  animation: slide-in ease 0.3s;
 }
 
 .sidebar-sub-title {
   font-size: 12px;
   font-weight: 400px;
+  animation: slide-in ease 0.3s;
 }
 
 .sidebar-body {
@@ -171,6 +173,7 @@
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
+  animation: slide-in ease 0.3s;
 }
 
 .chat-item-delete {
@@ -197,6 +200,7 @@
   color: rgb(166, 166, 166);
   font-size: 12px;
   margin-top: 8px;
+  animation: slide-in ease 0.3s;
 }
 
 .chat-item-count,
@@ -206,6 +210,69 @@
   white-space: nowrap;
 }
 
+.narrow-sidebar {
+  .sidebar-title,
+  .sidebar-sub-title {
+    display: none;
+  }
+  .sidebar-logo {
+    position: relative;
+    display: flex;
+    justify-content: center;
+  }
+
+  .chat-item {
+    padding: 0;
+    min-height: 50px;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    transition: all ease 0.3s;
+
+    &:hover {
+      .chat-item-narrow {
+        transform: scale(0.7) translateX(-50%);
+      }
+    }
+  }
+
+  .chat-item-narrow {
+    font-weight: bolder;
+    font-size: 24px;
+    line-height: 0;
+    font-weight: lighter;
+    color: var(--black);
+    transform: translateX(0);
+    transition: all ease 0.3s;
+    opacity: 0.1;
+    padding: 4px;
+  }
+
+  .chat-item-delete {
+    top: 15px;
+  }
+
+  .chat-item:hover > .chat-item-delete {
+    opacity: 0.5;
+    right: 5px;
+  }
+
+  .sidebar-tail {
+    flex-direction: column;
+    align-items: center;
+
+    .sidebar-actions {
+      flex-direction: column;
+      align-items: center;
+
+      .sidebar-action {
+        margin-right: 0;
+        margin-bottom: 15px;
+      }
+    }
+  }
+}
+
 .sidebar-tail {
   display: flex;
   justify-content: space-between;

+ 44 - 161
app/components/home.tsx

@@ -2,32 +2,31 @@
 
 require("../polyfill");
 
-import { useState, useEffect, useRef } from "react";
+import { useState, useEffect } from "react";
 
-import { IconButton } from "./button";
 import styles from "./home.module.scss";
 
-import SettingsIcon from "../icons/settings.svg";
-import GithubIcon from "../icons/github.svg";
-import ChatGptIcon from "../icons/chatgpt.svg";
-
 import BotIcon from "../icons/bot.svg";
-import AddIcon from "../icons/add.svg";
 import LoadingIcon from "../icons/three-dots.svg";
-import CloseIcon from "../icons/close.svg";
 
 import { useChatStore } from "../store";
 import { getCSSVar, useMobileScreen } from "../utils";
-import Locale from "../locales";
 import { Chat } from "./chat";
 
 import dynamic from "next/dynamic";
-import { REPO_URL } from "../constant";
+import { Path } from "../constant";
 import { ErrorBoundary } from "./error";
 
+import {
+  HashRouter as Router,
+  Routes,
+  Route,
+  useLocation,
+} from "react-router-dom";
+
 export function Loading(props: { noLogo?: boolean }) {
   return (
-    <div className={styles["loading-content"]}>
+    <div className={styles["loading-content"] + " no-dark"}>
       {!props.noLogo && <BotIcon />}
       <LoadingIcon />
     </div>
@@ -38,11 +37,11 @@ const Settings = dynamic(async () => (await import("./settings")).Settings, {
   loading: () => <Loading noLogo />,
 });
 
-const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
+const SideBar = dynamic(async () => (await import("./sidebar")).SideBar, {
   loading: () => <Loading noLogo />,
 });
 
-function useSwitchTheme() {
+export function useSwitchTheme() {
   const config = useChatStore((state) => state.config);
 
   useEffect(() => {
@@ -73,50 +72,6 @@ function useSwitchTheme() {
   }, [config.theme]);
 }
 
-function useDragSideBar() {
-  const limit = (x: number) => Math.min(500, Math.max(220, x));
-
-  const chatStore = useChatStore();
-  const startX = useRef(0);
-  const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300);
-  const lastUpdateTime = useRef(Date.now());
-
-  const handleMouseMove = useRef((e: MouseEvent) => {
-    if (Date.now() < lastUpdateTime.current + 100) {
-      return;
-    }
-    lastUpdateTime.current = Date.now();
-    const d = e.clientX - startX.current;
-    const nextWidth = limit(startDragWidth.current + d);
-    chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth));
-  });
-
-  const handleMouseUp = useRef(() => {
-    startDragWidth.current = chatStore.config.sidebarWidth ?? 300;
-    window.removeEventListener("mousemove", handleMouseMove.current);
-    window.removeEventListener("mouseup", handleMouseUp.current);
-  });
-
-  const onDragMouseDown = (e: MouseEvent) => {
-    startX.current = e.clientX;
-
-    window.addEventListener("mousemove", handleMouseMove.current);
-    window.addEventListener("mouseup", handleMouseUp.current);
-  };
-  const isMobileScreen = useMobileScreen();
-
-  useEffect(() => {
-    const sideBarWidth = isMobileScreen
-      ? "100vw"
-      : `${limit(chatStore.config.sidebarWidth ?? 300)}px`;
-    document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
-  }, [chatStore.config.sidebarWidth, isMobileScreen]);
-
-  return {
-    onDragMouseDown,
-  };
-}
-
 const useHasHydrated = () => {
   const [hasHydrated, setHasHydrated] = useState<boolean>(false);
 
@@ -127,130 +82,58 @@ const useHasHydrated = () => {
   return hasHydrated;
 };
 
-function _Home() {
-  const [createNewSession, currentIndex, removeSession] = useChatStore(
-    (state) => [
-      state.newSession,
-      state.currentSessionIndex,
-      state.removeSession,
-    ],
-  );
-  const chatStore = useChatStore();
-  const loading = !useHasHydrated();
-  const [showSideBar, setShowSideBar] = useState(true);
-
-  // setting
-  const [openSettings, setOpenSettings] = useState(false);
+function WideScreen() {
   const config = useChatStore((state) => state.config);
 
-  // drag side bar
-  const { onDragMouseDown } = useDragSideBar();
-  const isMobileScreen = useMobileScreen();
-
-  useSwitchTheme();
-
-  if (loading) {
-    return <Loading />;
-  }
-
   return (
     <div
       className={`${
-        config.tightBorder && !isMobileScreen
-          ? styles["tight-container"]
-          : styles.container
+        config.tightBorder ? styles["tight-container"] : styles.container
       }`}
     >
-      <div
-        className={styles.sidebar + ` ${showSideBar && styles["sidebar-show"]}`}
-      >
-        <div className={styles["sidebar-header"]}>
-          <div className={styles["sidebar-title"]}>ChatGPT Next</div>
-          <div className={styles["sidebar-sub-title"]}>
-            Build your own AI assistant.
-          </div>
-          <div className={styles["sidebar-logo"]}>
-            <ChatGptIcon />
-          </div>
-        </div>
+      <SideBar />
 
-        <div
-          className={styles["sidebar-body"]}
-          onClick={() => {
-            setOpenSettings(false);
-            setShowSideBar(false);
-          }}
-        >
-          <ChatList />
-        </div>
+      <div className={styles["window-content"]}>
+        <Routes>
+          <Route path={Path.Home} element={<Chat />} />
+          <Route path={Path.Chat} element={<Chat />} />
+          <Route path={Path.Settings} element={<Settings />} />
+        </Routes>
+      </div>
+    </div>
+  );
+}
 
-        <div className={styles["sidebar-tail"]}>
-          <div className={styles["sidebar-actions"]}>
-            <div className={styles["sidebar-action"] + " " + styles.mobile}>
-              <IconButton
-                icon={<CloseIcon />}
-                onClick={chatStore.deleteSession}
-              />
-            </div>
-            <div className={styles["sidebar-action"]}>
-              <IconButton
-                icon={<SettingsIcon />}
-                onClick={() => {
-                  setOpenSettings(true);
-                  setShowSideBar(false);
-                }}
-                shadow
-              />
-            </div>
-            <div className={styles["sidebar-action"]}>
-              <a href={REPO_URL} target="_blank">
-                <IconButton icon={<GithubIcon />} shadow />
-              </a>
-            </div>
-          </div>
-          <div>
-            <IconButton
-              icon={<AddIcon />}
-              text={Locale.Home.NewChat}
-              onClick={() => {
-                createNewSession();
-                setShowSideBar(false);
-              }}
-              shadow
-            />
-          </div>
-        </div>
+function MobileScreen() {
+  const location = useLocation();
+  const isHome = location.pathname === Path.Home;
 
-        <div
-          className={styles["sidebar-drag"]}
-          onMouseDown={(e) => onDragMouseDown(e as any)}
-        ></div>
-      </div>
+  return (
+    <div className={styles.container}>
+      <SideBar className={isHome ? styles["sidebar-show"] : ""} />
 
       <div className={styles["window-content"]}>
-        {openSettings ? (
-          <Settings
-            closeSettings={() => {
-              setOpenSettings(false);
-              setShowSideBar(true);
-            }}
-          />
-        ) : (
-          <Chat
-            key="chat"
-            showSideBar={() => setShowSideBar(true)}
-            sideBarShowing={showSideBar}
-          />
-        )}
+        <Routes>
+          <Route path={Path.Home} element={null} />
+          <Route path={Path.Chat} element={<Chat />} />
+          <Route path={Path.Settings} element={<Settings />} />
+        </Routes>
       </div>
     </div>
   );
 }
 
 export function Home() {
+  const isMobileScreen = useMobileScreen();
+  useSwitchTheme();
+
+  if (!useHasHydrated()) {
+    return <Loading />;
+  }
+
   return (
     <ErrorBoundary>
-      <_Home></_Home>
+      <Router>{isMobileScreen ? <MobileScreen /> : <WideScreen />}</Router>
     </ErrorBoundary>
   );
 }

+ 6 - 4
app/components/settings.tsx

@@ -29,10 +29,11 @@ import { Avatar } from "./chat";
 import Locale, { AllLangs, changeLang, getLang } from "../locales";
 import { copyToClipboard, getEmojiUrl } from "../utils";
 import Link from "next/link";
-import { UPDATE_URL } from "../constant";
+import { Path, UPDATE_URL } from "../constant";
 import { Prompt, SearchService, usePromptStore } from "../store/prompt";
 import { ErrorBoundary } from "./error";
 import { InputRange } from "./input-range";
+import { useNavigate } from "react-router-dom";
 
 function UserPromptModal(props: { onClose?: () => void }) {
   const promptStore = usePromptStore();
@@ -176,7 +177,8 @@ function PasswordInput(props: HTMLProps<HTMLInputElement>) {
   );
 }
 
-export function Settings(props: { closeSettings: () => void }) {
+export function Settings() {
+  const navigate = useNavigate();
   const [showEmojiPicker, setShowEmojiPicker] = useState(false);
   const [config, updateConfig, resetConfig, clearAllData, clearSessions] =
     useChatStore((state) => [
@@ -235,7 +237,7 @@ export function Settings(props: { closeSettings: () => void }) {
   useEffect(() => {
     const keydownEvent = (e: KeyboardEvent) => {
       if (e.key === "Escape") {
-        props.closeSettings();
+        navigate(Path.Home);
       }
     };
     document.addEventListener("keydown", keydownEvent);
@@ -290,7 +292,7 @@ export function Settings(props: { closeSettings: () => void }) {
           <div className={styles["window-action-button"]}>
             <IconButton
               icon={<CloseIcon />}
-              onClick={props.closeSettings}
+              onClick={() => navigate(Path.Home)}
               bordered
               title={Locale.Settings.Actions.Close}
             />

+ 146 - 0
app/components/sidebar.tsx

@@ -0,0 +1,146 @@
+import { useState, useEffect, useRef } from "react";
+
+import styles from "./home.module.scss";
+
+import { IconButton } from "./button";
+import SettingsIcon from "../icons/settings.svg";
+import GithubIcon from "../icons/github.svg";
+import ChatGptIcon from "../icons/chatgpt.svg";
+import AddIcon from "../icons/add.svg";
+import CloseIcon from "../icons/close.svg";
+import Locale from "../locales";
+
+import { useChatStore } from "../store";
+
+import {
+  MAX_SIDEBAR_WIDTH,
+  MIN_SIDEBAR_WIDTH,
+  NARROW_SIDEBAR_WIDTH,
+  Path,
+  REPO_URL,
+} from "../constant";
+
+import { HashRouter as Router, Link, useNavigate } from "react-router-dom";
+import { useMobileScreen } from "../utils";
+import { ChatList } from "./chat-list";
+
+function useDragSideBar() {
+  const limit = (x: number) => Math.min(MAX_SIDEBAR_WIDTH, x);
+
+  const chatStore = useChatStore();
+  const startX = useRef(0);
+  const startDragWidth = useRef(chatStore.config.sidebarWidth ?? 300);
+  const lastUpdateTime = useRef(Date.now());
+
+  const handleMouseMove = useRef((e: MouseEvent) => {
+    if (Date.now() < lastUpdateTime.current + 50) {
+      return;
+    }
+    lastUpdateTime.current = Date.now();
+    const d = e.clientX - startX.current;
+    const nextWidth = limit(startDragWidth.current + d);
+    chatStore.updateConfig((config) => (config.sidebarWidth = nextWidth));
+  });
+
+  const handleMouseUp = useRef(() => {
+    startDragWidth.current = chatStore.config.sidebarWidth ?? 300;
+    window.removeEventListener("mousemove", handleMouseMove.current);
+    window.removeEventListener("mouseup", handleMouseUp.current);
+  });
+
+  const onDragMouseDown = (e: MouseEvent) => {
+    startX.current = e.clientX;
+
+    window.addEventListener("mousemove", handleMouseMove.current);
+    window.addEventListener("mouseup", handleMouseUp.current);
+  };
+  const isMobileScreen = useMobileScreen();
+  const shouldNarrow =
+    !isMobileScreen && chatStore.config.sidebarWidth < MIN_SIDEBAR_WIDTH;
+
+  useEffect(() => {
+    const barWidth = shouldNarrow
+      ? NARROW_SIDEBAR_WIDTH
+      : limit(chatStore.config.sidebarWidth ?? 300);
+    const sideBarWidth = isMobileScreen ? "100vw" : `${barWidth}px`;
+    document.documentElement.style.setProperty("--sidebar-width", sideBarWidth);
+  }, [chatStore.config.sidebarWidth, isMobileScreen, shouldNarrow]);
+
+  return {
+    onDragMouseDown,
+    shouldNarrow,
+  };
+}
+
+export function SideBar(props: { className?: string }) {
+  const chatStore = useChatStore();
+
+  // drag side bar
+  const { onDragMouseDown, shouldNarrow } = useDragSideBar();
+  const navigate = useNavigate();
+
+  return (
+    <div
+      className={`${styles.sidebar} ${props.className} ${
+        shouldNarrow && styles["narrow-sidebar"]
+      }`}
+    >
+      <div className={styles["sidebar-header"]}>
+        <div className={styles["sidebar-title"]}>ChatGPT Next</div>
+        <div className={styles["sidebar-sub-title"]}>
+          Build your own AI assistant.
+        </div>
+        <div className={styles["sidebar-logo"]}>
+          <ChatGptIcon />
+        </div>
+      </div>
+
+      <div
+        className={styles["sidebar-body"]}
+        onClick={(e) => {
+          if (e.target === e.currentTarget) {
+            navigate(Path.Home);
+          }
+        }}
+      >
+        <ChatList narrow={shouldNarrow} />
+      </div>
+
+      <div className={styles["sidebar-tail"]}>
+        <div className={styles["sidebar-actions"]}>
+          <div className={styles["sidebar-action"] + " " + styles.mobile}>
+            <IconButton
+              icon={<CloseIcon />}
+              onClick={chatStore.deleteSession}
+            />
+          </div>
+          <div className={styles["sidebar-action"]}>
+            <Link to={Path.Settings}>
+              <IconButton icon={<SettingsIcon />} shadow />
+            </Link>
+          </div>
+          <div className={styles["sidebar-action"]}>
+            <a href={REPO_URL} target="_blank">
+              <IconButton icon={<GithubIcon />} shadow />
+            </a>
+          </div>
+        </div>
+        <div>
+          <IconButton
+            icon={<AddIcon />}
+            text={shouldNarrow ? undefined : Locale.Home.NewChat}
+            onClick={() => {
+              chatStore.newSession();
+            }}
+            shadow
+          />
+        </div>
+      </div>
+
+      <div
+        className={styles["sidebar-drag"]}
+        onMouseDown={(e) => onDragMouseDown(e as any)}
+      ></div>
+    </div>
+  );
+}

+ 10 - 0
app/constant.ts

@@ -6,3 +6,13 @@ export const UPDATE_URL = `${REPO_URL}#keep-updated`;
 export const FETCH_COMMIT_URL = `https://api.github.com/repos/${OWNER}/${REPO}/commits?per_page=1`;
 export const FETCH_TAG_URL = `https://api.github.com/repos/${OWNER}/${REPO}/tags?per_page=1`;
 export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
+
+export enum Path {
+  Home = "/",
+  Chat = "/chat",
+  Settings = "/settings",
+}
+
+export const MAX_SIDEBAR_WIDTH = 500;
+export const MIN_SIDEBAR_WIDTH = 230;
+export const NARROW_SIDEBAR_WIDTH = 100;

+ 4 - 1
app/utils.ts

@@ -49,7 +49,7 @@ export function isIOS() {
 }
 
 export function useMobileScreen() {
-  const [isMobileScreen_, setIsMobileScreen] = useState(false);
+  const [isMobileScreen_, setIsMobileScreen] = useState(isMobileScreen());
   useEffect(() => {
     const onResize = () => {
       setIsMobileScreen(isMobileScreen());
@@ -66,6 +66,9 @@ export function useMobileScreen() {
 }
 
 export function isMobileScreen() {
+  if (typeof window === "undefined") {
+    return false;
+  }
   return window.innerWidth <= 600;
 }
 

+ 1 - 0
package.json

@@ -25,6 +25,7 @@
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "react-markdown": "^8.0.5",
+    "react-router-dom": "^6.10.0",
     "rehype-highlight": "^6.0.0",
     "rehype-katex": "^6.0.2",
     "remark-breaks": "^3.0.2",

+ 20 - 0
yarn.lock

@@ -1189,6 +1189,11 @@
     tiny-glob "^0.2.9"
     tslib "^2.4.0"
 
+"@remix-run/router@1.5.0":
+  version "1.5.0"
+  resolved "https://registry.npmmirror.com/@remix-run/router/-/router-1.5.0.tgz#57618e57942a5f0131374a9fdb0167e25a117fdc"
+  integrity sha512-bkUDCp8o1MvFO+qxkODcbhSqRa6P2GXgrGZVpt0dCXNW2HCSCqYI0ZoAqEOSAjRWmmlKcYgFvN4B4S+zo/f8kg==
+
 "@rushstack/eslint-patch@^1.1.3":
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.2.0.tgz#8be36a1f66f3265389e90b5f9c9962146758f728"
@@ -4296,6 +4301,21 @@ react-redux@^8.0.4:
     react-is "^18.0.0"
     use-sync-external-store "^1.0.0"
 
+react-router-dom@^6.10.0:
+  version "6.10.0"
+  resolved "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.10.0.tgz#090ddc5c84dc41b583ce08468c4007c84245f61f"
+  integrity sha512-E5dfxRPuXKJqzwSe/qGcqdwa18QiWC6f3H3cWXM24qj4N0/beCIf/CWTipop2xm7mR0RCS99NnaqPNjHtrAzCg==
+  dependencies:
+    "@remix-run/router" "1.5.0"
+    react-router "6.10.0"
+
+react-router@6.10.0:
+  version "6.10.0"
+  resolved "https://registry.npmmirror.com/react-router/-/react-router-6.10.0.tgz#230f824fde9dd0270781b5cb497912de32c0a971"
+  integrity sha512-Nrg0BWpQqrC3ZFFkyewrflCud9dio9ME3ojHCF/WLsprJVzkq3q3UeEhMCAW1dobjeGbWgjNn/PVF6m46ANxXQ==
+  dependencies:
+    "@remix-run/router" "1.5.0"
+
 react@^18.2.0:
   version "18.2.0"
   resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"