diff --git a/web/package.json b/web/package.json index 3c3f5bf7..2ceebc09 100644 --- a/web/package.json +++ b/web/package.json @@ -8,9 +8,11 @@ "lint": "eslint --ext .js,.ts,.tsx, src" }, "dependencies": { + "@reduxjs/toolkit": "^1.8.1", "lodash-es": "^4.17.21", "react": "^18.1.0", - "react-dom": "^18.1.0" + "react-dom": "^18.1.0", + "react-redux": "^8.0.1" }, "devDependencies": { "@types/lodash-es": "^4.17.5", diff --git a/web/src/App.tsx b/web/src/App.tsx index c16d16f4..0fe4dc72 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,12 +1,9 @@ -import { useContext } from "react"; -import appContext from "./stores/appContext"; import { appRouterSwitch } from "./routers"; +import { useAppSelector } from "./store"; import "./less/app.less"; function App() { - const { - locationState: { pathname }, - } = useContext(appContext); + const pathname = useAppSelector((state) => state.location.pathname); return <>{appRouterSwitch(pathname)}; } diff --git a/web/src/components/CreateShortcutDialog.tsx b/web/src/components/CreateShortcutDialog.tsx index 72749b3f..d24a6018 100644 --- a/web/src/components/CreateShortcutDialog.tsx +++ b/web/src/components/CreateShortcutDialog.tsx @@ -48,11 +48,9 @@ const CreateShortcutDialog: React.FC = (props: Props) => { try { if (shortcutId) { - const editedShortcut = await shortcutService.updateShortcut(shortcutId, title, JSON.stringify(filters)); - shortcutService.editShortcut(shortcutService.convertResponseModelShortcut(editedShortcut)); + await shortcutService.updateShortcut(shortcutId, title, JSON.stringify(filters)); } else { - const shortcut = await shortcutService.createShortcut(title, JSON.stringify(filters)); - shortcutService.pushShortcut(shortcutService.convertResponseModelShortcut(shortcut)); + await shortcutService.createShortcut(title, JSON.stringify(filters)); } } catch (error: any) { toastHelper.error(error.message); diff --git a/web/src/components/Dialog.tsx b/web/src/components/Dialog.tsx index 0e449896..5c6e0847 100644 --- a/web/src/components/Dialog.tsx +++ b/web/src/components/Dialog.tsx @@ -1,7 +1,6 @@ import { createRoot } from "react-dom/client"; -import appContext from "../stores/appContext"; -import Provider from "../labs/Provider"; -import appStore from "../stores/appStore"; +import { Provider } from "react-redux"; +import store from "../store"; import { ANIMATION_DURATION } from "../helpers/consts"; import "../less/dialog.less"; @@ -69,11 +68,7 @@ export function showDialog( ); if (config.useAppContext) { - Fragment = ( - - {Fragment} - - ); + Fragment = {Fragment}; } dialog.render(Fragment); diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx index 83860988..d584b010 100644 --- a/web/src/components/Memo.tsx +++ b/web/src/components/Memo.tsx @@ -4,7 +4,7 @@ import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG, UNKNOWN_ID } from "../ import { parseMarkedToHtml, parseRawTextToHtml } from "../helpers/marked"; import utils from "../helpers/utils"; import useToggle from "../hooks/useToggle"; -import { globalStateService, memoService } from "../services"; +import { editorStateService, memoService } from "../services"; import Only from "./common/OnlyWhen"; import Image from "./Image"; import showMemoCardDialog from "./MemoCardDialog"; @@ -50,23 +50,23 @@ const Memo: React.FC = (props: Props) => { }; const handleMarkMemoClick = () => { - globalStateService.setMarkMemoId(memo.id); + editorStateService.setMarkMemo(memo.id); }; const handleEditMemoClick = () => { - globalStateService.setEditMemoId(memo.id); + editorStateService.setEditMemo(memo.id); }; const handleDeleteMemoClick = async () => { if (showConfirmDeleteBtn) { try { - await memoService.hideMemoById(memo.id); + await memoService.archiveMemoById(memo.id); } catch (error: any) { toastHelper.error(error.message); } - if (globalStateService.getState().editMemoId === memo.id) { - globalStateService.setEditMemoId(UNKNOWN_ID); + if (editorStateService.getState().editMemoId === memo.id) { + editorStateService.setEditMemo(UNKNOWN_ID); } } else { toggleConfirmDeleteBtn(); @@ -163,15 +163,9 @@ export function formatMemoContent(content: string) { }) .join(""); - const { shouldUseMarkdownParser, shouldSplitMemoWord, shouldHideImageUrl } = globalStateService.getState(); + content = parseMarkedToHtml(content); - if (shouldUseMarkdownParser) { - content = parseMarkedToHtml(content); - } - - if (shouldHideImageUrl) { - content = content.replace(IMAGE_URL_REG, ""); - } + content = content.replace(IMAGE_URL_REG, ""); content = content .replace(TAG_REG, "#$1") @@ -179,11 +173,7 @@ export function formatMemoContent(content: string) { .replace(MEMO_LINK_REG, "$1"); // Add space in english and chinese - if (shouldSplitMemoWord) { - content = content - .replace(/([\u4e00-\u9fa5])([A-Za-z0-9?.,;[\]]+)/g, "$1 $2") - .replace(/([A-Za-z0-9?.,;[\]]+)([\u4e00-\u9fa5])/g, "$1 $2"); - } + content = content.replace(/([\u4e00-\u9fa5])([A-Za-z0-9?.,;[\]]+)/g, "$1 $2").replace(/([A-Za-z0-9?.,;[\]]+)([\u4e00-\u9fa5])/g, "$1 $2"); const tempDivContainer = document.createElement("div"); tempDivContainer.innerHTML = content; diff --git a/web/src/components/MemoCardDialog.tsx b/web/src/components/MemoCardDialog.tsx index 361d1329..0a4a5e14 100644 --- a/web/src/components/MemoCardDialog.tsx +++ b/web/src/components/MemoCardDialog.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useCallback } from "react"; import { IMAGE_URL_REG, MEMO_LINK_REG, UNKNOWN_ID } from "../helpers/consts"; import utils from "../helpers/utils"; -import { globalStateService, memoService } from "../services"; +import { editorStateService, memoService } from "../services"; import { parseHtmlToRawText } from "../helpers/marked"; import { formatMemoContent } from "./Memo"; import toastHelper from "./Toast"; @@ -96,7 +96,7 @@ const MemoCardDialog: React.FC = (props: Props) => { const handleEditMemoBtnClick = useCallback(() => { props.destroy(); - globalStateService.setEditMemoId(memo.id); + editorStateService.setEditMemo(memo.id); }, [memo.id]); return ( diff --git a/web/src/components/MemoEditor.tsx b/web/src/components/MemoEditor.tsx index 149d69f3..a551950e 100644 --- a/web/src/components/MemoEditor.tsx +++ b/web/src/components/MemoEditor.tsx @@ -1,6 +1,6 @@ -import React, { useCallback, useContext, useEffect, useMemo, useRef } from "react"; -import appContext from "../stores/appContext"; -import { globalStateService, locationService, memoService, resourceService } from "../services"; +import React, { useCallback, useEffect, useMemo, useRef } from "react"; +import { editorStateService, locationService, memoService, resourceService } from "../services"; +import { useAppSelector } from "../store"; import { UNKNOWN_ID } from "../helpers/consts"; import { storage } from "../helpers/storage"; import useToggle from "../hooks/useToggle"; @@ -44,32 +44,36 @@ interface Props {} const MemoEditor: React.FC = () => { const { - globalState, - memoState: { tags }, - } = useContext(appContext); + editor: editorState, + memo: { tags }, + } = useAppSelector((state) => state); const [isTagSeletorShown, toggleTagSeletor] = useToggle(false); const editorRef = useRef(null); - const prevGlobalStateRef = useRef(globalState); + const prevGlobalStateRef = useRef(editorState); const tagSeletorRef = useRef(null); useEffect(() => { - if (globalState.markMemoId !== UNKNOWN_ID) { + if (editorState.markMemoId && editorState.markMemoId !== UNKNOWN_ID) { const editorCurrentValue = editorRef.current?.getContent(); - const memoLinkText = `${editorCurrentValue ? "\n" : ""}Mark: [@MEMO](${globalState.markMemoId})`; + const memoLinkText = `${editorCurrentValue ? "\n" : ""}Mark: [@MEMO](${editorState.markMemoId})`; editorRef.current?.insertText(memoLinkText); - globalStateService.setMarkMemoId(UNKNOWN_ID); + editorStateService.setMarkMemo(UNKNOWN_ID); } - if (globalState.editMemoId !== UNKNOWN_ID && globalState.editMemoId !== prevGlobalStateRef.current.editMemoId) { - const editMemo = memoService.getMemoById(globalState.editMemoId); + if ( + editorState.editMemoId && + editorState.editMemoId !== UNKNOWN_ID && + editorState.editMemoId !== prevGlobalStateRef.current.editMemoId + ) { + const editMemo = memoService.getMemoById(editorState.editMemoId ?? UNKNOWN_ID); if (editMemo) { editorRef.current?.setContent(editMemo.content ?? ""); editorRef.current?.focus(); } } - prevGlobalStateRef.current = globalState; - }, [globalState.markMemoId, globalState.editMemoId]); + prevGlobalStateRef.current = editorState; + }, [editorState.markMemoId, editorState.editMemoId]); useEffect(() => { if (!editorRef.current) { @@ -144,18 +148,18 @@ const MemoEditor: React.FC = () => { return; } - const { editMemoId } = globalStateService.getState(); + const { editMemoId } = editorStateService.getState(); try { - if (editMemoId !== UNKNOWN_ID) { - const prevMemo = memoService.getMemoById(editMemoId); + if (editMemoId && editMemoId !== UNKNOWN_ID) { + const prevMemo = memoService.getMemoById(editMemoId ?? UNKNOWN_ID); if (prevMemo && prevMemo.content !== content) { const editedMemo = await memoService.updateMemo(prevMemo.id, content); editedMemo.createdTs = Date.now(); memoService.editMemo(editedMemo); } - globalStateService.setEditMemoId(UNKNOWN_ID); + editorStateService.setEditMemo(UNKNOWN_ID); } else { const newMemo = await memoService.createMemo(content); memoService.pushMemo(newMemo); @@ -169,7 +173,7 @@ const MemoEditor: React.FC = () => { }, []); const handleCancelBtnClick = useCallback(() => { - globalStateService.setEditMemoId(UNKNOWN_ID); + editorStateService.setEditMemo(UNKNOWN_ID); editorRef.current?.setContent(""); setEditorContentCache(""); }, []); @@ -259,7 +263,7 @@ const MemoEditor: React.FC = () => { } }, []); - const isEditing = globalState.editMemoId !== UNKNOWN_ID; + const isEditing = Boolean(editorState.editMemoId && editorState.editMemoId !== UNKNOWN_ID); const editorConfig = useMemo( () => ({ diff --git a/web/src/components/MemoFilter.tsx b/web/src/components/MemoFilter.tsx index 9e8b4842..ac7cd29b 100644 --- a/web/src/components/MemoFilter.tsx +++ b/web/src/components/MemoFilter.tsx @@ -1,5 +1,4 @@ -import { useContext } from "react"; -import appContext from "../stores/appContext"; +import { useAppSelector } from "../store"; import { locationService, shortcutService } from "../services"; import utils from "../helpers/utils"; import { getTextWithMemoType } from "../helpers/filter"; @@ -9,10 +8,9 @@ interface FilterProps {} const MemoFilter: React.FC = () => { const { - locationState: { query }, - } = useContext(appContext); - - const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query; + location: { query }, + } = useAppSelector((state) => state); + const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query ?? {}; const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null; const showFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut); @@ -38,7 +36,7 @@ const MemoFilter: React.FC = () => {
{ - locationService.setMemoTypeQuery(""); + locationService.setMemoTypeQuery(undefined); }} > 📦 {getTextWithMemoType(memoType as MemoSpecType)} diff --git a/web/src/components/MemoList.tsx b/web/src/components/MemoList.tsx index 41ca052b..bddcc1ed 100644 --- a/web/src/components/MemoList.tsx +++ b/web/src/components/MemoList.tsx @@ -1,6 +1,6 @@ -import { useCallback, useContext, useEffect, useRef, useState } from "react"; -import appContext from "../stores/appContext"; +import { useCallback, useEffect, useRef, useState } from "react"; import { locationService, memoService, shortcutService } from "../services"; +import { useAppSelector } from "../store"; import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/consts"; import utils from "../helpers/utils"; import { checkShouldShowMemoWithFilters } from "../helpers/filter"; @@ -12,13 +12,13 @@ interface Props {} const MemoList: React.FC = () => { const { - locationState: { query }, - memoState: { memos }, - } = useContext(appContext); + location: { query }, + memo: { memos }, + } = useAppSelector((state) => state); const [isFetching, setFetchStatus] = useState(true); const wrapperElement = useRef(null); - const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query; + const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId } = query ?? {}; const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null; const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut); @@ -78,7 +78,7 @@ const MemoList: React.FC = () => { const pinnedMemos = shownMemos.filter((m) => m.pinned); const unpinnedMemos = shownMemos.filter((m) => !m.pinned); - const sortedMemos = pinnedMemos.concat(unpinnedMemos); + const sortedMemos = pinnedMemos.concat(unpinnedMemos).filter((m) => m.rowStatus === "NORMAL"); useEffect(() => { memoService @@ -100,7 +100,7 @@ const MemoList: React.FC = () => { const targetEl = event.target as HTMLElement; if (targetEl.tagName === "SPAN" && targetEl.className === "tag-span") { const tagName = targetEl.innerText.slice(1); - const currTagQuery = locationService.getState().query.tag; + const currTagQuery = locationService.getState().query?.tag; if (currTagQuery === tagName) { locationService.setTagQuery(""); } else { diff --git a/web/src/components/MemosHeader.tsx b/web/src/components/MemosHeader.tsx index 089381ae..0b05fb45 100644 --- a/web/src/components/MemosHeader.tsx +++ b/web/src/components/MemosHeader.tsx @@ -1,5 +1,5 @@ -import { useCallback, useContext, useEffect, useState } from "react"; -import appContext from "../stores/appContext"; +import { useCallback, useEffect, useState } from "react"; +import { useAppSelector } from "../store"; import SearchBar from "./SearchBar"; import { memoService, shortcutService } from "../services"; import "../less/memos-header.less"; @@ -10,25 +10,23 @@ interface Props {} const MemosHeader: React.FC = () => { const { - locationState: { - query: { shortcutId }, - }, - shortcutState: { shortcuts }, - } = useContext(appContext); + location: { query }, + shortcut: { shortcuts }, + } = useAppSelector((state) => state); const [titleText, setTitleText] = useState("MEMOS"); useEffect(() => { - if (!shortcutId) { + if (!query?.shortcutId) { setTitleText("MEMOS"); return; } - const shortcut = shortcutService.getShortcutById(shortcutId); + const shortcut = shortcutService.getShortcutById(query?.shortcutId); if (shortcut) { setTitleText(shortcut.title); } - }, [shortcutId, shortcuts]); + }, [query, shortcuts]); const handleMemoTextClick = useCallback(() => { const now = Date.now(); diff --git a/web/src/components/SearchBar.tsx b/web/src/components/SearchBar.tsx index 0aaeb93b..8a071707 100644 --- a/web/src/components/SearchBar.tsx +++ b/web/src/components/SearchBar.tsx @@ -1,22 +1,17 @@ -import { useContext } from "react"; -import appContext from "../stores/appContext"; import { locationService } from "../services"; +import { useAppSelector } from "../store"; import { memoSpecialTypes } from "../helpers/filter"; import "../less/search-bar.less"; interface Props {} const SearchBar: React.FC = () => { - const { - locationState: { - query: { type: memoType }, - }, - } = useContext(appContext); + const memoType = useAppSelector((state) => state.location.query?.type); - const handleMemoTypeItemClick = (type: MemoSpecType | "") => { - const { type: prevType } = locationService.getState().query; + const handleMemoTypeItemClick = (type: MemoSpecType | undefined) => { + const { type: prevType } = locationService.getState().query ?? {}; if (type === prevType) { - type = ""; + type = undefined; } locationService.setMemoTypeQuery(type); }; diff --git a/web/src/components/SettingDialog.tsx b/web/src/components/SettingDialog.tsx index 142d0555..e0718aa7 100644 --- a/web/src/components/SettingDialog.tsx +++ b/web/src/components/SettingDialog.tsx @@ -1,5 +1,5 @@ -import { useContext, useState } from "react"; -import appContext from "../stores/appContext"; +import { useState } from "react"; +import { useAppSelector } from "../store"; import { showDialog } from "./Dialog"; import MyAccountSection from "./Settings/MyAccountSection"; import PreferencesSection from "./Settings/PreferencesSection"; @@ -16,8 +16,8 @@ interface State { const SettingDialog: React.FC = (props: Props) => { const { - userState: { user }, - } = useContext(appContext); + user: { user }, + } = useAppSelector((state) => state); const { destroy } = props; const [state, setState] = useState({ selectedSection: "my-account", diff --git a/web/src/components/Settings/MyAccountSection.tsx b/web/src/components/Settings/MyAccountSection.tsx index a534a8a3..ea3d4f43 100644 --- a/web/src/components/Settings/MyAccountSection.tsx +++ b/web/src/components/Settings/MyAccountSection.tsx @@ -1,5 +1,5 @@ -import { useContext, useState } from "react"; -import appContext from "../../stores/appContext"; +import { useState } from "react"; +import { useAppSelector } from "../../store"; import { userService } from "../../services"; import { validate, ValidatorConfig } from "../../helpers/validator"; import toastHelper from "../Toast"; @@ -17,7 +17,7 @@ const validateConfig: ValidatorConfig = { interface Props {} const MyAccountSection: React.FC = () => { - const { userState } = useContext(appContext); + const { user: userState } = useAppSelector((state) => state); const user = userState.user as User; const [username, setUsername] = useState(user.name); const openAPIRoute = `${window.location.origin}/h/${user.openId}/memo`; diff --git a/web/src/components/ShortcutList.tsx b/web/src/components/ShortcutList.tsx index 7b001a27..def1555d 100644 --- a/web/src/components/ShortcutList.tsx +++ b/web/src/components/ShortcutList.tsx @@ -1,6 +1,6 @@ -import { useContext, useEffect } from "react"; +import { useEffect } from "react"; import { locationService, shortcutService } from "../services"; -import appContext from "../stores/appContext"; +import { useAppSelector } from "../store"; import { UNKNOWN_ID } from "../helpers/consts"; import utils from "../helpers/utils"; import useToggle from "../hooks/useToggle"; @@ -13,11 +13,9 @@ interface Props {} const ShortcutList: React.FC = () => { const { - shortcutState: { shortcuts }, - locationState: { - query: { shortcutId }, - }, - } = useContext(appContext); + location: { query }, + shortcut: { shortcuts }, + } = useAppSelector((state) => state); const loadingState = useLoading(); const pinnedShortcuts = shortcuts .filter((s) => s.rowStatus === "ARCHIVED") @@ -48,7 +46,7 @@ const ShortcutList: React.FC = () => {

{sortedShortcuts.map((s) => { - return ; + return ; })}
@@ -80,7 +78,7 @@ const ShortcutContainer: React.FC = (props: ShortcutCont if (showConfirmDeleteBtn) { try { - await shortcutService.deleteShortcut(shortcut.id); + await shortcutService.deleteShortcutById(shortcut.id); } catch (error: any) { toastHelper.error(error.message); } diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx index 1800b4fb..6a32502c 100644 --- a/web/src/components/Sidebar.tsx +++ b/web/src/components/Sidebar.tsx @@ -1,5 +1,4 @@ -import { useContext } from "react"; -import appContext from "../stores/appContext"; +import { useAppSelector } from "../store"; import utils from "../helpers/utils"; import showDailyMemoDiaryDialog from "./DailyMemoDiaryDialog"; import showSettingDialog from "./SettingDialog"; @@ -14,9 +13,9 @@ interface Props {} const Sidebar: React.FC = () => { const { - memoState: { memos, tags }, - userState: { user }, - } = useContext(appContext); + memo: { memos, tags }, + user: { user }, + } = useAppSelector((state) => state); const createdDays = user ? Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24) : 0; const handleMyAccountBtnClick = () => { diff --git a/web/src/components/TagList.tsx b/web/src/components/TagList.tsx index 46ca0654..9ab38ede 100644 --- a/web/src/components/TagList.tsx +++ b/web/src/components/TagList.tsx @@ -1,5 +1,5 @@ -import { useContext, useEffect, useState } from "react"; -import appContext from "../stores/appContext"; +import { useEffect, useState } from "react"; +import { useAppSelector } from "../store"; import { locationService, memoService } from "../services"; import useToggle from "../hooks/useToggle"; import Only from "./common/OnlyWhen"; @@ -16,11 +16,9 @@ interface Props {} const TagList: React.FC = () => { const { - locationState: { - query: { tag: tagQuery }, - }, - memoState: { tags: tagsText, memos }, - } = useContext(appContext); + location: { query }, + memo: { memos, tags: tagsText }, + } = useAppSelector((state) => state); const [tags, setTags] = useState([]); useEffect(() => { @@ -73,9 +71,9 @@ const TagList: React.FC = () => {

Tags

{tags.map((t, idx) => ( - + ))} - +

Enter #tag to create a tag

@@ -87,7 +85,7 @@ const TagList: React.FC = () => { interface TagItemContainerProps { tag: Tag; - tagQuery: string; + tagQuery?: string; } const TagItemContainer: React.FC = (props: TagItemContainerProps) => { diff --git a/web/src/components/UsageHeatMap.tsx b/web/src/components/UsageHeatMap.tsx index fe659d07..972a611c 100644 --- a/web/src/components/UsageHeatMap.tsx +++ b/web/src/components/UsageHeatMap.tsx @@ -1,5 +1,5 @@ -import { useCallback, useContext, useEffect, useRef, useState } from "react"; -import appContext from "../stores/appContext"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useAppSelector } from "../store"; import { locationService } from "../services"; import { DAILY_TIMESTAMP } from "../helpers/consts"; import utils from "../helpers/utils"; @@ -36,8 +36,8 @@ const UsageHeatMap: React.FC = () => { const beginDayTimestemp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP; const { - memoState: { memos }, - } = useContext(appContext); + memo: { memos }, + } = useAppSelector((state) => state); const [allStat, setAllStat] = useState(getInitialUsageStat(usedDaysAmount, beginDayTimestemp)); const [popupStat, setPopupStat] = useState(null); const [currentStat, setCurrentStat] = useState(null); @@ -71,7 +71,7 @@ const UsageHeatMap: React.FC = () => { }, []); const handleUsageStatItemClick = useCallback((item: DailyUsageStat) => { - if (locationService.getState().query.duration?.from === item.timestamp) { + if (locationService.getState().query?.duration?.from === item.timestamp) { locationService.setFromAndToQuery(0, 0); setCurrentStat(null); } else if (item.count > 0) { diff --git a/web/src/components/UserBanner.tsx b/web/src/components/UserBanner.tsx index 2fb5d5b3..f8fe01cb 100644 --- a/web/src/components/UserBanner.tsx +++ b/web/src/components/UserBanner.tsx @@ -1,5 +1,5 @@ -import { useCallback, useContext, useState } from "react"; -import appContext from "../stores/appContext"; +import { useCallback, useState } from "react"; +import { useAppSelector } from "../store"; import { locationService } from "../services"; import MenuBtnsPopup from "./MenuBtnsPopup"; import "../less/user-banner.less"; @@ -8,8 +8,8 @@ interface Props {} const UserBanner: React.FC = () => { const { - userState: { user }, - } = useContext(appContext); + user: { user }, + } = useAppSelector((state) => state); const username = user ? user.name : "Memos"; const [shouldShowPopupBtns, setShouldShowPopupBtns] = useState(false); diff --git a/web/src/less/shortcut-list.less b/web/src/less/shortcut-list.less index 3ff54118..72ac7724 100644 --- a/web/src/less/shortcut-list.less +++ b/web/src/less/shortcut-list.less @@ -42,7 +42,7 @@ > .shortcut-container { .flex(row, space-between, center); - @apply w-full h-10 py-0 px-4 mt-2 rounded-lg text-base cursor-pointer select-none shrink-0; + @apply w-full h-10 py-0 px-4 mt-px first:mt-2 rounded-lg text-base cursor-pointer select-none shrink-0; &:hover { background-color: @bg-gray; diff --git a/web/src/main.tsx b/web/src/main.tsx index 683c8d5d..2efa1b8d 100644 --- a/web/src/main.tsx +++ b/web/src/main.tsx @@ -1,8 +1,8 @@ import React from "react"; import { createRoot } from "react-dom/client"; -import Provider from "./labs/Provider"; -import appContext from "./stores/appContext"; -import appStore from "./stores/appStore"; +import { Provider } from "react-redux"; +import store from "./store"; +import { updateStateWithLocation } from "./store/modules/location"; import App from "./App"; import "./helpers/polyfill"; import "./less/global.less"; @@ -12,8 +12,15 @@ const container = document.getElementById("root"); const root = createRoot(container as HTMLElement); root.render( - + ); + +window.onload = () => { + store.dispatch(updateStateWithLocation()); + window.onpopstate = () => { + store.dispatch(updateStateWithLocation()); + }; +}; diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 9c856b70..0e145eb1 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,15 +1,15 @@ -import { useContext, useEffect } from "react"; +import { useEffect } from "react"; import { locationService, userService } from "../services"; import { homeRouterSwitch } from "../routers"; -import appContext from "../stores/appContext"; +import { useAppSelector } from "../store"; import Sidebar from "../components/Sidebar"; import useLoading from "../hooks/useLoading"; import "../less/home.less"; function Home() { const { - locationState: { pathname }, - } = useContext(appContext); + location: { pathname }, + } = useAppSelector((state) => state); const loadingState = useLoading(); useEffect(() => { diff --git a/web/src/services/editorStateService.ts b/web/src/services/editorStateService.ts new file mode 100644 index 00000000..9960da67 --- /dev/null +++ b/web/src/services/editorStateService.ts @@ -0,0 +1,18 @@ +import store from "../store"; +import { setEditMemoId, setMarkMemoId } from "../store/modules/editor"; + +const editorStateService = { + getState: () => { + return store.getState().editor; + }, + + setEditMemo: (editMemoId: MemoId) => { + store.dispatch(setEditMemoId(editMemoId)); + }, + + setMarkMemo: (markMemoId: MemoId) => { + store.dispatch(setMarkMemoId(markMemoId)); + }, +}; + +export default editorStateService; diff --git a/web/src/services/globalStateService.ts b/web/src/services/globalStateService.ts deleted file mode 100644 index b7ab054a..00000000 --- a/web/src/services/globalStateService.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { storage } from "../helpers/storage"; -import appStore from "../stores/appStore"; -import { AppSetting } from "../stores/globalStateStore"; - -class GlobalStateService { - constructor() { - const cachedSetting = storage.get(["shouldSplitMemoWord", "shouldHideImageUrl", "shouldUseMarkdownParser"]); - const defaultAppSetting = { - shouldSplitMemoWord: cachedSetting.shouldSplitMemoWord ?? true, - shouldHideImageUrl: cachedSetting.shouldHideImageUrl ?? true, - shouldUseMarkdownParser: cachedSetting.shouldUseMarkdownParser ?? true, - }; - - this.setAppSetting(defaultAppSetting); - } - - public getState = () => { - return appStore.getState().globalState; - }; - - public setEditMemoId = (editMemoId: MemoId) => { - appStore.dispatch({ - type: "SET_EDIT_MEMO_ID", - payload: { - editMemoId, - }, - }); - }; - - public setMarkMemoId = (markMemoId: MemoId) => { - appStore.dispatch({ - type: "SET_MARK_MEMO_ID", - payload: { - markMemoId, - }, - }); - }; - - public setAppSetting = (appSetting: Partial) => { - appStore.dispatch({ - type: "SET_APP_SETTING", - payload: appSetting, - }); - storage.set(appSetting); - }; -} - -const globalStateService = new GlobalStateService(); - -export default globalStateService; diff --git a/web/src/services/index.ts b/web/src/services/index.ts index db94c4b8..4ecd0580 100644 --- a/web/src/services/index.ts +++ b/web/src/services/index.ts @@ -1,8 +1,8 @@ -import globalStateService from "./globalStateService"; +import editorStateService from "./editorStateService"; import locationService from "./locationService"; import memoService from "./memoService"; import shortcutService from "./shortcutService"; import userService from "./userService"; import resourceService from "./resourceService"; -export { globalStateService, locationService, memoService, shortcutService, userService, resourceService }; +export { editorStateService, locationService, memoService, shortcutService, userService, resourceService }; diff --git a/web/src/services/locationService.ts b/web/src/services/locationService.ts index 6e4881f2..27f087c2 100644 --- a/web/src/services/locationService.ts +++ b/web/src/services/locationService.ts @@ -1,9 +1,10 @@ import utils from "../helpers/utils"; -import appStore from "../stores/appStore"; +import store from "../store"; +import { setQuery, setPathname } from "../store/modules/location"; const updateLocationUrl = (method: "replace" | "push" = "replace") => { - const { query, pathname, hash } = appStore.getState().locationState; - let queryString = utils.transformObjectToParamsString(query); + const { query, pathname, hash } = store.getState().location; + let queryString = utils.transformObjectToParamsString(query ?? {}); if (queryString) { queryString = "?" + queryString; } else { @@ -17,180 +18,98 @@ const updateLocationUrl = (method: "replace" | "push" = "replace") => { } }; -class LocationService { - constructor() { - this.updateStateWithLocation(); - window.onpopstate = () => { - this.updateStateWithLocation(); - }; - } - - public updateStateWithLocation = () => { - const { pathname, search, hash } = window.location; - const urlParams = new URLSearchParams(search); - const state: AppLocation = { - pathname: "/", - hash: "", - query: { - tag: "", - duration: null, - text: "", - type: "", - }, - }; - state.query.tag = urlParams.get("tag") ?? ""; - state.query.type = (urlParams.get("type") ?? "") as MemoSpecType; - state.query.text = urlParams.get("text") ?? ""; - state.query.shortcutId = Number(urlParams.get("shortcutId")) ?? undefined; - const from = parseInt(urlParams.get("from") ?? "0"); - const to = parseInt(urlParams.get("to") ?? "0"); - if (to > from && to !== 0) { - state.query.duration = { - from, - to, - }; - } - state.hash = hash; - state.pathname = this.getValidPathname(pathname); - appStore.dispatch({ - type: "SET_LOCATION", - payload: state, - }); - }; - - public getState = () => { - return appStore.getState().locationState; - }; - - public clearQuery = () => { - appStore.dispatch({ - type: "SET_QUERY", - payload: { - tag: "", - duration: null, - text: "", - type: "", - }, - }); +const locationService = { + getState: () => { + return store.getState().location; + }, + clearQuery: () => { + store.dispatch(setQuery({})); updateLocationUrl(); - }; - - public setQuery = (query: Query) => { - appStore.dispatch({ - type: "SET_QUERY", - payload: query, - }); + }, + setQuery: (query: Query) => { + store.dispatch(setQuery(query)); updateLocationUrl(); - }; - - public setHash = (hash: string) => { - appStore.dispatch({ - type: "SET_HASH", - payload: { - hash, - }, - }); + }, + setPathname: (pathname: AppRouter) => { + store.dispatch(setPathname(pathname)); updateLocationUrl(); - }; - - public setPathname = (pathname: string) => { - appStore.dispatch({ - type: "SET_PATHNAME", - payload: { - pathname, - }, - }); - - updateLocationUrl(); - }; - - public pushHistory = (pathname: string) => { - appStore.dispatch({ - type: "SET_PATHNAME", - payload: { - pathname, - }, - }); + }, + pushHistory: (pathname: AppRouter) => { + store.dispatch(setPathname(pathname)); updateLocationUrl("push"); - }; - - public replaceHistory = (pathname: string) => { - appStore.dispatch({ - type: "SET_PATHNAME", - payload: { - pathname, - }, - }); + }, + replaceHistory: (pathname: AppRouter) => { + store.dispatch(setPathname(pathname)); updateLocationUrl("replace"); - }; - - public setMemoTypeQuery = (type: MemoSpecType | "" = "") => { - appStore.dispatch({ - type: "SET_TYPE", - payload: { - type, - }, - }); + }, + setMemoTypeQuery: (type?: MemoSpecType) => { + const { query } = store.getState().location; + store.dispatch( + setQuery({ + ...query, + type: type, + }) + ); updateLocationUrl(); - }; - - public setMemoShortcut = (shortcutId?: ShortcutId) => { - appStore.dispatch({ - type: "SET_SHORTCUT_ID", - payload: shortcutId, - }); + }, + setMemoShortcut: (shortcutId?: ShortcutId) => { + const { query } = store.getState().location; + store.dispatch( + setQuery({ + ...query, + shortcutId: shortcutId, + }) + ); updateLocationUrl(); - }; - - public setTextQuery = (text: string) => { - appStore.dispatch({ - type: "SET_TEXT", - payload: { - text, - }, - }); + }, + setTextQuery: (text?: string) => { + const { query } = store.getState().location; + store.dispatch( + setQuery({ + ...query, + text: text, + }) + ); updateLocationUrl(); - }; - - public setTagQuery = (tag: string) => { - appStore.dispatch({ - type: "SET_TAG_QUERY", - payload: { - tag, - }, - }); + }, + setTagQuery: (tag?: string) => { + const { query } = store.getState().location; + store.dispatch( + setQuery({ + ...query, + tag: tag, + }) + ); updateLocationUrl(); - }; + }, - public setFromAndToQuery = (from: number, to: number) => { - appStore.dispatch({ - type: "SET_DURATION_QUERY", - payload: { + setFromAndToQuery: (from: number, to: number) => { + const { query } = store.getState().location; + store.dispatch( + setQuery({ + ...query, duration: { from, to }, - }, - }); - + }) + ); updateLocationUrl(); - }; + }, - public getValidPathname = (pathname: string): AppRouter => { + getValidPathname: (pathname: string): AppRouter => { if (["/", "/signin"].includes(pathname)) { return pathname as AppRouter; } else { return "/"; } - }; -} - -const locationService = new LocationService(); + }, +}; export default locationService; diff --git a/web/src/services/memoService.ts b/web/src/services/memoService.ts index c22c1f63..cddf2741 100644 --- a/web/src/services/memoService.ts +++ b/web/src/services/memoService.ts @@ -1,99 +1,92 @@ import api from "../helpers/api"; import { TAG_REG } from "../helpers/consts"; import utils from "../helpers/utils"; -import appStore from "../stores/appStore"; +import { patchMemo, setMemos, setTags } from "../store/modules/memo"; +import store from "../store"; import userService from "./userService"; -class MemoService { - public initialized = false; +const convertResponseModelMemo = (memo: Memo): Memo => { + return { + ...memo, + createdTs: memo.createdTs * 1000, + updatedTs: memo.updatedTs * 1000, + }; +}; - public getState() { - return appStore.getState().memoState; - } +const memoService = { + getState: () => { + return store.getState().memo; + }, - public async fetchAllMemos() { + fetchAllMemos: async () => { if (!userService.getState().user) { return false; } const data = await api.getMyMemos(); - const memos: Memo[] = data.filter((m) => m.rowStatus !== "ARCHIVED").map((m) => this.convertResponseModelMemo(m)); - appStore.dispatch({ - type: "SET_MEMOS", - payload: { - memos, - }, - }); - - if (!this.initialized) { - this.initialized = true; - } + const memos: Memo[] = data.filter((m) => m.rowStatus !== "ARCHIVED").map((m) => convertResponseModelMemo(m)); + store.dispatch(setMemos(memos)); return memos; - } + }, - public async fetchDeletedMemos() { + fetchDeletedMemos: async () => { if (!userService.getState().user) { return false; } const data = await api.getMyArchivedMemos(); const deletedMemos: Memo[] = data.map((m) => { - return this.convertResponseModelMemo(m); + return convertResponseModelMemo(m); }); return deletedMemos; - } + }, - public pushMemo(memo: Memo) { - appStore.dispatch({ - type: "INSERT_MEMO", - payload: { - memo: { - ...memo, - }, - }, - }); - } + pushMemo: (memo: Memo) => { + store.dispatch(setMemos(memoService.getState().memos.concat(memo))); + }, - public getMemoById(id: MemoId) { - for (const m of this.getState().memos) { + getMemoById: (id: MemoId) => { + for (const m of memoService.getState().memos) { if (m.id === id) { return m; } } return null; - } + }, + + archiveMemoById: async (id: MemoId) => { + const memo = memoService.getMemoById(id); + if (!memo) { + return; + } - public async hideMemoById(id: MemoId) { await api.archiveMemo(id); - appStore.dispatch({ - type: "DELETE_MEMO_BY_ID", - payload: { - id: id, - }, - }); - } + store.dispatch( + patchMemo({ + ...memo, + rowStatus: "ARCHIVED", + }) + ); + }, - public async restoreMemoById(id: MemoId) { + restoreMemoById: async (id: MemoId) => { await api.restoreMemo(id); memoService.clearMemos(); memoService.fetchAllMemos(); - } + }, - public async deleteMemoById(id: MemoId) { + deleteMemoById: async (id: MemoId) => { await api.deleteMemo(id); - } + }, - public editMemo(memo: Memo) { - appStore.dispatch({ - type: "EDIT_MEMO", - payload: memo, - }); - } + editMemo: (memo: Memo) => { + store.dispatch(patchMemo(memo)); + }, - public updateTagsState() { - const { memos } = this.getState(); + updateTagsState: () => { + const { memos } = memoService.getState(); const tagsSet = new Set(); for (const m of memos) { for (const t of Array.from(m.content.match(TAG_REG) ?? [])) { @@ -101,69 +94,49 @@ class MemoService { } } - appStore.dispatch({ - type: "SET_TAGS", - payload: { - tags: Array.from(tagsSet).filter((t) => Boolean(t)), - }, - }); - } + store.dispatch(setTags(Array.from(tagsSet).filter((t) => Boolean(t)))); + }, - public clearMemos() { - appStore.dispatch({ - type: "SET_MEMOS", - payload: { - memos: [], - }, - }); - } + clearMemos: () => { + store.dispatch(setMemos([])); + }, - public async getLinkedMemos(memoId: MemoId): Promise { - const { memos } = this.getState(); + getLinkedMemos: async (memoId: MemoId): Promise => { + const { memos } = memoService.getState(); return memos.filter((m) => m.content.includes(`${memoId}`)); - } + }, - public async createMemo(content: string): Promise { + createMemo: async (content: string): Promise => { const memo = await api.createMemo({ content, }); - return this.convertResponseModelMemo(memo); - } + return convertResponseModelMemo(memo); + }, - public async updateMemo(memoId: MemoId, content: string): Promise { + updateMemo: async (memoId: MemoId, content: string): Promise => { const memo = await api.patchMemo({ id: memoId, content, }); - return this.convertResponseModelMemo(memo); - } + return convertResponseModelMemo(memo); + }, - public async pinMemo(memoId: MemoId) { + pinMemo: async (memoId: MemoId) => { await api.pinMemo(memoId); - } + }, - public async unpinMemo(memoId: MemoId) { + unpinMemo: async (memoId: MemoId) => { await api.unpinMemo(memoId); - } + }, - public async importMemo(content: string, createdAt: string) { + importMemo: async (content: string, createdAt: string) => { const createdTs = Math.floor(utils.getTimeStampByDate(createdAt) / 1000); await api.createMemo({ content, createdTs, }); - } - - private convertResponseModelMemo(memo: Memo): Memo { - return { - ...memo, - createdTs: memo.createdTs * 1000, - updatedTs: memo.updatedTs * 1000, - }; - } -} - -const memoService = new MemoService(); + }, +}; export default memoService; diff --git a/web/src/services/resourceService.ts b/web/src/services/resourceService.ts index 3bf3622a..96ae99ae 100644 --- a/web/src/services/resourceService.ts +++ b/web/src/services/resourceService.ts @@ -1,12 +1,12 @@ import api from "../helpers/api"; -class ResourceService { +const resourceService = { /** * Upload resource file to server, * @param file file * @returns resource: id, filename */ - public async upload(file: File) { + async upload(file: File) { const { name: filename, size } = file; if (size > 64 << 20) { @@ -18,9 +18,7 @@ class ResourceService { const data = await api.uploadFile(formData); return data; - } -} - -const resourceService = new ResourceService(); + }, +}; export default resourceService; diff --git a/web/src/services/shortcutService.ts b/web/src/services/shortcutService.ts index 017eb015..3b9685cc 100644 --- a/web/src/services/shortcutService.ts +++ b/web/src/services/shortcutService.ts @@ -1,97 +1,77 @@ import userService from "./userService"; import api from "../helpers/api"; -import appStore from "../stores/appStore"; import { UNKNOWN_ID } from "../helpers/consts"; +import store from "../store/"; +import { deleteShortcut, patchShortcut, setShortcuts } from "../store/modules/shortcut"; -class ShortcutService { - public getState() { - return appStore.getState().shortcutState; - } +const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => { + return { + ...shortcut, + createdTs: shortcut.createdTs * 1000, + updatedTs: shortcut.updatedTs * 1000, + }; +}; - public async getMyAllShortcuts() { +const shortcutService = { + getState: () => { + return store.getState().shortcut; + }, + + getMyAllShortcuts: async () => { if (!userService.getState().user) { return false; } const data = await api.getMyShortcuts(); - appStore.dispatch({ - type: "SET_SHORTCUTS", - payload: { - shortcuts: data.map((s) => this.convertResponseModelShortcut(s)), - }, - }); - return data; - } + const shortcuts = data.map((s) => convertResponseModelShortcut(s)); + store.dispatch(setShortcuts(shortcuts)); + return shortcuts; + }, - public getShortcutById(id: ShortcutId) { + getShortcutById: (id: ShortcutId) => { if (id === UNKNOWN_ID) { return null; } - for (const s of this.getState().shortcuts) { + for (const s of shortcutService.getState().shortcuts) { if (s.id === id) { return s; } } return null; - } + }, - public pushShortcut(shortcut: Shortcut) { - appStore.dispatch({ - type: "INSERT_SHORTCUT", - payload: { - shortcut: { - ...shortcut, - }, - }, - }); - } + pushShortcut: (shortcut: Shortcut) => { + store.dispatch(setShortcuts(shortcutService.getState().shortcuts.concat(shortcut))); + }, - public editShortcut(shortcut: Shortcut) { - appStore.dispatch({ - type: "UPDATE_SHORTCUT", - payload: shortcut, - }); - } + editShortcut: (shortcut: Shortcut) => { + store.dispatch(patchShortcut(shortcut)); + }, - public async deleteShortcut(shortcutId: ShortcutId) { + deleteShortcutById: async (shortcutId: ShortcutId) => { await api.deleteShortcutById(shortcutId); - appStore.dispatch({ - type: "DELETE_SHORTCUT_BY_ID", - payload: { - id: shortcutId, - }, - }); - } + store.dispatch(deleteShortcut(shortcutId)); + }, - public async createShortcut(title: string, payload: string) { + createShortcut: async (title: string, payload: string) => { const data = await api.createShortcut(title, payload); - return data; - } + shortcutService.pushShortcut(convertResponseModelShortcut(data)); + }, - public async updateShortcut(shortcutId: ShortcutId, title: string, payload: string) { + updateShortcut: async (shortcutId: ShortcutId, title: string, payload: string) => { const data = await api.updateShortcut(shortcutId, title, payload); - return data; - } + store.dispatch(patchShortcut(convertResponseModelShortcut(data))); + }, - public async pinShortcut(shortcutId: ShortcutId) { + pinShortcut: async (shortcutId: ShortcutId) => { await api.pinShortcut(shortcutId); - } + }, - public async unpinShortcut(shortcutId: ShortcutId) { + unpinShortcut: async (shortcutId: ShortcutId) => { await api.unpinShortcut(shortcutId); - } - - public convertResponseModelShortcut(shortcut: Shortcut): Shortcut { - return { - ...shortcut, - createdTs: shortcut.createdTs * 1000, - updatedTs: shortcut.updatedTs * 1000, - }; - } -} - -const shortcutService = new ShortcutService(); + }, +}; export default shortcutService; diff --git a/web/src/services/userService.ts b/web/src/services/userService.ts index fe3eb2f3..cde54af9 100644 --- a/web/src/services/userService.ts +++ b/web/src/services/userService.ts @@ -1,68 +1,55 @@ import api from "../helpers/api"; -import appStore from "../stores/appStore"; +import { signin, signout } from "../store/modules/user"; +import store from "../store"; -class UserService { - public getState() { - return appStore.getState().userState; - } +const convertResponseModelUser = (user: User): User => { + return { + ...user, + createdTs: user.createdTs * 1000, + updatedTs: user.updatedTs * 1000, + }; +}; - public async doSignIn() { +const userService = { + getState: () => { + return store.getState().user; + }, + + doSignIn: async () => { const user = await api.getUser(); if (user) { - appStore.dispatch({ - type: "LOGIN", - payload: { - user: this.convertResponseModelUser(user), - }, - }); + store.dispatch(signin(convertResponseModelUser(user))); } else { userService.doSignOut(); } return user; - } + }, - public async doSignOut() { - appStore.dispatch({ - type: "SIGN_OUT", - payload: null, - }); + doSignOut: async () => { + store.dispatch(signout); api.signout().catch(() => { // do nth }); - } + }, - public async updateUsername(name: string): Promise { + updateUsername: async (name: string): Promise => { await api.patchUser({ name, }); - } + }, - public async updatePassword(password: string): Promise { + updatePassword: async (password: string): Promise => { await api.patchUser({ password, }); - } + }, - public async resetOpenId(): Promise { + resetOpenId: async (): Promise => { const user = await api.patchUser({ resetOpenId: true, }); - appStore.dispatch({ - type: "RESET_OPENID", - payload: user.openId, - }); return user.openId; - } - - private convertResponseModelUser(user: User): User { - return { - ...user, - createdTs: user.createdTs * 1000, - updatedTs: user.updatedTs * 1000, - }; - } -} - -const userService = new UserService(); + }, +}; export default userService; diff --git a/web/src/store/index.ts b/web/src/store/index.ts new file mode 100644 index 00000000..59f80da6 --- /dev/null +++ b/web/src/store/index.ts @@ -0,0 +1,25 @@ +import { configureStore } from "@reduxjs/toolkit"; +import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; +import userReducer from "./modules/user"; +import memoReducer from "./modules/memo"; +import editorReducer from "./modules/editor"; +import shortcutReducer from "./modules/shortcut"; +import locationReducer from "./modules/location"; + +const store = configureStore({ + reducer: { + user: userReducer, + memo: memoReducer, + editor: editorReducer, + shortcut: shortcutReducer, + location: locationReducer, + }, +}); + +type AppState = ReturnType; +type AppDispatch = typeof store.dispatch; + +export const useAppSelector: TypedUseSelectorHook = useSelector; +export const useAppDispatch = () => useDispatch(); + +export default store; diff --git a/web/src/store/modules/editor.ts b/web/src/store/modules/editor.ts new file mode 100644 index 00000000..b5e6bb20 --- /dev/null +++ b/web/src/store/modules/editor.ts @@ -0,0 +1,23 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +interface State { + markMemoId?: MemoId; + editMemoId?: MemoId; +} + +const editorSlice = createSlice({ + name: "editor", + initialState: {} as State, + reducers: { + setMarkMemoId: (state, action: PayloadAction>) => { + state.markMemoId = action.payload; + }, + setEditMemoId: (state, action: PayloadAction>) => { + state.editMemoId = action.payload; + }, + }, +}); + +export const { setEditMemoId, setMarkMemoId } = editorSlice.actions; + +export default editorSlice.reducer; diff --git a/web/src/store/modules/location.ts b/web/src/store/modules/location.ts new file mode 100644 index 00000000..a5037b81 --- /dev/null +++ b/web/src/store/modules/location.ts @@ -0,0 +1,62 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +interface State { + pathname: AppRouter; + hash: string; + query?: Query; +} + +const getValidPathname = (pathname: string): AppRouter => { + if (["/", "/signin"].includes(pathname)) { + return pathname as AppRouter; + } else { + return "/"; + } +}; + +const getStateFromLocation = () => { + const { pathname, search, hash } = window.location; + const urlParams = new URLSearchParams(search); + const state: State = { + pathname: getValidPathname(pathname), + hash: hash, + }; + + if (search !== "") { + state.query = {}; + state.query.tag = urlParams.get("tag") ?? undefined; + state.query.type = (urlParams.get("type") as MemoSpecType) ?? undefined; + state.query.text = urlParams.get("text") ?? undefined; + state.query.shortcutId = Number(urlParams.get("shortcutId")) ?? undefined; + const from = parseInt(urlParams.get("from") ?? "0"); + const to = parseInt(urlParams.get("to") ?? "0"); + if (to > from && to !== 0) { + state.query.duration = { + from, + to, + }; + } + } + + return state; +}; + +const locationSlice = createSlice({ + name: "location", + initialState: getStateFromLocation(), + reducers: { + updateStateWithLocation: () => { + return getStateFromLocation(); + }, + setPathname: (state, action: PayloadAction) => { + state.pathname = action.payload; + }, + setQuery: (state, action: PayloadAction>) => { + state.query = action.payload; + }, + }, +}); + +export const { setPathname, setQuery, updateStateWithLocation } = locationSlice.actions; + +export default locationSlice.reducer; diff --git a/web/src/store/modules/memo.ts b/web/src/store/modules/memo.ts new file mode 100644 index 00000000..0dc66280 --- /dev/null +++ b/web/src/store/modules/memo.ts @@ -0,0 +1,44 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +interface State { + memos: Memo[]; + tags: string[]; +} + +const memoSlice = createSlice({ + name: "memo", + initialState: { + memos: [], + tags: [], + } as State, + reducers: { + setMemos: (state, action: PayloadAction) => { + state.memos = action.payload; + }, + setTags: (state, action: PayloadAction) => { + state.tags = action.payload; + }, + createMemo: (state, action: PayloadAction) => { + state.memos = state.memos.concat(action.payload); + }, + patchMemo: (state, action: PayloadAction>) => { + state.memos = state.memos.map((m) => { + if (m.id === action.payload.id) { + return { + ...m, + ...action.payload, + }; + } else { + return m; + } + }); + }, + deleteMemo: (state, action: PayloadAction) => { + state.memos = [...state.memos].filter((memo) => memo.id !== action.payload); + }, + }, +}); + +export const { setMemos, setTags, createMemo, patchMemo, deleteMemo } = memoSlice.actions; + +export default memoSlice.reducer; diff --git a/web/src/store/modules/shortcut.ts b/web/src/store/modules/shortcut.ts new file mode 100644 index 00000000..c691e95f --- /dev/null +++ b/web/src/store/modules/shortcut.ts @@ -0,0 +1,39 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +interface State { + shortcuts: Shortcut[]; +} + +const shortcutSlice = createSlice({ + name: "memo", + initialState: { + shortcuts: [], + } as State, + reducers: { + setShortcuts: (state, action: PayloadAction) => { + state.shortcuts = action.payload; + }, + createShortcut: (state, action: PayloadAction) => { + state.shortcuts = state.shortcuts.concat(action.payload); + }, + patchShortcut: (state, action: PayloadAction>) => { + state.shortcuts = state.shortcuts.map((s) => { + if (s.id === action.payload.id) { + return { + ...s, + ...action.payload, + }; + } else { + return s; + } + }); + }, + deleteShortcut: (state, action: PayloadAction) => { + state.shortcuts = [...state.shortcuts].filter((shortcut) => shortcut.id !== action.payload); + }, + }, +}); + +export const { setShortcuts, createShortcut, patchShortcut, deleteShortcut } = shortcutSlice.actions; + +export default shortcutSlice.reducer; diff --git a/web/src/store/modules/user.ts b/web/src/store/modules/user.ts new file mode 100644 index 00000000..53fa3a6a --- /dev/null +++ b/web/src/store/modules/user.ts @@ -0,0 +1,34 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; + +interface State { + user?: User; +} + +const userSlice = createSlice({ + name: "user", + initialState: {} as State, + reducers: { + signin: (state, action: PayloadAction) => { + return { + ...state, + user: action.payload, + }; + }, + signout: (state) => { + return { + ...state, + user: undefined, + }; + }, + patchUser: (state, action: PayloadAction>) => { + state.user = { + ...state.user, + ...action.payload, + } as User; + }, + }, +}); + +export const { signin, signout, patchUser } = userSlice.actions; + +export default userSlice.reducer; diff --git a/web/src/stores/appContext.ts b/web/src/stores/appContext.ts deleted file mode 100644 index 2027308d..00000000 --- a/web/src/stores/appContext.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createContext } from "react"; -import appStore from "./appStore"; - -const appContext = createContext(appStore.getState()); - -export default appContext; diff --git a/web/src/stores/appStore.ts b/web/src/stores/appStore.ts deleted file mode 100644 index 5bd31652..00000000 --- a/web/src/stores/appStore.ts +++ /dev/null @@ -1,36 +0,0 @@ -import combineReducers from "../labs/combineReducers"; -import createStore from "../labs/createStore"; -import * as globalStore from "./globalStateStore"; -import * as locationStore from "./locationStore"; -import * as memoStore from "./memoStore"; -import * as userStore from "./userStore"; -import * as shortcutStore from "./shortcutStore"; - -interface AppState { - globalState: globalStore.State; - locationState: locationStore.State; - memoState: memoStore.State; - userState: userStore.State; - shortcutState: shortcutStore.State; -} - -type AppStateActions = globalStore.Actions | locationStore.Actions | memoStore.Actions | userStore.Actions | shortcutStore.Actions; - -const appStore = createStore( - { - globalState: globalStore.defaultState, - locationState: locationStore.defaultState, - memoState: memoStore.defaultState, - userState: userStore.defaultState, - shortcutState: shortcutStore.defaultState, - }, - combineReducers({ - globalState: globalStore.reducer, - locationState: locationStore.reducer, - memoState: memoStore.reducer, - userState: userStore.reducer, - shortcutState: shortcutStore.reducer, - }) -); - -export default appStore; diff --git a/web/src/stores/globalStateStore.ts b/web/src/stores/globalStateStore.ts deleted file mode 100644 index 671b0f7f..00000000 --- a/web/src/stores/globalStateStore.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { UNKNOWN_ID } from "../helpers/consts"; - -export interface AppSetting { - shouldSplitMemoWord: boolean; - shouldHideImageUrl: boolean; - shouldUseMarkdownParser: boolean; -} - -export interface State extends AppSetting { - markMemoId: MemoId; - editMemoId: MemoId; -} - -interface SetMarkMemoIdAction { - type: "SET_MARK_MEMO_ID"; - payload: { - markMemoId: MemoId; - }; -} - -interface SetEditMemoIdAction { - type: "SET_EDIT_MEMO_ID"; - payload: { - editMemoId: MemoId; - }; -} - -interface SetAppSettingAction { - type: "SET_APP_SETTING"; - payload: Partial; -} - -export type Actions = SetEditMemoIdAction | SetMarkMemoIdAction | SetAppSettingAction; - -export function reducer(state: State, action: Actions) { - switch (action.type) { - case "SET_MARK_MEMO_ID": { - if (action.payload.markMemoId === state.markMemoId) { - return state; - } - - return { - ...state, - markMemoId: action.payload.markMemoId, - }; - } - case "SET_EDIT_MEMO_ID": { - if (action.payload.editMemoId === state.editMemoId) { - return state; - } - - return { - ...state, - editMemoId: action.payload.editMemoId, - }; - } - case "SET_APP_SETTING": { - return { - ...state, - ...action.payload, - }; - } - default: { - return state; - } - } -} - -export const defaultState: State = { - markMemoId: UNKNOWN_ID, - editMemoId: UNKNOWN_ID, - shouldSplitMemoWord: true, - shouldHideImageUrl: true, - shouldUseMarkdownParser: true, -}; diff --git a/web/src/stores/locationStore.ts b/web/src/stores/locationStore.ts deleted file mode 100644 index 46e5f6d5..00000000 --- a/web/src/stores/locationStore.ts +++ /dev/null @@ -1,187 +0,0 @@ -export type State = AppLocation; - -interface SetLocationAction { - type: "SET_LOCATION"; - payload: State; -} - -interface SetPathnameAction { - type: "SET_PATHNAME"; - payload: { - pathname: string; - }; -} - -interface SetQueryAction { - type: "SET_QUERY"; - payload: Query; -} - -interface SetShortcutIdAction { - type: "SET_SHORTCUT_ID"; - payload: ShortcutId | undefined; -} - -interface SetTagQueryAction { - type: "SET_TAG_QUERY"; - payload: { - tag: string; - }; -} - -interface SetFromAndToQueryAction { - type: "SET_DURATION_QUERY"; - payload: { - duration: Duration | null; - }; -} - -interface SetTypeAction { - type: "SET_TYPE"; - payload: { - type: MemoSpecType | ""; - }; -} - -interface SetTextAction { - type: "SET_TEXT"; - payload: { - text: string; - }; -} - -interface SetHashAction { - type: "SET_HASH"; - payload: { - hash: string; - }; -} - -export type Actions = - | SetLocationAction - | SetPathnameAction - | SetQueryAction - | SetTagQueryAction - | SetFromAndToQueryAction - | SetTypeAction - | SetTextAction - | SetShortcutIdAction - | SetHashAction; - -export function reducer(state: State, action: Actions) { - switch (action.type) { - case "SET_LOCATION": { - return action.payload; - } - case "SET_PATHNAME": { - if (action.payload.pathname === state.pathname) { - return state; - } - - return { - ...state, - pathname: action.payload.pathname, - }; - } - case "SET_HASH": { - if (action.payload.hash === state.hash) { - return state; - } - - return { - ...state, - hash: action.payload.hash, - }; - } - case "SET_QUERY": { - return { - ...state, - query: { - ...action.payload, - }, - }; - } - case "SET_TAG_QUERY": { - if (action.payload.tag === state.query.tag) { - return state; - } - - return { - ...state, - query: { - ...state.query, - tag: action.payload.tag, - }, - }; - } - case "SET_DURATION_QUERY": { - if (action.payload.duration === state.query.duration) { - return state; - } - - return { - ...state, - query: { - ...state.query, - duration: { - ...state.query.duration, - ...action.payload.duration, - }, - }, - }; - } - case "SET_TYPE": { - if (action.payload.type === state.query.type) { - return state; - } - - return { - ...state, - query: { - ...state.query, - type: action.payload.type, - }, - }; - } - case "SET_TEXT": { - if (action.payload.text === state.query.text) { - return state; - } - - return { - ...state, - query: { - ...state.query, - text: action.payload.text, - }, - }; - } - case "SET_SHORTCUT_ID": { - if (action.payload === state.query.shortcutId) { - return state; - } - - return { - ...state, - query: { - ...state.query, - shortcutId: action.payload, - }, - }; - } - default: { - return state; - } - } -} - -export const defaultState: State = { - pathname: "/", - hash: "", - query: { - tag: "", - duration: null, - type: "", - text: "", - }, -}; diff --git a/web/src/stores/memoStore.ts b/web/src/stores/memoStore.ts deleted file mode 100644 index 6d2e0714..00000000 --- a/web/src/stores/memoStore.ts +++ /dev/null @@ -1,98 +0,0 @@ -import utils from "../helpers/utils"; - -export interface State { - memos: Memo[]; - tags: string[]; -} - -interface SetMemosAction { - type: "SET_MEMOS"; - payload: { - memos: Memo[]; - }; -} - -interface SetTagsAction { - type: "SET_TAGS"; - payload: { - tags: string[]; - }; -} - -interface InsertMemoAction { - type: "INSERT_MEMO"; - payload: { - memo: Memo; - }; -} - -interface DeleteMemoByIdAction { - type: "DELETE_MEMO_BY_ID"; - payload: { - id: MemoId; - }; -} - -interface EditMemoByIdAction { - type: "EDIT_MEMO"; - payload: Memo; -} - -export type Actions = SetMemosAction | SetTagsAction | InsertMemoAction | DeleteMemoByIdAction | EditMemoByIdAction; - -export function reducer(state: State, action: Actions): State { - switch (action.type) { - case "SET_MEMOS": { - const memos = utils.dedupeObjectWithId(action.payload.memos.sort((a, b) => b.createdTs - a.createdTs)); - - return { - ...state, - memos: [...memos], - }; - } - case "SET_TAGS": { - return { - ...state, - tags: action.payload.tags, - }; - } - case "INSERT_MEMO": { - const memos = utils.dedupeObjectWithId([action.payload.memo, ...state.memos].sort((a, b) => b.createdTs - a.createdTs)); - return { - ...state, - memos, - }; - } - case "DELETE_MEMO_BY_ID": { - return { - ...state, - memos: [...state.memos].filter((memo) => memo.id !== action.payload.id), - }; - } - case "EDIT_MEMO": { - const memos = state.memos.map((m) => { - if (m.id === action.payload.id) { - return { - ...m, - ...action.payload, - }; - } else { - return m; - } - }); - - return { - ...state, - memos, - }; - } - default: { - return state; - } - } -} - -export const defaultState: State = { - memos: [], - tags: [], -}; diff --git a/web/src/stores/shortcutStore.ts b/web/src/stores/shortcutStore.ts deleted file mode 100644 index 30e6754e..00000000 --- a/web/src/stores/shortcutStore.ts +++ /dev/null @@ -1,92 +0,0 @@ -import utils from "../helpers/utils"; - -export interface State { - shortcuts: Shortcut[]; -} - -interface SetShortcutsAction { - type: "SET_SHORTCUTS"; - payload: { - shortcuts: Shortcut[]; - }; -} - -interface InsertShortcutAction { - type: "INSERT_SHORTCUT"; - payload: { - shortcut: Shortcut; - }; -} - -interface DeleteShortcutByIdAction { - type: "DELETE_SHORTCUT_BY_ID"; - payload: { - id: ShortcutId; - }; -} - -interface UpdateShortcutAction { - type: "UPDATE_SHORTCUT"; - payload: Shortcut; -} - -export type Actions = SetShortcutsAction | InsertShortcutAction | DeleteShortcutByIdAction | UpdateShortcutAction; - -export function reducer(state: State, action: Actions): State { - switch (action.type) { - case "SET_SHORTCUTS": { - const shortcuts = utils.dedupeObjectWithId( - action.payload.shortcuts - .sort((a, b) => utils.getTimeStampByDate(b.createdTs) - utils.getTimeStampByDate(a.createdTs)) - .sort((a, b) => utils.getTimeStampByDate(b.updatedTs) - utils.getTimeStampByDate(a.updatedTs)) - ); - - return { - ...state, - shortcuts, - }; - } - case "INSERT_SHORTCUT": { - const shortcuts = utils.dedupeObjectWithId( - [action.payload.shortcut, ...state.shortcuts].sort( - (a, b) => utils.getTimeStampByDate(b.createdTs) - utils.getTimeStampByDate(a.createdTs) - ) - ); - - return { - ...state, - shortcuts, - }; - } - case "DELETE_SHORTCUT_BY_ID": { - return { - ...state, - shortcuts: [...state.shortcuts].filter((shortcut) => shortcut.id !== action.payload.id), - }; - } - case "UPDATE_SHORTCUT": { - const shortcuts = state.shortcuts.map((m) => { - if (m.id === action.payload.id) { - return { - ...m, - ...action.payload, - }; - } else { - return m; - } - }); - - return { - ...state, - shortcuts, - }; - } - default: { - return state; - } - } -} - -export const defaultState: State = { - shortcuts: [], -}; diff --git a/web/src/stores/userStore.ts b/web/src/stores/userStore.ts deleted file mode 100644 index 18f0125b..00000000 --- a/web/src/stores/userStore.ts +++ /dev/null @@ -1,52 +0,0 @@ -export interface State { - user: User | null; -} - -interface SignInAction { - type: "LOGIN"; - payload: State; -} - -interface SignOutAction { - type: "SIGN_OUT"; - payload: null; -} - -interface ResetOpenIdAction { - type: "RESET_OPENID"; - payload: string; -} - -export type Actions = SignInAction | SignOutAction | ResetOpenIdAction; - -export function reducer(state: State, action: Actions): State { - switch (action.type) { - case "LOGIN": { - return { - user: action.payload.user, - }; - } - case "SIGN_OUT": { - return { - user: null, - }; - } - case "RESET_OPENID": { - if (!state.user) { - return state; - } - - return { - user: { - ...state.user, - openId: action.payload, - }, - }; - } - default: { - return state; - } - } -} - -export const defaultState: State = { user: null }; diff --git a/web/src/types/common.d.ts b/web/src/types/common.d.ts index e7804f6b..91263426 100644 --- a/web/src/types/common.d.ts +++ b/web/src/types/common.d.ts @@ -9,3 +9,5 @@ type FunctionType = (...args: unknown[]) => unknown; interface KVObject { [key: string]: T; } + +type Option = T | undefined; diff --git a/web/src/types/location.d.ts b/web/src/types/location.d.ts index 4f4b30a4..10eb1517 100644 --- a/web/src/types/location.d.ts +++ b/web/src/types/location.d.ts @@ -4,10 +4,10 @@ interface Duration { } interface Query { - tag: string; - duration: Duration | null; - type: MemoSpecType | ""; - text: string; + tag?: string; + duration?: Duration; + type?: MemoSpecType; + text?: string; shortcutId?: ShortcutId; } diff --git a/web/yarn.lock b/web/yarn.lock index 939ab907..2431e718 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -202,6 +202,13 @@ "@babel/plugin-syntax-jsx" "^7.16.7" "@babel/types" "^7.17.0" +"@babel/runtime@^7.12.1", "@babel/runtime@^7.9.2": + version "7.18.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.18.0.tgz#6d77142a19cb6088f0af662af1ada37a604d34ae" + integrity sha512-YMQvx/6nKEaucl0MY56mwIG483xk8SDNdlUwb2Ts6FUpr7fm85DxEmsY18LXBNhcTz6tO6JwZV8w1W06v8UKeg== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.16.7": version "7.16.7" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" @@ -303,6 +310,16 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@reduxjs/toolkit@^1.8.1": + version "1.8.1" + resolved "https://registry.yarnpkg.com/@reduxjs/toolkit/-/toolkit-1.8.1.tgz#94ee1981b8cf9227cda40163a04704a9544c9a9f" + integrity sha512-Q6mzbTpO9nOYRnkwpDlFOAbQnd3g7zj7CtHAZWz5SzE5lcV97Tf8f3SzOO8BoPOMYBFgfZaqTUZqgGu+a0+Fng== + dependencies: + immer "^9.0.7" + redux "^4.1.2" + redux-thunk "^2.4.1" + reselect "^4.1.5" + "@rollup/pluginutils@^4.2.0": version "4.2.1" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" @@ -311,6 +328,14 @@ estree-walker "^2.0.1" picomatch "^2.2.2" +"@types/hoist-non-react-statics@^3.3.1": + version "3.3.1" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" + integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== + dependencies: + "@types/react" "*" + hoist-non-react-statics "^3.3.0" + "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" @@ -363,6 +388,11 @@ resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39" integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== +"@types/use-sync-external-store@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43" + integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA== + "@typescript-eslint/eslint-plugin@^5.6.0": version "5.19.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.19.0.tgz#9608a4b6d0427104bccf132f058cba629a6553c0" @@ -1340,6 +1370,13 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" + integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== + dependencies: + react-is "^16.7.0" + iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -1357,6 +1394,11 @@ image-size@~0.5.0: resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c" integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= +immer@^9.0.7: + version "9.0.14" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.14.tgz#e05b83b63999d26382bb71676c9d827831248a48" + integrity sha512-ubBeqQutOSLIFCUBN03jGeOS6a3DoYlSYwYJTa+gSKEZKU5redJIqkIdZ3JVv/4RZpfcXdAWH5zCNLWPRv2WDw== + import-fresh@^3.0.0, import-fresh@^3.2.1: version "3.3.0" resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" @@ -1913,11 +1955,28 @@ react-dom@^18.1.0: loose-envify "^1.1.0" scheduler "^0.22.0" -react-is@^16.13.1: +react-is@^16.13.1, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-is@^18.0.0: + version "18.1.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.1.0.tgz#61aaed3096d30eacf2a2127118b5b41387d32a67" + integrity sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg== + +react-redux@^8.0.1: + version "8.0.1" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-8.0.1.tgz#2bc029f5ada9b443107914c373a2750f6bc0f40c" + integrity sha512-LMZMsPY4DYdZfLJgd7i79n5Kps5N9XVLCJJeWAaPYTV+Eah2zTuBjTxKtNEbjiyitbq80/eIkm55CYSLqAub3w== + dependencies: + "@babel/runtime" "^7.12.1" + "@types/hoist-non-react-statics" "^3.3.1" + "@types/use-sync-external-store" "^0.0.3" + hoist-non-react-statics "^3.3.2" + react-is "^18.0.0" + use-sync-external-store "^1.0.0" + react-refresh@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.12.0.tgz#28ac0a2c30ef2bb3433d5fd0621e69a6d774c3a4" @@ -1937,6 +1996,23 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +redux-thunk@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.1.tgz#0dd8042cf47868f4b29699941de03c9301a75714" + integrity sha512-OOYGNY5Jy2TWvTL1KgAlVy6dcx3siPJ1wTq741EPyUKfn6W6nChdICjZwCd0p8AZBs5kWpZlbkXW2nE/zjUa+Q== + +redux@^4.1.2: + version "4.2.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.0.tgz#46f10d6e29b6666df758780437651eeb2b969f13" + integrity sha512-oSBmcKKIuIR4ME29/AeNUnl5L+hvBq7OaJWzaptTQJAntaPvxIJqfnjbaEiCzzaIz+XmVILfqAM3Ob0aXLPfjA== + dependencies: + "@babel/runtime" "^7.9.2" + +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + regexp.prototype.flags@^1.4.1: version "1.4.2" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.2.tgz#bf635117a2f4b755595ebb0c0ee2d2a49b2084db" @@ -1950,6 +2026,11 @@ regexpp@^3.2.0: resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== +reselect@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.1.5.tgz#852c361247198da6756d07d9296c2b51eddb79f6" + integrity sha512-uVdlz8J7OO+ASpBYoz1Zypgx0KasCY20H+N8JD13oUMtPvSHQuscrHop4KbXrbsBcdB9Ds7lVK7eRkBIfO43vQ== + resolve-from@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" @@ -2234,6 +2315,11 @@ uri-js@^4.2.2: dependencies: punycode "^2.1.0" +use-sync-external-store@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.1.0.tgz#3343c3fe7f7e404db70f8c687adf5c1652d34e82" + integrity sha512-SEnieB2FPKEVne66NpXPd1Np4R1lTNKfjuy3XdIoPQKYBAFdzbzSZlSn1KJZUiihQLQC5Znot4SBz1EOTBwQAQ== + util-deprecate@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"