diff --git a/web/src/App.tsx b/web/src/App.tsx index 7c89376a7..5a6b53059 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -2,29 +2,30 @@ import { useColorScheme } from "@mui/joy"; import { useEffect, Suspense } from "react"; import { useTranslation } from "react-i18next"; import { RouterProvider } from "react-router-dom"; -import { globalService, locationService } from "./services"; -import { useAppSelector } from "./store"; import router from "./router"; +import { useLocationStore, useGlobalStore } from "./store/module"; import * as storage from "./helpers/storage"; import { getSystemColorScheme } from "./helpers/utils"; import Loading from "./pages/Loading"; -function App() { +const App = () => { const { i18n } = useTranslation(); - const { appearance, locale, systemStatus } = useAppSelector((state) => state.global); + const globalStore = useGlobalStore(); + const locationStore = useLocationStore(); const { mode, setMode } = useColorScheme(); + const { appearance, locale, systemStatus } = globalStore.state; useEffect(() => { - locationService.updateStateWithLocation(); + locationStore.updateStateWithLocation(); window.onpopstate = () => { - locationService.updateStateWithLocation(); + locationStore.updateStateWithLocation(); }; }, []); useEffect(() => { const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); const handleColorSchemeChange = (e: MediaQueryListEvent) => { - if (globalService.getState().appearance === "system") { + if (globalStore.getState().appearance === "system") { const mode = e.matches ? "dark" : "light"; setMode(mode); } @@ -91,6 +92,6 @@ function App() { ); -} +}; export default App; diff --git a/web/src/components/AboutSiteDialog.tsx b/web/src/components/AboutSiteDialog.tsx index dd8098357..6aead8373 100644 --- a/web/src/components/AboutSiteDialog.tsx +++ b/web/src/components/AboutSiteDialog.tsx @@ -1,5 +1,5 @@ import { useTranslation } from "react-i18next"; -import { useAppSelector } from "../store"; +import { useGlobalStore } from "../store/module"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; import GitHubBadge from "./GitHubBadge"; @@ -9,7 +9,8 @@ type Props = DialogProps; const AboutSiteDialog: React.FC = ({ destroy }: Props) => { const { t } = useTranslation(); - const profile = useAppSelector((state) => state.global.systemStatus.profile); + const globalStore = useGlobalStore(); + const profile = globalStore.state.systemStatus.profile; const handleCloseBtnClick = () => { destroy(); diff --git a/web/src/components/AppearanceSelect.tsx b/web/src/components/AppearanceSelect.tsx index 2ddf3f04d..69a00942d 100644 --- a/web/src/components/AppearanceSelect.tsx +++ b/web/src/components/AppearanceSelect.tsx @@ -1,15 +1,16 @@ import { Option, Select } from "@mui/joy"; import { useTranslation } from "react-i18next"; -import { globalService, userService } from "../services"; -import { useAppSelector } from "../store"; +import { useGlobalStore, useUserStore } from "../store/module"; import Icon from "./Icon"; const appearanceList = ["system", "light", "dark"]; const AppearanceSelect = () => { - const user = useAppSelector((state) => state.user.user); - const appearance = useAppSelector((state) => state.global.appearance); const { t } = useTranslation(); + const globalStore = useGlobalStore(); + const userStore = useUserStore(); + const { appearance } = globalStore.state; + const user = userStore.state.user; const getPrefixIcon = (apperance: Appearance) => { const className = "w-4 h-auto"; @@ -24,9 +25,9 @@ const AppearanceSelect = () => { const handleSelectChange = async (appearance: Appearance) => { if (user) { - await userService.upsertUserSetting("appearance", appearance); + await userStore.upsertUserSetting("appearance", appearance); } - globalService.setAppearance(appearance); + globalStore.setAppearance(appearance); }; return ( diff --git a/web/src/components/ArchivedMemo.tsx b/web/src/components/ArchivedMemo.tsx index a919460e7..95d981f6a 100644 --- a/web/src/components/ArchivedMemo.tsx +++ b/web/src/components/ArchivedMemo.tsx @@ -1,7 +1,7 @@ import { useTranslation } from "react-i18next"; +import { useMemoStore } from "../store/module"; import * as utils from "../helpers/utils"; import useToggle from "../hooks/useToggle"; -import { memoService } from "../services"; import toastHelper from "./Toast"; import MemoContent from "./MemoContent"; import MemoResources from "./MemoResources"; @@ -14,13 +14,13 @@ interface Props { const ArchivedMemo: React.FC = (props: Props) => { const { memo } = props; const { t } = useTranslation(); - + const memoStore = useMemoStore(); const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false); const handleDeleteMemoClick = async () => { if (showConfirmDeleteBtn) { try { - await memoService.deleteMemoById(memo.id); + await memoStore.deleteMemoById(memo.id); } catch (error: any) { console.error(error); toastHelper.error(error.response.data.message); @@ -32,11 +32,11 @@ const ArchivedMemo: React.FC = (props: Props) => { const handleRestoreMemoClick = async () => { try { - await memoService.patchMemo({ + await memoStore.patchMemo({ id: memo.id, rowStatus: "NORMAL", }); - await memoService.fetchMemos(); + await memoStore.fetchMemos(); toastHelper.info(t("message.restored-successfully")); } catch (error: any) { console.error(error); diff --git a/web/src/components/ArchivedMemoDialog.tsx b/web/src/components/ArchivedMemoDialog.tsx index 1b6085fd4..a68ac5743 100644 --- a/web/src/components/ArchivedMemoDialog.tsx +++ b/web/src/components/ArchivedMemoDialog.tsx @@ -1,8 +1,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useMemoStore } from "../store/module"; import useLoading from "../hooks/useLoading"; -import { memoService } from "../services"; -import { useAppSelector } from "../store"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; import toastHelper from "./Toast"; @@ -14,12 +13,13 @@ type Props = DialogProps; const ArchivedMemoDialog: React.FC = (props: Props) => { const { t } = useTranslation(); const { destroy } = props; - const memos = useAppSelector((state) => state.memo.memos); + const memoStore = useMemoStore(); + const memos = memoStore.state.memos; const loadingState = useLoading(); const [archivedMemos, setArchivedMemos] = useState([]); useEffect(() => { - memoService + memoStore .fetchArchivedMemos() .then((result) => { setArchivedMemos(result); diff --git a/web/src/components/ChangeMemberPasswordDialog.tsx b/web/src/components/ChangeMemberPasswordDialog.tsx index 873961a01..ce49643ce 100644 --- a/web/src/components/ChangeMemberPasswordDialog.tsx +++ b/web/src/components/ChangeMemberPasswordDialog.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useUserStore } from "../store/module"; import { validate, ValidatorConfig } from "../helpers/validator"; -import { userService } from "../services"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; import toastHelper from "./Toast"; @@ -20,6 +20,7 @@ interface Props extends DialogProps { const ChangeMemberPasswordDialog: React.FC = (props: Props) => { const { user: propsUser, destroy } = props; const { t } = useTranslation(); + const userStore = useUserStore(); const [newPassword, setNewPassword] = useState(""); const [newPasswordAgain, setNewPasswordAgain] = useState(""); @@ -60,7 +61,7 @@ const ChangeMemberPasswordDialog: React.FC = (props: Props) => { } try { - await userService.patchUser({ + await userStore.patchUser({ id: propsUser.id, password: newPassword, }); diff --git a/web/src/components/ChangeMemoCreatedTsDialog.tsx b/web/src/components/ChangeMemoCreatedTsDialog.tsx index b43e5de19..02a1889ab 100644 --- a/web/src/components/ChangeMemoCreatedTsDialog.tsx +++ b/web/src/components/ChangeMemoCreatedTsDialog.tsx @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { memoService } from "../services"; +import { useMemoStore } from "../store/module"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; import toastHelper from "./Toast"; @@ -14,11 +14,12 @@ interface Props extends DialogProps { const ChangeMemoCreatedTsDialog: React.FC = (props: Props) => { const { t } = useTranslation(); const { destroy, memoId } = props; + const memoStore = useMemoStore(); const [createdAt, setCreatedAt] = useState(""); const maxDatetimeValue = dayjs().format("YYYY-MM-DDTHH:mm"); useEffect(() => { - memoService.getMemoById(memoId).then((memo) => { + memoStore.getMemoById(memoId).then((memo) => { if (memo) { const datetime = dayjs(memo.createdTs).format("YYYY-MM-DDTHH:mm"); setCreatedAt(datetime); @@ -48,7 +49,7 @@ const ChangeMemoCreatedTsDialog: React.FC = (props: Props) => { } try { - await memoService.patchMemo({ + await memoStore.patchMemo({ id: memoId, createdTs, }); diff --git a/web/src/components/ChangePasswordDialog.tsx b/web/src/components/ChangePasswordDialog.tsx index 45c61e924..599eeda60 100644 --- a/web/src/components/ChangePasswordDialog.tsx +++ b/web/src/components/ChangePasswordDialog.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useUserStore } from "../store/module"; import { validate, ValidatorConfig } from "../helpers/validator"; -import { userService } from "../services"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; import toastHelper from "./Toast"; @@ -17,6 +17,7 @@ type Props = DialogProps; const ChangePasswordDialog: React.FC = ({ destroy }: Props) => { const { t } = useTranslation(); + const userStore = useUserStore(); const [newPassword, setNewPassword] = useState(""); const [newPasswordAgain, setNewPasswordAgain] = useState(""); @@ -57,8 +58,8 @@ const ChangePasswordDialog: React.FC = ({ destroy }: Props) => { } try { - const user = userService.getState().user as User; - await userService.patchUser({ + const user = userStore.getState().user as User; + await userStore.patchUser({ id: user.id, password: newPassword, }); diff --git a/web/src/components/ChangeResourceFilenameDialog.tsx b/web/src/components/ChangeResourceFilenameDialog.tsx index f1c5dad2e..82878f99e 100644 --- a/web/src/components/ChangeResourceFilenameDialog.tsx +++ b/web/src/components/ChangeResourceFilenameDialog.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { resourceService } from "../services"; +import { useResourceStore } from "../store/module"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; import toastHelper from "./Toast"; @@ -24,8 +24,9 @@ const validateFilename = (filename: string): boolean => { }; const ChangeResourceFilenameDialog: React.FC = (props: Props) => { - const { t } = useTranslation(); const { destroy, resourceId, resourceFilename } = props; + const { t } = useTranslation(); + const resourceStore = useResourceStore(); const [filename, setFilename] = useState(resourceFilename); const handleFilenameChanged = (e: React.ChangeEvent) => { @@ -47,7 +48,7 @@ const ChangeResourceFilenameDialog: React.FC = (props: Props) => { return; } try { - await resourceService.patchResource({ + await resourceStore.patchResource({ id: resourceId, filename: filename, }); diff --git a/web/src/components/CreateShortcutDialog.tsx b/web/src/components/CreateShortcutDialog.tsx index dc851253f..ee71d9f3a 100644 --- a/web/src/components/CreateShortcutDialog.tsx +++ b/web/src/components/CreateShortcutDialog.tsx @@ -1,7 +1,7 @@ import dayjs from "dayjs"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { memoService, shortcutService } from "../services"; +import { useMemoStore, useShortcutStore } from "../store/module"; import { filterConsts, getDefaultFilter, relationConsts } from "../helpers/filter"; import useLoading from "../hooks/useLoading"; import Icon from "./Icon"; @@ -16,6 +16,7 @@ interface Props extends DialogProps { const CreateShortcutDialog: React.FC = (props: Props) => { const { destroy, shortcutId } = props; + const shortcutStore = useShortcutStore(); const [title, setTitle] = useState(""); const [filters, setFilters] = useState([]); const requestState = useLoading(false); @@ -23,7 +24,7 @@ const CreateShortcutDialog: React.FC = (props: Props) => { useEffect(() => { if (shortcutId) { - const shortcutTemp = shortcutService.getShortcutById(shortcutId); + const shortcutTemp = shortcutStore.getShortcutById(shortcutId); if (shortcutTemp) { setTitle(shortcutTemp.title); const temp = JSON.parse(shortcutTemp.payload); @@ -52,13 +53,13 @@ const CreateShortcutDialog: React.FC = (props: Props) => { } try { if (shortcutId) { - await shortcutService.patchShortcut({ + await shortcutStore.patchShortcut({ id: shortcutId, title, payload: JSON.stringify(filters), }); } else { - await shortcutService.createShortcut({ + await shortcutStore.createShortcut({ title, payload: JSON.stringify(filters), }); @@ -161,9 +162,9 @@ interface MemoFilterInputerProps { const MemoFilterInputer: React.FC = (props: MemoFilterInputerProps) => { const { index, filter, handleFilterChange, handleFilterRemove } = props; const { t } = useTranslation(); + const memoStore = useMemoStore(); const [value, setValue] = useState(filter.value.value); - - const tags = Array.from(memoService.getState().tags); + const tags = Array.from(memoStore.getState().tags); const { type } = filter; const operatorDataSource = Object.values(filterConsts[type as FilterType].operators).map(({ text, value }) => ({ text: t(text), value })); diff --git a/web/src/components/DailyReviewDialog.tsx b/web/src/components/DailyReviewDialog.tsx index c4c6f869f..502f94cf0 100644 --- a/web/src/components/DailyReviewDialog.tsx +++ b/web/src/components/DailyReviewDialog.tsx @@ -1,6 +1,6 @@ import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useAppSelector } from "../store"; +import { useMemoStore } from "../store/module"; import toImage from "../labs/html2image"; import useToggle from "../hooks/useToggle"; import { DAILY_TIMESTAMP } from "../helpers/consts"; @@ -21,7 +21,8 @@ const weekdayChineseStrArray = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] const DailyReviewDialog: React.FC = (props: Props) => { const { t } = useTranslation(); - const memos = useAppSelector((state) => state.memo.memos); + const memoStore = useMemoStore(); + const memos = memoStore.state.memos; const [currentDateStamp, setCurrentDateStamp] = useState(utils.getDateStampByDate(utils.getDateString(props.currentDateStamp))); const [showDatePicker, toggleShowDatePicker] = useToggle(false); const memosElRef = useRef(null); diff --git a/web/src/components/Memo.tsx b/web/src/components/Memo.tsx index 774b8c87f..2817c3964 100644 --- a/web/src/components/Memo.tsx +++ b/web/src/components/Memo.tsx @@ -3,7 +3,7 @@ import dayjs from "dayjs"; import { memo, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { editorStateService, locationService, memoService, userService } from "../services"; +import { useEditorStore, useLocationStore, useMemoStore, useUserStore } from "../store/module"; import Icon from "./Icon"; import toastHelper from "./Toast"; import MemoContent from "./MemoContent"; @@ -30,9 +30,13 @@ const Memo: React.FC = (props: Props) => { const { memo, highlightWord } = props; const { t, i18n } = useTranslation(); const navigate = useNavigate(); + const editorStore = useEditorStore(); + const locationStore = useLocationStore(); + const userStore = useUserStore(); + const memoStore = useMemoStore(); const [displayTimeStr, setDisplayTimeStr] = useState(getFormatedMemoTimeStr(memo.displayTs, i18n.language)); const memoContainerRef = useRef(null); - const isVisitorMode = userService.isVisitorMode(); + const isVisitorMode = userStore.isVisitorMode(); useEffect(() => { let intervalFlag: any = -1; @@ -59,9 +63,9 @@ const Memo: React.FC = (props: Props) => { const handleTogglePinMemoBtnClick = async () => { try { if (memo.pinned) { - await memoService.unpinMemo(memo.id); + await memoStore.unpinMemo(memo.id); } else { - await memoService.pinMemo(memo.id); + await memoStore.pinMemo(memo.id); } } catch (error) { // do nth @@ -69,12 +73,12 @@ const Memo: React.FC = (props: Props) => { }; const handleEditMemoClick = () => { - editorStateService.setEditMemoWithId(memo.id); + editorStore.setEditMemoWithId(memo.id); }; const handleArchiveMemoClick = async () => { try { - await memoService.patchMemo({ + await memoStore.patchMemo({ id: memo.id, rowStatus: "ARCHIVED", }); @@ -83,8 +87,8 @@ const Memo: React.FC = (props: Props) => { toastHelper.error(error.response.data.message); } - if (editorStateService.getState().editMemoId === memo.id) { - editorStateService.clearEditMemo(); + if (editorStore.getState().editMemoId === memo.id) { + editorStore.clearEditMemo(); } }; @@ -97,14 +101,14 @@ const Memo: React.FC = (props: Props) => { if (targetEl.className === "tag-span") { const tagName = targetEl.innerText.slice(1); - const currTagQuery = locationService.getState().query?.tag; + const currTagQuery = locationStore.getState().query?.tag; if (currTagQuery === tagName) { - locationService.setTagQuery(undefined); + locationStore.setTagQuery(undefined); } else { - locationService.setTagQuery(tagName); + locationStore.setTagQuery(tagName); } } else if (targetEl.classList.contains("todo-block")) { - if (userService.isVisitorMode()) { + if (userStore.isVisitorMode()) { return; } @@ -128,7 +132,7 @@ const Memo: React.FC = (props: Props) => { finalContent += `${tempList[i]}`; } } - await memoService.patchMemo({ + await memoStore.patchMemo({ id: memo.id, content: finalContent, }); @@ -151,7 +155,7 @@ const Memo: React.FC = (props: Props) => { return; } - editorStateService.setEditMemoWithId(memo.id); + editorStore.setEditMemoWithId(memo.id); }; const handleMemoDisplayTimeClick = () => { @@ -159,11 +163,11 @@ const Memo: React.FC = (props: Props) => { }; const handleMemoVisibilityClick = (visibility: Visibility) => { - const currVisibilityQuery = locationService.getState().query?.visibility; + const currVisibilityQuery = locationStore.getState().query?.visibility; if (currVisibilityQuery === visibility) { - locationService.setMemoVisibilityQuery(undefined); + locationStore.setMemoVisibilityQuery(undefined); } else { - locationService.setMemoVisibilityQuery(visibility); + locationStore.setMemoVisibilityQuery(visibility); } }; diff --git a/web/src/components/MemoContent.tsx b/web/src/components/MemoContent.tsx index 9c7179afe..a486eb038 100644 --- a/web/src/components/MemoContent.tsx +++ b/web/src/components/MemoContent.tsx @@ -1,9 +1,9 @@ import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; +import { useUserStore } from "../store/module"; import { marked } from "../labs/marked"; import { highlightWithWord } from "../labs/highlighter"; import Icon from "./Icon"; -import { useAppSelector } from "../store"; import "../less/memo-content.less"; export interface DisplayConfig { @@ -36,7 +36,8 @@ const MemoContent: React.FC = (props: Props) => { return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content; }, [content]); const { t } = useTranslation(); - const user = useAppSelector((state) => state.user.user); + const userStore = useUserStore(); + const user = userStore.state.user; const [state, setState] = useState({ expandButtonStatus: -1, @@ -84,6 +85,9 @@ const MemoContent: React.FC = (props: Props) => { setState({ expandButtonStatus: Number(expandButtonStatus) as ExpandButtonStatus, }); + if (!expandButtonStatus) { + memoContentContainerRef.current?.scrollIntoView(); + } }; return ( diff --git a/web/src/components/MemoEditor.tsx b/web/src/components/MemoEditor.tsx index 73cc73ece..15ff907bd 100644 --- a/web/src/components/MemoEditor.tsx +++ b/web/src/components/MemoEditor.tsx @@ -3,8 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useTranslation } from "react-i18next"; import { deleteMemoResource, upsertMemoResource } from "../helpers/api"; import { TAB_SPACE_WIDTH, UNKNOWN_ID, VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts"; -import { editorStateService, locationService, memoService, resourceService } from "../services"; -import { useAppSelector } from "../store"; +import { useEditorStore, useLocationStore, useMemoStore, useResourceStore, useUserStore } from "../store/module"; import * as storage from "../helpers/storage"; import Icon from "./Icon"; import toastHelper from "./Toast"; @@ -41,19 +40,25 @@ interface State { const MemoEditor = () => { const { t, i18n } = useTranslation(); - const user = useAppSelector((state) => state.user.user as User); - const setting = user.setting; - const editorState = useAppSelector((state) => state.editor); - const tags = useAppSelector((state) => state.memo.tags); + const userStore = useUserStore(); + const editorStore = useEditorStore(); + const locationStore = useLocationStore(); + const memoStore = useMemoStore(); + const resourceStore = useResourceStore(); + const [state, setState] = useState({ isUploadingResource: false, fullscreen: false, shouldShowEmojiPicker: false, }); const [allowSave, setAllowSave] = useState(false); + const editorState = editorStore.state; const prevEditorStateRef = useRef(editorState); const editorRef = useRef(null); const tagSelectorRef = useRef(null); + const user = userStore.state.user as User; + const setting = user.setting; + const tags = memoStore.state.tags; const memoVisibilityOptionSelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => { return { value: item.value, @@ -64,22 +69,22 @@ const MemoEditor = () => { useEffect(() => { const { editingMemoIdCache, editingMemoVisibilityCache } = storage.get(["editingMemoIdCache", "editingMemoVisibilityCache"]); if (editingMemoIdCache) { - editorStateService.setEditMemoWithId(editingMemoIdCache); + editorStore.setEditMemoWithId(editingMemoIdCache); } if (editingMemoVisibilityCache) { - editorStateService.setMemoVisibility(editingMemoVisibilityCache as "PUBLIC" | "PROTECTED" | "PRIVATE"); + editorStore.setMemoVisibility(editingMemoVisibilityCache as "PUBLIC" | "PROTECTED" | "PRIVATE"); } else { - editorStateService.setMemoVisibility(setting.memoVisibility); + editorStore.setMemoVisibility(setting.memoVisibility); } }, []); useEffect(() => { if (editorState.editMemoId) { - memoService.getMemoById(editorState.editMemoId ?? UNKNOWN_ID).then((memo) => { + memoStore.getMemoById(editorState.editMemoId ?? UNKNOWN_ID).then((memo) => { if (memo) { handleEditorFocus(); - editorStateService.setMemoVisibility(memo.visibility); - editorStateService.setResourceList(memo.resourceList); + editorStore.setMemoVisibility(memo.visibility); + editorStore.setResourceList(memo.resourceList); editorRef.current?.setContent(memo.content ?? ""); } }); @@ -180,8 +185,8 @@ const MemoEditor = () => { } } if (uploadedResourceList.length > 0) { - const resourceList = editorStateService.getState().resourceList; - editorStateService.setResourceList([...resourceList, ...uploadedResourceList]); + const resourceList = editorStore.getState().resourceList; + editorStore.setResourceList([...resourceList, ...uploadedResourceList]); } }; @@ -210,7 +215,7 @@ const MemoEditor = () => { let resource = undefined; try { - resource = await resourceService.upload(file); + resource = await resourceStore.upload(file); } catch (error: any) { console.error(error); toastHelper.error(error.response.data.message); @@ -233,26 +238,26 @@ const MemoEditor = () => { } try { - const { editMemoId } = editorStateService.getState(); + const { editMemoId } = editorStore.getState(); if (editMemoId && editMemoId !== UNKNOWN_ID) { - const prevMemo = await memoService.getMemoById(editMemoId ?? UNKNOWN_ID); + const prevMemo = await memoStore.getMemoById(editMemoId ?? UNKNOWN_ID); if (prevMemo) { - await memoService.patchMemo({ + await memoStore.patchMemo({ id: prevMemo.id, content, visibility: editorState.memoVisibility, resourceIdList: editorState.resourceList.map((resource) => resource.id), }); } - editorStateService.clearEditMemo(); + editorStore.clearEditMemo(); } else { - await memoService.createMemo({ + await memoStore.createMemo({ content, visibility: editorState.memoVisibility, resourceIdList: editorState.resourceList.map((resource) => resource.id), }); - locationService.clearQuery(); + locationStore.clearQuery(); } } catch (error: any) { console.error(error); @@ -265,7 +270,7 @@ const MemoEditor = () => { fullscreen: false, }; }); - editorStateService.clearResourceList(); + editorStore.clearResourceList(); setEditorContentCache(""); storage.remove(["editingMemoVisibilityCache"]); editorRef.current?.setContent(""); @@ -273,8 +278,8 @@ const MemoEditor = () => { const handleCancelEdit = () => { if (editorState.editMemoId) { - editorStateService.clearEditMemo(); - editorStateService.clearResourceList(); + editorStore.clearEditMemo(); + editorStore.clearResourceList(); editorRef.current?.setContent(""); setEditorContentCache(""); storage.remove(["editingMemoVisibilityCache"]); @@ -338,7 +343,7 @@ const MemoEditor = () => { } } } - editorStateService.setResourceList([...editorState.resourceList, ...resourceList]); + editorStore.setResourceList([...editorState.resourceList, ...resourceList]); document.body.removeChild(inputEl); }; inputEl.click(); @@ -361,7 +366,7 @@ const MemoEditor = () => { }, []); const handleDeleteResource = async (resourceId: ResourceId) => { - editorStateService.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId)); + editorStore.setResourceList(editorState.resourceList.filter((resource) => resource.id !== resourceId)); if (editorState.editMemoId) { await deleteMemoResource(editorState.editMemoId, resourceId); } @@ -369,7 +374,7 @@ const MemoEditor = () => { const handleMemoVisibilityOptionChanged = async (value: string) => { const visibilityValue = value as Visibility; - editorStateService.setMemoVisibility(visibilityValue); + editorStore.setMemoVisibility(visibilityValue); setEditingMemoVisibilityCache(visibilityValue); }; diff --git a/web/src/components/MemoFilter.tsx b/web/src/components/MemoFilter.tsx index 16a6de28b..1a31ad957 100644 --- a/web/src/components/MemoFilter.tsx +++ b/web/src/components/MemoFilter.tsx @@ -1,6 +1,5 @@ import { useTranslation } from "react-i18next"; -import { useAppSelector } from "../store"; -import { locationService, shortcutService } from "../services"; +import { useLocationStore, useShortcutStore } from "../store/module"; import * as utils from "../helpers/utils"; import { getTextWithMemoType } from "../helpers/filter"; import Icon from "./Icon"; @@ -8,10 +7,11 @@ import "../less/memo-filter.less"; const MemoFilter = () => { const { t } = useTranslation(); - useAppSelector((state) => state.shortcut.shortcuts); - const query = useAppSelector((state) => state.location.query); + const locationStore = useLocationStore(); + const shortcutStore = useShortcutStore(); + const query = locationStore.state.query; const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query; - const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null; + const shortcut = shortcutId ? shortcutStore.getShortcutById(shortcutId) : null; const showFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut || visibility); return ( @@ -20,7 +20,7 @@ const MemoFilter = () => { { - locationService.setMemoShortcut(undefined); + locationStore.setMemoShortcut(undefined); }} > {shortcut?.title} @@ -28,7 +28,7 @@ const MemoFilter = () => { { - locationService.setTagQuery(undefined); + locationStore.setTagQuery(undefined); }} > {tagQuery} @@ -36,7 +36,7 @@ const MemoFilter = () => { { - locationService.setMemoTypeQuery(undefined); + locationStore.setMemoTypeQuery(undefined); }} > {t(getTextWithMemoType(memoType as MemoSpecType))} @@ -44,7 +44,7 @@ const MemoFilter = () => { { - locationService.setMemoVisibilityQuery(undefined); + locationStore.setMemoVisibilityQuery(undefined); }} > {visibility} @@ -53,7 +53,7 @@ const MemoFilter = () => { { - locationService.setFromAndToQuery(); + locationStore.setFromAndToQuery(); }} > {utils.getDateString(duration.from)} to {utils.getDateString(duration.to)} @@ -62,7 +62,7 @@ const MemoFilter = () => { { - locationService.setTextQuery(undefined); + locationStore.setTextQuery(undefined); }} > {textQuery} diff --git a/web/src/components/MemoList.tsx b/web/src/components/MemoList.tsx index 223d8f758..55c7a00eb 100644 --- a/web/src/components/MemoList.tsx +++ b/web/src/components/MemoList.tsx @@ -1,10 +1,9 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { memoService, shortcutService } from "../services"; -import { DEFAULT_MEMO_LIMIT } from "../services/memoService"; -import { useAppSelector } from "../store"; +import { useLocationStore, useMemoStore, useShortcutStore, useUserStore } from "../store/module"; import { TAG_REG, LINK_REG } from "../labs/marked/parser"; import * as utils from "../helpers/utils"; +import { DEFAULT_MEMO_LIMIT } from "../helpers/consts"; import { checkShouldShowMemoWithFilters } from "../helpers/filter"; import toastHelper from "./Toast"; import Memo from "./Memo"; @@ -12,14 +11,18 @@ import "../less/memo-list.less"; const MemoList = () => { const { t } = useTranslation(); - const query = useAppSelector((state) => state.location.query); - const memoDisplayTsOption = useAppSelector((state) => state.user.user?.setting.memoDisplayTsOption); - const { memos, isFetching } = useAppSelector((state) => state.memo); + const userStore = useUserStore(); + const memoStore = useMemoStore(); + const shortcutStore = useShortcutStore(); + const locationStore = useLocationStore(); + const query = locationStore.state.query; + const memoDisplayTsOption = userStore.state.user?.setting.memoDisplayTsOption; + const { memos, isFetching } = memoStore.state; const [isComplete, setIsComplete] = useState(false); const [highlightWord, setHighlightWord] = useState(""); const { tag: tagQuery, duration, type: memoType, text: textQuery, shortcutId, visibility } = query ?? {}; - const shortcut = shortcutId ? shortcutService.getShortcutById(shortcutId) : null; + const shortcut = shortcutId ? shortcutStore.getShortcutById(shortcutId) : null; const showMemoFilter = Boolean(tagQuery || (duration && duration.from < duration.to) || memoType || textQuery || shortcut || visibility); const shownMemos = @@ -84,7 +87,7 @@ const MemoList = () => { const sortedMemos = pinnedMemos.concat(unpinnedMemos).filter((m) => m.rowStatus === "NORMAL"); useEffect(() => { - memoService + memoStore .fetchMemos() .then((fetchedMemos) => { if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) { @@ -118,7 +121,7 @@ const MemoList = () => { const handleFetchMoreClick = async () => { try { - const fetchedMemos = await memoService.fetchMemos(DEFAULT_MEMO_LIMIT, memos.length); + const fetchedMemos = await memoStore.fetchMemos(DEFAULT_MEMO_LIMIT, memos.length); if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) { setIsComplete(true); } else { diff --git a/web/src/components/MemosHeader.tsx b/web/src/components/MemosHeader.tsx index 303022d95..795825daa 100644 --- a/web/src/components/MemosHeader.tsx +++ b/web/src/components/MemosHeader.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useState } from "react"; -import { memoService, shortcutService } from "../services"; -import { useAppSelector } from "../store"; +import { useLocationStore, useMemoStore, useShortcutStore } from "../store/module"; import Icon from "./Icon"; import SearchBar from "./SearchBar"; import { toggleSidebar } from "./Sidebar"; @@ -9,8 +8,11 @@ import "../less/memos-header.less"; let prevRequestTimestamp = Date.now(); const MemosHeader = () => { - const query = useAppSelector((state) => state.location.query); - const shortcuts = useAppSelector((state) => state.shortcut.shortcuts); + const locationStore = useLocationStore(); + const memoStore = useMemoStore(); + const shortcutStore = useShortcutStore(); + const query = locationStore.state.query; + const shortcuts = shortcutStore.state.shortcuts; const [titleText, setTitleText] = useState("MEMOS"); useEffect(() => { @@ -19,7 +21,7 @@ const MemosHeader = () => { return; } - const shortcut = shortcutService.getShortcutById(query?.shortcutId); + const shortcut = shortcutStore.getShortcutById(query?.shortcutId); if (shortcut) { setTitleText(shortcut.title); } @@ -29,7 +31,7 @@ const MemosHeader = () => { const now = Date.now(); if (now - prevRequestTimestamp > 1 * 1000) { prevRequestTimestamp = now; - memoService.fetchMemos().catch(() => { + memoStore.fetchMemos().catch(() => { // do nth }); } diff --git a/web/src/components/ResourcesDialog.tsx b/web/src/components/ResourcesDialog.tsx index 259e7aa7d..dbd2b8b7f 100644 --- a/web/src/components/ResourcesDialog.tsx +++ b/web/src/components/ResourcesDialog.tsx @@ -3,8 +3,7 @@ import copy from "copy-to-clipboard"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import useLoading from "../hooks/useLoading"; -import { resourceService } from "../services"; -import { useAppSelector } from "../store"; +import { useResourceStore } from "../store/module"; import Icon from "./Icon"; import toastHelper from "./Toast"; import Dropdown from "./common/Dropdown"; @@ -24,13 +23,14 @@ const ResourcesDialog: React.FC = (props: Props) => { const { destroy } = props; const { t } = useTranslation(); const loadingState = useLoading(); - const { resources } = useAppSelector((state) => state.resource); + const resourceStore = useResourceStore(); + const resources = resourceStore.state.resources; const [state, setState] = useState({ isUploadingResource: false, }); useEffect(() => { - resourceService + resourceStore .fetchResourceList() .catch((error) => { console.error(error); @@ -66,7 +66,7 @@ const ResourcesDialog: React.FC = (props: Props) => { for (const file of inputEl.files) { try { - await resourceService.upload(file); + await resourceStore.upload(file); } catch (error: any) { console.error(error); toastHelper.error(error.response.data.message); @@ -127,7 +127,7 @@ const ResourcesDialog: React.FC = (props: Props) => { style: "warning", onConfirm: async () => { for (const resource of unusedResources) { - await resourceService.deleteResourceById(resource.id); + await resourceStore.deleteResourceById(resource.id); } }, }); @@ -144,7 +144,7 @@ const ResourcesDialog: React.FC = (props: Props) => { content: warningText, style: "warning", onConfirm: async () => { - await resourceService.deleteResourceById(resource.id); + await resourceStore.deleteResourceById(resource.id); }, }); }; diff --git a/web/src/components/ResourcesSelectorDialog.tsx b/web/src/components/ResourcesSelectorDialog.tsx index b31b98cf0..d0ca80f5d 100644 --- a/web/src/components/ResourcesSelectorDialog.tsx +++ b/web/src/components/ResourcesSelectorDialog.tsx @@ -2,8 +2,7 @@ import { Checkbox, Tooltip } from "@mui/joy"; import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import useLoading from "../hooks/useLoading"; -import { editorStateService, resourceService } from "../services"; -import { useAppSelector } from "../store"; +import { useEditorStore, useResourceStore } from "../store/module"; import Icon from "./Icon"; import toastHelper from "./Toast"; import { generateDialog } from "./Dialog"; @@ -20,14 +19,15 @@ const ResourcesSelectorDialog: React.FC = (props: Props) => { const { destroy } = props; const { t } = useTranslation(); const loadingState = useLoading(); - const { resources } = useAppSelector((state) => state.resource); - const editorState = useAppSelector((state) => state.editor); + const editorStore = useEditorStore(); + const resourceStore = useResourceStore(); + const resources = resourceStore.state.resources; const [state, setState] = useState({ checkedArray: [], }); useEffect(() => { - resourceService + resourceStore .fetchResourceList() .catch((error) => { console.error(error); @@ -39,7 +39,7 @@ const ResourcesSelectorDialog: React.FC = (props: Props) => { }, []); useEffect(() => { - const checkedResourceIdArray = editorState.resourceList.map((resource) => resource.id); + const checkedResourceIdArray = editorStore.state.resourceList.map((resource) => resource.id); setState({ checkedArray: resources.map((resource) => { return checkedResourceIdArray.includes(resource.id); @@ -75,7 +75,7 @@ const ResourcesSelectorDialog: React.FC = (props: Props) => { const resourceList = resources.filter((_, index) => { return state.checkedArray[index]; }); - editorStateService.setResourceList(resourceList); + editorStore.setResourceList(resourceList); destroy(); }; diff --git a/web/src/components/SearchBar.tsx b/web/src/components/SearchBar.tsx index 5a2912776..37a77a6a0 100644 --- a/web/src/components/SearchBar.tsx +++ b/web/src/components/SearchBar.tsx @@ -1,33 +1,33 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { locationService } from "../services"; -import { useAppSelector } from "../store"; +import { useLocationStore } from "../store/module"; import { memoSpecialTypes } from "../helpers/filter"; import Icon from "./Icon"; import "../less/search-bar.less"; const SearchBar = () => { const { t } = useTranslation(); - const memoType = useAppSelector((state) => state.location.query?.type); + const locationStore = useLocationStore(); + const memoType = locationStore.state.query.type; const [queryText, setQueryText] = useState(""); useEffect(() => { - const text = locationService.getState().query.text; + const text = locationStore.getState().query.text; setQueryText(text === undefined ? "" : text); - }, [locationService.getState().query.text]); + }, [locationStore.getState().query.text]); const handleMemoTypeItemClick = (type: MemoSpecType | undefined) => { - const { type: prevType } = locationService.getState().query ?? {}; + const { type: prevType } = locationStore.getState().query ?? {}; if (type === prevType) { type = undefined; } - locationService.setMemoTypeQuery(type); + locationStore.setMemoTypeQuery(type); }; const handleTextQueryInput = (event: React.FormEvent) => { const text = event.currentTarget.value; setQueryText(text); - locationService.setTextQuery(text.length === 0 ? undefined : text); + locationStore.setTextQuery(text.length === 0 ? undefined : text); }; return ( diff --git a/web/src/components/SettingDialog.tsx b/web/src/components/SettingDialog.tsx index e9da0b2de..646c2a0aa 100644 --- a/web/src/components/SettingDialog.tsx +++ b/web/src/components/SettingDialog.tsx @@ -1,6 +1,5 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { useAppSelector } from "../store"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; import MyAccountSection from "./Settings/MyAccountSection"; @@ -8,6 +7,7 @@ import PreferencesSection from "./Settings/PreferencesSection"; import MemberSection from "./Settings/MemberSection"; import SystemSection from "./Settings/SystemSection"; import "../less/setting-dialog.less"; +import { useUserStore } from "../store/module"; type Props = DialogProps; @@ -20,7 +20,8 @@ interface State { const SettingDialog: React.FC = (props: Props) => { const { destroy } = props; const { t } = useTranslation(); - const user = useAppSelector((state) => state.user.user); + const userStore = useUserStore(); + const user = userStore.state.user; const [state, setState] = useState({ selectedSection: "my-account", }); diff --git a/web/src/components/Settings/MemberSection.tsx b/web/src/components/Settings/MemberSection.tsx index 1d7504a33..79d83998a 100644 --- a/web/src/components/Settings/MemberSection.tsx +++ b/web/src/components/Settings/MemberSection.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { userService } from "../../services"; -import { useAppSelector } from "../../store"; +import { useUserStore } from "../../store/module"; import * as api from "../../helpers/api"; import toastHelper from "../Toast"; import Dropdown from "../common/Dropdown"; @@ -16,7 +15,8 @@ interface State { const PreferencesSection = () => { const { t } = useTranslation(); - const currentUser = useAppSelector((state) => state.user.user); + const userStore = useUserStore(); + const currentUser = userStore.state.user; const [state, setState] = useState({ createUserUsername: "", createUserPassword: "", @@ -80,7 +80,7 @@ const PreferencesSection = () => { content: `❗️Are you sure to archive ${user.username}?`, style: "warning", onConfirm: async () => { - await userService.patchUser({ + await userStore.patchUser({ id: user.id, rowStatus: "ARCHIVED", }); @@ -90,7 +90,7 @@ const PreferencesSection = () => { }; const handleRestoreUserClick = async (user: User) => { - await userService.patchUser({ + await userStore.patchUser({ id: user.id, rowStatus: "NORMAL", }); @@ -103,7 +103,7 @@ const PreferencesSection = () => { content: `Are you sure to delete ${user.username}? THIS ACTION IS IRREVERSIABLE.❗️`, style: "warning", onConfirm: async () => { - await userService.deleteUser({ + await userStore.deleteUser({ id: user.id, }); fetchUserList(); diff --git a/web/src/components/Settings/MyAccountSection.tsx b/web/src/components/Settings/MyAccountSection.tsx index c912ed142..b732fe8ce 100644 --- a/web/src/components/Settings/MyAccountSection.tsx +++ b/web/src/components/Settings/MyAccountSection.tsx @@ -1,6 +1,5 @@ import { useTranslation } from "react-i18next"; -import { useAppSelector } from "../../store"; -import { userService } from "../../services"; +import { useUserStore } from "../../store/module"; import { showCommonDialog } from "../Dialog/CommonDialog"; import showChangePasswordDialog from "../ChangePasswordDialog"; import showUpdateAccountDialog from "../UpdateAccountDialog"; @@ -8,7 +7,8 @@ import "../../less/settings/my-account-section.less"; const MyAccountSection = () => { const { t } = useTranslation(); - const user = useAppSelector((state) => state.user.user as User); + const userStore = useUserStore(); + const user = userStore.state.user as User; const openAPIRoute = `${window.location.origin}/api/memo?openId=${user.openId}`; const handleResetOpenIdBtnClick = async () => { @@ -17,7 +17,7 @@ const MyAccountSection = () => { content: "❗️The existing API will be invalidated and a new one will be generated, are you sure you want to reset?", style: "warning", onConfirm: async () => { - await userService.patchUser({ + await userStore.patchUser({ id: user.id, resetOpenId: true, }); diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx index 65575c91a..e26100b9b 100644 --- a/web/src/components/Settings/PreferencesSection.tsx +++ b/web/src/components/Settings/PreferencesSection.tsx @@ -1,7 +1,6 @@ import { Select, Switch, Option } from "@mui/joy"; import { useTranslation } from "react-i18next"; -import { globalService, userService } from "../../services"; -import { useAppSelector } from "../../store"; +import { useGlobalStore, useUserStore } from "../../store/module"; import { VISIBILITY_SELECTOR_ITEMS, MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS } from "../../helpers/consts"; import Icon from "../Icon"; import AppearanceSelect from "../AppearanceSelect"; @@ -44,7 +43,9 @@ const localeSelectorItems = [ const PreferencesSection = () => { const { t } = useTranslation(); - const { setting, localSetting } = useAppSelector((state) => state.user.user as User); + const globalStore = useGlobalStore(); + const userStore = useUserStore(); + const { setting, localSetting } = userStore.state.user as User; const visibilitySelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => { return { value: item.value, @@ -60,20 +61,20 @@ const PreferencesSection = () => { }); const handleLocaleChanged = async (value: string) => { - await userService.upsertUserSetting("locale", value); - globalService.setLocale(value as Locale); + await userStore.upsertUserSetting("locale", value); + globalStore.setLocale(value as Locale); }; const handleDefaultMemoVisibilityChanged = async (value: string) => { - await userService.upsertUserSetting("memoVisibility", value); + await userStore.upsertUserSetting("memoVisibility", value); }; const handleMemoDisplayTsOptionChanged = async (value: string) => { - await userService.upsertUserSetting("memoDisplayTsOption", value); + await userStore.upsertUserSetting("memoDisplayTsOption", value); }; const handleIsFoldingEnabledChanged = (event: React.ChangeEvent) => { - userService.upsertLocalSetting("enableFoldMemo", event.target.checked); + userStore.upsertLocalSetting("enableFoldMemo", event.target.checked); }; return ( diff --git a/web/src/components/ShareMemoDialog.tsx b/web/src/components/ShareMemoDialog.tsx index 32294b2b3..b5610500b 100644 --- a/web/src/components/ShareMemoDialog.tsx +++ b/web/src/components/ShareMemoDialog.tsx @@ -4,10 +4,10 @@ import { useTranslation } from "react-i18next"; import copy from "copy-to-clipboard"; import { toLower } from "lodash"; import toImage from "../labs/html2image"; +import { useMemoStore, useUserStore } from "../store/module"; import { VISIBILITY_SELECTOR_ITEMS } from "../helpers/consts"; import * as utils from "../helpers/utils"; import { getMemoStats } from "../helpers/api"; -import { memoService, userService } from "../services"; import useLoading from "../hooks/useLoading"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; @@ -29,7 +29,9 @@ interface State { const ShareMemoDialog: React.FC = (props: Props) => { const { memo: propsMemo, destroy } = props; const { t } = useTranslation(); - const user = userService.getState().user as User; + const userStore = useUserStore(); + const memoStore = useMemoStore(); + const user = userStore.state.user as User; const [state, setState] = useState({ memoAmount: 0, memoVisibility: propsMemo.visibility, @@ -113,7 +115,7 @@ const ShareMemoDialog: React.FC = (props: Props) => { ...state, memoVisibility: visibilityValue, }); - await memoService.patchMemo({ + await memoStore.patchMemo({ id: memo.id, visibility: visibilityValue, }); diff --git a/web/src/components/ShortcutList.tsx b/web/src/components/ShortcutList.tsx index 89c5d60a8..a3bc97c72 100644 --- a/web/src/components/ShortcutList.tsx +++ b/web/src/components/ShortcutList.tsx @@ -1,7 +1,6 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { locationService, shortcutService } from "../services"; -import { useAppSelector } from "../store"; +import { useLocationStore, useShortcutStore } from "../store/module"; import * as utils from "../helpers/utils"; import useToggle from "../hooks/useToggle"; import useLoading from "../hooks/useLoading"; @@ -11,10 +10,12 @@ import showCreateShortcutDialog from "./CreateShortcutDialog"; import "../less/shortcut-list.less"; const ShortcutList = () => { - const query = useAppSelector((state) => state.location.query); - const shortcuts = useAppSelector((state) => state.shortcut.shortcuts); - const loadingState = useLoading(); const { t } = useTranslation(); + const locationStore = useLocationStore(); + const shortcutStore = useShortcutStore(); + const query = locationStore.state.query; + const shortcuts = shortcutStore.state.shortcuts; + const loadingState = useLoading(); const pinnedShortcuts = shortcuts .filter((s) => s.rowStatus === "ARCHIVED") @@ -25,7 +26,7 @@ const ShortcutList = () => { const sortedShortcuts = pinnedShortcuts.concat(unpinnedShortcuts); useEffect(() => { - shortcutService + shortcutStore .getMyAllShortcuts() .catch(() => { // do nth @@ -60,13 +61,15 @@ interface ShortcutContainerProps { const ShortcutContainer: React.FC = (props: ShortcutContainerProps) => { const { shortcut, isActive } = props; const { t } = useTranslation(); + const locationStore = useLocationStore(); + const shortcutStore = useShortcutStore(); const [showConfirmDeleteBtn, toggleConfirmDeleteBtn] = useToggle(false); const handleShortcutClick = () => { if (isActive) { - locationService.setMemoShortcut(undefined); + locationStore.setMemoShortcut(undefined); } else { - locationService.setMemoShortcut(shortcut.id); + locationStore.setMemoShortcut(shortcut.id); } }; @@ -75,10 +78,10 @@ const ShortcutContainer: React.FC = (props: ShortcutCont if (showConfirmDeleteBtn) { try { - await shortcutService.deleteShortcutById(shortcut.id); - if (locationService.getState().query?.shortcutId === shortcut.id) { + await shortcutStore.deleteShortcutById(shortcut.id); + if (locationStore.getState().query?.shortcutId === shortcut.id) { // need clear shortcut filter - locationService.setMemoShortcut(undefined); + locationStore.setMemoShortcut(undefined); } } catch (error: any) { console.error(error); @@ -102,7 +105,7 @@ const ShortcutContainer: React.FC = (props: ShortcutCont id: shortcut.id, rowStatus: shortcut.rowStatus === "ARCHIVED" ? "NORMAL" : "ARCHIVED", }; - await shortcutService.patchShortcut(shortcutPatch); + await shortcutStore.patchShortcut(shortcutPatch); } catch (error) { // do nth } diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx index 7ecfeef4c..7317ad3e2 100644 --- a/web/src/components/Sidebar.tsx +++ b/web/src/components/Sidebar.tsx @@ -2,8 +2,7 @@ import { isUndefined } from "lodash-es"; import { useEffect } from "react"; import { Link } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { userService } from "../services"; -import { useAppSelector } from "../store"; +import { useLocationStore, useUserStore } from "../store/module"; import showDailyReviewDialog from "./DailyReviewDialog"; import showSettingDialog from "./SettingDialog"; import UserBanner from "./UserBanner"; @@ -14,11 +13,13 @@ import "../less/siderbar.less"; const Sidebar = () => { const { t } = useTranslation(); - const location = useAppSelector((state) => state.location); + const userStore = useUserStore(); + const locationStore = useLocationStore(); + const query = locationStore.state.query; useEffect(() => { toggleSidebar(false); - }, [location.query]); + }, [query]); const handleSettingBtnClick = () => { showSettingDialog(); @@ -34,7 +35,7 @@ const Sidebar = () => { showDailyReviewDialog()}> 📅 {t("sidebar.daily-review")} - {!userService.isVisitorMode() && ( + {!userStore.isVisitorMode() && ( <> 🏂 {t("common.explore")} @@ -45,7 +46,7 @@ const Sidebar = () => { > )} - {!userService.isVisitorMode() && } + {!userStore.isVisitorMode() && } > diff --git a/web/src/components/TagList.tsx b/web/src/components/TagList.tsx index 3a614291d..0613ed930 100644 --- a/web/src/components/TagList.tsx +++ b/web/src/components/TagList.tsx @@ -1,7 +1,6 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useAppSelector } from "../store"; -import { locationService, memoService, userService } from "../services"; +import { useLocationStore, useMemoStore, useUserStore } from "../store/module"; import useToggle from "../hooks/useToggle"; import Icon from "./Icon"; import "../less/tag-list.less"; @@ -14,13 +13,16 @@ interface Tag { const TagList = () => { const { t } = useTranslation(); - const { memos, tags: tagsText } = useAppSelector((state) => state.memo); - const query = useAppSelector((state) => state.location.query); + const locationStore = useLocationStore(); + const userStore = useUserStore(); + const memoStore = useMemoStore(); + const { memos, tags: tagsText } = memoStore.state; + const query = locationStore.state.query; const [tags, setTags] = useState([]); useEffect(() => { if (memos.length > 0) { - memoService.updateTagsState(); + memoStore.updateTagsState(); } }, [memos]); @@ -75,7 +77,7 @@ const TagList = () => { {tags.map((t, idx) => ( ))} - {!userService.isVisitorMode() && tags.length === 0 && {t("tag-list.tip-text")}} + {!userStore.isVisitorMode() && tags.length === 0 && {t("tag-list.tip-text")}} ); @@ -87,6 +89,7 @@ interface TagItemContainerProps { } const TagItemContainer: React.FC = (props: TagItemContainerProps) => { + const locationStore = useLocationStore(); const { tag, tagQuery } = props; const isActive = tagQuery === tag.text; const hasSubTags = tag.subTags.length > 0; @@ -94,9 +97,9 @@ const TagItemContainer: React.FC = (props: TagItemContain const handleTagClick = () => { if (isActive) { - locationService.setTagQuery(undefined); + locationStore.setTagQuery(undefined); } else { - locationService.setTagQuery(tag.text); + locationStore.setTagQuery(tag.text); } }; diff --git a/web/src/components/UpdateAccountDialog.tsx b/web/src/components/UpdateAccountDialog.tsx index 5591b9ec8..01493e9c9 100644 --- a/web/src/components/UpdateAccountDialog.tsx +++ b/web/src/components/UpdateAccountDialog.tsx @@ -1,12 +1,11 @@ import { isEqual } from "lodash"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useAppSelector } from "../store"; -import { userService } from "../services"; +import { useUserStore } from "../store/module"; +import { validate, ValidatorConfig } from "../helpers/validator"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; import toastHelper from "./Toast"; -import { validate, ValidatorConfig } from "../helpers/validator"; const validateConfig: ValidatorConfig = { minLength: 4, @@ -25,7 +24,8 @@ interface State { const UpdateAccountDialog: React.FC = ({ destroy }: Props) => { const { t } = useTranslation(); - const user = useAppSelector((state) => state.user.user as User); + const userStore = useUserStore(); + const user = userStore.state.user as User; const [state, setState] = useState({ username: user.username, nickname: user.nickname, @@ -78,7 +78,7 @@ const UpdateAccountDialog: React.FC = ({ destroy }: Props) => { } try { - const user = userService.getState().user as User; + const user = userStore.getState().user as User; const userPatch: UserPatch = { id: user.id, }; @@ -91,7 +91,7 @@ const UpdateAccountDialog: React.FC = ({ destroy }: Props) => { if (!isEqual(user.email, state.email)) { userPatch.email = state.email; } - await userService.patchUser(userPatch); + await userStore.patchUser(userPatch); toastHelper.info("Update succeed"); handleCloseBtnClick(); } catch (error: any) { diff --git a/web/src/components/UpdateVersionBanner.tsx b/web/src/components/UpdateVersionBanner.tsx index 31651270d..89792a383 100644 --- a/web/src/components/UpdateVersionBanner.tsx +++ b/web/src/components/UpdateVersionBanner.tsx @@ -1,9 +1,9 @@ import { useEffect, useState } from "react"; -import { useAppSelector } from "../store"; import * as api from "../helpers/api"; import * as storage from "../helpers/storage"; import Icon from "./Icon"; import "../less/about-site-dialog.less"; +import { useGlobalStore } from "../store/module"; interface State { latestVersion: string; @@ -11,7 +11,8 @@ interface State { } const UpdateVersionBanner: React.FC = () => { - const profile = useAppSelector((state) => state.global.systemStatus.profile); + const globalStore = useGlobalStore(); + const profile = globalStore.state.systemStatus.profile; const [state, setState] = useState({ latestVersion: "", show: false, diff --git a/web/src/components/UsageHeatMap.tsx b/web/src/components/UsageHeatMap.tsx index 234395f39..ccf612259 100644 --- a/web/src/components/UsageHeatMap.tsx +++ b/web/src/components/UsageHeatMap.tsx @@ -1,6 +1,5 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { useAppSelector } from "../store"; -import { locationService, userService } from "../services"; +import { useLocationStore, useMemoStore, useUserStore } from "../store/module"; import { getMemoStats } from "../helpers/api"; import { DAILY_TIMESTAMP } from "../helpers/consts"; import * as utils from "../helpers/utils"; @@ -28,19 +27,21 @@ interface DailyUsageStat { } const UsageHeatMap = () => { + const locationStore = useLocationStore(); + const userStore = useUserStore(); + const memoStore = useMemoStore(); const todayTimeStamp = utils.getDateStampByDate(Date.now()); const todayDay = new Date(todayTimeStamp).getDay() + 1; const nullCell = new Array(7 - todayDay).fill(0); const usedDaysAmount = (tableConfig.width - 1) * tableConfig.height + todayDay; const beginDayTimestamp = todayTimeStamp - usedDaysAmount * DAILY_TIMESTAMP; - - const { memos } = useAppSelector((state) => state.memo); + const memos = memoStore.state.memos; const [allStat, setAllStat] = useState(getInitialUsageStat(usedDaysAmount, beginDayTimestamp)); const [currentStat, setCurrentStat] = useState(null); const containerElRef = useRef(null); useEffect(() => { - getMemoStats(userService.getCurrentUserId()) + getMemoStats(userStore.getCurrentUserId()) .then(({ data: { data } }) => { const newStat: DailyUsageStat[] = getInitialUsageStat(usedDaysAmount, beginDayTimestamp); for (const record of data) { @@ -84,11 +85,11 @@ const UsageHeatMap = () => { }, []); const handleUsageStatItemClick = useCallback((item: DailyUsageStat) => { - if (locationService.getState().query?.duration?.from === item.timestamp) { - locationService.setFromAndToQuery(); + if (locationStore.getState().query?.duration?.from === item.timestamp) { + locationStore.setFromAndToQuery(); setCurrentStat(null); } else if (item.count > 0) { - locationService.setFromAndToQuery(item.timestamp, item.timestamp + DAILY_TIMESTAMP); + locationStore.setFromAndToQuery(item.timestamp, item.timestamp + DAILY_TIMESTAMP); setCurrentStat(item); } }, []); diff --git a/web/src/components/UserBanner.tsx b/web/src/components/UserBanner.tsx index 182adcd62..f7f67da57 100644 --- a/web/src/components/UserBanner.tsx +++ b/web/src/components/UserBanner.tsx @@ -1,11 +1,9 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; +import { useLocationStore, useMemoStore, useUserStore } from "../store/module"; import { getMemoStats } from "../helpers/api"; import * as utils from "../helpers/utils"; -import userService from "../services/userService"; -import { locationService } from "../services"; -import { useAppSelector } from "../store"; import Icon from "./Icon"; import Dropdown from "./common/Dropdown"; import showResourcesDialog from "./ResourcesDialog"; @@ -16,12 +14,15 @@ import "../less/user-banner.less"; const UserBanner = () => { const { t } = useTranslation(); const navigate = useNavigate(); - const { user, owner } = useAppSelector((state) => state.user); - const { memos, tags } = useAppSelector((state) => state.memo); + const locationStore = useLocationStore(); + const userStore = useUserStore(); + const memoStore = useMemoStore(); + const { user, owner } = userStore.state; + const { memos, tags } = memoStore.state; const [username, setUsername] = useState("Memos"); const [memoAmount, setMemoAmount] = useState(0); const [createdDays, setCreatedDays] = useState(0); - const isVisitorMode = userService.isVisitorMode(); + const isVisitorMode = userStore.isVisitorMode(); useEffect(() => { if (isVisitorMode) { @@ -37,7 +38,7 @@ const UserBanner = () => { }, [isVisitorMode, user, owner]); useEffect(() => { - getMemoStats(userService.getCurrentUserId()) + getMemoStats(userStore.getCurrentUserId()) .then(({ data: { data } }) => { setMemoAmount(data.length); }) @@ -47,7 +48,7 @@ const UserBanner = () => { }, [memos]); const handleUsernameClick = useCallback(() => { - locationService.clearQuery(); + locationStore.clearQuery(); }, []); const handleResourcesBtnClick = () => { @@ -78,7 +79,7 @@ const UserBanner = () => { actionsClassName="min-w-36" actions={ <> - {!userService.isVisitorMode() && ( + {!userStore.isVisitorMode() && ( <> { > 🤠 {t("common.about")} - {!userService.isVisitorMode() && ( + {!userStore.isVisitorMode() && ( { const { t, i18n } = useTranslation(); const navigate = useNavigate(); - const systemStatus = useAppSelector((state) => state.global.systemStatus); + const globalStore = useGlobalStore(); + const userStore = useUserStore(); + const systemStatus = globalStore.state.systemStatus; const actionBtnLoadingState = useLoading(false); const mode = systemStatus.profile.mode; const [username, setUsername] = useState(mode === "dev" ? "demohero" : ""); const [password, setPassword] = useState(mode === "dev" ? "secret" : ""); useEffect(() => { - userService.doSignOut().catch(); + userStore.doSignOut().catch(); }, []); const handleUsernameInputChanged = (e: React.ChangeEvent) => { @@ -62,7 +63,7 @@ const Auth = () => { try { actionBtnLoadingState.setLoading(); await api.signin(username, password); - const user = await userService.doSignIn(); + const user = await userStore.doSignIn(); if (user) { navigate("/"); } else { @@ -95,7 +96,7 @@ const Auth = () => { try { actionBtnLoadingState.setLoading(); await api.signup(username, password, role); - const user = await userService.doSignIn(); + const user = await userStore.doSignIn(); if (user) { navigate("/"); } else { @@ -109,7 +110,7 @@ const Auth = () => { }; const handleLocaleItemClick = (locale: Locale) => { - globalService.setLocale(locale); + globalStore.setLocale(locale); }; return ( diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index fa588a74e..8332885c2 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -2,9 +2,8 @@ import dayjs from "dayjs"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link } from "react-router-dom"; -import { memoService } from "../services"; -import { DEFAULT_MEMO_LIMIT } from "../services/memoService"; -import { useAppSelector } from "../store"; +import { useLocationStore, useMemoStore, useUserStore } from "../store/module"; +import { DEFAULT_MEMO_LIMIT } from "../helpers/consts"; import useLoading from "../hooks/useLoading"; import toastHelper from "../components/Toast"; import MemoContent from "../components/MemoContent"; @@ -17,8 +16,11 @@ interface State { const Explore = () => { const { t, i18n } = useTranslation(); - const user = useAppSelector((state) => state.user.user); - const location = useAppSelector((state) => state.location); + const locationStore = useLocationStore(); + const userStore = useUserStore(); + const memoStore = useMemoStore(); + const user = userStore.state.user; + const location = locationStore.state; const [state, setState] = useState({ memos: [], }); @@ -26,7 +28,7 @@ const Explore = () => { const loadingState = useLoading(); useEffect(() => { - memoService.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length).then((memos) => { + memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length).then((memos) => { if (memos.length < DEFAULT_MEMO_LIMIT) { setIsComplete(true); } @@ -39,7 +41,7 @@ const Explore = () => { const handleFetchMoreClick = async () => { try { - const fetchedMemos = await memoService.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length); + const fetchedMemos = await memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length); if (fetchedMemos.length < DEFAULT_MEMO_LIMIT) { setIsComplete(true); } else { diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index e302be4a5..567feece6 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,8 +1,7 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useLocation } from "react-router-dom"; -import { globalService, userService } from "../services"; -import { useAppSelector } from "../store"; +import { useGlobalStore, useUserStore } from "../store/module"; import toastHelper from "../components/Toast"; import Sidebar from "../components/Sidebar"; import MemosHeader from "../components/MemosHeader"; @@ -15,12 +14,14 @@ import "../less/home.less"; function Home() { const { t } = useTranslation(); const location = useLocation(); - const user = useAppSelector((state) => state.user.user); + const globalStore = useGlobalStore(); + const userStore = useUserStore(); + const user = userStore.state.user; useEffect(() => { - const { owner } = userService.getState(); + const { owner } = userStore.getState(); - if (userService.isVisitorMode()) { + if (userStore.isVisitorMode()) { if (!owner) { toastHelper.error(t("message.user-not-found")); } @@ -29,7 +30,7 @@ function Home() { useEffect(() => { if (user?.setting.locale) { - globalService.setLocale(user.setting.locale); + globalStore.setLocale(user.setting.locale); } }, [user?.setting.locale]); @@ -43,11 +44,11 @@ function Home() { - {!userService.isVisitorMode() && } + {!userStore.isVisitorMode() && } - {userService.isVisitorMode() && ( + {userStore.isVisitorMode() && ( {user ? ( (window.location.href = "/")}> diff --git a/web/src/pages/MemoDetail.tsx b/web/src/pages/MemoDetail.tsx index be51628e8..f47727e04 100644 --- a/web/src/pages/MemoDetail.tsx +++ b/web/src/pages/MemoDetail.tsx @@ -2,9 +2,8 @@ import dayjs from "dayjs"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Link, useParams } from "react-router-dom"; -import { memoService } from "../services"; import { UNKNOWN_ID } from "../helpers/consts"; -import { useAppSelector } from "../store"; +import { useLocationStore, useMemoStore, useUserStore } from "../store/module"; import useLoading from "../hooks/useLoading"; import toastHelper from "../components/Toast"; import MemoContent from "../components/MemoContent"; @@ -18,8 +17,11 @@ interface State { const MemoDetail = () => { const { t, i18n } = useTranslation(); const params = useParams(); - const user = useAppSelector((state) => state.user.user); - const location = useAppSelector((state) => state.location); + const locationStore = useLocationStore(); + const memoStore = useMemoStore(); + const userStore = useUserStore(); + const user = userStore.state.user; + const location = locationStore.state; const [state, setState] = useState({ memo: { id: UNKNOWN_ID, @@ -30,7 +32,7 @@ const MemoDetail = () => { useEffect(() => { const memoId = Number(params.memoId); if (memoId && !isNaN(memoId)) { - memoService + memoStore .fetchMemoById(memoId) .then((memo) => { setState({ diff --git a/web/src/router/index.tsx b/web/src/router/index.tsx index 467fe9548..13b084a8b 100644 --- a/web/src/router/index.tsx +++ b/web/src/router/index.tsx @@ -1,7 +1,8 @@ import { createBrowserRouter, redirect } from "react-router-dom"; import { lazy } from "react"; import { isNullorUndefined } from "../helpers/utils"; -import { globalService, userService } from "../services"; +import store from "../store"; +import { initialGlobalState, initialUserState } from "../store/module"; const Auth = lazy(() => import("../pages/Auth")); const Explore = lazy(() => import("../pages/Explore")); @@ -14,7 +15,7 @@ const router = createBrowserRouter([ element: , loader: async () => { try { - await globalService.initialState(); + await initialGlobalState(); } catch (error) { // do nth } @@ -26,13 +27,13 @@ const router = createBrowserRouter([ element: , loader: async () => { try { - await globalService.initialState(); - await userService.initialState(); + await initialGlobalState(); + await initialUserState(); } catch (error) { // do nth } - const { host, user } = userService.getState(); + const { host, user } = store.getState().user; if (isNullorUndefined(host)) { return redirect("/auth"); } else if (isNullorUndefined(user)) { @@ -46,13 +47,13 @@ const router = createBrowserRouter([ element: , loader: async () => { try { - await globalService.initialState(); - await userService.initialState(); + await initialGlobalState(); + await initialUserState(); } catch (error) { // do nth } - const { host } = userService.getState(); + const { host } = store.getState().user; if (isNullorUndefined(host)) { return redirect("/auth"); } @@ -64,13 +65,13 @@ const router = createBrowserRouter([ element: , loader: async () => { try { - await globalService.initialState(); - await userService.initialState(); + await initialGlobalState(); + await initialUserState(); } catch (error) { // do nth } - const { host } = userService.getState(); + const { host } = store.getState().user; if (isNullorUndefined(host)) { return redirect("/auth"); } @@ -82,13 +83,13 @@ const router = createBrowserRouter([ element: , loader: async () => { try { - await globalService.initialState(); - await userService.initialState(); + await initialGlobalState(); + await initialUserState(); } catch (error) { // do nth } - const { host } = userService.getState(); + const { host } = store.getState().user; if (isNullorUndefined(host)) { return redirect("/auth"); } diff --git a/web/src/services/README.md b/web/src/services/README.md deleted file mode 100644 index bcf4597ec..000000000 --- a/web/src/services/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Services - -What should service do? - -- request data api and throw error; -- dispatch state actions; diff --git a/web/src/services/editorStateService.ts b/web/src/services/editorStateService.ts deleted file mode 100644 index a20b0d2fa..000000000 --- a/web/src/services/editorStateService.ts +++ /dev/null @@ -1,30 +0,0 @@ -import store from "../store"; -import { setEditMemoId, setMemoVisibility, setResourceList } from "../store/modules/editor"; - -const editorStateService = { - getState: () => { - return store.getState().editor; - }, - - setEditMemoWithId: (editMemoId: MemoId) => { - store.dispatch(setEditMemoId(editMemoId)); - }, - - clearEditMemo: () => { - store.dispatch(setEditMemoId()); - }, - - setMemoVisibility: (memoVisibility: Visibility) => { - store.dispatch(setMemoVisibility(memoVisibility)); - }, - - setResourceList: (resourceList: Resource[]) => { - store.dispatch(setResourceList(resourceList)); - }, - - clearResourceList: () => { - store.dispatch(setResourceList([])); - }, -}; - -export default editorStateService; diff --git a/web/src/services/globalService.ts b/web/src/services/globalService.ts deleted file mode 100644 index 92889a30b..000000000 --- a/web/src/services/globalService.ts +++ /dev/null @@ -1,51 +0,0 @@ -import store from "../store"; -import * as api from "../helpers/api"; -import * as storage from "../helpers/storage"; -import { setAppearance, setGlobalState, setLocale } from "../store/modules/global"; - -const globalService = { - getState: () => { - return store.getState().global; - }, - - initialState: async () => { - const defaultGlobalState = { - locale: "en" as Locale, - appearance: "system" as Appearance, - systemStatus: { - allowSignUp: false, - additionalStyle: "", - additionalScript: "", - } as SystemStatus, - }; - - const { locale: storageLocale, appearance: storageAppearance } = storage.get(["locale", "appearance"]); - if (storageLocale) { - defaultGlobalState.locale = storageLocale; - } - if (storageAppearance) { - defaultGlobalState.appearance = storageAppearance; - } - - try { - const { data } = (await api.getSystemStatus()).data; - if (data) { - defaultGlobalState.systemStatus = data; - } - } catch (error) { - // do nth - } - - store.dispatch(setGlobalState(defaultGlobalState)); - }, - - setLocale: (locale: Locale) => { - store.dispatch(setLocale(locale)); - }, - - setAppearance: (appearance: Appearance) => { - store.dispatch(setAppearance(appearance)); - }, -}; - -export default globalService; diff --git a/web/src/services/index.ts b/web/src/services/index.ts deleted file mode 100644 index 2ddfd65f8..000000000 --- a/web/src/services/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import globalService from "./globalService"; -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 { globalService, editorStateService, locationService, memoService, shortcutService, userService, resourceService }; diff --git a/web/src/services/locationService.ts b/web/src/services/locationService.ts deleted file mode 100644 index a1e31d324..000000000 --- a/web/src/services/locationService.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { stringify } from "qs"; -import store from "../store"; -import { setQuery, setPathname, Query, updateStateWithLocation, updatePathnameStateWithLocation } from "../store/modules/location"; - -const updateLocationUrl = (method: "replace" | "push" = "replace") => { - // avoid pathname confusion when entering from non-home page - store.dispatch(updatePathnameStateWithLocation()); - - const { query, pathname, hash } = store.getState().location; - let queryString = stringify(query); - if (queryString) { - queryString = "?" + queryString; - } else { - queryString = ""; - } - - if (method === "replace") { - window.history.replaceState(null, "", pathname + hash + queryString); - } else { - window.history.pushState(null, "", pathname + hash + queryString); - } - store.dispatch(updateStateWithLocation()); -}; - -const locationService = { - getState: () => { - return store.getState().location; - }, - - updateStateWithLocation: () => { - store.dispatch(updateStateWithLocation()); - }, - - setPathname: (pathname: string) => { - store.dispatch(setPathname(pathname)); - updateLocationUrl(); - }, - - pushHistory: (pathname: string) => { - store.dispatch(setPathname(pathname)); - updateLocationUrl("push"); - }, - - replaceHistory: (pathname: string) => { - store.dispatch(setPathname(pathname)); - updateLocationUrl("replace"); - }, - - setQuery: (query: Query) => { - store.dispatch(setQuery(query)); - updateLocationUrl(); - }, - - clearQuery: () => { - store.dispatch( - setQuery({ - tag: undefined, - type: undefined, - duration: undefined, - text: undefined, - shortcutId: undefined, - visibility: undefined, - }) - ); - updateLocationUrl(); - }, - - setMemoTypeQuery: (type?: MemoSpecType) => { - store.dispatch( - setQuery({ - type: type, - }) - ); - updateLocationUrl(); - }, - - setMemoShortcut: (shortcutId?: ShortcutId) => { - store.dispatch( - setQuery({ - shortcutId: shortcutId, - }) - ); - updateLocationUrl(); - }, - - setTextQuery: (text?: string) => { - store.dispatch( - setQuery({ - text: text, - }) - ); - updateLocationUrl(); - }, - - setTagQuery: (tag?: string) => { - store.dispatch( - setQuery({ - tag: tag, - }) - ); - updateLocationUrl(); - }, - - setFromAndToQuery: (from?: number, to?: number) => { - let duration = undefined; - if (from && to && from < to) { - duration = { - from, - to, - }; - } - - store.dispatch( - setQuery({ - duration, - }) - ); - updateLocationUrl(); - }, - - setMemoVisibilityQuery: (visibility?: Visibility) => { - store.dispatch( - setQuery({ - visibility: visibility, - }) - ); - updateLocationUrl(); - }, -}; - -export default locationService; diff --git a/web/src/services/memoService.ts b/web/src/services/memoService.ts deleted file mode 100644 index ba8059213..000000000 --- a/web/src/services/memoService.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { uniqBy } from "lodash"; -import * as api from "../helpers/api"; -import { createMemo, deleteMemo, patchMemo, setIsFetching, setMemos, setTags } from "../store/modules/memo"; -import store from "../store"; -import userService from "./userService"; - -export const DEFAULT_MEMO_LIMIT = 30; - -const convertResponseModelMemo = (memo: Memo): Memo => { - return { - ...memo, - createdTs: memo.createdTs * 1000, - updatedTs: memo.updatedTs * 1000, - displayTs: memo.displayTs * 1000, - }; -}; - -const memoService = { - getState: () => { - return store.getState().memo; - }, - - fetchMemos: async (limit = DEFAULT_MEMO_LIMIT, offset = 0) => { - store.dispatch(setIsFetching(true)); - const memoFind: MemoFind = { - rowStatus: "NORMAL", - limit, - offset, - }; - if (userService.isVisitorMode()) { - memoFind.creatorId = userService.getUserIdFromPath(); - } - const { data } = (await api.getMemoList(memoFind)).data; - const fetchedMemos = data.map((m) => convertResponseModelMemo(m)); - if (offset === 0) { - store.dispatch(setMemos([])); - } - const memos = memoService.getState().memos; - store.dispatch(setMemos(uniqBy(memos.concat(fetchedMemos), "id"))); - store.dispatch(setIsFetching(false)); - - return fetchedMemos; - }, - - fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => { - const memoFind: MemoFind = { - rowStatus: "NORMAL", - limit, - offset, - }; - - const { data } = (await api.getAllMemos(memoFind)).data; - const memos = data.map((m) => convertResponseModelMemo(m)); - return memos; - }, - - fetchArchivedMemos: async () => { - const memoFind: MemoFind = { - rowStatus: "ARCHIVED", - }; - if (userService.isVisitorMode()) { - memoFind.creatorId = userService.getUserIdFromPath(); - } - const { data } = (await api.getMemoList(memoFind)).data; - const archivedMemos = data.map((m) => { - return convertResponseModelMemo(m); - }); - return archivedMemos; - }, - - fetchMemoById: async (memoId: MemoId) => { - const { data } = (await api.getMemoById(memoId)).data; - const memo = convertResponseModelMemo(data); - - return memo; - }, - - getMemoById: async (memoId: MemoId) => { - for (const m of memoService.getState().memos) { - if (m.id === memoId) { - return m; - } - } - - return await memoService.fetchMemoById(memoId); - }, - - updateTagsState: async () => { - const tagFind: TagFind = {}; - if (userService.isVisitorMode()) { - tagFind.creatorId = userService.getUserIdFromPath(); - } - const { data } = (await api.getTagList(tagFind)).data; - store.dispatch(setTags(data)); - }, - - getLinkedMemos: async (memoId: MemoId): Promise => { - const { memos } = memoService.getState(); - const regex = new RegExp(`[@(.+?)](${memoId})`); - return memos.filter((m) => m.content.match(regex)); - }, - - createMemo: async (memoCreate: MemoCreate) => { - const { data } = (await api.createMemo(memoCreate)).data; - const memo = convertResponseModelMemo(data); - store.dispatch(createMemo(memo)); - return memo; - }, - - patchMemo: async (memoPatch: MemoPatch): Promise => { - const { data } = (await api.patchMemo(memoPatch)).data; - const memo = convertResponseModelMemo(data); - store.dispatch(patchMemo(memo)); - return memo; - }, - - pinMemo: async (memoId: MemoId) => { - await api.pinMemo(memoId); - store.dispatch( - patchMemo({ - id: memoId, - pinned: true, - }) - ); - }, - - unpinMemo: async (memoId: MemoId) => { - await api.unpinMemo(memoId); - store.dispatch( - patchMemo({ - id: memoId, - pinned: false, - }) - ); - }, - - deleteMemoById: async (memoId: MemoId) => { - await api.deleteMemo(memoId); - store.dispatch(deleteMemo(memoId)); - }, -}; - -export default memoService; diff --git a/web/src/services/resourceService.ts b/web/src/services/resourceService.ts deleted file mode 100644 index 0fa82eebb..000000000 --- a/web/src/services/resourceService.ts +++ /dev/null @@ -1,54 +0,0 @@ -import * as api from "../helpers/api"; -import store from "../store"; -import { patchResource, setResources, deleteResource } from "../store/modules/resource"; - -const convertResponseModelResource = (resource: Resource): Resource => { - return { - ...resource, - createdTs: resource.createdTs * 1000, - updatedTs: resource.updatedTs * 1000, - }; -}; - -const resourceService = { - getState: () => { - return store.getState().resource; - }, - - async fetchResourceList(): Promise { - const { data } = (await api.getResourceList()).data; - const resourceList = data.map((m) => convertResponseModelResource(m)); - store.dispatch(setResources(resourceList)); - return resourceList; - }, - - async upload(file: File): Promise { - const { name: filename, size } = file; - - if (size > 64 << 20) { - return Promise.reject("overload max size: 8MB"); - } - - const formData = new FormData(); - formData.append("file", file, filename); - const { data } = (await api.uploadFile(formData)).data; - const resource = convertResponseModelResource(data); - const resourceList = resourceService.getState().resources; - store.dispatch(setResources([resource, ...resourceList])); - return resource; - }, - - async deleteResourceById(id: ResourceId) { - await api.deleteResourceById(id); - store.dispatch(deleteResource(id)); - }, - - async patchResource(resourcePatch: ResourcePatch): Promise { - const { data } = (await api.patchResource(resourcePatch)).data; - const resource = convertResponseModelResource(data); - store.dispatch(patchResource(resource)); - return resource; - }, -}; - -export default resourceService; diff --git a/web/src/services/shortcutService.ts b/web/src/services/shortcutService.ts deleted file mode 100644 index 4126dc2f0..000000000 --- a/web/src/services/shortcutService.ts +++ /dev/null @@ -1,52 +0,0 @@ -import * as api from "../helpers/api"; -import store from "../store/"; -import { createShortcut, deleteShortcut, patchShortcut, setShortcuts } from "../store/modules/shortcut"; - -const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => { - return { - ...shortcut, - createdTs: shortcut.createdTs * 1000, - updatedTs: shortcut.updatedTs * 1000, - }; -}; - -const shortcutService = { - getState: () => { - return store.getState().shortcut; - }, - - getMyAllShortcuts: async () => { - const { data } = (await api.getShortcutList()).data; - const shortcuts = data.map((s) => convertResponseModelShortcut(s)); - store.dispatch(setShortcuts(shortcuts)); - }, - - getShortcutById: (id: ShortcutId) => { - for (const s of shortcutService.getState().shortcuts) { - if (s.id === id) { - return s; - } - } - - return null; - }, - - createShortcut: async (shortcutCreate: ShortcutCreate) => { - const { data } = (await api.createShortcut(shortcutCreate)).data; - const shortcut = convertResponseModelShortcut(data); - store.dispatch(createShortcut(shortcut)); - }, - - patchShortcut: async (shortcutPatch: ShortcutPatch) => { - const { data } = (await api.patchShortcut(shortcutPatch)).data; - const shortcut = convertResponseModelShortcut(data); - store.dispatch(patchShortcut(shortcut)); - }, - - deleteShortcutById: async (shortcutId: ShortcutId) => { - await api.deleteShortcutById(shortcutId); - store.dispatch(deleteShortcut(shortcutId)); - }, -}; - -export default shortcutService; diff --git a/web/src/services/userService.ts b/web/src/services/userService.ts deleted file mode 100644 index bcdaa0f76..000000000 --- a/web/src/services/userService.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { globalService, locationService } from "."; -import * as api from "../helpers/api"; -import * as storage from "../helpers/storage"; -import { UNKNOWN_ID } from "../helpers/consts"; -import store from "../store"; -import { setUser, patchUser, setHost, setOwner } from "../store/modules/user"; -import { getSystemColorScheme } from "../helpers/utils"; - -const defaultSetting: Setting = { - locale: "en", - appearance: getSystemColorScheme(), - memoVisibility: "PRIVATE", - memoDisplayTsOption: "created_ts", -}; - -const defaultLocalSetting: LocalSetting = { - enableFoldMemo: true, -}; - -export const convertResponseModelUser = (user: User): User => { - const setting: Setting = { - ...defaultSetting, - }; - const { localSetting: storageLocalSetting } = storage.get(["localSetting"]); - const localSetting: LocalSetting = { - ...defaultLocalSetting, - ...storageLocalSetting, - }; - - if (user.userSettingList) { - for (const userSetting of user.userSettingList) { - (setting as any)[userSetting.key] = JSON.parse(userSetting.value); - } - } - - return { - ...user, - setting, - localSetting, - createdTs: user.createdTs * 1000, - updatedTs: user.updatedTs * 1000, - }; -}; - -const userService = { - getState: () => { - return store.getState().user; - }, - - initialState: async () => { - const { systemStatus } = globalService.getState(); - if (systemStatus.host) { - store.dispatch(setHost(convertResponseModelUser(systemStatus.host))); - } - - const ownerUserId = userService.getUserIdFromPath(); - if (ownerUserId) { - const { data: owner } = (await api.getUserById(ownerUserId)).data; - if (owner) { - store.dispatch(setOwner(convertResponseModelUser(owner))); - } - } - - const { data } = (await api.getMyselfUser()).data; - if (data) { - const user = convertResponseModelUser(data); - store.dispatch(setUser(user)); - if (user.setting.locale) { - globalService.setLocale(user.setting.locale); - } - if (user.setting.appearance) { - globalService.setAppearance(user.setting.appearance); - } - } - }, - - getCurrentUserId: () => { - if (userService.isVisitorMode()) { - return userService.getUserIdFromPath() || UNKNOWN_ID; - } else { - return userService.getState().user?.id || UNKNOWN_ID; - } - }, - - isVisitorMode: () => { - return !(userService.getUserIdFromPath() === undefined); - }, - - getUserIdFromPath: () => { - const userIdRegex = /^\/u\/(\d+).*/; - const result = locationService.getState().pathname.match(userIdRegex); - if (result && result.length === 2) { - return Number(result[1]); - } - return undefined; - }, - - doSignIn: async () => { - const { data: user } = (await api.getMyselfUser()).data; - if (user) { - store.dispatch(setUser(convertResponseModelUser(user))); - } else { - userService.doSignOut(); - } - return user; - }, - - doSignOut: async () => { - store.dispatch(setUser()); - await api.signout(); - }, - - getUserById: async (userId: UserId) => { - const { data: user } = (await api.getUserById(userId)).data; - if (user) { - return convertResponseModelUser(user); - } else { - return undefined; - } - }, - - upsertUserSetting: async (key: keyof Setting, value: any) => { - await api.upsertUserSetting({ - key: key as any, - value: JSON.stringify(value), - }); - await userService.doSignIn(); - }, - - upsertLocalSetting: async (key: keyof LocalSetting, value: any) => { - storage.set({ localSetting: { [key]: value } }); - store.dispatch(patchUser({ localSetting: { [key]: value } })); - }, - - patchUser: async (userPatch: UserPatch): Promise => { - const { data } = (await api.patchUser(userPatch)).data; - if (userPatch.id === store.getState().user.user?.id) { - const user = convertResponseModelUser(data); - store.dispatch(patchUser(user)); - } - }, - - deleteUser: async (userDelete: UserDelete) => { - await api.deleteUser(userDelete); - }, -}; - -export default userService; diff --git a/web/src/store/index.ts b/web/src/store/index.ts index 6ce54d7ff..98349ae74 100644 --- a/web/src/store/index.ts +++ b/web/src/store/index.ts @@ -1,12 +1,12 @@ import { configureStore } from "@reduxjs/toolkit"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; -import globalReducer from "./modules/global"; -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"; -import resourceReducer from "./modules/resource"; +import globalReducer from "./reducer/global"; +import userReducer from "./reducer/user"; +import memoReducer from "./reducer/memo"; +import editorReducer from "./reducer/editor"; +import shortcutReducer from "./reducer/shortcut"; +import locationReducer from "./reducer/location"; +import resourceReducer from "./reducer/resource"; const store = configureStore({ reducer: { diff --git a/web/src/store/module/editor.ts b/web/src/store/module/editor.ts new file mode 100644 index 000000000..93608de31 --- /dev/null +++ b/web/src/store/module/editor.ts @@ -0,0 +1,28 @@ +import store, { useAppSelector } from ".."; +import { setEditMemoId, setMemoVisibility, setResourceList } from "../reducer/editor"; + +export const useEditorStore = () => { + const state = useAppSelector((state) => state.editor); + + return { + state, + getState: () => { + return store.getState().editor; + }, + setEditMemoWithId: (editMemoId: MemoId) => { + store.dispatch(setEditMemoId(editMemoId)); + }, + clearEditMemo: () => { + store.dispatch(setEditMemoId()); + }, + setMemoVisibility: (memoVisibility: Visibility) => { + store.dispatch(setMemoVisibility(memoVisibility)); + }, + setResourceList: (resourceList: Resource[]) => { + store.dispatch(setResourceList(resourceList)); + }, + clearResourceList: () => { + store.dispatch(setResourceList([])); + }, + }; +}; diff --git a/web/src/store/module/global.ts b/web/src/store/module/global.ts new file mode 100644 index 000000000..ed366e3e3 --- /dev/null +++ b/web/src/store/module/global.ts @@ -0,0 +1,52 @@ +import * as api from "../../helpers/api"; +import * as storage from "../../helpers/storage"; +import store, { useAppSelector } from "../"; +import { setAppearance, setGlobalState, setLocale } from "../reducer/global"; + +export const initialGlobalState = async () => { + const defaultGlobalState = { + locale: "en" as Locale, + appearance: "system" as Appearance, + systemStatus: { + allowSignUp: false, + additionalStyle: "", + additionalScript: "", + } as SystemStatus, + }; + + const { locale: storageLocale, appearance: storageAppearance } = storage.get(["locale", "appearance"]); + if (storageLocale) { + defaultGlobalState.locale = storageLocale; + } + if (storageAppearance) { + defaultGlobalState.appearance = storageAppearance; + } + + try { + const { data } = (await api.getSystemStatus()).data; + if (data) { + defaultGlobalState.systemStatus = data; + } + } catch (error) { + // do nth + } + + store.dispatch(setGlobalState(defaultGlobalState)); +}; + +export const useGlobalStore = () => { + const state = useAppSelector((state) => state.global); + + return { + state, + getState: () => { + return store.getState().global; + }, + setLocale: (locale: Locale) => { + store.dispatch(setLocale(locale)); + }, + setAppearance: (appearance: Appearance) => { + store.dispatch(setAppearance(appearance)); + }, + }; +}; diff --git a/web/src/store/module/index.ts b/web/src/store/module/index.ts new file mode 100644 index 000000000..95a237f84 --- /dev/null +++ b/web/src/store/module/index.ts @@ -0,0 +1,7 @@ +export * from "./editor"; +export * from "./global"; +export * from "./location"; +export * from "./memo"; +export * from "./resource"; +export * from "./shortcut"; +export * from "./user"; diff --git a/web/src/store/module/location.ts b/web/src/store/module/location.ts new file mode 100644 index 000000000..0feb32dc1 --- /dev/null +++ b/web/src/store/module/location.ts @@ -0,0 +1,122 @@ +import { stringify } from "qs"; +import store, { useAppSelector } from "../"; +import { setQuery, setPathname, Query, updateStateWithLocation, updatePathnameStateWithLocation } from "../reducer/location"; + +const updateLocationUrl = (method: "replace" | "push" = "replace") => { + // avoid pathname confusion when entering from non-home page + store.dispatch(updatePathnameStateWithLocation()); + + const { query, pathname, hash } = store.getState().location; + let queryString = stringify(query); + if (queryString) { + queryString = "?" + queryString; + } else { + queryString = ""; + } + + if (method === "replace") { + window.history.replaceState(null, "", pathname + hash + queryString); + } else { + window.history.pushState(null, "", pathname + hash + queryString); + } + store.dispatch(updateStateWithLocation()); +}; + +export const useLocationStore = () => { + const state = useAppSelector((state) => state.location); + + return { + state, + getState: () => { + return store.getState().location; + }, + updateStateWithLocation: () => { + store.dispatch(updateStateWithLocation()); + }, + setPathname: (pathname: string) => { + store.dispatch(setPathname(pathname)); + updateLocationUrl(); + }, + pushHistory: (pathname: string) => { + store.dispatch(setPathname(pathname)); + updateLocationUrl("push"); + }, + replaceHistory: (pathname: string) => { + store.dispatch(setPathname(pathname)); + updateLocationUrl("replace"); + }, + setQuery: (query: Query) => { + store.dispatch(setQuery(query)); + updateLocationUrl(); + }, + clearQuery: () => { + store.dispatch( + setQuery({ + tag: undefined, + type: undefined, + duration: undefined, + text: undefined, + shortcutId: undefined, + visibility: undefined, + }) + ); + updateLocationUrl(); + }, + setMemoTypeQuery: (type?: MemoSpecType) => { + store.dispatch( + setQuery({ + type: type, + }) + ); + updateLocationUrl(); + }, + setMemoShortcut: (shortcutId?: ShortcutId) => { + store.dispatch( + setQuery({ + shortcutId: shortcutId, + }) + ); + updateLocationUrl(); + }, + setTextQuery: (text?: string) => { + store.dispatch( + setQuery({ + text: text, + }) + ); + updateLocationUrl(); + }, + setTagQuery: (tag?: string) => { + store.dispatch( + setQuery({ + tag: tag, + }) + ); + updateLocationUrl(); + }, + setFromAndToQuery: (from?: number, to?: number) => { + let duration = undefined; + if (from && to && from < to) { + duration = { + from, + to, + }; + } + + store.dispatch( + setQuery({ + duration, + }) + ); + updateLocationUrl(); + }, + setMemoVisibilityQuery: (visibility?: Visibility) => { + store.dispatch( + setQuery({ + visibility: visibility, + }) + ); + updateLocationUrl(); + }, + }; +}; diff --git a/web/src/store/module/memo.ts b/web/src/store/module/memo.ts new file mode 100644 index 000000000..4da129ce1 --- /dev/null +++ b/web/src/store/module/memo.ts @@ -0,0 +1,135 @@ +import { uniqBy } from "lodash"; +import * as api from "../../helpers/api"; +import { DEFAULT_MEMO_LIMIT } from "../../helpers/consts"; +import { useUserStore } from "./"; +import store, { useAppSelector } from "../"; +import { createMemo, deleteMemo, patchMemo, setIsFetching, setMemos, setTags } from "../reducer/memo"; + +const convertResponseModelMemo = (memo: Memo): Memo => { + return { + ...memo, + createdTs: memo.createdTs * 1000, + updatedTs: memo.updatedTs * 1000, + displayTs: memo.displayTs * 1000, + }; +}; + +export const useMemoStore = () => { + const state = useAppSelector((state) => state.memo); + const userStore = useUserStore(); + + const fetchMemoById = async (memoId: MemoId) => { + const { data } = (await api.getMemoById(memoId)).data; + const memo = convertResponseModelMemo(data); + + return memo; + }; + + return { + state, + getState: () => { + return store.getState().memo; + }, + fetchMemos: async (limit = DEFAULT_MEMO_LIMIT, offset = 0) => { + store.dispatch(setIsFetching(true)); + const memoFind: MemoFind = { + rowStatus: "NORMAL", + limit, + offset, + }; + if (userStore.isVisitorMode()) { + memoFind.creatorId = userStore.getUserIdFromPath(); + } + const { data } = (await api.getMemoList(memoFind)).data; + const fetchedMemos = data.map((m) => convertResponseModelMemo(m)); + if (offset === 0) { + store.dispatch(setMemos([])); + } + const memos = state.memos; + store.dispatch(setMemos(uniqBy(memos.concat(fetchedMemos), "id"))); + store.dispatch(setIsFetching(false)); + + return fetchedMemos; + }, + fetchAllMemos: async (limit = DEFAULT_MEMO_LIMIT, offset?: number) => { + const memoFind: MemoFind = { + rowStatus: "NORMAL", + limit, + offset, + }; + + const { data } = (await api.getAllMemos(memoFind)).data; + const memos = data.map((m) => convertResponseModelMemo(m)); + return memos; + }, + fetchArchivedMemos: async () => { + const memoFind: MemoFind = { + rowStatus: "ARCHIVED", + }; + if (userStore.isVisitorMode()) { + memoFind.creatorId = userStore.getUserIdFromPath(); + } + const { data } = (await api.getMemoList(memoFind)).data; + const archivedMemos = data.map((m) => { + return convertResponseModelMemo(m); + }); + return archivedMemos; + }, + fetchMemoById, + getMemoById: async (memoId: MemoId) => { + for (const m of state.memos) { + if (m.id === memoId) { + return m; + } + } + + return await fetchMemoById(memoId); + }, + updateTagsState: async () => { + const tagFind: TagFind = {}; + if (userStore.isVisitorMode()) { + tagFind.creatorId = userStore.getUserIdFromPath(); + } + const { data } = (await api.getTagList(tagFind)).data; + store.dispatch(setTags(data)); + }, + getLinkedMemos: async (memoId: MemoId): Promise => { + const regex = new RegExp(`[@(.+?)](${memoId})`); + return state.memos.filter((m) => m.content.match(regex)); + }, + createMemo: async (memoCreate: MemoCreate) => { + const { data } = (await api.createMemo(memoCreate)).data; + const memo = convertResponseModelMemo(data); + store.dispatch(createMemo(memo)); + return memo; + }, + patchMemo: async (memoPatch: MemoPatch): Promise => { + const { data } = (await api.patchMemo(memoPatch)).data; + const memo = convertResponseModelMemo(data); + store.dispatch(patchMemo(memo)); + return memo; + }, + pinMemo: async (memoId: MemoId) => { + await api.pinMemo(memoId); + store.dispatch( + patchMemo({ + id: memoId, + pinned: true, + }) + ); + }, + unpinMemo: async (memoId: MemoId) => { + await api.unpinMemo(memoId); + store.dispatch( + patchMemo({ + id: memoId, + pinned: false, + }) + ); + }, + deleteMemoById: async (memoId: MemoId) => { + await api.deleteMemo(memoId); + store.dispatch(deleteMemo(memoId)); + }, + }; +}; diff --git a/web/src/store/module/resource.ts b/web/src/store/module/resource.ts new file mode 100644 index 000000000..8d87253aa --- /dev/null +++ b/web/src/store/module/resource.ts @@ -0,0 +1,53 @@ +import store, { useAppSelector } from "../"; +import { patchResource, setResources, deleteResource } from "../reducer/resource"; +import * as api from "../../helpers/api"; + +const convertResponseModelResource = (resource: Resource): Resource => { + return { + ...resource, + createdTs: resource.createdTs * 1000, + updatedTs: resource.updatedTs * 1000, + }; +}; + +export const useResourceStore = () => { + const state = useAppSelector((state) => state.resource); + + return { + state, + getState: () => { + return store.getState().resource; + }, + async fetchResourceList(): Promise { + const { data } = (await api.getResourceList()).data; + const resourceList = data.map((m) => convertResponseModelResource(m)); + store.dispatch(setResources(resourceList)); + return resourceList; + }, + async upload(file: File): Promise { + const { name: filename, size } = file; + + if (size > 64 << 20) { + return Promise.reject("overload max size: 8MB"); + } + + const formData = new FormData(); + formData.append("file", file, filename); + const { data } = (await api.uploadFile(formData)).data; + const resource = convertResponseModelResource(data); + const resourceList = state.resources; + store.dispatch(setResources([resource, ...resourceList])); + return resource; + }, + async deleteResourceById(id: ResourceId) { + await api.deleteResourceById(id); + store.dispatch(deleteResource(id)); + }, + async patchResource(resourcePatch: ResourcePatch): Promise { + const { data } = (await api.patchResource(resourcePatch)).data; + const resource = convertResponseModelResource(data); + store.dispatch(patchResource(resource)); + return resource; + }, + }; +}; diff --git a/web/src/store/module/shortcut.ts b/web/src/store/module/shortcut.ts new file mode 100644 index 000000000..ffd8a88ea --- /dev/null +++ b/web/src/store/module/shortcut.ts @@ -0,0 +1,49 @@ +import store, { useAppSelector } from "../"; +import { createShortcut, deleteShortcut, patchShortcut, setShortcuts } from "../reducer/shortcut"; +import * as api from "../../helpers/api"; + +const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => { + return { + ...shortcut, + createdTs: shortcut.createdTs * 1000, + updatedTs: shortcut.updatedTs * 1000, + }; +}; + +export const useShortcutStore = () => { + const state = useAppSelector((state) => state.shortcut); + return { + state, + getState: () => { + return store.getState().shortcut; + }, + getMyAllShortcuts: async () => { + const { data } = (await api.getShortcutList()).data; + const shortcuts = data.map((s) => convertResponseModelShortcut(s)); + store.dispatch(setShortcuts(shortcuts)); + }, + getShortcutById: (id: ShortcutId) => { + for (const s of state.shortcuts) { + if (s.id === id) { + return s; + } + } + + return null; + }, + createShortcut: async (shortcutCreate: ShortcutCreate) => { + const { data } = (await api.createShortcut(shortcutCreate)).data; + const shortcut = convertResponseModelShortcut(data); + store.dispatch(createShortcut(shortcut)); + }, + patchShortcut: async (shortcutPatch: ShortcutPatch) => { + const { data } = (await api.patchShortcut(shortcutPatch)).data; + const shortcut = convertResponseModelShortcut(data); + store.dispatch(patchShortcut(shortcut)); + }, + deleteShortcutById: async (shortcutId: ShortcutId) => { + await api.deleteShortcutById(shortcutId); + store.dispatch(deleteShortcut(shortcutId)); + }, + }; +}; diff --git a/web/src/store/module/user.ts b/web/src/store/module/user.ts new file mode 100644 index 000000000..f7440f008 --- /dev/null +++ b/web/src/store/module/user.ts @@ -0,0 +1,151 @@ +import store, { useAppSelector } from ".."; +import * as api from "../../helpers/api"; +import * as storage from "../../helpers/storage"; +import { UNKNOWN_ID } from "../../helpers/consts"; +import { getSystemColorScheme } from "../../helpers/utils"; +import { setAppearance, setLocale } from "../reducer/global"; +import { setUser, patchUser, setHost, setOwner } from "../reducer/user"; + +const defaultSetting: Setting = { + locale: "en", + appearance: getSystemColorScheme(), + memoVisibility: "PRIVATE", + memoDisplayTsOption: "created_ts", +}; + +const defaultLocalSetting: LocalSetting = { + enableFoldMemo: true, +}; + +export const convertResponseModelUser = (user: User): User => { + const setting: Setting = { + ...defaultSetting, + }; + const { localSetting: storageLocalSetting } = storage.get(["localSetting"]); + const localSetting: LocalSetting = { + ...defaultLocalSetting, + ...storageLocalSetting, + }; + + if (user.userSettingList) { + for (const userSetting of user.userSettingList) { + (setting as any)[userSetting.key] = JSON.parse(userSetting.value); + } + } + + return { + ...user, + setting, + localSetting, + createdTs: user.createdTs * 1000, + updatedTs: user.updatedTs * 1000, + }; +}; + +export const initialUserState = async () => { + const { systemStatus } = store.getState().global; + + if (systemStatus.host) { + store.dispatch(setHost(convertResponseModelUser(systemStatus.host))); + } + + const ownerUserId = getUserIdFromPath(); + if (ownerUserId) { + const { data: owner } = (await api.getUserById(ownerUserId)).data; + if (owner) { + store.dispatch(setOwner(convertResponseModelUser(owner))); + } + } + + const { data } = (await api.getMyselfUser()).data; + if (data) { + const user = convertResponseModelUser(data); + store.dispatch(setUser(user)); + if (user.setting.locale) { + store.dispatch(setLocale(user.setting.locale)); + } + if (user.setting.appearance) { + store.dispatch(setAppearance(user.setting.appearance)); + } + } +}; + +const getUserIdFromPath = () => { + const { pathname } = store.getState().location; + const userIdRegex = /^\/u\/(\d+).*/; + const result = pathname.match(userIdRegex); + if (result && result.length === 2) { + return Number(result[1]); + } + return undefined; +}; + +const doSignIn = async () => { + const { data: user } = (await api.getMyselfUser()).data; + if (user) { + store.dispatch(setUser(convertResponseModelUser(user))); + } else { + doSignOut(); + } + return user; +}; + +const doSignOut = async () => { + store.dispatch(setUser()); + await api.signout(); +}; + +export const useUserStore = () => { + const state = useAppSelector((state) => state.user); + + const isVisitorMode = () => { + return !(getUserIdFromPath() === undefined); + }; + + return { + state, + getState: () => { + return store.getState().user; + }, + isVisitorMode, + getUserIdFromPath, + doSignIn, + doSignOut, + getCurrentUserId: () => { + if (isVisitorMode()) { + return getUserIdFromPath() || UNKNOWN_ID; + } else { + return state.user?.id || UNKNOWN_ID; + } + }, + getUserById: async (userId: UserId) => { + const { data: user } = (await api.getUserById(userId)).data; + if (user) { + return convertResponseModelUser(user); + } else { + return undefined; + } + }, + upsertUserSetting: async (key: keyof Setting, value: any) => { + await api.upsertUserSetting({ + key: key as any, + value: JSON.stringify(value), + }); + await doSignIn(); + }, + upsertLocalSetting: async (key: keyof LocalSetting, value: any) => { + storage.set({ localSetting: { [key]: value } }); + store.dispatch(patchUser({ localSetting: { [key]: value } })); + }, + patchUser: async (userPatch: UserPatch): Promise => { + const { data } = (await api.patchUser(userPatch)).data; + if (userPatch.id === store.getState().user.user?.id) { + const user = convertResponseModelUser(data); + store.dispatch(patchUser(user)); + } + }, + deleteUser: async (userDelete: UserDelete) => { + await api.deleteUser(userDelete); + }, + }; +}; diff --git a/web/src/store/modules/editor.ts b/web/src/store/reducer/editor.ts similarity index 100% rename from web/src/store/modules/editor.ts rename to web/src/store/reducer/editor.ts index 996d56290..4a960d675 100644 --- a/web/src/store/modules/editor.ts +++ b/web/src/store/reducer/editor.ts @@ -1,9 +1,9 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; interface State { - editMemoId?: MemoId; memoVisibility: Visibility; resourceList: Resource[]; + editMemoId?: MemoId; } const editorSlice = createSlice({ diff --git a/web/src/store/modules/global.ts b/web/src/store/reducer/global.ts similarity index 100% rename from web/src/store/modules/global.ts rename to web/src/store/reducer/global.ts diff --git a/web/src/store/modules/location.ts b/web/src/store/reducer/location.ts similarity index 100% rename from web/src/store/modules/location.ts rename to web/src/store/reducer/location.ts diff --git a/web/src/store/modules/memo.ts b/web/src/store/reducer/memo.ts similarity index 100% rename from web/src/store/modules/memo.ts rename to web/src/store/reducer/memo.ts diff --git a/web/src/store/modules/resource.ts b/web/src/store/reducer/resource.ts similarity index 100% rename from web/src/store/modules/resource.ts rename to web/src/store/reducer/resource.ts diff --git a/web/src/store/modules/shortcut.ts b/web/src/store/reducer/shortcut.ts similarity index 100% rename from web/src/store/modules/shortcut.ts rename to web/src/store/reducer/shortcut.ts diff --git a/web/src/store/modules/user.ts b/web/src/store/reducer/user.ts similarity index 100% rename from web/src/store/modules/user.ts rename to web/src/store/reducer/user.ts
{t("tag-list.tip-text")}