diff --git a/web/src/components/DailyReviewDialog.tsx b/web/src/components/DailyReviewDialog.tsx deleted file mode 100644 index 9ce841e96..000000000 --- a/web/src/components/DailyReviewDialog.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import { useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { useMemoStore, useUserStore } from "../store/module"; -import toImage from "../labs/html2image"; -import useToggle from "../hooks/useToggle"; -import { DAILY_TIMESTAMP } from "../helpers/consts"; -import * as utils from "../helpers/utils"; -import Icon from "./Icon"; -import { generateDialog } from "./Dialog"; -import DatePicker from "./base/DatePicker"; -import showPreviewImageDialog from "./PreviewImageDialog"; -import DailyMemo from "./DailyMemo"; -import "../less/daily-review-dialog.less"; - -interface Props extends DialogProps { - currentDateStamp: DateStamp; -} - -const monthChineseStrArray = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec"]; -const weekdayChineseStrArray = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; - -const DailyReviewDialog: React.FC = (props: Props) => { - const { t } = useTranslation(); - const memoStore = useMemoStore(); - const memos = memoStore.state.memos; - - const userStore = useUserStore(); - const { localSetting } = userStore.state.user as User; - const [currentDateStamp, setCurrentDateStamp] = useState(utils.getDateStampByDate(utils.getDateString(props.currentDateStamp))); - const [showDatePicker, toggleShowDatePicker] = useToggle(false); - const memosElRef = useRef(null); - const currentDate = new Date(currentDateStamp); - const dailyMemos = memos - .filter((m) => { - const createdTimestamp = utils.getTimeStampByDate(m.createdTs); - const currentDateStampWithOffset = currentDateStamp + utils.convertToMillis(localSetting); - return ( - m.rowStatus === "NORMAL" && - createdTimestamp >= currentDateStampWithOffset && - createdTimestamp < currentDateStampWithOffset + DAILY_TIMESTAMP - ); - }) - .sort((a, b) => utils.getTimeStampByDate(a.createdTs) - utils.getTimeStampByDate(b.createdTs)); - - const handleShareBtnClick = () => { - if (!memosElRef.current) { - return; - } - - toggleShowDatePicker(false); - - toImage(memosElRef.current, { - pixelRatio: window.devicePixelRatio * 2, - }) - .then((url) => { - showPreviewImageDialog(url); - }) - .catch(() => { - // do nth - }); - }; - - const handleDataPickerChange = (datestamp: DateStamp): void => { - setCurrentDateStamp(datestamp); - toggleShowDatePicker(false); - }; - - return ( - <> -
-

toggleShowDatePicker()}> - 📅 {t("common.daily-review")} -

-
- - - - / - -
- -
-
-
-
{currentDate.getFullYear()}
-
-
{monthChineseStrArray[currentDate.getMonth()]}
-
{currentDate.getDate()}
-
{weekdayChineseStrArray[currentDate.getDay()]}
-
-
- {dailyMemos.length === 0 ? ( -
-

{t("daily-review.oops-nothing")}

-
- ) : ( -
- {dailyMemos.map((memo) => ( - - ))} -
- )} -
- - ); -}; - -export default function showDailyReviewDialog(datestamp: DateStamp = Date.now()): void { - generateDialog( - { - className: "daily-review-dialog", - dialogName: "daily-review-dialog", - }, - DailyReviewDialog, - { currentDateStamp: datestamp } - ); -} diff --git a/web/src/components/Header.tsx b/web/src/components/Header.tsx index bcdc03a97..80955e448 100644 --- a/web/src/components/Header.tsx +++ b/web/src/components/Header.tsx @@ -1,10 +1,9 @@ import { useEffect } from "react"; -import { Link } from "react-router-dom"; +import { NavLink } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { useLayoutStore, useLocationStore, useUserStore } from "../store/module"; +import { useLayoutStore, useUserStore } from "../store/module"; import { resolution } from "../utils/layout"; import Icon from "./Icon"; -import showDailyReviewDialog from "./DailyReviewDialog"; import showResourcesDialog from "./ResourcesDialog"; import showSettingDialog from "./SettingDialog"; import showAskAIDialog from "./AskAIDialog"; @@ -14,9 +13,9 @@ import UserBanner from "./UserBanner"; const Header = () => { const { t } = useTranslation(); const userStore = useUserStore(); - const locationStore = useLocationStore(); const layoutStore = useLayoutStore(); const showHeader = layoutStore.state.showHeader; + const isVisitorMode = userStore.isVisitorMode() && !userStore.state.user; useEffect(() => { const handleWindowResize = () => { @@ -49,26 +48,47 @@ const Header = () => { >
- locationStore.clearQuery()} - > - {t("common.home")} - - - + + `${ + isActive && "bg-white dark:bg-zinc-700 shadow" + } px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700` + } + > + <> + {t("common.home")} + + + + `${ + isActive && "bg-white dark:bg-zinc-700 shadow" + } px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700` + } + > + <> + {t("common.daily-review")} + + + + )} + + `${ + isActive && "bg-white dark:bg-zinc-700 shadow" + } px-4 pr-5 py-2 rounded-lg flex flex-row items-center text-lg dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700` + } > - {t("common.explore")} - - {!userStore.isVisitorMode() && ( + <> + {t("common.explore")} + + + {!isVisitorMode && ( <> + + +
+ + +
+
+
{currentDate.getFullYear()}
+
+
+ {monthChineseStrArray[currentDate.getMonth()]} +
+
{currentDate.getDate()}
+
{weekdayChineseStrArray[currentDate.getDay()]}
+
+
+ {dailyMemos.length === 0 ? ( +
+

{t("daily-review.oops-nothing")}

+
+ ) : ( +
+ {dailyMemos.map((memo) => ( + + ))} +
+ )} +
+ + + ); +}; + +export default DailyReview; diff --git a/web/src/pages/Explore.tsx b/web/src/pages/Explore.tsx index e8e3e02e9..ee383424c 100644 --- a/web/src/pages/Explore.tsx +++ b/web/src/pages/Explore.tsx @@ -1,14 +1,13 @@ import { useEffect, useState } from "react"; import { toast } from "react-hot-toast"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; -import { useGlobalStore, useLocationStore, useMemoStore, useUserStore } from "../store/module"; +import { useLocationStore, useMemoStore } from "../store/module"; import { TAG_REG } from "../labs/marked/parser"; import { DEFAULT_MEMO_LIMIT } from "../helpers/consts"; import useLoading from "../hooks/useLoading"; -import Icon from "../components/Icon"; import MemoFilter from "../components/MemoFilter"; import Memo from "../components/Memo"; +import MobileHeader from "../components/MobileHeader"; interface State { memos: Memo[]; @@ -16,10 +15,7 @@ interface State { const Explore = () => { const { t } = useTranslation(); - const navigate = useNavigate(); - const globalStore = useGlobalStore(); const locationStore = useLocationStore(); - const userStore = useUserStore(); const memoStore = useMemoStore(); const query = locationStore.state.query; const [state, setState] = useState({ @@ -27,8 +23,6 @@ const Explore = () => { }); const [isComplete, setIsComplete] = useState(false); const loadingState = useLoading(); - const customizedProfile = globalStore.state.systemStatus.customizedProfile; - const user = userStore.state.user; const location = locationStore.state; useEffect(() => { @@ -88,33 +82,11 @@ const Explore = () => { } }; - const handleTitleClick = () => { - if (user) { - navigate("/"); - } else { - navigate("/auth"); - } - }; - return ( -
-
-
- - {customizedProfile.name} -
-
- - - -
-
+
+ {!loadingState.isLoading && ( -
+
{sortedMemos.map((memo) => { return ; diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx index 98f06b7a5..064a47841 100644 --- a/web/src/pages/Home.tsx +++ b/web/src/pages/Home.tsx @@ -1,6 +1,5 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { useLocation } from "react-router-dom"; import { toast } from "react-hot-toast"; import { useGlobalStore, useUserStore } from "../store/module"; import MemoEditor from "../components/MemoEditor"; @@ -11,20 +10,19 @@ import HomeSidebar from "../components/HomeSidebar"; function Home() { const { t } = useTranslation(); - const location = useLocation(); const globalStore = useGlobalStore(); const userStore = useUserStore(); const user = userStore.state.user; useEffect(() => { - const { owner } = userStore.getState(); - - if (userStore.isVisitorMode()) { - if (!owner) { + const currentUserId = userStore.getCurrentUserId(); + userStore.getUserById(currentUserId).then((user) => { + if (!user) { toast.error(t("message.user-not-found")); + return; } - } - }, [location]); + }); + }, [userStore.getCurrentUserId()]); useEffect(() => { if (user?.setting.locale) { diff --git a/web/src/router/index.tsx b/web/src/router/index.tsx index 4471070c9..9846a976a 100644 --- a/web/src/router/index.tsx +++ b/web/src/router/index.tsx @@ -3,6 +3,7 @@ import { lazy } from "react"; import { isNullorUndefined } from "../helpers/utils"; import store from "../store"; import { initialGlobalState, initialUserState } from "../store/module"; +import DailyReview from "../pages/DailyReview"; const Root = lazy(() => import("../layouts/Root")); const Auth = lazy(() => import("../pages/Auth")); @@ -67,45 +68,64 @@ const router = createBrowserRouter([ return null; }, }, - ], - }, - { - path: "/u/:userId", - element: , - loader: async () => { - await initialGlobalStateLoader(); + { + path: "/u/:userId", + element: , + loader: async () => { + await initialGlobalStateLoader(); - try { - await initialUserState(); - } catch (error) { - // do nth - } + try { + await initialUserState(); + } catch (error) { + // do nth + } - const { host } = store.getState().user; - if (isNullorUndefined(host)) { - return redirect("/auth"); - } - return null; - }, - }, - { - path: "/explore", - element: , - loader: async () => { - await initialGlobalStateLoader(); + const { host } = store.getState().user; + if (isNullorUndefined(host)) { + return redirect("/auth"); + } + return null; + }, + }, + { + path: "explore", + element: , + loader: async () => { + await initialGlobalStateLoader(); - try { - await initialUserState(); - } catch (error) { - // do nth - } + try { + await initialUserState(); + } catch (error) { + // do nth + } - const { host } = store.getState().user; - if (isNullorUndefined(host)) { - return redirect("/auth"); - } - return null; - }, + const { host } = store.getState().user; + if (isNullorUndefined(host)) { + return redirect("/auth"); + } + return null; + }, + }, + { + path: "review", + element: , + loader: async () => { + await initialGlobalStateLoader(); + + try { + await initialUserState(); + } catch (error) { + // do nth + } + + const { host } = store.getState().user; + if (isNullorUndefined(host)) { + return redirect("/auth"); + } + return null; + }, + }, + ], }, { path: "/m/:memoId", diff --git a/web/src/store/module/memo.ts b/web/src/store/module/memo.ts index 36012db63..ee707b306 100644 --- a/web/src/store/module/memo.ts +++ b/web/src/store/module/memo.ts @@ -1,9 +1,9 @@ -import { omit, uniqBy } from "lodash-es"; +import { omit } from "lodash-es"; 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 } from "../reducer/memo"; +import { createMemo, deleteMemo, patchMemo, setIsFetching, upsertMemos } from "../reducer/memo"; const convertResponseModelMemo = (memo: Memo): Memo => { return { @@ -41,11 +41,7 @@ export const useMemoStore = () => { } 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(upsertMemos(fetchedMemos)); store.dispatch(setIsFetching(false)); return fetchedMemos; diff --git a/web/src/store/module/user.ts b/web/src/store/module/user.ts index beadd828f..c4720834a 100644 --- a/web/src/store/module/user.ts +++ b/web/src/store/module/user.ts @@ -4,7 +4,7 @@ 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"; +import { setUser, patchUser, setHost, setUserById } from "../reducer/user"; const defaultSetting: Setting = { locale: "en", @@ -50,14 +50,6 @@ export const initialUserState = async () => { 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); @@ -72,7 +64,7 @@ export const initialUserState = async () => { }; const getUserIdFromPath = () => { - const { pathname } = store.getState().location; + const pathname = location.pathname; const userIdRegex = /^\/u\/(\d+).*/; const result = pathname.match(userIdRegex); if (result && result.length === 2) { @@ -99,7 +91,7 @@ export const useUserStore = () => { const state = useAppSelector((state) => state.user); const isVisitorMode = () => { - return !(getUserIdFromPath() === undefined); + return state.user === undefined || (getUserIdFromPath() && state.user.id !== getUserIdFromPath()); }; return { @@ -119,9 +111,11 @@ export const useUserStore = () => { } }, getUserById: async (userId: UserId) => { - const { data: user } = (await api.getUserById(userId)).data; - if (user) { - return convertResponseModelUser(user); + const { data } = (await api.getUserById(userId)).data; + if (data) { + const user = convertResponseModelUser(data); + store.dispatch(setUserById(user)); + return user; } else { return undefined; } diff --git a/web/src/store/reducer/memo.ts b/web/src/store/reducer/memo.ts index c558cc175..be0039113 100644 --- a/web/src/store/reducer/memo.ts +++ b/web/src/store/reducer/memo.ts @@ -1,4 +1,5 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { uniqBy } from "lodash-es"; interface State { memos: Memo[]; @@ -13,10 +14,10 @@ const memoSlice = createSlice({ isFetching: true, } as State, reducers: { - setMemos: (state, action: PayloadAction) => { + upsertMemos: (state, action: PayloadAction) => { return { ...state, - memos: action.payload, + memos: uniqBy([...state.memos, ...action.payload], "id"), }; }, createMemo: (state, action: PayloadAction) => { @@ -59,6 +60,6 @@ const memoSlice = createSlice({ }, }); -export const { setMemos, createMemo, patchMemo, deleteMemo, setIsFetching } = memoSlice.actions; +export const { upsertMemos, createMemo, patchMemo, deleteMemo, setIsFetching } = memoSlice.actions; export default memoSlice.reducer; diff --git a/web/src/store/reducer/user.ts b/web/src/store/reducer/user.ts index a1d34e81a..b4b59a723 100644 --- a/web/src/store/reducer/user.ts +++ b/web/src/store/reducer/user.ts @@ -1,17 +1,19 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { cloneDeep } from "lodash-es"; interface State { // host is the user who hist the system host?: User; - // owner is the user who owns the page. If in `/u/101`, then owner's id is `101` - owner?: User; // user is the user who is currently logged in user?: User; + userById: { [key: UserId]: User }; } const userSlice = createSlice({ name: "user", - initialState: {} as State, + initialState: { + userById: {}, + } as State, reducers: { setHost: (state, action: PayloadAction) => { return { @@ -19,16 +21,18 @@ const userSlice = createSlice({ host: action.payload, }; }, - setOwner: (state, action: PayloadAction) => { + setUser: (state, action: PayloadAction) => { return { ...state, - owner: action.payload, + user: action.payload, }; }, - setUser: (state, action: PayloadAction) => { + setUserById: (state, action: PayloadAction) => { + const userById = cloneDeep(state.userById); + userById[action.payload.id] = action.payload; return { ...state, - user: action.payload, + userById: userById, }; }, patchUser: (state, action: PayloadAction>) => { @@ -43,6 +47,6 @@ const userSlice = createSlice({ }, }); -export const { setHost, setOwner, setUser, patchUser } = userSlice.actions; +export const { setHost, setUser, setUserById, patchUser } = userSlice.actions; export default userSlice.reducer;