mirror of
				https://github.com/usememos/memos.git
				synced 2025-06-05 22:09:59 +02:00 
			
		
		
		
	feat: personal memos page (#105)
* feat: no need to log in to view memos * chore: add a normal user to seed * feat: page for other members * fix: replace window.location * fix: can not get username on home * fix: check userID * fix: can visit other user's page after login * fix: do not redirect on wrong path * fix: path error when clicked heatmap * refactor: revise for review * chore: remove unused import * refactor: revise for review * feat: update each user's route to /u/:userId. * chore: eslint for import sort * refactor: revise for review
This commit is contained in:
		| @@ -59,6 +59,10 @@ func BasicAuthMiddleware(s *Server, next echo.HandlerFunc) echo.HandlerFunc { | |||||||
| 			return next(c) | 			return next(c) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		if common.HasPrefixes(c.Path(), "/api/memo", "/api/tag", "/api/shortcut", "/api/user/:id/name") && c.Request().Method == http.MethodGet { | ||||||
|  | 			return next(c) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		// If there is openId in query string and related user is found, then skip auth. | 		// If there is openId in query string and related user is found, then skip auth. | ||||||
| 		openID := c.QueryParam("openId") | 		openID := c.QueryParam("openId") | ||||||
| 		if openID != "" { | 		if openID != "" { | ||||||
|   | |||||||
| @@ -60,7 +60,29 @@ func (s *Server) registerMemoRoutes(g *echo.Group) { | |||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	g.GET("/memo", func(c echo.Context) error { | 	g.GET("/memo", func(c echo.Context) error { | ||||||
| 		userID := c.Get(getUserIDContextKey()).(int) | 		userID, ok := c.Get(getUserIDContextKey()).(int) | ||||||
|  | 		if !ok { | ||||||
|  | 			if c.QueryParam("userID") != "" { | ||||||
|  | 				var err error | ||||||
|  | 				userID, err = strconv.Atoi(c.QueryParam("userID")) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.QueryParam("userID"))) | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				ownerUserType := api.Owner | ||||||
|  | 				ownerUser, err := s.Store.FindUser(&api.UserFind{ | ||||||
|  | 					Role: &ownerUserType, | ||||||
|  | 				}) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find owner user").SetInternal(err) | ||||||
|  | 				} | ||||||
|  | 				if ownerUser == nil { | ||||||
|  | 					return echo.NewHTTPError(http.StatusNotFound, "Owner user do not exist") | ||||||
|  | 				} | ||||||
|  | 				userID = ownerUser.ID | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		memoFind := &api.MemoFind{ | 		memoFind := &api.MemoFind{ | ||||||
| 			CreatorID: &userID, | 			CreatorID: &userID, | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -59,7 +59,29 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) { | |||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
| 	g.GET("/shortcut", func(c echo.Context) error { | 	g.GET("/shortcut", func(c echo.Context) error { | ||||||
| 		userID := c.Get(getUserIDContextKey()).(int) | 		userID, ok := c.Get(getUserIDContextKey()).(int) | ||||||
|  | 		if !ok { | ||||||
|  | 			if c.QueryParam("userID") != "" { | ||||||
|  | 				var err error | ||||||
|  | 				userID, err = strconv.Atoi(c.QueryParam("userID")) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.QueryParam("userID"))) | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				ownerUserType := api.Owner | ||||||
|  | 				ownerUser, err := s.Store.FindUser(&api.UserFind{ | ||||||
|  | 					Role: &ownerUserType, | ||||||
|  | 				}) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find owner user").SetInternal(err) | ||||||
|  | 				} | ||||||
|  | 				if ownerUser == nil { | ||||||
|  | 					return echo.NewHTTPError(http.StatusNotFound, "Owner user do not exist") | ||||||
|  | 				} | ||||||
|  | 				userID = ownerUser.ID | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		shortcutFind := &api.ShortcutFind{ | 		shortcutFind := &api.ShortcutFind{ | ||||||
| 			CreatorID: &userID, | 			CreatorID: &userID, | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -2,9 +2,11 @@ package server | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
|  | 	"fmt" | ||||||
| 	"net/http" | 	"net/http" | ||||||
| 	"regexp" | 	"regexp" | ||||||
| 	"sort" | 	"sort" | ||||||
|  | 	"strconv" | ||||||
|  |  | ||||||
| 	"github.com/usememos/memos/api" | 	"github.com/usememos/memos/api" | ||||||
|  |  | ||||||
| @@ -13,7 +15,29 @@ import ( | |||||||
|  |  | ||||||
| func (s *Server) registerTagRoutes(g *echo.Group) { | func (s *Server) registerTagRoutes(g *echo.Group) { | ||||||
| 	g.GET("/tag", func(c echo.Context) error { | 	g.GET("/tag", func(c echo.Context) error { | ||||||
| 		userID := c.Get(getUserIDContextKey()).(int) | 		userID, ok := c.Get(getUserIDContextKey()).(int) | ||||||
|  | 		if !ok { | ||||||
|  | 			if c.QueryParam("userID") != "" { | ||||||
|  | 				var err error | ||||||
|  | 				userID, err = strconv.Atoi(c.QueryParam("userID")) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.QueryParam("userID"))) | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				ownerUserType := api.Owner | ||||||
|  | 				ownerUser, err := s.Store.FindUser(&api.UserFind{ | ||||||
|  | 					Role: &ownerUserType, | ||||||
|  | 				}) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find owner user").SetInternal(err) | ||||||
|  | 				} | ||||||
|  | 				if ownerUser == nil { | ||||||
|  | 					return echo.NewHTTPError(http.StatusNotFound, "Owner user do not exist") | ||||||
|  | 				} | ||||||
|  | 				userID = ownerUser.ID | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		contentSearch := "#" | 		contentSearch := "#" | ||||||
| 		normalRowStatus := api.Normal | 		normalRowStatus := api.Normal | ||||||
| 		memoFind := api.MemoFind{ | 		memoFind := api.MemoFind{ | ||||||
|   | |||||||
| @@ -51,6 +51,29 @@ func (s *Server) registerUserRoutes(g *echo.Group) { | |||||||
| 		return nil | 		return nil | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | 	g.GET("/user/:id/name", func(c echo.Context) error { | ||||||
|  | 		id, err := strconv.Atoi(c.Param("id")) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return echo.NewHTTPError(http.StatusBadRequest, "Malformatted user id").SetInternal(err) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		user, err := s.Store.FindUser(&api.UserFind{ | ||||||
|  | 			ID: &id, | ||||||
|  | 		}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch user").SetInternal(err) | ||||||
|  | 		} | ||||||
|  | 		if user == nil { | ||||||
|  | 			return echo.NewHTTPError(http.StatusNotFound, "User not found") | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) | ||||||
|  | 		if err := json.NewEncoder(c.Response().Writer).Encode(composeResponse(user.Name)); err != nil { | ||||||
|  | 			return echo.NewHTTPError(http.StatusInternalServerError, "Failed to encode user response").SetInternal(err) | ||||||
|  | 		} | ||||||
|  | 		return nil | ||||||
|  | 	}) | ||||||
|  |  | ||||||
| 	// GET /api/user/me is used to check if the user is logged in. | 	// GET /api/user/me is used to check if the user is logged in. | ||||||
| 	g.GET("/user/me", func(c echo.Context) error { | 	g.GET("/user/me", func(c echo.Context) error { | ||||||
| 		userSessionID := c.Get(getUserIDContextKey()) | 		userSessionID := c.Get(getUserIDContextKey()) | ||||||
|   | |||||||
| @@ -17,3 +17,23 @@ VALUES | |||||||
|     -- raw password: secret |     -- raw password: secret | ||||||
|     '$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK' |     '$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK' | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | INSERT INTO  | ||||||
|  |   user ( | ||||||
|  |     `id`,  | ||||||
|  |     `email`, | ||||||
|  |     `role`, | ||||||
|  |     `name`,  | ||||||
|  |     `open_id`, | ||||||
|  |     `password_hash` | ||||||
|  |   ) | ||||||
|  | VALUES | ||||||
|  |   ( | ||||||
|  |     102,  | ||||||
|  |     'jack@usememos.com', | ||||||
|  |     'USER', | ||||||
|  |     'Jack', | ||||||
|  |     'jack_open_id', | ||||||
|  |     -- raw password: secret | ||||||
|  |     '$2a$14$ajq8Q7fbtFRQvXpdCq7Jcuy.Rx1h/L4J60Otx.gyNLbAYctGMJ9tK' | ||||||
|  |   ); | ||||||
|   | |||||||
| @@ -42,3 +42,16 @@ VALUES | |||||||
|     '好好学习,天天向上。🤜🤛',  |     '好好学习,天天向上。🤜🤛',  | ||||||
|     101 |     101 | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  | INSERT INTO  | ||||||
|  |   memo ( | ||||||
|  |     `id`, | ||||||
|  |     `content`,  | ||||||
|  |     `creator_id` | ||||||
|  |   ) | ||||||
|  | VALUES | ||||||
|  |   ( | ||||||
|  |     104, | ||||||
|  |     '好好学习,天天向上。🤜🤛',  | ||||||
|  |     102 | ||||||
|  |   ); | ||||||
|   | |||||||
| @@ -23,6 +23,12 @@ | |||||||
|     ], |     ], | ||||||
|     "@typescript-eslint/no-empty-interface": ["off"], |     "@typescript-eslint/no-empty-interface": ["off"], | ||||||
|     "@typescript-eslint/no-explicit-any": ["off"], |     "@typescript-eslint/no-explicit-any": ["off"], | ||||||
|     "react/react-in-jsx-scope": "off" |     "react/react-in-jsx-scope": "off", | ||||||
|  |     "sort-imports": [ | ||||||
|  |       "error", | ||||||
|  |       { | ||||||
|  |         "memberSyntaxSortOrder": ["all", "multiple", "single", "none"] | ||||||
|  |       } | ||||||
|  |     ] | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,7 +3,7 @@ import { escape, indexOf } from "lodash-es"; | |||||||
| import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG, UNKNOWN_ID } from "../helpers/consts"; | import { IMAGE_URL_REG, LINK_REG, MEMO_LINK_REG, TAG_REG, UNKNOWN_ID } from "../helpers/consts"; | ||||||
| import { DONE_BLOCK_REG, parseMarkedToHtml, TODO_BLOCK_REG } from "../helpers/marked"; | import { DONE_BLOCK_REG, parseMarkedToHtml, TODO_BLOCK_REG } from "../helpers/marked"; | ||||||
| import * as utils from "../helpers/utils"; | import * as utils from "../helpers/utils"; | ||||||
| import { editorStateService, locationService, memoService } from "../services"; | import { editorStateService, locationService, memoService, userService } from "../services"; | ||||||
| import Only from "./common/OnlyWhen"; | import Only from "./common/OnlyWhen"; | ||||||
| import Image from "./Image"; | import Image from "./Image"; | ||||||
| import showMemoCardDialog from "./MemoCardDialog"; | import showMemoCardDialog from "./MemoCardDialog"; | ||||||
| @@ -112,7 +112,7 @@ const Memo: React.FC<Props> = (props: Props) => { | |||||||
|       } else { |       } else { | ||||||
|         locationService.setTagQuery(tagName); |         locationService.setTagQuery(tagName); | ||||||
|       } |       } | ||||||
|     } else if (targetEl.classList.contains("todo-block")) { |     } else if (targetEl.classList.contains("todo-block") && userService.isNotVisitor()) { | ||||||
|       const status = targetEl.dataset?.value; |       const status = targetEl.dataset?.value; | ||||||
|       const todoElementList = [...(memoContainerRef.current?.querySelectorAll(`span.todo-block[data-value=${status}]`) ?? [])]; |       const todoElementList = [...(memoContainerRef.current?.querySelectorAll(`span.todo-block[data-value=${status}]`) ?? [])]; | ||||||
|       for (const element of todoElementList) { |       for (const element of todoElementList) { | ||||||
| @@ -158,6 +158,7 @@ const Memo: React.FC<Props> = (props: Props) => { | |||||||
|             <span className="ml-2">PINNED</span> |             <span className="ml-2">PINNED</span> | ||||||
|           </Only> |           </Only> | ||||||
|         </span> |         </span> | ||||||
|  |         {userService.isNotVisitor() && ( | ||||||
|           <div className="btns-container"> |           <div className="btns-container"> | ||||||
|             <span className="btn more-action-btn"> |             <span className="btn more-action-btn"> | ||||||
|               <img className="icon-img" src="/icons/more.svg" /> |               <img className="icon-img" src="/icons/more.svg" /> | ||||||
| @@ -190,6 +191,7 @@ const Memo: React.FC<Props> = (props: Props) => { | |||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |         )} | ||||||
|       </div> |       </div> | ||||||
|       <div |       <div | ||||||
|         ref={memoContainerRef} |         ref={memoContainerRef} | ||||||
|   | |||||||
| @@ -55,6 +55,10 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => { | |||||||
|     window.location.reload(); |     window.location.reload(); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   const handleSignInBtnClick = async () => { | ||||||
|  |     locationService.replaceHistory("/signin"); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className={`menu-btns-popup ${shownStatus ? "" : "hidden"}`} ref={popupElRef}> |     <div className={`menu-btns-popup ${shownStatus ? "" : "hidden"}`} ref={popupElRef}> | ||||||
|       <button className="btn action-btn" onClick={handleAboutBtnClick}> |       <button className="btn action-btn" onClick={handleAboutBtnClick}> | ||||||
| @@ -63,8 +67,8 @@ const MenuBtnsPopup: React.FC<Props> = (props: Props) => { | |||||||
|       <button className="btn action-btn" onClick={handlePingBtnClick}> |       <button className="btn action-btn" onClick={handlePingBtnClick}> | ||||||
|         <span className="icon">🎯</span> Ping |         <span className="icon">🎯</span> Ping | ||||||
|       </button> |       </button> | ||||||
|       <button className="btn action-btn" onClick={handleSignOutBtnClick}> |       <button className="btn action-btn" onClick={userService.isNotVisitor() ? handleSignOutBtnClick : handleSignInBtnClick}> | ||||||
|         <span className="icon">👋</span> Sign out |         <span className="icon">👋</span> {userService.isNotVisitor() ? "Sign out" : "Sign in"} | ||||||
|       </button> |       </button> | ||||||
|     </div> |     </div> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { useEffect } from "react"; | import { useEffect } from "react"; | ||||||
| import { locationService, shortcutService } from "../services"; | import { locationService, shortcutService, userService } from "../services"; | ||||||
| import { useAppSelector } from "../store"; | import { useAppSelector } from "../store"; | ||||||
| import * as utils from "../helpers/utils"; | import * as utils from "../helpers/utils"; | ||||||
| import useToggle from "../hooks/useToggle"; | import useToggle from "../hooks/useToggle"; | ||||||
| @@ -38,9 +38,11 @@ const ShortcutList: React.FC<Props> = () => { | |||||||
|     <div className="shortcuts-wrapper"> |     <div className="shortcuts-wrapper"> | ||||||
|       <p className="title-text"> |       <p className="title-text"> | ||||||
|         <span className="normal-text">Shortcuts</span> |         <span className="normal-text">Shortcuts</span> | ||||||
|  |         {userService.isNotVisitor() && ( | ||||||
|           <span className="btn" onClick={() => showCreateShortcutDialog()}> |           <span className="btn" onClick={() => showCreateShortcutDialog()}> | ||||||
|             <img src="/icons/add.svg" alt="add shortcut" /> |             <img src="/icons/add.svg" alt="add shortcut" /> | ||||||
|           </span> |           </span> | ||||||
|  |         )} | ||||||
|       </p> |       </p> | ||||||
|       <div className="shortcuts-container"> |       <div className="shortcuts-container"> | ||||||
|         {sortedShortcuts.map((s) => { |         {sortedShortcuts.map((s) => { | ||||||
| @@ -114,6 +116,7 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont | |||||||
|         <div className="shortcut-text-container"> |         <div className="shortcut-text-container"> | ||||||
|           <span className="shortcut-text">{shortcut.title}</span> |           <span className="shortcut-text">{shortcut.title}</span> | ||||||
|         </div> |         </div> | ||||||
|  |         {userService.isNotVisitor() && ( | ||||||
|           <div className="btns-container"> |           <div className="btns-container"> | ||||||
|             <span className="action-btn toggle-btn"> |             <span className="action-btn toggle-btn"> | ||||||
|               <img className="icon-img" src="/icons/more.svg" /> |               <img className="icon-img" src="/icons/more.svg" /> | ||||||
| @@ -136,6 +139,7 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont | |||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|           </div> |           </div> | ||||||
|  |         )} | ||||||
|       </div> |       </div> | ||||||
|     </> |     </> | ||||||
|   ); |   ); | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import UserBanner from "./UserBanner"; | |||||||
| import UsageHeatMap from "./UsageHeatMap"; | import UsageHeatMap from "./UsageHeatMap"; | ||||||
| import ShortcutList from "./ShortcutList"; | import ShortcutList from "./ShortcutList"; | ||||||
| import TagList from "./TagList"; | import TagList from "./TagList"; | ||||||
|  | import { userService } from "../services"; | ||||||
| import "../less/siderbar.less"; | import "../less/siderbar.less"; | ||||||
|  |  | ||||||
| interface Props {} | interface Props {} | ||||||
| @@ -48,6 +49,8 @@ const Sidebar: React.FC<Props> = () => { | |||||||
|         </div> |         </div> | ||||||
|       </div> |       </div> | ||||||
|       <UsageHeatMap /> |       <UsageHeatMap /> | ||||||
|  |       {userService.isNotVisitor() && ( | ||||||
|  |         <> | ||||||
|           <div className="action-btns-container"> |           <div className="action-btns-container"> | ||||||
|             <button className="btn action-btn" onClick={() => showDailyReviewDialog()}> |             <button className="btn action-btn" onClick={() => showDailyReviewDialog()}> | ||||||
|               <span className="icon">📅</span> Daily Review |               <span className="icon">📅</span> Daily Review | ||||||
| @@ -59,6 +62,8 @@ const Sidebar: React.FC<Props> = () => { | |||||||
|               <span className="icon">🗂</span> Archived |               <span className="icon">🗂</span> Archived | ||||||
|             </button> |             </button> | ||||||
|           </div> |           </div> | ||||||
|  |         </> | ||||||
|  |       )} | ||||||
|       <ShortcutList /> |       <ShortcutList /> | ||||||
|       <TagList /> |       <TagList /> | ||||||
|     </aside> |     </aside> | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { useEffect, useState } from "react"; | import { useEffect, useState } from "react"; | ||||||
| import { useAppSelector } from "../store"; | import { useAppSelector } from "../store"; | ||||||
| import { locationService, memoService } from "../services"; | import { locationService, memoService, userService } from "../services"; | ||||||
| import useToggle from "../hooks/useToggle"; | import useToggle from "../hooks/useToggle"; | ||||||
| import Only from "./common/OnlyWhen"; | import Only from "./common/OnlyWhen"; | ||||||
| import * as utils from "../helpers/utils"; | import * as utils from "../helpers/utils"; | ||||||
| @@ -71,7 +71,7 @@ const TagList: React.FC<Props> = () => { | |||||||
|         {tags.map((t, idx) => ( |         {tags.map((t, idx) => ( | ||||||
|           <TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={query?.tag} /> |           <TagItemContainer key={t.text + "-" + idx} tag={t} tagQuery={query?.tag} /> | ||||||
|         ))} |         ))} | ||||||
|         <Only when={tags.length < 5}> |         <Only when={userService.isNotVisitor() && tags.length < 5}> | ||||||
|           <p className="tag-tip-container"> |           <p className="tag-tip-container"> | ||||||
|             Enter <span className="code-text">#tag </span> to create a tag |             Enter <span className="code-text">#tag </span> to create a tag | ||||||
|           </p> |           </p> | ||||||
|   | |||||||
| @@ -73,9 +73,6 @@ const UsageHeatMap: React.FC<Props> = () => { | |||||||
|       locationService.setFromAndToQuery(); |       locationService.setFromAndToQuery(); | ||||||
|       setCurrentStat(null); |       setCurrentStat(null); | ||||||
|     } else if (item.count > 0) { |     } else if (item.count > 0) { | ||||||
|       if (!["/"].includes(locationService.getState().pathname)) { |  | ||||||
|         locationService.setPathname("/"); |  | ||||||
|       } |  | ||||||
|       locationService.setFromAndToQuery(item.timestamp, item.timestamp + DAILY_TIMESTAMP); |       locationService.setFromAndToQuery(item.timestamp, item.timestamp + DAILY_TIMESTAMP); | ||||||
|       setCurrentStat(item); |       setCurrentStat(item); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,7 +1,10 @@ | |||||||
| import { useCallback, useState } from "react"; | import * as api from "../helpers/api"; | ||||||
| import { useAppSelector } from "../store"; | import { useCallback, useEffect, useState } from "react"; | ||||||
| import { locationService } from "../services"; |  | ||||||
| import MenuBtnsPopup from "./MenuBtnsPopup"; | import MenuBtnsPopup from "./MenuBtnsPopup"; | ||||||
|  | import { getUserIdFromPath } from "../services/userService"; | ||||||
|  | import { locationService } from "../services"; | ||||||
|  | import toastHelper from "./Toast"; | ||||||
|  | import { useAppSelector } from "../store"; | ||||||
| import "../less/user-banner.less"; | import "../less/user-banner.less"; | ||||||
|  |  | ||||||
| interface Props {} | interface Props {} | ||||||
| @@ -10,10 +13,9 @@ const UserBanner: React.FC<Props> = () => { | |||||||
|   const user = useAppSelector((state) => state.user.user); |   const user = useAppSelector((state) => state.user.user); | ||||||
|   const [shouldShowPopupBtns, setShouldShowPopupBtns] = useState(false); |   const [shouldShowPopupBtns, setShouldShowPopupBtns] = useState(false); | ||||||
|  |  | ||||||
|   const username = user ? user.name : "Memos"; |   const [username, setUsername] = useState(user ? user.name : "Memos"); | ||||||
|  |  | ||||||
|   const handleUsernameClick = useCallback(() => { |   const handleUsernameClick = useCallback(() => { | ||||||
|     locationService.pushHistory("/"); |  | ||||||
|     locationService.clearQuery(); |     locationService.clearQuery(); | ||||||
|   }, []); |   }, []); | ||||||
|  |  | ||||||
| @@ -21,6 +23,27 @@ const UserBanner: React.FC<Props> = () => { | |||||||
|     setShouldShowPopupBtns(true); |     setShouldShowPopupBtns(true); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   useEffect(() => { | ||||||
|  |     if (username === "Memos") { | ||||||
|  |       if (locationService.getState().pathname === "/") { | ||||||
|  |         api.getSystemStatus().then(({ data }) => { | ||||||
|  |           const { data: status } = data; | ||||||
|  |           setUsername(status.owner.name); | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         api | ||||||
|  |           .getUserNameById(Number(getUserIdFromPath())) | ||||||
|  |           .then(({ data }) => { | ||||||
|  |             const { data: username } = data; | ||||||
|  |             setUsername(username); | ||||||
|  |           }) | ||||||
|  |           .catch(() => { | ||||||
|  |             toastHelper.error("User not found"); | ||||||
|  |           }); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className="user-banner-container"> |     <div className="user-banner-container"> | ||||||
|       <div className="username-container" onClick={handleUsernameClick}> |       <div className="username-container" onClick={handleUsernameClick}> | ||||||
|   | |||||||
| @@ -44,16 +44,20 @@ export function getUserList() { | |||||||
|   return axios.get<ResponseObject<User[]>>("/api/user"); |   return axios.get<ResponseObject<User[]>>("/api/user"); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export function getUserNameById(id: number) { | ||||||
|  |   return axios.get<ResponseObject<string>>(`/api/user/${id}/name`); | ||||||
|  | } | ||||||
|  |  | ||||||
| export function patchUser(userPatch: UserPatch) { | export function patchUser(userPatch: UserPatch) { | ||||||
|   return axios.patch<ResponseObject<User>>("/api/user/me", userPatch); |   return axios.patch<ResponseObject<User>>("/api/user/me", userPatch); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getMemoList() { | export function getMemoList(userId?: number) { | ||||||
|   return axios.get<ResponseObject<Memo[]>>("/api/memo"); |   return axios.get<ResponseObject<Memo[]>>(`/api/memo${userId ? "?userID=" + userId : ""}`); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getArchivedMemoList() { | export function getArchivedMemoList(userId?: number) { | ||||||
|   return axios.get<ResponseObject<Memo[]>>("/api/memo?rowStatus=ARCHIVED"); |   return axios.get<ResponseObject<Memo[]>>(`/api/memo?rowStatus=ARCHIVED${userId ? "&userID=" + userId : ""}`); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function createMemo(memoCreate: MemoCreate) { | export function createMemo(memoCreate: MemoCreate) { | ||||||
| @@ -80,8 +84,8 @@ export function deleteMemo(memoId: MemoId) { | |||||||
|   return axios.delete(`/api/memo/${memoId}`); |   return axios.delete(`/api/memo/${memoId}`); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getShortcutList() { | export function getShortcutList(userId?: number) { | ||||||
|   return axios.get<ResponseObject<Shortcut[]>>("/api/shortcut"); |   return axios.get<ResponseObject<Shortcut[]>>(`/api/shortcut${userId ? "?userID=" + userId : ""}`); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function createShortcut(shortcutCreate: ShortcutCreate) { | export function createShortcut(shortcutCreate: ShortcutCreate) { | ||||||
| @@ -100,6 +104,6 @@ export function uploadFile(formData: FormData) { | |||||||
|   return axios.post<ResponseObject<Resource>>("/api/resource", formData); |   return axios.post<ResponseObject<Resource>>("/api/resource", formData); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function getTagList() { | export function getTagList(userId?: number) { | ||||||
|   return axios.get<ResponseObject<string[]>>("/api/tag"); |   return axios.get<ResponseObject<string[]>>(`/api/tag${userId ? "?userID=" + userId : ""}`); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -12,6 +12,9 @@ function Home() { | |||||||
|   const loadingState = useLoading(); |   const loadingState = useLoading(); | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|  |     if (window.location.pathname !== locationService.getState().pathname) { | ||||||
|  |       locationService.replaceHistory("/"); | ||||||
|  |     } | ||||||
|     const { user } = userService.getState(); |     const { user } = userService.getState(); | ||||||
|     if (!user) { |     if (!user) { | ||||||
|       userService |       userService | ||||||
| @@ -20,11 +23,10 @@ function Home() { | |||||||
|           // do nth |           // do nth | ||||||
|         }) |         }) | ||||||
|         .finally(() => { |         .finally(() => { | ||||||
|           if (userService.getState().user) { |           if (userService.getState().user && locationService.getState().pathname !== "/") { | ||||||
|             loadingState.setFinish(); |             locationService.replaceHistory("/"); | ||||||
|           } else { |  | ||||||
|             locationService.replaceHistory("/signin"); |  | ||||||
|           } |           } | ||||||
|  |           loadingState.setFinish(); | ||||||
|         }); |         }); | ||||||
|     } else { |     } else { | ||||||
|       loadingState.setFinish(); |       loadingState.setFinish(); | ||||||
| @@ -39,7 +41,7 @@ function Home() { | |||||||
|           <main className="memos-wrapper"> |           <main className="memos-wrapper"> | ||||||
|             <div className="memos-editor-wrapper"> |             <div className="memos-editor-wrapper"> | ||||||
|               <MemosHeader /> |               <MemosHeader /> | ||||||
|               <MemoEditor /> |               {userService.isNotVisitor() && <MemoEditor />} | ||||||
|               <MemoFilter /> |               <MemoFilter /> | ||||||
|             </div> |             </div> | ||||||
|             <MemoList /> |             <MemoList /> | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import * as api from "../helpers/api"; | import * as api from "../helpers/api"; | ||||||
| import { createMemo, patchMemo, setMemos, setTags } from "../store/modules/memo"; | import { createMemo, patchMemo, setMemos, setTags } from "../store/modules/memo"; | ||||||
| import store from "../store"; | import store from "../store"; | ||||||
|  | import { getUserIdFromPath } from "./userService"; | ||||||
|  |  | ||||||
| const convertResponseModelMemo = (memo: Memo): Memo => { | const convertResponseModelMemo = (memo: Memo): Memo => { | ||||||
|   return { |   return { | ||||||
| @@ -16,7 +17,7 @@ const memoService = { | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   fetchAllMemos: async () => { |   fetchAllMemos: async () => { | ||||||
|     const { data } = (await api.getMemoList()).data; |     const { data } = (await api.getMemoList(getUserIdFromPath())).data; | ||||||
|     const memos = data.filter((m) => m.rowStatus !== "ARCHIVED").map((m) => convertResponseModelMemo(m)); |     const memos = data.filter((m) => m.rowStatus !== "ARCHIVED").map((m) => convertResponseModelMemo(m)); | ||||||
|     store.dispatch(setMemos(memos)); |     store.dispatch(setMemos(memos)); | ||||||
|  |  | ||||||
| @@ -24,7 +25,7 @@ const memoService = { | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   fetchArchivedMemos: async () => { |   fetchArchivedMemos: async () => { | ||||||
|     const { data } = (await api.getArchivedMemoList()).data; |     const { data } = (await api.getArchivedMemoList(getUserIdFromPath())).data; | ||||||
|     const archivedMemos = data.map((m) => { |     const archivedMemos = data.map((m) => { | ||||||
|       return convertResponseModelMemo(m); |       return convertResponseModelMemo(m); | ||||||
|     }); |     }); | ||||||
| @@ -42,7 +43,7 @@ const memoService = { | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   updateTagsState: async () => { |   updateTagsState: async () => { | ||||||
|     const { data } = (await api.getTagList()).data; |     const { data } = (await api.getTagList(getUserIdFromPath())).data; | ||||||
|     store.dispatch(setTags(data)); |     store.dispatch(setTags(data)); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import * as api from "../helpers/api"; | import * as api from "../helpers/api"; | ||||||
| import store from "../store/"; | import store from "../store/"; | ||||||
| import { createShortcut, deleteShortcut, patchShortcut, setShortcuts } from "../store/modules/shortcut"; | import { createShortcut, deleteShortcut, patchShortcut, setShortcuts } from "../store/modules/shortcut"; | ||||||
|  | import { getUserIdFromPath } from "./userService"; | ||||||
|  |  | ||||||
| const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => { | const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => { | ||||||
|   return { |   return { | ||||||
| @@ -16,7 +17,7 @@ const shortcutService = { | |||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   getMyAllShortcuts: async () => { |   getMyAllShortcuts: async () => { | ||||||
|     const { data } = (await api.getShortcutList()).data; |     const { data } = (await api.getShortcutList(getUserIdFromPath())).data; | ||||||
|     const shortcuts = data.map((s) => convertResponseModelShortcut(s)); |     const shortcuts = data.map((s) => convertResponseModelShortcut(s)); | ||||||
|     store.dispatch(setShortcuts(shortcuts)); |     store.dispatch(setShortcuts(shortcuts)); | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import { locationService } from "."; | ||||||
| import * as api from "../helpers/api"; | import * as api from "../helpers/api"; | ||||||
| import store from "../store"; | import store from "../store"; | ||||||
| import { setUser, patchUser } from "../store/modules/user"; | import { setUser, patchUser } from "../store/modules/user"; | ||||||
| @@ -10,11 +11,20 @@ const convertResponseModelUser = (user: User): User => { | |||||||
|   }; |   }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const getUserIdFromPath = () => { | ||||||
|  |   const path = locationService.getState().pathname.slice(3); | ||||||
|  |   return !isNaN(Number(path)) ? Number(path) : undefined; | ||||||
|  | }; | ||||||
|  |  | ||||||
| const userService = { | const userService = { | ||||||
|   getState: () => { |   getState: () => { | ||||||
|     return store.getState().user; |     return store.getState().user; | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|  |   isNotVisitor: () => { | ||||||
|  |     return store.getState().user.user !== undefined; | ||||||
|  |   }, | ||||||
|  |  | ||||||
|   doSignIn: async () => { |   doSignIn: async () => { | ||||||
|     const { data: user } = (await api.getUser()).data; |     const { data: user } = (await api.getUser()).data; | ||||||
|     if (user) { |     if (user) { | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ interface State { | |||||||
| } | } | ||||||
|  |  | ||||||
| const getValidPathname = (pathname: string): string => { | const getValidPathname = (pathname: string): string => { | ||||||
|   if (["/", "/signin"].includes(pathname)) { |   if (["/", "/signin"].includes(pathname) || pathname.match(/^\/u\/(\d+)/)) { | ||||||
|     return pathname; |     return pathname; | ||||||
|   } else { |   } else { | ||||||
|     return "/"; |     return "/"; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user