refactor: visitor view (#107)

* refactor: update api

* refactor: visitor view

* chore: update seed data
pull/109/head
boojack 3 years ago committed by GitHub
parent 346d219cd5
commit 6f32643d7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,6 +1,6 @@
package api package api
type Login struct { type Signin struct {
Email string `json:"email"` Email string `json:"email"`
Password string `json:"password"` Password string `json:"password"`
} }

@ -13,33 +13,33 @@ import (
) )
func (s *Server) registerAuthRoutes(g *echo.Group) { func (s *Server) registerAuthRoutes(g *echo.Group) {
g.POST("/auth/login", func(c echo.Context) error { g.POST("/auth/signin", func(c echo.Context) error {
login := &api.Login{} signin := &api.Signin{}
if err := json.NewDecoder(c.Request().Body).Decode(login); err != nil { if err := json.NewDecoder(c.Request().Body).Decode(signin); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Malformatted login request").SetInternal(err) return echo.NewHTTPError(http.StatusBadRequest, "Malformatted signin request").SetInternal(err)
} }
userFind := &api.UserFind{ userFind := &api.UserFind{
Email: &login.Email, Email: &signin.Email,
} }
user, err := s.Store.FindUser(userFind) user, err := s.Store.FindUser(userFind)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", login.Email)).SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Failed to find user by email %s", signin.Email)).SetInternal(err)
} }
if user == nil { if user == nil {
return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with email %s", login.Email)) return echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("User not found with email %s", signin.Email))
} else if user.RowStatus == api.Archived { } else if user.RowStatus == api.Archived {
return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", login.Email)) return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("User has been archived with email %s", signin.Email))
} }
// Compare the stored hashed password, with the hashed version of the password that was received. // Compare the stored hashed password, with the hashed version of the password that was received.
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(login.Password)); err != nil { if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(signin.Password)); err != nil {
// If the two passwords don't match, return a 401 status. // If the two passwords don't match, return a 401 status.
return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect password").SetInternal(err) return echo.NewHTTPError(http.StatusUnauthorized, "Incorrect password").SetInternal(err)
} }
if err = setUserSession(c, user); err != nil { if err = setUserSession(c, user); err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set login session").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to set signin session").SetInternal(err)
} }
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8) c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSONCharsetUTF8)

@ -60,31 +60,17 @@ func (s *Server) registerMemoRoutes(g *echo.Group) {
}) })
g.GET("/memo", func(c echo.Context) error { g.GET("/memo", func(c echo.Context) error {
userID, ok := c.Get(getUserIDContextKey()).(int) memoFind := &api.MemoFind{}
if !ok {
if c.QueryParam("userID") != "" { if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
var err error memoFind.CreatorID = &userID
userID, err = strconv.Atoi(c.QueryParam("userID")) } else {
if err != nil { userID, ok := c.Get(getUserIDContextKey()).(int)
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.QueryParam("userID"))) if !ok {
} return echo.NewHTTPError(http.StatusBadRequest, "Missing creatorId to find memo")
} 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.CreatorID = &userID
CreatorID: &userID,
} }
rowStatus := api.RowStatus(c.QueryParam("rowStatus")) rowStatus := api.RowStatus(c.QueryParam("rowStatus"))

@ -45,7 +45,7 @@ func NewServer(profile *profile.Profile) *Server {
HTML5: true, HTML5: true,
})) }))
// In dev mode, set the const secret key to make login session persistence. // In dev mode, set the const secret key to make signin session persistence.
secret := []byte("usememos") secret := []byte("usememos")
if profile.Mode == "prod" { if profile.Mode == "prod" {
secret = securecookie.GenerateRandomKey(16) secret = securecookie.GenerateRandomKey(16)

@ -59,32 +59,19 @@ func (s *Server) registerShortcutRoutes(g *echo.Group) {
}) })
g.GET("/shortcut", func(c echo.Context) error { g.GET("/shortcut", func(c echo.Context) error {
userID, ok := c.Get(getUserIDContextKey()).(int) shortcutFind := &api.ShortcutFind{}
if !ok {
if c.QueryParam("userID") != "" { if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
var err error shortcutFind.CreatorID = &userID
userID, err = strconv.Atoi(c.QueryParam("userID")) } else {
if err != nil { userID, ok := c.Get(getUserIDContextKey()).(int)
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("ID is not a number: %s", c.QueryParam("userID"))) if !ok {
} return echo.NewHTTPError(http.StatusBadRequest, "Missing creatorId to find shortcut")
} 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.CreatorID = &userID
CreatorID: &userID,
} }
list, err := s.Store.FindShortcutList(shortcutFind) list, err := s.Store.FindShortcutList(shortcutFind)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch shortcut list").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to fetch shortcut list").SetInternal(err)

@ -2,7 +2,6 @@ package server
import ( import (
"encoding/json" "encoding/json"
"fmt"
"net/http" "net/http"
"regexp" "regexp"
"sort" "sort"
@ -15,37 +14,24 @@ 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, 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{
CreatorID: &userID,
ContentSearch: &contentSearch, ContentSearch: &contentSearch,
RowStatus: &normalRowStatus, RowStatus: &normalRowStatus,
} }
if userID, err := strconv.Atoi(c.QueryParam("creatorId")); err == nil {
memoFind.CreatorID = &userID
} else {
userID, ok := c.Get(getUserIDContextKey()).(int)
if !ok {
return echo.NewHTTPError(http.StatusBadRequest, "Missing creatorId to find shortcut")
}
memoFind.CreatorID = &userID
}
memoList, err := s.Store.FindMemoList(&memoFind) memoList, err := s.Store.FindMemoList(&memoFind)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err)

@ -52,6 +52,24 @@ INSERT INTO
VALUES VALUES
( (
104, 104,
'好好学习,天天向上。🤜🤛', '#TODO
- [x] Take more photos about **🌄 sunset**;
- [ ] Clean the classroom;
- [ ] Watch *👦 The Boys*;
(👆 click to toggle status)
',
102
);
INSERT INTO
memo (
`id`,
`content`,
`creator_id`
)
VALUES
(
105,
'三人行,必有我师焉!👨‍🏫',
102 102
); );

@ -11,12 +11,14 @@
"@reduxjs/toolkit": "^1.8.1", "@reduxjs/toolkit": "^1.8.1",
"axios": "^0.27.2", "axios": "^0.27.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"qs": "^6.11.0",
"react": "^18.1.0", "react": "^18.1.0",
"react-dom": "^18.1.0", "react-dom": "^18.1.0",
"react-redux": "^8.0.1" "react-redux": "^8.0.1"
}, },
"devDependencies": { "devDependencies": {
"@types/lodash-es": "^4.17.5", "@types/lodash-es": "^4.17.5",
"@types/qs": "^6.9.7",
"@types/react": "^18.0.9", "@types/react": "^18.0.9",
"@types/react-dom": "^18.0.4", "@types/react-dom": "^18.0.4",
"@typescript-eslint/eslint-plugin": "^5.6.0", "@typescript-eslint/eslint-plugin": "^5.6.0",

@ -112,7 +112,11 @@ const Memo: React.FC<Props> = (props: Props) => {
} else { } else {
locationService.setTagQuery(tagName); locationService.setTagQuery(tagName);
} }
} else if (targetEl.classList.contains("todo-block") && userService.isNotVisitor()) { } else if (targetEl.classList.contains("todo-block")) {
if (userService.isVisitorMode()) {
return;
}
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,40 +162,38 @@ 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 ${userService.isVisitorMode() ? "!hidden" : ""}`}>
<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" /> </span>
</span> <div className="more-action-btns-wrapper">
<div className="more-action-btns-wrapper"> <div className="more-action-btns-container">
<div className="more-action-btns-container"> <div className="btns-container">
<div className="btns-container"> <div className="btn" onClick={handleTogglePinMemoBtnClick}>
<div className="btn" onClick={handleTogglePinMemoBtnClick}> <img className="icon-img" src="/icons/pin.svg" alt="" />
<img className="icon-img" src="/icons/pin.svg" alt="" /> <span className="tip-text">{memo.pinned ? "Unpin" : "Pin"}</span>
<span className="tip-text">{memo.pinned ? "Unpin" : "Pin"}</span> </div>
</div> <div className="btn" onClick={handleEditMemoClick}>
<div className="btn" onClick={handleEditMemoClick}> <img className="icon-img" src="/icons/edit.svg" alt="" />
<img className="icon-img" src="/icons/edit.svg" alt="" /> <span className="tip-text">Edit</span>
<span className="tip-text">Edit</span> </div>
</div> <div className="btn" onClick={handleGenMemoImageBtnClick}>
<div className="btn" onClick={handleGenMemoImageBtnClick}> <img className="icon-img" src="/icons/share.svg" alt="" />
<img className="icon-img" src="/icons/share.svg" alt="" /> <span className="tip-text">Share</span>
<span className="tip-text">Share</span>
</div>
</div> </div>
<span className="btn" onClick={handleMarkMemoClick}>
Mark
</span>
<span className="btn" onClick={handleShowMemoStoryDialog}>
View Story
</span>
<span className="btn archive-btn" onClick={handleArchiveMemoClick}>
Archive
</span>
</div> </div>
<span className="btn" onClick={handleMarkMemoClick}>
Mark
</span>
<span className="btn" onClick={handleShowMemoStoryDialog}>
View Story
</span>
<span className="btn archive-btn" onClick={handleArchiveMemoClick}>
Archive
</span>
</div> </div>
</div> </div>
)} </div>
</div> </div>
<div <div
ref={memoContainerRef} ref={memoContainerRef}

@ -67,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={userService.isNotVisitor() ? handleSignOutBtnClick : handleSignInBtnClick}> <button className="btn action-btn" onClick={!userService.isVisitorMode() ? handleSignOutBtnClick : handleSignInBtnClick}>
<span className="icon">👋</span> {userService.isNotVisitor() ? "Sign out" : "Sign in"} <span className="icon">👋</span> {!userService.isVisitorMode() ? "Sign out" : "Sign in"}
</button> </button>
</div> </div>
); );

@ -4,6 +4,7 @@ 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";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
import Only from "./common/OnlyWhen";
import toastHelper from "./Toast"; import toastHelper from "./Toast";
import showCreateShortcutDialog from "./CreateShortcutDialog"; import showCreateShortcutDialog from "./CreateShortcutDialog";
import "../less/shortcut-list.less"; import "../less/shortcut-list.less";
@ -38,11 +39,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() && ( <Only when={!userService.isVisitorMode()}>
<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>
)} </Only>
</p> </p>
<div className="shortcuts-container"> <div className="shortcuts-container">
{sortedShortcuts.map((s) => { {sortedShortcuts.map((s) => {
@ -66,9 +67,6 @@ const ShortcutContainer: React.FC<ShortcutContainerProps> = (props: ShortcutCont
if (isActive) { if (isActive) {
locationService.setMemoShortcut(undefined); locationService.setMemoShortcut(undefined);
} else { } else {
if (!["/"].includes(locationService.getState().pathname)) {
locationService.setPathname("/");
}
locationService.setMemoShortcut(shortcut.id); locationService.setMemoShortcut(shortcut.id);
} }
}; };
@ -116,30 +114,28 @@ 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 ${userService.isVisitorMode() ? "!hidden" : ""}`}>
<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" /> </span>
</span> <div className="action-btns-wrapper">
<div className="action-btns-wrapper"> <div className="action-btns-container">
<div className="action-btns-container"> <span className="btn" onClick={handlePinShortcutBtnClick}>
<span className="btn" onClick={handlePinShortcutBtnClick}> {shortcut.rowStatus === "ARCHIVED" ? "Unpin" : "Pin"}
{shortcut.rowStatus === "ARCHIVED" ? "Unpin" : "Pin"} </span>
</span> <span className="btn" onClick={handleEditShortcutBtnClick}>
<span className="btn" onClick={handleEditShortcutBtnClick}> Edit
Edit </span>
</span> <span
<span className={`btn delete-btn ${showConfirmDeleteBtn ? "final-confirm" : ""}`}
className={`btn delete-btn ${showConfirmDeleteBtn ? "final-confirm" : ""}`} onClick={handleDeleteMemoClick}
onClick={handleDeleteMemoClick} onMouseLeave={handleDeleteBtnMouseLeave}
onMouseLeave={handleDeleteBtnMouseLeave} >
> {showConfirmDeleteBtn ? "Delete!" : "Delete"}
{showConfirmDeleteBtn ? "Delete!" : "Delete"} </span>
</span>
</div>
</div> </div>
</div> </div>
)} </div>
</div> </div>
</> </>
); );

@ -1,6 +1,7 @@
import { useAppSelector } from "../store"; import { useAppSelector } from "../store";
import * as utils from "../helpers/utils"; import * as utils from "../helpers/utils";
import { userService } from "../services"; import { userService } from "../services";
import Only from "./common/OnlyWhen";
import showDailyReviewDialog from "./DailyReviewDialog"; import showDailyReviewDialog from "./DailyReviewDialog";
import showSettingDialog from "./SettingDialog"; import showSettingDialog from "./SettingDialog";
import showArchivedMemoDialog from "./ArchivedMemoDialog"; import showArchivedMemoDialog from "./ArchivedMemoDialog";
@ -49,21 +50,19 @@ const Sidebar: React.FC<Props> = () => {
</div> </div>
</div> </div>
<UsageHeatMap /> <UsageHeatMap />
{userService.isNotVisitor() && ( <Only when={!userService.isVisitorMode()}>
<> <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 </button>
</button> <button className="btn action-btn" onClick={handleMyAccountBtnClick}>
<button className="btn action-btn" onClick={handleMyAccountBtnClick}> <span className="icon"></span> Setting
<span className="icon"></span> Setting </button>
</button> <button className="btn action-btn" onClick={handleArchivedBtnClick}>
<button className="btn action-btn" onClick={handleArchivedBtnClick}> <span className="icon">🗂</span> Archived
<span className="icon">🗂</span> Archived </button>
</button> </div>
</div> </Only>
</>
)}
<ShortcutList /> <ShortcutList />
<TagList /> <TagList />
</aside> </aside>

@ -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={userService.isNotVisitor() && tags.length < 5}> <Only when={!userService.isVisitorMode() && 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>
@ -97,9 +97,6 @@ const TagItemContainer: React.FC<TagItemContainerProps> = (props: TagItemContain
locationService.setTagQuery(undefined); locationService.setTagQuery(undefined);
} else { } else {
utils.copyTextToClipboard(`#${tag.text} `); utils.copyTextToClipboard(`#${tag.text} `);
if (!["/"].includes(locationService.getState().pathname)) {
locationService.setPathname("/");
}
locationService.setTagQuery(tag.text); locationService.setTagQuery(tag.text);
} }
}; };

@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import * as api from "../helpers/api"; import * as api from "../helpers/api";
import { getUserIdFromPath } from "../services/userService"; import userService from "../services/userService";
import { locationService } from "../services"; import { locationService } from "../services";
import { useAppSelector } from "../store"; import { useAppSelector } from "../store";
import toastHelper from "./Toast"; import toastHelper from "./Toast";
@ -31,15 +31,18 @@ const UserBanner: React.FC<Props> = () => {
setUsername(status.owner.name); setUsername(status.owner.name);
}); });
} else { } else {
api const currentUserId = userService.getCurrentUserId();
.getUserNameById(Number(getUserIdFromPath())) if (currentUserId) {
.then(({ data }) => { api
const { data: username } = data; .getUserNameById(currentUserId)
setUsername(username); .then(({ data }) => {
}) const { data: username } = data;
.catch(() => { setUsername(username);
toastHelper.error("User not found"); })
}); .catch(() => {
toastHelper.error("User not found");
});
}
} }
} }
}, []); }, []);

@ -12,8 +12,8 @@ export function getSystemStatus() {
return axios.get<ResponseObject<SystemStatus>>("/api/status"); return axios.get<ResponseObject<SystemStatus>>("/api/status");
} }
export function login(email: string, password: string) { export function signin(email: string, password: string) {
return axios.post<ResponseObject<User>>("/api/auth/login", { return axios.post<ResponseObject<User>>("/api/auth/signin", {
email, email,
password, password,
}); });
@ -52,12 +52,15 @@ 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(userId?: number) { export function getMemoList(memoFind?: MemoFind) {
return axios.get<ResponseObject<Memo[]>>(`/api/memo${userId ? "?userID=" + userId : ""}`); const queryList = [];
} if (memoFind?.creatorId) {
queryList.push(`creatorId=${memoFind.creatorId}`);
export function getArchivedMemoList(userId?: number) { }
return axios.get<ResponseObject<Memo[]>>(`/api/memo?rowStatus=ARCHIVED${userId ? "&userID=" + userId : ""}`); if (memoFind?.rowStatus) {
queryList.push(`rowStatus=${memoFind.rowStatus}`);
}
return axios.get<ResponseObject<Memo[]>>(`/api/memo?${queryList.join("&")}`);
} }
export function createMemo(memoCreate: MemoCreate) { export function createMemo(memoCreate: MemoCreate) {
@ -84,8 +87,12 @@ export function deleteMemo(memoId: MemoId) {
return axios.delete(`/api/memo/${memoId}`); return axios.delete(`/api/memo/${memoId}`);
} }
export function getShortcutList(userId?: number) { export function getShortcutList(shortcutFind: ShortcutFind) {
return axios.get<ResponseObject<Shortcut[]>>(`/api/shortcut${userId ? "?userID=" + userId : ""}`); const queryList = [];
if (shortcutFind?.creatorId) {
queryList.push(`creatorId=${shortcutFind.creatorId}`);
}
return axios.get<ResponseObject<Shortcut[]>>(`/api/shortcut?${queryList.join("&")}`);
} }
export function createShortcut(shortcutCreate: ShortcutCreate) { export function createShortcut(shortcutCreate: ShortcutCreate) {
@ -104,6 +111,10 @@ export function uploadFile(formData: FormData) {
return axios.post<ResponseObject<Resource>>("/api/resource", formData); return axios.post<ResponseObject<Resource>>("/api/resource", formData);
} }
export function getTagList(userId?: number) { export function getTagList(tagFind?: TagFind) {
return axios.get<ResponseObject<string[]>>(`/api/tag${userId ? "?userID=" + userId : ""}`); const queryList = [];
if (tagFind?.creatorId) {
queryList.push(`creatorId=${tagFind.creatorId}`);
}
return axios.get<ResponseObject<string[]>>(`/api/tag?${queryList.join("&")}`);
} }

@ -126,38 +126,6 @@ export function throttle(fn: FunctionType, delay: number) {
}; };
} }
export function transformObjectToParamsString(object: KVObject): string {
const params = [];
const keys = Object.keys(object).sort();
for (const key of keys) {
const val = object[key];
if (val) {
if (typeof val === "object") {
params.push(...transformObjectToParamsString(val).split("&"));
} else {
params.push(`${key}=${val}`);
}
}
}
return params.join("&");
}
export function transformParamsStringToObject(paramsString: string): KVObject {
const object: KVObject = {};
const params = paramsString.split("&");
for (const p of params) {
const [key, val] = p.split("=");
if (key && val) {
object[key] = val;
}
}
return object;
}
export function filterObjectNullKeys(object: KVObject): KVObject { export function filterObjectNullKeys(object: KVObject): KVObject {
if (!object) { if (!object) {
return {}; return {};

@ -1,7 +1,8 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { locationService, userService } from "../services"; import { userService } from "../services";
import Sidebar from "../components/Sidebar";
import useLoading from "../hooks/useLoading"; import useLoading from "../hooks/useLoading";
import Only from "../components/common/OnlyWhen";
import Sidebar from "../components/Sidebar";
import MemosHeader from "../components/MemosHeader"; import MemosHeader from "../components/MemosHeader";
import MemoEditor from "../components/MemoEditor"; import MemoEditor from "../components/MemoEditor";
import MemoFilter from "../components/MemoFilter"; import MemoFilter from "../components/MemoFilter";
@ -12,25 +13,14 @@ function Home() {
const loadingState = useLoading(); const loadingState = useLoading();
useEffect(() => { useEffect(() => {
if (window.location.pathname !== locationService.getState().pathname) { userService
locationService.replaceHistory("/"); .doSignIn()
} .catch(() => {
const { user } = userService.getState(); // do nth
if (!user) { })
userService .finally(() => {
.doSignIn() loadingState.setFinish();
.catch(() => { });
// do nth
})
.finally(() => {
if (userService.getState().user && locationService.getState().pathname !== "/") {
locationService.replaceHistory("/");
}
loadingState.setFinish();
});
} else {
loadingState.setFinish();
}
}, []); }, []);
return ( return (
@ -41,7 +31,9 @@ function Home() {
<main className="memos-wrapper"> <main className="memos-wrapper">
<div className="memos-editor-wrapper"> <div className="memos-editor-wrapper">
<MemosHeader /> <MemosHeader />
{userService.isNotVisitor() && <MemoEditor />} <Only when={!userService.isVisitorMode()}>
<MemoEditor />
</Only>
<MemoFilter /> <MemoFilter />
</div> </div>
<MemoList /> <MemoList />

@ -63,7 +63,7 @@ const Signin: React.FC<Props> = () => {
try { try {
actionBtnLoadingState.setLoading(); actionBtnLoadingState.setLoading();
await api.login(email, password); await api.signin(email, password);
const user = await userService.doSignIn(); const user = await userService.doSignIn();
if (user) { if (user) {
locationService.replaceHistory("/"); locationService.replaceHistory("/");

@ -1,10 +1,10 @@
import * as utils from "../helpers/utils"; import { stringify } from "qs";
import store from "../store"; import store from "../store";
import { setQuery, setPathname, Query } from "../store/modules/location"; import { setQuery, setPathname, Query } from "../store/modules/location";
const updateLocationUrl = (method: "replace" | "push" = "replace") => { const updateLocationUrl = (method: "replace" | "push" = "replace") => {
const { query, pathname, hash } = store.getState().location; const { query, pathname, hash } = store.getState().location;
let queryString = utils.transformObjectToParamsString(query ?? {}); let queryString = stringify(query);
if (queryString) { if (queryString) {
queryString = "?" + queryString; queryString = "?" + queryString;
} else { } else {

@ -1,7 +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"; import userService from "./userService";
const convertResponseModelMemo = (memo: Memo): Memo => { const convertResponseModelMemo = (memo: Memo): Memo => {
return { return {
@ -17,7 +17,10 @@ const memoService = {
}, },
fetchAllMemos: async () => { fetchAllMemos: async () => {
const { data } = (await api.getMemoList(getUserIdFromPath())).data; const memoFind: MemoFind = {
creatorId: userService.getCurrentUserId(),
};
const { data } = (await api.getMemoList(memoFind)).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));
@ -25,7 +28,11 @@ const memoService = {
}, },
fetchArchivedMemos: async () => { fetchArchivedMemos: async () => {
const { data } = (await api.getArchivedMemoList(getUserIdFromPath())).data; const memoFind: MemoFind = {
creatorId: userService.getCurrentUserId(),
rowStatus: "ARCHIVED",
};
const { data } = (await api.getMemoList(memoFind)).data;
const archivedMemos = data.map((m) => { const archivedMemos = data.map((m) => {
return convertResponseModelMemo(m); return convertResponseModelMemo(m);
}); });
@ -43,7 +50,10 @@ const memoService = {
}, },
updateTagsState: async () => { updateTagsState: async () => {
const { data } = (await api.getTagList(getUserIdFromPath())).data; const tagFind: TagFind = {
creatorId: userService.getCurrentUserId(),
};
const { data } = (await api.getTagList(tagFind)).data;
store.dispatch(setTags(data)); store.dispatch(setTags(data));
}, },

@ -1,7 +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"; import userService from "./userService";
const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => { const convertResponseModelShortcut = (shortcut: Shortcut): Shortcut => {
return { return {
@ -17,7 +17,10 @@ const shortcutService = {
}, },
getMyAllShortcuts: async () => { getMyAllShortcuts: async () => {
const { data } = (await api.getShortcutList(getUserIdFromPath())).data; const shortcutFind: ShortcutFind = {
creatorId: userService.getCurrentUserId(),
};
const { data } = (await api.getShortcutList(shortcutFind)).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 { isUndefined } from "lodash-es";
import { locationService } from "."; import { locationService } from ".";
import * as api from "../helpers/api"; import * as api from "../helpers/api";
import store from "../store"; import store from "../store";
@ -11,18 +12,26 @@ 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: () => { isVisitorMode: () => {
return store.getState().user.user !== undefined; return !isUndefined(userService.getUserIdFromPath());
},
getCurrentUserId: () => {
return userService.getUserIdFromPath() ?? store.getState().user.user?.id;
},
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 () => { doSignIn: async () => {

@ -20,7 +20,8 @@ interface State {
} }
const getValidPathname = (pathname: string): string => { const getValidPathname = (pathname: string): string => {
if (["/", "/signin"].includes(pathname) || pathname.match(/^\/u\/(\d+)/)) { const userPageUrlRegex = /^\/u\/\d+.*/;
if (["/", "/signin"].includes(pathname) || userPageUrlRegex.test(pathname)) {
return pathname; return pathname;
} else { } else {
return "/"; return "/";

@ -22,3 +22,8 @@ interface MemoPatch {
content?: string; content?: string;
rowStatus?: RowStatus; rowStatus?: RowStatus;
} }
interface MemoFind {
creatorId?: UserId;
rowStatus?: RowStatus;
}

@ -3,6 +3,7 @@ type ShortcutId = number;
interface Shortcut { interface Shortcut {
id: ShortcutId; id: ShortcutId;
creatorId: UserId;
rowStatus: RowStatus; rowStatus: RowStatus;
createdTs: TimeStamp; createdTs: TimeStamp;
updatedTs: TimeStamp; updatedTs: TimeStamp;
@ -22,3 +23,7 @@ interface ShortcutPatch {
payload?: string; payload?: string;
rowStatus?: RowStatus; rowStatus?: RowStatus;
} }
interface ShortcutFind {
creatorId?: UserId;
}

@ -0,0 +1,3 @@
interface TagFind {
creatorId?: UserId;
}

@ -358,6 +358,11 @@
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
"@types/qs@^6.9.7":
version "6.9.7"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb"
integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==
"@types/react-dom@^18.0.4": "@types/react-dom@^18.0.4":
version "18.0.4" version "18.0.4"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.4.tgz#dcbcadb277bcf6c411ceff70069424c57797d375" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.4.tgz#dcbcadb277bcf6c411ceff70069424c57797d375"
@ -1988,6 +1993,13 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
qs@^6.11.0:
version "6.11.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
dependencies:
side-channel "^1.0.4"
queue-microtask@^1.2.2: queue-microtask@^1.2.2:
version "1.2.3" version "1.2.3"
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"

Loading…
Cancel
Save