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"
},
"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",

View File

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

View File

@ -48,11 +48,9 @@ const CreateShortcutDialog: React.FC<Props> = (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);

View File

@ -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<T extends DialogProps>(
);
if (config.useAppContext) {
Fragment = (
<Provider store={appStore} context={appContext}>
{Fragment}
</Provider>
);
Fragment = <Provider store={store}>{Fragment}</Provider>;
}
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 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: 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, "<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>");
// 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;

View File

@ -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: Props) => {
const handleEditMemoBtnClick = useCallback(() => {
props.destroy();
globalStateService.setEditMemoId(memo.id);
editorStateService.setEditMemo(memo.id);
}, [memo.id]);
return (

View File

@ -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<Props> = () => {
const {
globalState,
memoState: { tags },
} = useContext(appContext);
editor: editorState,
memo: { tags },
} = useAppSelector((state) => state);
const [isTagSeletorShown, toggleTagSeletor] = useToggle(false);
const editorRef = useRef<EditorRefActions>(null);
const prevGlobalStateRef = useRef(globalState);
const prevGlobalStateRef = useRef(editorState);
const tagSeletorRef = useRef<HTMLDivElement>(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<Props> = () => {
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<Props> = () => {
}, []);
const handleCancelBtnClick = useCallback(() => {
globalStateService.setEditMemoId(UNKNOWN_ID);
editorStateService.setEditMemo(UNKNOWN_ID);
editorRef.current?.setContent("");
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(
() => ({

View File

@ -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<FilterProps> = () => {
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<FilterProps> = () => {
<div
className={"filter-item-container " + (memoType ? "" : "hidden")}
onClick={() => {
locationService.setMemoTypeQuery("");
locationService.setMemoTypeQuery(undefined);
}}
>
<span className="icon-text">📦</span> {getTextWithMemoType(memoType as MemoSpecType)}

View File

@ -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<Props> = () => {
const {
locationState: { query },
memoState: { memos },
} = useContext(appContext);
location: { query },
memo: { memos },
} = useAppSelector((state) => state);
const [isFetching, setFetchStatus] = useState(true);
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 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 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<Props> = () => {
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 {

View File

@ -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<Props> = () => {
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();

View File

@ -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<Props> = () => {
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);
};

View File

@ -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: Props) => {
const {
userState: { user },
} = useContext(appContext);
user: { user },
} = useAppSelector((state) => state);
const { destroy } = props;
const [state, setState] = useState<State>({
selectedSection: "my-account",

View File

@ -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<Props> = () => {
const { userState } = useContext(appContext);
const { user: userState } = useAppSelector((state) => state);
const user = userState.user as User;
const [username, setUsername] = useState<string>(user.name);
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 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<Props> = () => {
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<Props> = () => {
</p>
<div className="shortcuts-container">
{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>
@ -80,7 +78,7 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
if (showConfirmDeleteBtn) {
try {
await shortcutService.deleteShortcut(shortcut.id);
await shortcutService.deleteShortcutById(shortcut.id);
} catch (error: any) {
toastHelper.error(error.message);
}

View File

@ -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<Props> = () => {
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 = () => {

View File

@ -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<Props> = () => {
const {
locationState: {
query: { tag: tagQuery },
},
memoState: { tags: tagsText, memos },
} = useContext(appContext);
location: { query },
memo: { memos, tags: tagsText },
} = useAppSelector((state) => state);
const [tags, setTags] = useState<Tag[]>([]);
useEffect(() => {
@ -73,9 +71,9 @@ const TagList: React.FC<Props> = () => {
<p className="title-text">Tags</p>
<div className="tags-container">
{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">
Enter <span className="code-text">#tag </span> to create a tag
</p>
@ -87,7 +85,7 @@ const TagList: React.FC<Props> = () => {
interface TagItemContainerProps {
tag: Tag;
tagQuery: string;
tagQuery?: string;
}
const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContainerProps) => {

View File

@ -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<Props> = () => {
const beginDayTimestemp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP;
const {
memoState: { memos },
} = useContext(appContext);
memo: { memos },
} = useAppSelector((state) => state);
const [allStat, setAllStat] = useState<DailyUsageStat[]>(getInitialUsageStat(usedDaysAmount, beginDayTimestemp));
const [popupStat, setPopupStat] = 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) => {
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) {

View File

@ -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<Props> = () => {
const {
userState: { user },
} = useContext(appContext);
user: { user },
} = useAppSelector((state) => state);
const username = user ? user.name : "Memos";
const [shouldShowPopupBtns, setShouldShowPopupBtns] = useState(false);

View File

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

View File

@ -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(
<React.StrictMode>
<Provider store={appStore} context={appContext}>
<Provider store={store}>
<App />
</Provider>
</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 { 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(() => {

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

View File

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

View File

@ -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<string>();
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<Memo[]> {
const { memos } = this.getState();
getLinkedMemos: async (memoId: MemoId): Promise<Memo[]> => {
const { memos } = memoService.getState();
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({
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({
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;

View File

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

View File

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

View File

@ -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<void> {
updateUsername: async (name: string): Promise<void> => {
await api.patchUser({
name,
});
}
},
public async updatePassword(password: string): Promise<void> {
updatePassword: async (password: string): Promise<void> => {
await api.patchUser({
password,
});
}
},
public async resetOpenId(): Promise<string> {
resetOpenId: async (): Promise<string> => {
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;

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> {
[key: string]: T;
}
type Option<T> = T | undefined;

View File

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

View File

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