refactor: use redux

This commit is contained in:
boojack
2022-05-21 12:21:06 +08:00
parent 2e9152e223
commit c2e5a1a524
45 changed files with 674 additions and 1101 deletions

View File

@ -8,9 +8,11 @@
"lint": "eslint --ext .js,.ts,.tsx, src" "lint": "eslint --ext .js,.ts,.tsx, src"
}, },
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^1.8.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0" "react-dom": "^18.1.0",
"react-redux": "^8.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash-es": "^4.17.5", "@types/lodash-es": "^4.17.5",

View File

@ -1,12 +1,9 @@
import { useContext } from "react";
import appContext from "./stores/appContext";
import { appRouterSwitch } from "./routers"; import { appRouterSwitch } from "./routers";
import { useAppSelector } from "./store";
import "./less/app.less"; import "./less/app.less";
function App() { function App() {
const { const pathname = useAppSelector((state) => state.location.pathname);
locationState: { pathname },
} = useContext(appContext);
return <>{appRouterSwitch(pathname)}</>; return <>{appRouterSwitch(pathname)}</>;
} }

View File

@ -48,11 +48,9 @@ const CreateShortcutDialog: React.FC<Props> = (props: Props) => {
try { try {
if (shortcutId) { if (shortcutId) {
const editedShortcut = await shortcutService.updateShortcut(shortcutId, title, JSON.stringify(filters)); await shortcutService.updateShortcut(shortcutId, title, JSON.stringify(filters));
shortcutService.editShortcut(shortcutService.convertResponseModelShortcut(editedShortcut));
} else { } else {
const shortcut = await shortcutService.createShortcut(title, JSON.stringify(filters)); await shortcutService.createShortcut(title, JSON.stringify(filters));
shortcutService.pushShortcut(shortcutService.convertResponseModelShortcut(shortcut));
} }
} catch (error: any) { } catch (error: any) {
toastHelper.error(error.message); toastHelper.error(error.message);

View File

@ -1,7 +1,6 @@
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import appContext from "../stores/appContext"; import { Provider } from "react-redux";
import Provider from "../labs/Provider"; import store from "../store";
import appStore from "../stores/appStore";
import { ANIMATION_DURATION } from "../helpers/consts"; import { ANIMATION_DURATION } from "../helpers/consts";
import "../less/dialog.less"; import "../less/dialog.less";
@ -69,11 +68,7 @@ export function showDialog<T extends DialogProps>(
); );
if (config.useAppContext) { if (config.useAppContext) {
Fragment = ( Fragment = <Provider store={store}>{Fragment}</Provider>;
<Provider store={appStore} context={appContext}>
{Fragment}
</Provider>
);
} }
dialog.render(Fragment); dialog.render(Fragment);

View File

@ -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 { parseMarkedToHtml, parseRawTextToHtml } from "../helpers/marked";
import utils from "../helpers/utils"; import utils from "../helpers/utils";
import useToggle from "../hooks/useToggle"; import useToggle from "../hooks/useToggle";
import { globalStateService, memoService } from "../services"; import { editorStateService, memoService } from "../services";
import Only from "./common/OnlyWhen"; import Only from "./common/OnlyWhen";
import Image from "./Image"; import Image from "./Image";
import showMemoCardDialog from "./MemoCardDialog"; import showMemoCardDialog from "./MemoCardDialog";
@ -50,23 +50,23 @@ const Memo: React.FC<Props> = (props: Props) => {
}; };
const handleMarkMemoClick = () => { const handleMarkMemoClick = () => {
globalStateService.setMarkMemoId(memo.id); editorStateService.setMarkMemo(memo.id);
}; };
const handleEditMemoClick = () => { const handleEditMemoClick = () => {
globalStateService.setEditMemoId(memo.id); editorStateService.setEditMemo(memo.id);
}; };
const handleDeleteMemoClick = async () => { const handleDeleteMemoClick = async () => {
if (showConfirmDeleteBtn) { if (showConfirmDeleteBtn) {
try { try {
await memoService.hideMemoById(memo.id); await memoService.archiveMemoById(memo.id);
} catch (error: any) { } catch (error: any) {
toastHelper.error(error.message); toastHelper.error(error.message);
} }
if (globalStateService.getState().editMemoId === memo.id) { if (editorStateService.getState().editMemoId === memo.id) {
globalStateService.setEditMemoId(UNKNOWN_ID); editorStateService.setEditMemo(UNKNOWN_ID);
} }
} else { } else {
toggleConfirmDeleteBtn(); toggleConfirmDeleteBtn();
@ -163,15 +163,9 @@ export function formatMemoContent(content: string) {
}) })
.join(""); .join("");
const { shouldUseMarkdownParser, shouldSplitMemoWord, shouldHideImageUrl } = globalStateService.getState(); content = parseMarkedToHtml(content);
if (shouldUseMarkdownParser) { content = content.replace(IMAGE_URL_REG, "");
content = parseMarkedToHtml(content);
}
if (shouldHideImageUrl) {
content = content.replace(IMAGE_URL_REG, "");
}
content = content content = content
.replace(TAG_REG, "<span class='tag-span'>#$1</span>") .replace(TAG_REG, "<span class='tag-span'>#$1</span>")
@ -179,11 +173,7 @@ export function formatMemoContent(content: string) {
.replace(MEMO_LINK_REG, "<span class='memo-link-text' data-value='$2'>$1</span>"); .replace(MEMO_LINK_REG, "<span class='memo-link-text' data-value='$2'>$1</span>");
// Add space in english and chinese // 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"); const tempDivContainer = document.createElement("div");
tempDivContainer.innerHTML = content; tempDivContainer.innerHTML = content;

View File

@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import { IMAGE_URL_REG, MEMO_LINK_REG, UNKNOWN_ID } from "../helpers/consts"; import { IMAGE_URL_REG, MEMO_LINK_REG, UNKNOWN_ID } from "../helpers/consts";
import utils from "../helpers/utils"; import utils from "../helpers/utils";
import { globalStateService, memoService } from "../services"; import { editorStateService, memoService } from "../services";
import { parseHtmlToRawText } from "../helpers/marked"; import { parseHtmlToRawText } from "../helpers/marked";
import { formatMemoContent } from "./Memo"; import { formatMemoContent } from "./Memo";
import toastHelper from "./Toast"; import toastHelper from "./Toast";
@ -96,7 +96,7 @@ const MemoCardDialog: React.FC<Props> = (props: Props) => {
const handleEditMemoBtnClick = useCallback(() => { const handleEditMemoBtnClick = useCallback(() => {
props.destroy(); props.destroy();
globalStateService.setEditMemoId(memo.id); editorStateService.setEditMemo(memo.id);
}, [memo.id]); }, [memo.id]);
return ( return (

View File

@ -1,6 +1,6 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef } from "react"; import React, { useCallback, useEffect, useMemo, useRef } from "react";
import appContext from "../stores/appContext"; import { editorStateService, locationService, memoService, resourceService } from "../services";
import { globalStateService, locationService, memoService, resourceService } from "../services"; import { useAppSelector } from "../store";
import { UNKNOWN_ID } from "../helpers/consts"; import { UNKNOWN_ID } from "../helpers/consts";
import { storage } from "../helpers/storage"; import { storage } from "../helpers/storage";
import useToggle from "../hooks/useToggle"; import useToggle from "../hooks/useToggle";
@ -44,32 +44,36 @@ interface Props {}
const MemoEditor: React.FC<Props> = () => { const MemoEditor: React.FC<Props> = () => {
const { const {
globalState, editor: editorState,
memoState: { tags }, memo: { tags },
} = useContext(appContext); } = useAppSelector((state) => state);
const [isTagSeletorShown, toggleTagSeletor] = useToggle(false); const [isTagSeletorShown, toggleTagSeletor] = useToggle(false);
const editorRef = useRef<EditorRefActions>(null); const editorRef = useRef<EditorRefActions>(null);
const prevGlobalStateRef = useRef(globalState); const prevGlobalStateRef = useRef(editorState);
const tagSeletorRef = useRef<HTMLDivElement>(null); const tagSeletorRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
if (globalState.markMemoId !== UNKNOWN_ID) { if (editorState.markMemoId && editorState.markMemoId !== UNKNOWN_ID) {
const editorCurrentValue = editorRef.current?.getContent(); 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); editorRef.current?.insertText(memoLinkText);
globalStateService.setMarkMemoId(UNKNOWN_ID); editorStateService.setMarkMemo(UNKNOWN_ID);
} }
if (globalState.editMemoId !== UNKNOWN_ID && globalState.editMemoId !== prevGlobalStateRef.current.editMemoId) { if (
const editMemo = memoService.getMemoById(globalState.editMemoId); editorState.editMemoId &&
editorState.editMemoId !== UNKNOWN_ID &&
editorState.editMemoId !== prevGlobalStateRef.current.editMemoId
) {
const editMemo = memoService.getMemoById(editorState.editMemoId ?? UNKNOWN_ID);
if (editMemo) { if (editMemo) {
editorRef.current?.setContent(editMemo.content ?? ""); editorRef.current?.setContent(editMemo.content ?? "");
editorRef.current?.focus(); editorRef.current?.focus();
} }
} }
prevGlobalStateRef.current = globalState; prevGlobalStateRef.current = editorState;
}, [globalState.markMemoId, globalState.editMemoId]); }, [editorState.markMemoId, editorState.editMemoId]);
useEffect(() => { useEffect(() => {
if (!editorRef.current) { if (!editorRef.current) {
@ -144,18 +148,18 @@ const MemoEditor: React.FC<Props> = () => {
return; return;
} }
const { editMemoId } = globalStateService.getState(); const { editMemoId } = editorStateService.getState();
try { try {
if (editMemoId !== UNKNOWN_ID) { if (editMemoId && editMemoId !== UNKNOWN_ID) {
const prevMemo = memoService.getMemoById(editMemoId); const prevMemo = memoService.getMemoById(editMemoId ?? UNKNOWN_ID);
if (prevMemo && prevMemo.content !== content) { if (prevMemo && prevMemo.content !== content) {
const editedMemo = await memoService.updateMemo(prevMemo.id, content); const editedMemo = await memoService.updateMemo(prevMemo.id, content);
editedMemo.createdTs = Date.now(); editedMemo.createdTs = Date.now();
memoService.editMemo(editedMemo); memoService.editMemo(editedMemo);
} }
globalStateService.setEditMemoId(UNKNOWN_ID); editorStateService.setEditMemo(UNKNOWN_ID);
} else { } else {
const newMemo = await memoService.createMemo(content); const newMemo = await memoService.createMemo(content);
memoService.pushMemo(newMemo); memoService.pushMemo(newMemo);
@ -169,7 +173,7 @@ const MemoEditor: React.FC<Props> = () => {
}, []); }, []);
const handleCancelBtnClick = useCallback(() => { const handleCancelBtnClick = useCallback(() => {
globalStateService.setEditMemoId(UNKNOWN_ID); editorStateService.setEditMemo(UNKNOWN_ID);
editorRef.current?.setContent(""); editorRef.current?.setContent("");
setEditorContentCache(""); setEditorContentCache("");
}, []); }, []);
@ -259,7 +263,7 @@ const MemoEditor: React.FC<Props> = () => {
} }
}, []); }, []);
const isEditing = globalState.editMemoId !== UNKNOWN_ID; const isEditing = Boolean(editorState.editMemoId && editorState.editMemoId !== UNKNOWN_ID);
const editorConfig = useMemo( const editorConfig = useMemo(
() => ({ () => ({

View File

@ -1,5 +1,4 @@
import { useContext } from "react"; import { useAppSelector } from "../store";
import appContext from "../stores/appContext";
import { locationService, shortcutService } from "../services"; import { locationService, shortcutService } from "../services";
import utils from "../helpers/utils"; import utils from "../helpers/utils";
import { getTextWithMemoType } from "../helpers/filter"; import { getTextWithMemoType } from "../helpers/filter";
@ -9,10 +8,9 @@ interface FilterProps {}
const MemoFilter: React.FC<FilterProps> = () => { const MemoFilter: React.FC<FilterProps> = () => {
const { const {
locationState: { query }, location: { query },
} = useContext(appContext); } = useAppSelector((state) => state);
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 shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null;
const showFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut); const showFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut);
@ -38,7 +36,7 @@ const MemoFilter: React.FC<FilterProps> = () => {
<div <div
className={"filter-item-container " + (memoType ? "" : "hidden")} className={"filter-item-container " + (memoType ? "" : "hidden")}
onClick={() => { onClick={() => {
locationService.setMemoTypeQuery(""); locationService.setMemoTypeQuery(undefined);
}} }}
> >
<span className="icon-text">📦</span> {getTextWithMemoType(memoType as MemoSpecType)} <span className="icon-text">📦</span> {getTextWithMemoType(memoType as MemoSpecType)}

View File

@ -1,6 +1,6 @@
import { useCallback, useContext, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import appContext from "../stores/appContext";
import { locationService, memoService, shortcutService } from "../services"; 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 { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG } from "../helpers/consts";
import utils from "../helpers/utils"; import utils from "../helpers/utils";
import { checkShouldShowMemoWithFilters } from "../helpers/filter"; import { checkShouldShowMemoWithFilters } from "../helpers/filter";
@ -12,13 +12,13 @@ interface Props {}
const MemoList: React.FC<Props> = () => { const MemoList: React.FC<Props> = () => {
const { const {
locationState: { query }, location: { query },
memoState: { memos }, memo: { memos },
} = useContext(appContext); } = useAppSelector((state) => state);
const [isFetching, setFetchStatus] = useState(true); const [isFetching, setFetchStatus] = useState(true);
const wrapperElement = useRef<HTMLDivElement>(null); const wrapperElement = useRef<HTMLDivElement>(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 shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null;
const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut); const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut);
@ -78,7 +78,7 @@ const MemoList: React.FC<Props> = () => {
const pinnedMemos = shownMemos.filter((m) => m.pinned); const pinnedMemos = shownMemos.filter((m) => m.pinned);
const unpinnedMemos = 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(() => { useEffect(() => {
memoService memoService
@ -100,7 +100,7 @@ const MemoList: React.FC<Props> = () => {
const targetEl = event.target as HTMLElement; const targetEl = event.target as HTMLElement;
if (targetEl.tagName === "SPAN" && targetEl.className === "tag-span") { if (targetEl.tagName === "SPAN" && targetEl.className === "tag-span") {
const tagName = targetEl.innerText.slice(1); const tagName = targetEl.innerText.slice(1);
const currTagQuery = locationService.getState().query.tag; const currTagQuery = locationService.getState().query?.tag;
if (currTagQuery === tagName) { if (currTagQuery === tagName) {
locationService.setTagQuery(""); locationService.setTagQuery("");
} else { } else {

View File

@ -1,5 +1,5 @@
import { useCallback, useContext, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import appContext from "../stores/appContext"; import { useAppSelector } from "../store";
import SearchBar from "./SearchBar"; import SearchBar from "./SearchBar";
import { memoService, shortcutService } from "../services"; import { memoService, shortcutService } from "../services";
import "../less/memos-header.less"; import "../less/memos-header.less";
@ -10,25 +10,23 @@ interface Props {}
const MemosHeader: React.FC<Props> = () => { const MemosHeader: React.FC<Props> = () => {
const { const {
locationState: { location: { query },
query: { shortcutId }, shortcut: { shortcuts },
}, } = useAppSelector((state) => state);
shortcutState: { shortcuts },
} = useContext(appContext);
const [titleText, setTitleText] = useState("MEMOS"); const [titleText, setTitleText] = useState("MEMOS");
useEffect(() => { useEffect(() => {
if (!shortcutId) { if (!query?.shortcutId) {
setTitleText("MEMOS"); setTitleText("MEMOS");
return; return;
} }
const shortcut = shortcutService.getShortcutById(shortcutId); const shortcut = shortcutService.getShortcutById(query?.shortcutId);
if (shortcut) { if (shortcut) {
setTitleText(shortcut.title); setTitleText(shortcut.title);
} }
}, [shortcutId, shortcuts]); }, [query, shortcuts]);
const handleMemoTextClick = useCallback(() => { const handleMemoTextClick = useCallback(() => {
const now = Date.now(); const now = Date.now();

View File

@ -1,22 +1,17 @@
import { useContext } from "react";
import appContext from "../stores/appContext";
import { locationService } from "../services"; import { locationService } from "../services";
import { useAppSelector } from "../store";
import { memoSpecialTypes } from "../helpers/filter"; import { memoSpecialTypes } from "../helpers/filter";
import "../less/search-bar.less"; import "../less/search-bar.less";
interface Props {} interface Props {}
const SearchBar: React.FC<Props> = () => { const SearchBar: React.FC<Props> = () => {
const { const memoType = useAppSelector((state) => state.location.query?.type);
locationState: {
query: { type: memoType },
},
} = useContext(appContext);
const handleMemoTypeItemClick = (type: MemoSpecType | "") => { const handleMemoTypeItemClick = (type: MemoSpecType | undefined) => {
const { type: prevType } = locationService.getState().query; const { type: prevType } = locationService.getState().query ?? {};
if (type === prevType) { if (type === prevType) {
type = ""; type = undefined;
} }
locationService.setMemoTypeQuery(type); locationService.setMemoTypeQuery(type);
}; };

View File

@ -1,5 +1,5 @@
import { useContext, useState } from "react"; import { useState } from "react";
import appContext from "../stores/appContext"; import { useAppSelector } from "../store";
import { showDialog } from "./Dialog"; import { showDialog } from "./Dialog";
import MyAccountSection from "./Settings/MyAccountSection"; import MyAccountSection from "./Settings/MyAccountSection";
import PreferencesSection from "./Settings/PreferencesSection"; import PreferencesSection from "./Settings/PreferencesSection";
@ -16,8 +16,8 @@ interface State {
const SettingDialog: React.FC<Props> = (props: Props) => { const SettingDialog: React.FC<Props> = (props: Props) => {
const { const {
userState: { user }, user: { user },
} = useContext(appContext); } = useAppSelector((state) => state);
const { destroy } = props; const { destroy } = props;
const [state, setState] = useState<State>({ const [state, setState] = useState<State>({
selectedSection: "my-account", selectedSection: "my-account",

View File

@ -1,5 +1,5 @@
import { useContext, useState } from "react"; import { useState } from "react";
import appContext from "../../stores/appContext"; import { useAppSelector } from "../../store";
import { userService } from "../../services"; import { userService } from "../../services";
import { validate, ValidatorConfig } from "../../helpers/validator"; import { validate, ValidatorConfig } from "../../helpers/validator";
import toastHelper from "../Toast"; import toastHelper from "../Toast";
@ -17,7 +17,7 @@ const validateConfig: ValidatorConfig = {
interface Props {} interface Props {}
const MyAccountSection: React.FC<Props> = () => { const MyAccountSection: React.FC<Props> = () => {
const { userState } = useContext(appContext); const { user: userState } = useAppSelector((state) => state);
const user = userState.user as User; const user = userState.user as User;
const [username, setUsername] = useState<string>(user.name); const [username, setUsername] = useState<string>(user.name);
const openAPIRoute = `${window.location.origin}/h/${user.openId}/memo`; const openAPIRoute = `${window.location.origin}/h/${user.openId}/memo`;

View File

@ -1,6 +1,6 @@
import { useContext, useEffect } from "react"; import { useEffect } from "react";
import { locationService, shortcutService } from "../services"; import { locationService, shortcutService } from "../services";
import appContext from "../stores/appContext"; import { useAppSelector } from "../store";
import { UNKNOWN_ID } from "../helpers/consts"; import { UNKNOWN_ID } from "../helpers/consts";
import utils from "../helpers/utils"; import utils from "../helpers/utils";
import useToggle from "../hooks/useToggle"; import useToggle from "../hooks/useToggle";
@ -13,11 +13,9 @@ interface Props {}
const ShortcutList: React.FC<Props> = () => { const ShortcutList: React.FC<Props> = () => {
const { const {
shortcutState: { shortcuts }, location: { query },
locationState: { shortcut: { shortcuts },
query: { shortcutId }, } = useAppSelector((state) => state);
},
} = useContext(appContext);
const loadingState = useLoading(); const loadingState = useLoading();
const pinnedShortcuts = shortcuts const pinnedShortcuts = shortcuts
.filter((s) => s.rowStatus === "ARCHIVED") .filter((s) => s.rowStatus === "ARCHIVED")
@ -48,7 +46,7 @@ const ShortcutList: React.FC<Props> = () => {
</p> </p>
<div className="shortcuts-container"> <div className="shortcuts-container">
{sortedShortcuts.map((s) => { {sortedShortcuts.map((s) => {
return <ShortcutContainer key={s.id} shortcut={s} isActive={s.id === Number(shortcutId)} />; return <ShortcutContainer key={s.id} shortcut={s} isActive={s.id === Number(query?.shortcutId)} />;
})} })}
</div> </div>
</div> </div>
@ -80,7 +78,7 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
if (showConfirmDeleteBtn) { if (showConfirmDeleteBtn) {
try { try {
await shortcutService.deleteShortcut(shortcut.id); await shortcutService.deleteShortcutById(shortcut.id);
} catch (error: any) { } catch (error: any) {
toastHelper.error(error.message); toastHelper.error(error.message);
} }

View File

@ -1,5 +1,4 @@
import { useContext } from "react"; import { useAppSelector } from "../store";
import appContext from "../stores/appContext";
import utils from "../helpers/utils"; import utils from "../helpers/utils";
import showDailyMemoDiaryDialog from "./DailyMemoDiaryDialog"; import showDailyMemoDiaryDialog from "./DailyMemoDiaryDialog";
import showSettingDialog from "./SettingDialog"; import showSettingDialog from "./SettingDialog";
@ -14,9 +13,9 @@ interface Props {}
const Sidebar: React.FC<Props> = () => { const Sidebar: React.FC<Props> = () => {
const { const {
memoState: { memos, tags }, memo: { memos, tags },
userState: { user }, user: { user },
} = useContext(appContext); } = useAppSelector((state) => state);
const createdDays = user ? Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24) : 0; const createdDays = user ? Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24) : 0;
const handleMyAccountBtnClick = () => { const handleMyAccountBtnClick = () => {

View File

@ -1,5 +1,5 @@
import { useContext, useEffect, useState } from "react"; import { useEffect, useState } from "react";
import appContext from "../stores/appContext"; import { useAppSelector } from "../store";
import { locationService, memoService } from "../services"; import { locationService, memoService } from "../services";
import useToggle from "../hooks/useToggle"; import useToggle from "../hooks/useToggle";
import Only from "./common/OnlyWhen"; import Only from "./common/OnlyWhen";
@ -16,11 +16,9 @@ interface Props {}
const TagList: React.FC<Props> = () => { const TagList: React.FC<Props> = () => {
const { const {
locationState: { location: { query },
query: { tag: tagQuery }, memo: { memos, tags: tagsText },
}, } = useAppSelector((state) => state);
memoState: { tags: tagsText, memos },
} = useContext(appContext);
const [tags, setTags] = useState<Tag[]>([]); const [tags, setTags] = useState<Tag[]>([]);
useEffect(() => { useEffect(() => {
@ -73,9 +71,9 @@ const TagList: React.FC<Props> = () => {
<p className="title-text">Tags</p> <p className="title-text">Tags</p>
<div className="tags-container"> <div className="tags-container">
{tags.map((t, idx) => ( {tags.map((t, idx) => (
<TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={tagQuery} /> <TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={query?.tag} />
))} ))}
<Only when={tags.length < 5 && memoService.initialized}> <Only when={tags.length < 5}>
<p className="tag-tip-container"> <p className="tag-tip-container">
Enter <span className="code-text">#tag </span> to create a tag Enter <span className="code-text">#tag </span> to create a tag
</p> </p>
@ -87,7 +85,7 @@ const TagList: React.FC<Props> = () => {
interface TagItemContainerProps { interface TagItemContainerProps {
tag: Tag; tag: Tag;
tagQuery: string; tagQuery?: string;
} }
const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContainerProps) => { const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContainerProps) => {

View File

@ -1,5 +1,5 @@
import { useCallback, useContext, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import appContext from "../stores/appContext"; import { useAppSelector } from "../store";
import { locationService } from "../services"; import { locationService } from "../services";
import { DAILY_TIMESTAMP } from "../helpers/consts"; import { DAILY_TIMESTAMP } from "../helpers/consts";
import utils from "../helpers/utils"; import utils from "../helpers/utils";
@ -36,8 +36,8 @@ const UsageHeatMap: React.FC<Props> = () => {
const beginDayTimestemp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP; const beginDayTimestemp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP;
const { const {
memoState: { memos }, memo: { memos },
} = useContext(appContext); } = useAppSelector((state) => state);
const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestemp)); const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestemp));
const [popupStat, setPopupStat] = useState<DailyUsageStat | null>(null); const [popupStat, setPopupStat] = useState<DailyUsageStat | null>(null);
const [currentStat, setCurrentStat] = useState<DailyUsageStat | null>(null); const [currentStat, setCurrentStat] = useState<DailyUsageStat | null>(null);
@ -71,7 +71,7 @@ const UsageHeatMap: React.FC<Props> = () => {
}, []); }, []);
const handleUsageStatItemClick = useCallback((item: DailyUsageStat) => { 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); locationService.setFromAndToQuery(0, 0);
setCurrentStat(null); setCurrentStat(null);
} else if (item.count > 0) { } else if (item.count > 0) {

View File

@ -1,5 +1,5 @@
import { useCallback, useContext, useState } from "react"; import { useCallback, useState } from "react";
import appContext from "../stores/appContext"; import { useAppSelector } from "../store";
import { locationService } from "../services"; import { locationService } from "../services";
import MenuBtnsPopup from "./MenuBtnsPopup"; import MenuBtnsPopup from "./MenuBtnsPopup";
import "../less/user-banner.less"; import "../less/user-banner.less";
@ -8,8 +8,8 @@ interface Props {}
const UserBanner: React.FC<Props> = () => { const UserBanner: React.FC<Props> = () => {
const { const {
userState: { user }, user: { user },
} = useContext(appContext); } = useAppSelector((state) => state);
const username = user ? user.name : "Memos"; const username = user ? user.name : "Memos";
const [shouldShowPopupBtns, setShouldShowPopupBtns] = useState(false); const [shouldShowPopupBtns, setShouldShowPopupBtns] = useState(false);

View File

@ -42,7 +42,7 @@
> .shortcut-container { > .shortcut-container {
.flex(row, space-between, center); .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 { &:hover {
background-color: @bg-gray; background-color: @bg-gray;

View File

@ -1,8 +1,8 @@
import React from "react"; import React from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import Provider from "./labs/Provider"; import { Provider } from "react-redux";
import appContext from "./stores/appContext"; import store from "./store";
import appStore from "./stores/appStore"; import { updateStateWithLocation } from "./store/modules/location";
import App from "./App"; import App from "./App";
import "./helpers/polyfill"; import "./helpers/polyfill";
import "./less/global.less"; import "./less/global.less";
@ -12,8 +12,15 @@ const container = document.getElementById("root");
const root = createRoot(container as HTMLElement); const root = createRoot(container as HTMLElement);
root.render( root.render(
<React.StrictMode> <React.StrictMode>
<Provider store={appStore} context={appContext}> <Provider store={store}>
<App /> <App />
</Provider> </Provider>
</React.StrictMode> </React.StrictMode>
); );
window.onload = () => {
store.dispatch(updateStateWithLocation());
window.onpopstate = () => {
store.dispatch(updateStateWithLocation());
};
};

View File

@ -1,15 +1,15 @@
import { useContext, useEffect } from "react"; import { useEffect } from "react";
import { locationService, userService } from "../services"; import { locationService, userService } from "../services";
import { homeRouterSwitch } from "../routers"; import { homeRouterSwitch } from "../routers";
import appContext from "../stores/appContext"; import { useAppSelector } from "../store";
import Sidebar from "../components/Sidebar"; import Sidebar from "../components/Sidebar";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
import "../less/home.less"; import "../less/home.less";
function Home() { function Home() {
const { const {
locationState: { pathname }, location: { pathname },
} = useContext(appContext); } = useAppSelector((state) => state);
const loadingState = useLoading(); const loadingState = useLoading();
useEffect(() => { useEffect(() => {

View File

@ -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;

View File

@ -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<AppSetting>) => {
appStore.dispatch({
type: "SET_APP_SETTING",
payload: appSetting,
});
storage.set(appSetting);
};
}
const globalStateService = new GlobalStateService();
export default globalStateService;

View File

@ -1,8 +1,8 @@
import globalStateService from "./globalStateService"; import editorStateService from "./editorStateService";
import locationService from "./locationService"; import locationService from "./locationService";
import memoService from "./memoService"; import memoService from "./memoService";
import shortcutService from "./shortcutService"; import shortcutService from "./shortcutService";
import userService from "./userService"; import userService from "./userService";
import resourceService from "./resourceService"; import resourceService from "./resourceService";
export { globalStateService, locationService, memoService, shortcutService, userService, resourceService }; export { editorStateService, locationService, memoService, shortcutService, userService, resourceService };

View File

@ -1,9 +1,10 @@
import utils from "../helpers/utils"; 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 updateLocationUrl = (method: "replace" | "push" = "replace") => {
const { query, pathname, hash } = appStore.getState().locationState; const { query, pathname, hash } = store.getState().location;
let queryString = utils.transformObjectToParamsString(query); let queryString = utils.transformObjectToParamsString(query ?? {});
if (queryString) { if (queryString) {
queryString = "?" + queryString; queryString = "?" + queryString;
} else { } else {
@ -17,180 +18,98 @@ const updateLocationUrl = (method: "replace" | "push" = "replace") => {
} }
}; };
class LocationService { const locationService = {
constructor() { getState: () => {
this.updateStateWithLocation(); return store.getState().location;
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: "",
},
});
clearQuery: () => {
store.dispatch(setQuery({}));
updateLocationUrl(); updateLocationUrl();
}; },
public setQuery = (query: Query) => {
appStore.dispatch({
type: "SET_QUERY",
payload: query,
});
setQuery: (query: Query) => {
store.dispatch(setQuery(query));
updateLocationUrl(); updateLocationUrl();
}; },
public setHash = (hash: string) => {
appStore.dispatch({
type: "SET_HASH",
payload: {
hash,
},
});
setPathname: (pathname: AppRouter) => {
store.dispatch(setPathname(pathname));
updateLocationUrl(); 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"); updateLocationUrl("push");
}; },
public replaceHistory = (pathname: string) => {
appStore.dispatch({
type: "SET_PATHNAME",
payload: {
pathname,
},
});
replaceHistory: (pathname: AppRouter) => {
store.dispatch(setPathname(pathname));
updateLocationUrl("replace"); 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(); 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(); 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(); 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(); updateLocationUrl();
}; },
public setFromAndToQuery = (from: number, to: number) => { setFromAndToQuery: (from: number, to: number) => {
appStore.dispatch({ const { query } = store.getState().location;
type: "SET_DURATION_QUERY", store.dispatch(
payload: { setQuery({
...query,
duration: { from, to }, duration: { from, to },
}, })
}); );
updateLocationUrl(); updateLocationUrl();
}; },
public getValidPathname = (pathname: string): AppRouter => { getValidPathname: (pathname: string): AppRouter => {
if (["/", "/signin"].includes(pathname)) { if (["/", "/signin"].includes(pathname)) {
return pathname as AppRouter; return pathname as AppRouter;
} else { } else {
return "/"; return "/";
} }
}; },
} };
const locationService = new LocationService();
export default locationService; export default locationService;

View File

@ -1,99 +1,92 @@
import api from "../helpers/api"; import api from "../helpers/api";
import { TAG_REG } from "../helpers/consts"; import { TAG_REG } from "../helpers/consts";
import utils from "../helpers/utils"; 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"; import userService from "./userService";
class MemoService { const convertResponseModelMemo = (memo: Memo): Memo => {
public initialized = false; return {
...memo,
createdTs: memo.createdTs * 1000,
updatedTs: memo.updatedTs * 1000,
};
};
public getState() { const memoService = {
return appStore.getState().memoState; getState: () => {
} return store.getState().memo;
},
public async fetchAllMemos() { fetchAllMemos: async () => {
if (!userService.getState().user) { if (!userService.getState().user) {
return false; return false;
} }
const data = await api.getMyMemos(); const data = await api.getMyMemos();
const memos: Memo[] = data.filter((m) => m.rowStatus !== "ARCHIVED").map((m) => this.convertResponseModelMemo(m)); const memos: Memo[] = data.filter((m) => m.rowStatus !== "ARCHIVED").map((m) => convertResponseModelMemo(m));
appStore.dispatch({ store.dispatch(setMemos(memos));
type: "SET_MEMOS",
payload: {
memos,
},
});
if (!this.initialized) {
this.initialized = true;
}
return memos; return memos;
} },
public async fetchDeletedMemos() { fetchDeletedMemos: async () => {
if (!userService.getState().user) { if (!userService.getState().user) {
return false; return false;
} }
const data = await api.getMyArchivedMemos(); const data = await api.getMyArchivedMemos();
const deletedMemos: Memo[] = data.map((m) => { const deletedMemos: Memo[] = data.map((m) => {
return this.convertResponseModelMemo(m); return convertResponseModelMemo(m);
}); });
return deletedMemos; return deletedMemos;
} },
public pushMemo(memo: Memo) { pushMemo: (memo: Memo) => {
appStore.dispatch({ store.dispatch(setMemos(memoService.getState().memos.concat(memo)));
type: "INSERT_MEMO", },
payload: {
memo: {
...memo,
},
},
});
}
public getMemoById(id: MemoId) { getMemoById: (id: MemoId) => {
for (const m of this.getState().memos) { for (const m of memoService.getState().memos) {
if (m.id === id) { if (m.id === id) {
return m; return m;
} }
} }
return null; return null;
} },
archiveMemoById: async (id: MemoId) => {
const memo = memoService.getMemoById(id);
if (!memo) {
return;
}
public async hideMemoById(id: MemoId) {
await api.archiveMemo(id); await api.archiveMemo(id);
appStore.dispatch({ store.dispatch(
type: "DELETE_MEMO_BY_ID", patchMemo({
payload: { ...memo,
id: id, rowStatus: "ARCHIVED",
}, })
}); );
} },
public async restoreMemoById(id: MemoId) { restoreMemoById: async (id: MemoId) => {
await api.restoreMemo(id); await api.restoreMemo(id);
memoService.clearMemos(); memoService.clearMemos();
memoService.fetchAllMemos(); memoService.fetchAllMemos();
} },
public async deleteMemoById(id: MemoId) { deleteMemoById: async (id: MemoId) => {
await api.deleteMemo(id); await api.deleteMemo(id);
} },
public editMemo(memo: Memo) { editMemo: (memo: Memo) => {
appStore.dispatch({ store.dispatch(patchMemo(memo));
type: "EDIT_MEMO", },
payload: memo,
});
}
public updateTagsState() { updateTagsState: () => {
const { memos } = this.getState(); const { memos } = memoService.getState();
const tagsSet = new Set<string>(); const tagsSet = new Set<string>();
for (const m of memos) { for (const m of memos) {
for (const t of Array.from(m.content.match(TAG_REG) ?? [])) { for (const t of Array.from(m.content.match(TAG_REG) ?? [])) {
@ -101,69 +94,49 @@ class MemoService {
} }
} }
appStore.dispatch({ store.dispatch(setTags(Array.from(tagsSet).filter((t) => Boolean(t))));
type: "SET_TAGS", },
payload: {
tags: Array.from(tagsSet).filter((t) => Boolean(t)),
},
});
}
public clearMemos() { clearMemos: () => {
appStore.dispatch({ store.dispatch(setMemos([]));
type: "SET_MEMOS", },
payload: {
memos: [],
},
});
}
public async getLinkedMemos(memoId: MemoId): Promise<Memo[]> { getLinkedMemos: async (memoId: MemoId): Promise<Memo[]> => {
const { memos } = this.getState(); const { memos } = memoService.getState();
return memos.filter((m) => m.content.includes(`${memoId}`)); return memos.filter((m) => m.content.includes(`${memoId}`));
} },
public async createMemo(content: string): Promise<Memo> { createMemo: async (content: string): Promise<Memo> => {
const memo = await api.createMemo({ const memo = await api.createMemo({
content, content,
}); });
return this.convertResponseModelMemo(memo); return convertResponseModelMemo(memo);
} },
public async updateMemo(memoId: MemoId, content: string): Promise<Memo> { updateMemo: async (memoId: MemoId, content: string): Promise<Memo> => {
const memo = await api.patchMemo({ const memo = await api.patchMemo({
id: memoId, id: memoId,
content, content,
}); });
return this.convertResponseModelMemo(memo); return convertResponseModelMemo(memo);
} },
public async pinMemo(memoId: MemoId) { pinMemo: async (memoId: MemoId) => {
await api.pinMemo(memoId); await api.pinMemo(memoId);
} },
public async unpinMemo(memoId: MemoId) { unpinMemo: async (memoId: MemoId) => {
await api.unpinMemo(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); const createdTs = Math.floor(utils.getTimeStampByDate(createdAt) / 1000);
await api.createMemo({ await api.createMemo({
content, content,
createdTs, createdTs,
}); });
} },
};
private convertResponseModelMemo(memo: Memo): Memo {
return {
...memo,
createdTs: memo.createdTs * 1000,
updatedTs: memo.updatedTs * 1000,
};
}
}
const memoService = new MemoService();
export default memoService; export default memoService;

View File

@ -1,12 +1,12 @@
import api from "../helpers/api"; import api from "../helpers/api";
class ResourceService { const resourceService = {
/** /**
* Upload resource file to server, * Upload resource file to server,
* @param file file * @param file file
* @returns resource: id, filename * @returns resource: id, filename
*/ */
public async upload(file: File) { async upload(file: File) {
const { name: filename, size } = file; const { name: filename, size } = file;
if (size > 64 << 20) { if (size > 64 << 20) {
@ -18,9 +18,7 @@ class ResourceService {
const data = await api.uploadFile(formData); const data = await api.uploadFile(formData);
return data; return data;
} },
} };
const resourceService = new ResourceService();
export default resourceService; export default resourceService;

View File

@ -1,97 +1,77 @@
import userService from "./userService"; import userService from "./userService";
import api from "../helpers/api"; import api from "../helpers/api";
import appStore from "../stores/appStore";
import { UNKNOWN_ID } from "../helpers/consts"; import { UNKNOWN_ID } from "../helpers/consts";
import store from "../store/";
import { deleteShortcut, patchShortcut, setShortcuts } from "../store/modules/shortcut";
class ShortcutService { const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => {
public getState() { return {
return appStore.getState().shortcutState; ...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) { if (!userService.getState().user) {
return false; return false;
} }
const data = await api.getMyShortcuts(); const data = await api.getMyShortcuts();
appStore.dispatch({ const shortcuts = data.map((s) => convertResponseModelShortcut(s));
type: "SET_SHORTCUTS", store.dispatch(setShortcuts(shortcuts));
payload: { return shortcuts;
shortcuts: data.map((s) => this.convertResponseModelShortcut(s)), },
},
});
return data;
}
public getShortcutById(id: ShortcutId) { getShortcutById: (id: ShortcutId) => {
if (id === UNKNOWN_ID) { if (id === UNKNOWN_ID) {
return null; return null;
} }
for (const s of this.getState().shortcuts) { for (const s of shortcutService.getState().shortcuts) {
if (s.id === id) { if (s.id === id) {
return s; return s;
} }
} }
return null; return null;
} },
public pushShortcut(shortcut: Shortcut) { pushShortcut: (shortcut: Shortcut) => {
appStore.dispatch({ store.dispatch(setShortcuts(shortcutService.getState().shortcuts.concat(shortcut)));
type: "INSERT_SHORTCUT", },
payload: {
shortcut: {
...shortcut,
},
},
});
}
public editShortcut(shortcut: Shortcut) { editShortcut: (shortcut: Shortcut) => {
appStore.dispatch({ store.dispatch(patchShortcut(shortcut));
type: "UPDATE_SHORTCUT", },
payload: shortcut,
});
}
public async deleteShortcut(shortcutId: ShortcutId) { deleteShortcutById: async (shortcutId: ShortcutId) => {
await api.deleteShortcutById(shortcutId); await api.deleteShortcutById(shortcutId);
appStore.dispatch({ store.dispatch(deleteShortcut(shortcutId));
type: "DELETE_SHORTCUT_BY_ID", },
payload: {
id: shortcutId,
},
});
}
public async createShortcut(title: string, payload: string) { createShortcut: async (title: string, payload: string) => {
const data = await api.createShortcut(title, payload); 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); 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); await api.pinShortcut(shortcutId);
} },
public async unpinShortcut(shortcutId: ShortcutId) { unpinShortcut: async (shortcutId: ShortcutId) => {
await api.unpinShortcut(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; export default shortcutService;

View File

@ -1,68 +1,55 @@
import api from "../helpers/api"; import api from "../helpers/api";
import appStore from "../stores/appStore"; import { signin, signout } from "../store/modules/user";
import store from "../store";
class UserService { const convertResponseModelUser = (user: User): User => {
public getState() { return {
return appStore.getState().userState; ...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(); const user = await api.getUser();
if (user) { if (user) {
appStore.dispatch({ store.dispatch(signin(convertResponseModelUser(user)));
type: "LOGIN",
payload: {
user: this.convertResponseModelUser(user),
},
});
} else { } else {
userService.doSignOut(); userService.doSignOut();
} }
return user; return user;
} },
public async doSignOut() { doSignOut: async () => {
appStore.dispatch({ store.dispatch(signout);
type: "SIGN_OUT",
payload: null,
});
api.signout().catch(() => { api.signout().catch(() => {
// do nth // do nth
}); });
} },
public async updateUsername(name: string): Promise<void> { updateUsername: async (name: string): Promise<void> => {
await api.patchUser({ await api.patchUser({
name, name,
}); });
} },
public async updatePassword(password: string): Promise<void> { updatePassword: async (password: string): Promise<void> => {
await api.patchUser({ await api.patchUser({
password, password,
}); });
} },
public async resetOpenId(): Promise<string> { resetOpenId: async (): Promise<string> => {
const user = await api.patchUser({ const user = await api.patchUser({
resetOpenId: true, resetOpenId: true,
}); });
appStore.dispatch({
type: "RESET_OPENID",
payload: user.openId,
});
return 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; export default userService;

25
web/src/store/index.ts Normal file
View File

@ -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<typeof store.getState>;
type AppDispatch = typeof store.dispatch;
export const useAppSelector: TypedUseSelectorHook<AppState> = useSelector;
export const useAppDispatch = () => useDispatch<AppDispatch>();
export default store;

View File

@ -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<Option<MemoId>>) => {
state.markMemoId = action.payload;
},
setEditMemoId: (state, action: PayloadAction<Option<MemoId>>) => {
state.editMemoId = action.payload;
},
},
});
export const { setEditMemoId, setMarkMemoId } = editorSlice.actions;
export default editorSlice.reducer;

View File

@ -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<AppRouter>) => {
state.pathname = action.payload;
},
setQuery: (state, action: PayloadAction<Partial<Query>>) => {
state.query = action.payload;
},
},
});
export const { setPathname, setQuery, updateStateWithLocation } = locationSlice.actions;
export default locationSlice.reducer;

View File

@ -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<Memo[]>) => {
state.memos = action.payload;
},
setTags: (state, action: PayloadAction<string[]>) => {
state.tags = action.payload;
},
createMemo: (state, action: PayloadAction<Memo>) => {
state.memos = state.memos.concat(action.payload);
},
patchMemo: (state, action: PayloadAction<Partial<Memo>>) => {
state.memos = state.memos.map((m) => {
if (m.id === action.payload.id) {
return {
...m,
...action.payload,
};
} else {
return m;
}
});
},
deleteMemo: (state, action: PayloadAction<MemoId>) => {
state.memos = [...state.memos].filter((memo) => memo.id !== action.payload);
},
},
});
export const { setMemos, setTags, createMemo, patchMemo, deleteMemo } = memoSlice.actions;
export default memoSlice.reducer;

View File

@ -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<Shortcut[]>) => {
state.shortcuts = action.payload;
},
createShortcut: (state, action: PayloadAction<Shortcut>) => {
state.shortcuts = state.shortcuts.concat(action.payload);
},
patchShortcut: (state, action: PayloadAction<Partial<Shortcut>>) => {
state.shortcuts = state.shortcuts.map((s) => {
if (s.id === action.payload.id) {
return {
...s,
...action.payload,
};
} else {
return s;
}
});
},
deleteShortcut: (state, action: PayloadAction<ShortcutId>) => {
state.shortcuts = [...state.shortcuts].filter((shortcut) => shortcut.id !== action.payload);
},
},
});
export const { setShortcuts, createShortcut, patchShortcut, deleteShortcut } = shortcutSlice.actions;
export default shortcutSlice.reducer;

View File

@ -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<User>) => {
return {
...state,
user: action.payload,
};
},
signout: (state) => {
return {
...state,
user: undefined,
};
},
patchUser: (state, action: PayloadAction<Partial<User>>) => {
state.user = {
...state.user,
...action.payload,
} as User;
},
},
});
export const { signin, signout, patchUser } = userSlice.actions;
export default userSlice.reducer;

View File

@ -1,6 +0,0 @@
import { createContext } from "react";
import appStore from "./appStore";
const appContext = createContext(appStore.getState());
export default appContext;

View File

@ -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<AppState, AppStateActions>(
{
globalState: globalStore.defaultState,
locationState: locationStore.defaultState,
memoState: memoStore.defaultState,
userState: userStore.defaultState,
shortcutState: shortcutStore.defaultState,
},
combineReducers<AppState, AppStateActions>({
globalState: globalStore.reducer,
locationState: locationStore.reducer,
memoState: memoStore.reducer,
userState: userStore.reducer,
shortcutState: shortcutStore.reducer,
})
);
export default appStore;

View File

@ -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<AppSetting>;
}
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,
};

View File

@ -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: "",
},
};

View File

@ -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: [],
};

View File

@ -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: [],
};

View File

@ -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 };

View File

@ -9,3 +9,5 @@ type FunctionType = (...args: unknown[]) => unknown;
interface KVObject<T = any> { interface KVObject<T = any> {
[key: string]: T; [key: string]: T;
} }
type Option<T> = T | undefined;

View File

@ -4,10 +4,10 @@ interface Duration {
} }
interface Query { interface Query {
tag: string; tag?: string;
duration: Duration | null; duration?: Duration;
type: MemoSpecType | ""; type?: MemoSpecType;
text: string; text?: string;
shortcutId?: ShortcutId; shortcutId?: ShortcutId;
} }

View File

@ -202,6 +202,13 @@
"@babel/plugin-syntax-jsx" "^7.16.7" "@babel/plugin-syntax-jsx" "^7.16.7"
"@babel/types" "^7.17.0" "@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": "@babel/template@^7.16.7":
version "7.16.7" version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.7.tgz#8d126c8701fde4d66b264b3eba3d96f07666d155"
@ -303,6 +310,16 @@
"@nodelib/fs.scandir" "2.1.5" "@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0" 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": "@rollup/pluginutils@^4.2.0":
version "4.2.1" version "4.2.1"
resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d"
@ -311,6 +328,14 @@
estree-walker "^2.0.1" estree-walker "^2.0.1"
picomatch "^2.2.2" 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": "@types/json-schema@^7.0.9":
version "7.0.11" version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" 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" resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.2.tgz#1a62f89525723dde24ba1b01b092bf5df8ad4d39"
integrity sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew== 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": "@typescript-eslint/eslint-plugin@^5.6.0":
version "5.19.0" version "5.19.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.19.0.tgz#9608a4b6d0427104bccf132f058cba629a6553c0" 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: dependencies:
function-bind "^1.1.1" 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: iconv-lite@^0.4.4:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 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" resolved "https://registry.yarnpkg.com/image-size/-/image-size-0.5.5.tgz#09dfd4ab9d20e29eb1c3e80b8990378df9e3cb9c"
integrity sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w= 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: import-fresh@^3.0.0, import-fresh@^3.2.1:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" 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" loose-envify "^1.1.0"
scheduler "^0.22.0" scheduler "^0.22.0"
react-is@^16.13.1: react-is@^16.13.1, react-is@^16.7.0:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== 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: react-refresh@^0.12.0:
version "0.12.0" version "0.12.0"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.12.0.tgz#28ac0a2c30ef2bb3433d5fd0621e69a6d774c3a4" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.12.0.tgz#28ac0a2c30ef2bb3433d5fd0621e69a6d774c3a4"
@ -1937,6 +1996,23 @@ readdirp@~3.6.0:
dependencies: dependencies:
picomatch "^2.2.1" 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: regexp.prototype.flags@^1.4.1:
version "1.4.2" version "1.4.2"
resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.2.tgz#bf635117a2f4b755595ebb0c0ee2d2a49b2084db" 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" resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2"
integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== 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: resolve-from@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6"
@ -2234,6 +2315,11 @@ uri-js@^4.2.2:
dependencies: dependencies:
punycode "^2.1.0" 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: util-deprecate@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"