chore: add user profile page (#2175)

chore: some enhancements
pull/2177/head
boojack 2 years ago committed by GitHub
parent 8c312e647d
commit 4af0d03e93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -169,6 +169,24 @@ func (s *APIV1Service) SignInSSO(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, "Incorrect login credentials, please try again")
}
if user == nil {
allowSignUpSetting, err := s.Store.GetSystemSetting(ctx, &store.FindSystemSetting{
Name: SystemSettingAllowSignUpName.String(),
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find system setting").SetInternal(err)
}
allowSignUpSettingValue := false
if allowSignUpSetting != nil {
err = json.Unmarshal([]byte(allowSignUpSetting.Value), &allowSignUpSettingValue)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting allow signup").SetInternal(err)
}
}
if !allowSignUpSettingValue {
return echo.NewHTTPError(http.StatusUnauthorized, "signup is disabled").SetInternal(err)
}
userCreate := &store.User{
Username: userInfo.Identifier,
// The new signup user should be normal user by default.

@ -656,7 +656,7 @@ func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resourc
return fmt.Errorf("Failed to find SystemSettingStorageServiceIDName: %s", err)
}
storageServiceID := DatabaseStorage
storageServiceID := LocalStorage
if systemSettingStorageServiceID != nil {
err = json.Unmarshal([]byte(systemSettingStorageServiceID.Value), &storageServiceID)
if err != nil {
@ -672,15 +672,13 @@ func SaveResourceBlob(ctx context.Context, s *store.Store, create *store.Resourc
}
create.Blob = fileBytes
return nil
}
// `LocalStorage` means save blob into local disk
if storageServiceID == LocalStorage {
} else if storageServiceID == LocalStorage {
// `LocalStorage` means save blob into local disk
systemSettingLocalStoragePath, err := s.GetSystemSetting(ctx, &store.FindSystemSetting{Name: SystemSettingLocalStoragePathName.String()})
if err != nil {
return fmt.Errorf("Failed to find SystemSettingLocalStoragePathName: %s", err)
}
localStoragePath := "assets/{filename}"
localStoragePath := "assets/{timestamp}_{filename}"
if systemSettingLocalStoragePath != nil && systemSettingLocalStoragePath.Value != "" {
err = json.Unmarshal([]byte(systemSettingLocalStoragePath.Value), &localStoragePath)
if err != nil {

@ -13,6 +13,7 @@ import (
const (
// LocalStorage means the storage service is local file system.
// Default storage service is local file system.
LocalStorage int32 = -1
// DatabaseStorage means the storage service is database.
DatabaseStorage int32 = 0
@ -214,7 +215,7 @@ func (s *APIV1Service) DeleteStorage(c echo.Context) error {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
}
if systemSetting != nil {
storageServiceID := DatabaseStorage
storageServiceID := LocalStorage
err = json.Unmarshal([]byte(systemSetting.Value), &storageServiceID)
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal storage service id").SetInternal(err)

@ -89,7 +89,7 @@ func (s *APIV1Service) GetSystemStatus(c echo.Context) error {
Appearance: "system",
ExternalURL: "",
},
StorageServiceID: DatabaseStorage,
StorageServiceID: LocalStorage,
LocalStoragePath: "assets/{timestamp}_{filename}",
MemoDisplayWithUpdatedTs: false,
}

@ -2,7 +2,7 @@ INSERT INTO
memo (`id`, `content`, `creator_id`)
VALUES
(
1001,
1,
"#Hello 👋 Welcome to memos.",
101
);
@ -16,7 +16,7 @@ INSERT INTO
)
VALUES
(
1002,
2,
'#TODO
- [x] Take more photos about **🌄 sunset**;
- [x] Clean the room;
@ -35,7 +35,7 @@ INSERT INTO
)
VALUES
(
1003,
3,
"**[Slash](https://github.com/boojack/slash)**: A bookmarking and url shortener, save and share your links very easily.
![](https://github.com/boojack/slash/raw/main/resources/demo.gif)
@ -54,7 +54,7 @@ INSERT INTO
)
VALUES
(
1004,
4,
'#TODO
- [x] Take more photos about **🌄 sunset**;
- [ ] Clean the classroom;
@ -74,7 +74,7 @@ INSERT INTO
)
VALUES
(
1005,
5,
'三人行,必有我师焉!👨‍🏫',
102,
'PUBLIC'

@ -1,9 +1,9 @@
INSERT INTO
memo_organizer (`memo_id`, `user_id`, `pinned`)
VALUES
(1001, 101, 1);
(1, 101, 1);
INSERT INTO
memo_organizer (`memo_id`, `user_id`, `pinned`)
VALUES
(1003, 101, 1);
(3, 101, 1);

@ -0,0 +1,27 @@
import { Dropdown, IconButton, Menu, MenuButton, MenuItem } from "@mui/joy";
import { useNavigate } from "react-router-dom";
import Icon from "./Icon";
const FloatingNavButton = () => {
const navigate = useNavigate();
return (
<>
<Dropdown>
<div className="fixed bottom-6 right-6">
<MenuButton
slots={{ root: IconButton }}
slotProps={{ root: { className: "!bg-white dark:!bg-zinc-900 drop-shadow", variant: "outlined", color: "neutral" } }}
>
<Icon.MoreVertical className="w-5 h-auto" />
</MenuButton>
</div>
<Menu placement="top-end">
<MenuItem onClick={() => navigate("/")}>Back to home</MenuItem>
</Menu>
</Dropdown>
</>
);
};
export default FloatingNavButton;

@ -1,5 +1,4 @@
import { Divider } from "@mui/joy";
import { isEqual, uniqWith } from "lodash-es";
import { memo, useEffect, useRef, useState } from "react";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
@ -7,7 +6,7 @@ import { Link } from "react-router-dom";
import { UNKNOWN_ID } from "@/helpers/consts";
import { getRelativeTimeString } from "@/helpers/datetime";
import { useFilterStore, useMemoStore, useUserStore } from "@/store/module";
import { useMemoCacheStore, useUserV1Store } from "@/store/v1";
import { useUserV1Store } from "@/store/v1";
import { useTranslate } from "@/utils/i18n";
import showChangeMemoCreatedTsDialog from "./ChangeMemoCreatedTsDialog";
import { showCommonDialog } from "./Dialog/CommonDialog";
@ -23,24 +22,20 @@ import "@/less/memo.less";
interface Props {
memo: Memo;
showCreator?: boolean;
showVisibility?: boolean;
showRelatedMemos?: boolean;
lazyRendering?: boolean;
}
const Memo: React.FC<Props> = (props: Props) => {
const { memo, showCreator, showRelatedMemos, lazyRendering } = props;
const { memo, lazyRendering } = props;
const { i18n } = useTranslation();
const t = useTranslate();
const filterStore = useFilterStore();
const userStore = useUserStore();
const memoStore = useMemoStore();
const memoCacheStore = useMemoCacheStore();
const userV1Store = useUserV1Store();
const [shouldRender, setShouldRender] = useState<boolean>(lazyRendering ? false : true);
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getRelativeTimeString(memo.displayTs));
const [relatedMemoList, setRelatedMemoList] = useState<Memo[]>([]);
const [displayTime, setDisplayTime] = useState<string>(getRelativeTimeString(memo.displayTs));
const memoContainerRef = useRef<HTMLDivElement>(null);
const readonly = userStore.isVisitorMode() || userStore.getCurrentUsername() !== memo.creatorUsername;
const creator = userV1Store.getUserByUsername(memo.creatorUsername);
@ -50,27 +45,12 @@ const Memo: React.FC<Props> = (props: Props) => {
userV1Store.getOrFetchUserByUsername(memo.creatorUsername);
}, [memo.creatorUsername]);
// Prepare related memos.
useEffect(() => {
Promise.allSettled(memo.relationList.map((memoRelation) => memoCacheStore.getOrFetchMemoById(memoRelation.relatedMemoId))).then(
(results) => {
const memoList = [];
for (const result of results) {
if (result.status === "fulfilled") {
memoList.push(result.value);
}
}
setRelatedMemoList(uniqWith(memoList, isEqual));
}
);
}, [memo.relationList]);
// Update display time string.
useEffect(() => {
let intervalFlag: any = -1;
if (Date.now() - memo.displayTs < 1000 * 60 * 60 * 24) {
intervalFlag = setInterval(() => {
setCreatedTimeStr(getRelativeTimeString(memo.displayTs));
setDisplayTime(getRelativeTimeString(memo.displayTs));
}, 1000 * 1);
}
@ -246,26 +226,25 @@ const Memo: React.FC<Props> = (props: Props) => {
<>
<div className={`memo-wrapper ${"memos-" + memo.id} ${memo.pinned && !readonly ? "pinned" : ""}`} ref={memoContainerRef}>
<div className="memo-top-wrapper">
<div className="status-text-container">
{showCreator && creator && (
<p className="w-full max-w-[calc(100%-20px)] flex flex-row justify-start items-center mr-1">
{creator && (
<>
<Link className="flex flex-row justify-start items-center" to={`/u/${memo.creatorUsername}`}>
<UserAvatar className="!w-5 !h-auto mr-1" avatarUrl={creator.avatarUrl} />
<span className="text-sm text-gray-600 dark:text-zinc-300">{creator.nickname}</span>
<span className="text-sm text-gray-600 max-w-[8em] truncate dark:text-zinc-300">{creator.nickname}</span>
</Link>
<Icon.Dot className="w-4 h-auto text-gray-400 dark:text-zinc-400" />
</>
)}
<Link className="time-text" to={`/m/${memo.id}`} onClick={handleMemoCreatedTimeClick}>
{createdTimeStr}
</Link>
</div>
<span className="text-sm text-gray-400" onClick={handleMemoCreatedTimeClick}>
{displayTime}
</span>
</p>
<div className="btns-container space-x-2">
{memo.pinned && <Icon.Bookmark className="w-4 h-auto rounded text-green-600" />}
{!readonly && (
<>
<span className="btn more-action-btn">
<Icon.MoreHorizontal className="icon-img" />
<Icon.MoreVertical className="icon-img" />
</span>
<div className="more-action-btns-wrapper">
<div className="more-action-btns-container min-w-[6em]">
@ -306,24 +285,8 @@ const Memo: React.FC<Props> = (props: Props) => {
onMemoContentDoubleClick={handleMemoContentDoubleClick}
/>
<MemoResourceListView resourceList={memo.resourceList} />
{!showRelatedMemos && <MemoRelationListView relationList={memo.relationList} />}
<MemoRelationListView relationList={memo.relationList} />
</div>
{showRelatedMemos && relatedMemoList.length > 0 && (
<>
<p className="text-sm dark:text-gray-300 my-2 pl-4 opacity-50 flex flex-row items-center">
<Icon.Link className="w-4 h-auto mr-1" />
<span>Related memos</span>
</p>
{relatedMemoList.map((relatedMemo) => {
return (
<div key={relatedMemo.id} className="w-full">
<Memo memo={relatedMemo} showCreator />
</div>
);
})}
</>
)}
</>
);
};

@ -9,12 +9,7 @@ import Empty from "./Empty";
import Memo from "./Memo";
import "@/less/memo-list.less";
interface Props {
showCreator?: boolean;
}
const MemoList: React.FC<Props> = (props: Props) => {
const { showCreator } = props;
const MemoList: React.FC = () => {
const t = useTranslate();
const memoStore = useMemoStore();
const userStore = useUserStore();
@ -142,7 +137,7 @@ const MemoList: React.FC<Props> = (props: Props) => {
return (
<div className="memo-list-container">
{sortedMemos.map((memo) => (
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} lazyRendering showVisibility showCreator={showCreator} />
<Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} lazyRendering showVisibility />
))}
{isFetching ? (
<div className="status-text-container fetching-tip">

@ -156,7 +156,10 @@ const PreferencesSection = () => {
{userList.map((user) => (
<tr key={user.id}>
<td className="whitespace-nowrap py-2 pl-4 pr-3 text-sm text-gray-900">{user.id}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.username}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">
{user.username}
<span className="ml-1 italic">{user.rowStatus === "ARCHIVED" && "(Archived)"}</span>
</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.nickname}</td>
<td className="whitespace-nowrap px-3 py-2 text-sm text-gray-500">{user.email}</td>
<td className="relative whitespace-nowrap py-2 pl-3 pr-4 text-right text-sm font-medium flex justify-end">

@ -8,7 +8,7 @@ interface Props {
const UserAvatar = (props: Props) => {
const { avatarUrl, className } = props;
return (
<div className={classNames(`w-8 h-8 overflow-clip`, className)}>
<div className={classNames(`w-8 h-auto overflow-clip rounded-full`, className)}>
<img className="w-full h-auto rounded-full min-w-full min-h-full object-cover" src={avatarUrl || "/logo.webp"} alt="" />
</div>
);

@ -130,29 +130,22 @@ export const getRelativeTimeString = (time: number, locale = i18n.language, form
// numeric: "auto" provides "yesterday" for 1 day ago, "always" provides "1 day ago"
const formatOpts = { style: formatStyle, numeric: "auto" } as Intl.RelativeTimeFormatOptions;
const relTime = new Intl.RelativeTimeFormat(locale, formatOpts);
if (pastTimeMillis < minMillis) {
return relTime.format(-Math.round(pastTimeMillis / secMillis), "second");
}
if (pastTimeMillis < hourMillis) {
return relTime.format(-Math.round(pastTimeMillis / minMillis), "minute");
}
if (pastTimeMillis < dayMillis) {
return relTime.format(-Math.round(pastTimeMillis / hourMillis), "hour");
}
if (pastTimeMillis < dayMillis * 7) {
return relTime.format(-Math.round(pastTimeMillis / dayMillis), "day");
}
if (pastTimeMillis < dayMillis * 30) {
return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 7)), "week");
}
if (pastTimeMillis < dayMillis * 365) {
return relTime.format(-Math.round(pastTimeMillis / (dayMillis * 30)), "month");
}

@ -1,5 +1,5 @@
.memo-content-wrapper {
@apply w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-200;
@apply w-full flex flex-col justify-start items-start text-gray-800 dark:text-gray-300;
> .memo-content-text {
@apply w-full max-w-full word-break text-base leading-6;

@ -8,30 +8,6 @@
> .memo-top-wrapper {
@apply flex flex-row justify-between items-center w-full h-6 mb-1;
> .status-text-container {
@apply flex flex-row justify-start items-center;
> .time-text {
@apply text-sm text-gray-400;
}
> .name-text {
@apply ml-1 text-sm text-gray-400 cursor-pointer hover:opacity-80;
}
> .status-text {
@apply text-xs cursor-pointer ml-2 rounded border px-1;
&.public {
@apply border-green-600 text-green-600;
}
&.protected {
@apply border-gray-400 text-gray-400;
}
}
}
> .btns-container {
@apply flex flex-row justify-end items-center relative shrink-0;
@ -56,7 +32,7 @@
@apply flex flex-row justify-center items-center leading-6 text-sm rounded hover:bg-gray-200 dark:hover:bg-zinc-600;
&.more-action-btn {
@apply w-auto opacity-60 cursor-default hover:bg-transparent;
@apply w-auto opacity-50 cursor-default hover:bg-transparent;
> .icon-img {
@apply w-4 h-auto dark:text-gray-300;

@ -93,7 +93,7 @@ const Explore = () => {
<main className="relative w-full h-auto flex flex-col justify-start items-start">
<MemoFilter />
{sortedMemos.map((memo) => {
return <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} showCreator />;
return <Memo key={`${memo.id}-${memo.displayTs}`} memo={memo} />;
})}
{isComplete ? (
memos.length === 0 && (

@ -33,7 +33,7 @@ const MemoDetail = () => {
}, [location]);
return (
<section className="relative top-0 w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
<section className="relative top-0 w-full min-h-full overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
<div className="relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-6">
<div className="max-w-2xl w-full flex flex-row justify-center items-center px-4 py-2 mt-2 bg-zinc-100 dark:bg-zinc-800">
<div className="detail-header flex flex-row justify-start items-center">
@ -45,7 +45,7 @@ const MemoDetail = () => {
(memo ? (
<>
<main className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4">
<Memo memo={memo} showCreator showRelatedMemos />
<Memo memo={memo} />
</main>
<div className="mt-4 w-full flex flex-row justify-center items-center gap-2">
<Link

@ -0,0 +1,74 @@
import { useEffect } from "react";
import { toast } from "react-hot-toast";
import FloatingNavButton from "@/components/FloatingNavButton";
import MemoFilter from "@/components/MemoFilter";
import MemoList from "@/components/MemoList";
import UserAvatar from "@/components/UserAvatar";
import useLoading from "@/hooks/useLoading";
import { useGlobalStore, useUserStore } from "@/store/module";
import { useTranslate } from "@/utils/i18n";
const UserProfile = () => {
const t = useTranslate();
const globalStore = useGlobalStore();
const userStore = useUserStore();
const loadingState = useLoading();
const user = userStore.state.user;
useEffect(() => {
const currentUsername = userStore.getCurrentUsername();
userStore
.getUserByUsername(currentUsername)
.then(() => {
loadingState.setFinish();
})
.catch((error) => {
console.error(error);
toast.error(t("message.user-not-found"));
});
}, [userStore.getCurrentUsername()]);
useEffect(() => {
if (user?.setting.locale) {
globalStore.setLocale(user.setting.locale);
}
}, [user?.setting.locale]);
return (
<>
<section className="relative top-0 w-full min-h-full overflow-x-hidden bg-zinc-100 dark:bg-zinc-800">
<div className="relative w-full min-h-full mx-auto flex flex-col justify-start items-center">
{!loadingState.isLoading &&
(user ? (
<>
<main className="relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4">
<div className="w-full flex flex-row justify-start items-start">
<div className="flex-grow shrink w-full">
<div className="w-full flex flex-col justify-start items-center py-8">
<UserAvatar className="w-16 h-auto mb-4 drop-shadow" avatarUrl={user?.avatarUrl} />
<div>
<p className="text-2xl font-bold text-gray-700 dark:text-gray-300">{user?.nickname}</p>
</div>
</div>
<div className="w-full h-auto flex flex-col justify-start items-start bg-zinc-100 dark:bg-zinc-800 rounded-lg">
<MemoFilter />
</div>
<MemoList />
</div>
</div>
</main>
</>
) : (
<>
<p>Not found</p>
</>
))}
</div>
</section>
<FloatingNavButton />
</>
);
};
export default UserProfile;

@ -13,6 +13,7 @@ const Auth = lazy(() => import("@/pages/Auth"));
const AuthCallback = lazy(() => import("@/pages/AuthCallback"));
const Explore = lazy(() => import("@/pages/Explore"));
const Home = lazy(() => import("@/pages/Home"));
const UserProfile = lazy(() => import("@/pages/UserProfile"));
const MemoDetail = lazy(() => import("@/pages/MemoDetail"));
const EmbedMemo = lazy(() => import("@/pages/EmbedMemo"));
const NotFound = lazy(() => import("@/pages/NotFound"));
@ -78,28 +79,6 @@ const router = createBrowserRouter([
return redirect("/explore");
},
},
{
path: "u/:username",
element: <Home />,
loader: async () => {
await initialGlobalStateLoader();
try {
await initialUserState();
} catch (error) {
// do nth
}
const { user } = store.getState().user;
const { systemStatus } = store.getState().global;
if (isNullorUndefined(user) && systemStatus.disablePublicMemos) {
return redirect("/auth");
}
return null;
},
},
{
path: "explore",
element: <Explore />,
@ -238,6 +217,20 @@ const router = createBrowserRouter([
return null;
},
},
{
path: "u/:username",
element: <UserProfile />,
loader: async () => {
await initialGlobalStateLoader();
try {
await initialUserState();
} catch (error) {
// do nth
}
return null;
},
},
{
path: "*",
element: <NotFound />,

Loading…
Cancel
Save