mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: update home layout (#1242)
This commit is contained in:
@ -63,7 +63,7 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => {
|
|||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text" onClick={() => toggleShowDatePicker()}>
|
<p className="title-text" onClick={() => toggleShowDatePicker()}>
|
||||||
<span className="icon-text">📅</span> {t("sidebar.daily-review")}
|
<span className="icon-text">📅</span> {t("common.daily-review")}
|
||||||
</p>
|
</p>
|
||||||
<div className="btns-container">
|
<div className="btns-container">
|
||||||
<button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp - DAILY_TIMESTAMP)}>
|
<button className="btn-text" onClick={() => setCurrentDateStamp(currentDateStamp - DAILY_TIMESTAMP)}>
|
||||||
|
92
web/src/components/Header.tsx
Normal file
92
web/src/components/Header.tsx
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
import { isUndefined } from "lodash-es";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { useLocationStore, useUserStore } from "../store/module";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
import showDailyReviewDialog from "./DailyReviewDialog";
|
||||||
|
import showResourcesDialog from "./ResourcesDialog";
|
||||||
|
import showSettingDialog from "./SettingDialog";
|
||||||
|
import UserBanner from "./UserBanner";
|
||||||
|
import "../less/header.less";
|
||||||
|
|
||||||
|
const Header = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const locationStore = useLocationStore();
|
||||||
|
const query = locationStore.state.query;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
toggleHeader(false);
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mask" onClick={() => toggleHeader(false)}></div>
|
||||||
|
<header className="header-wrapper">
|
||||||
|
<UserBanner />
|
||||||
|
<div className="w-full px-2 my-2 mt-4 flex flex-col justify-start items-start shrink-0 space-y-2">
|
||||||
|
<Link
|
||||||
|
to="/"
|
||||||
|
className="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"
|
||||||
|
>
|
||||||
|
<Icon.Home className="mr-4 w-6 h-auto opacity-80" /> {t("common.home")}
|
||||||
|
</Link>
|
||||||
|
<button
|
||||||
|
className="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"
|
||||||
|
onClick={() => showDailyReviewDialog()}
|
||||||
|
>
|
||||||
|
<Icon.Calendar className="mr-4 w-6 h-auto opacity-80" /> {t("common.daily-review")}
|
||||||
|
</button>
|
||||||
|
<Link
|
||||||
|
to="/explore"
|
||||||
|
className="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"
|
||||||
|
>
|
||||||
|
<Icon.Hash className="mr-4 w-6 h-auto opacity-80" /> {t("common.explore")}
|
||||||
|
</Link>
|
||||||
|
{!userStore.isVisitorMode() && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="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"
|
||||||
|
onClick={() => showResourcesDialog()}
|
||||||
|
>
|
||||||
|
<Icon.Paperclip className="mr-4 w-6 h-auto opacity-80" /> {t("common.resources")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="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"
|
||||||
|
onClick={() => showDailyReviewDialog()}
|
||||||
|
>
|
||||||
|
<Icon.Archive className="mr-4 w-6 h-auto opacity-80" /> {t("common.archive")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="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"
|
||||||
|
onClick={() => showSettingDialog()}
|
||||||
|
>
|
||||||
|
<Icon.Settings className="mr-4 w-6 h-auto opacity-80" /> {t("common.settings")}
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleHeader = (show?: boolean) => {
|
||||||
|
const headerEl = document.body.querySelector(".header-wrapper") as HTMLDivElement;
|
||||||
|
const maskEl = headerEl.previousSibling as HTMLDivElement;
|
||||||
|
|
||||||
|
if (isUndefined(show)) {
|
||||||
|
show = !headerEl.classList.contains("show");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
headerEl.classList.add("show");
|
||||||
|
maskEl.classList.add("show");
|
||||||
|
} else {
|
||||||
|
headerEl.classList.remove("show");
|
||||||
|
maskEl.classList.remove("show");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
48
web/src/components/HomeSidebar.tsx
Normal file
48
web/src/components/HomeSidebar.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
import { isUndefined } from "lodash-es";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import { useLocationStore } from "../store/module";
|
||||||
|
import ShortcutList from "./ShortcutList";
|
||||||
|
import TagList from "./TagList";
|
||||||
|
import SearchBar from "./SearchBar";
|
||||||
|
import "../less/home-sidebar.less";
|
||||||
|
|
||||||
|
const HomeSidebar = () => {
|
||||||
|
const locationStore = useLocationStore();
|
||||||
|
const query = locationStore.state.query;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
toggleHomeSidebar(false);
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="mask" onClick={() => toggleHomeSidebar(false)}></div>
|
||||||
|
<aside className="sidebar-wrapper">
|
||||||
|
<div className="pl-6 pr-2 mb-4 w-full">
|
||||||
|
<SearchBar />
|
||||||
|
</div>
|
||||||
|
<ShortcutList />
|
||||||
|
<TagList />
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toggleHomeSidebar = (show?: boolean) => {
|
||||||
|
const sidebarEl = document.body.querySelector(".sidebar-wrapper") as HTMLDivElement;
|
||||||
|
const maskEl = sidebarEl.previousSibling as HTMLDivElement;
|
||||||
|
|
||||||
|
if (isUndefined(show)) {
|
||||||
|
show = !sidebarEl.classList.contains("show");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (show) {
|
||||||
|
sidebarEl.classList.add("show");
|
||||||
|
maskEl.classList.add("show");
|
||||||
|
} else {
|
||||||
|
sidebarEl.classList.remove("show");
|
||||||
|
maskEl.classList.remove("show");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default HomeSidebar;
|
@ -17,6 +17,7 @@ import "../less/memo.less";
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
memo: Memo;
|
memo: Memo;
|
||||||
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
|
export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
|
||||||
@ -28,7 +29,7 @@ export const getFormatedMemoTimeStr = (time: number, locale = "en"): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Memo: React.FC<Props> = (props: Props) => {
|
const Memo: React.FC<Props> = (props: Props) => {
|
||||||
const { memo } = props;
|
const { memo, readonly } = props;
|
||||||
const { t, i18n } = useTranslation();
|
const { t, i18n } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const editorStore = useEditorStore();
|
const editorStore = useEditorStore();
|
||||||
@ -37,7 +38,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
const memoStore = useMemoStore();
|
const memoStore = useMemoStore();
|
||||||
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.createdTs, i18n.language));
|
const [createdTimeStr, setCreatedTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.createdTs, i18n.language));
|
||||||
const memoContainerRef = useRef<HTMLDivElement>(null);
|
const memoContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const isVisitorMode = userStore.isVisitorMode();
|
const isVisitorMode = userStore.isVisitorMode() || readonly;
|
||||||
const updatedTimeStr = getFormatedMemoTimeStr(memo.updatedTs, i18n.language);
|
const updatedTimeStr = getFormatedMemoTimeStr(memo.updatedTs, i18n.language);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -67,6 +68,10 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleTogglePinMemoBtnClick = async () => {
|
const handleTogglePinMemoBtnClick = async () => {
|
||||||
|
if (isVisitorMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (memo.pinned) {
|
if (memo.pinned) {
|
||||||
await memoStore.unpinMemo(memo.id);
|
await memoStore.unpinMemo(memo.id);
|
||||||
@ -79,10 +84,18 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEditMemoClick = () => {
|
const handleEditMemoClick = () => {
|
||||||
|
if (isVisitorMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
editorStore.setEditMemoWithId(memo.id);
|
editorStore.setEditMemoWithId(memo.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleArchiveMemoClick = async () => {
|
const handleArchiveMemoClick = async () => {
|
||||||
|
if (isVisitorMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await memoStore.patchMemo({
|
await memoStore.patchMemo({
|
||||||
id: memo.id,
|
id: memo.id,
|
||||||
@ -114,7 +127,7 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
locationStore.setTagQuery(tagName);
|
locationStore.setTagQuery(tagName);
|
||||||
}
|
}
|
||||||
} else if (targetEl.classList.contains("todo-block")) {
|
} else if (targetEl.classList.contains("todo-block")) {
|
||||||
if (userStore.isVisitorMode()) {
|
if (isVisitorMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,6 +166,10 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMemoContentDoubleClick = (e: React.MouseEvent) => {
|
const handleMemoContentDoubleClick = (e: React.MouseEvent) => {
|
||||||
|
if (isVisitorMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loginUser = userStore.state.user;
|
const loginUser = userStore.state.user;
|
||||||
if (loginUser && !loginUser.localSetting.enableDoubleClickEditing) {
|
if (loginUser && !loginUser.localSetting.enableDoubleClickEditing) {
|
||||||
return;
|
return;
|
||||||
@ -191,6 +208,11 @@ const Memo: React.FC<Props> = (props: Props) => {
|
|||||||
{createdTimeStr}
|
{createdTimeStr}
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{isVisitorMode && (
|
||||||
|
<a className="ml-2 opacity-60 text-sm" href={`/u/${memo.creatorId}`}>
|
||||||
|
@{memo.creatorName}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
{memo.visibility !== "PRIVATE" && !isVisitorMode && (
|
{memo.visibility !== "PRIVATE" && !isVisitorMode && (
|
||||||
<span
|
<span
|
||||||
className={`status-text ${memo.visibility.toLocaleLowerCase()}`}
|
className={`status-text ${memo.visibility.toLocaleLowerCase()}`}
|
||||||
|
@ -1,62 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import { useLocationStore, useMemoStore, useShortcutStore, useUserStore } from "../store/module";
|
|
||||||
import Icon from "./Icon";
|
|
||||||
import SearchBar from "./SearchBar";
|
|
||||||
import { toggleSidebar } from "./Sidebar";
|
|
||||||
import "../less/memos-header.less";
|
|
||||||
|
|
||||||
let prevRequestTimestamp = Date.now();
|
|
||||||
|
|
||||||
const MemosHeader = () => {
|
|
||||||
const locationStore = useLocationStore();
|
|
||||||
const memoStore = useMemoStore();
|
|
||||||
const shortcutStore = useShortcutStore();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const user = userStore.state.user;
|
|
||||||
const query = locationStore.state.query;
|
|
||||||
const shortcuts = shortcutStore.state.shortcuts;
|
|
||||||
const [titleText, setTitleText] = useState("MEMOS");
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!query?.shortcutId) {
|
|
||||||
setTitleText("MEMOS");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const shortcut = shortcutStore.getShortcutById(query?.shortcutId);
|
|
||||||
if (shortcut) {
|
|
||||||
setTitleText(shortcut.title);
|
|
||||||
}
|
|
||||||
}, [query, shortcuts]);
|
|
||||||
|
|
||||||
const handleTitleTextClick = useCallback(() => {
|
|
||||||
const now = Date.now();
|
|
||||||
if (now - prevRequestTimestamp > 1 * 1000) {
|
|
||||||
prevRequestTimestamp = now;
|
|
||||||
memoStore.fetchMemos().catch(() => {
|
|
||||||
// do nth
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="memos-header-container">
|
|
||||||
<div className="title-container">
|
|
||||||
<div className="action-btn" onClick={() => toggleSidebar(true)}>
|
|
||||||
<Icon.Menu className="icon-img" />
|
|
||||||
</div>
|
|
||||||
<span className="title-text" onClick={handleTitleTextClick}>
|
|
||||||
{titleText}
|
|
||||||
</span>
|
|
||||||
{user && (
|
|
||||||
<a className="dark:text-white" href={"/u/" + user.id + "/rss.xml"} target="_blank" rel="noreferrer">
|
|
||||||
<Icon.Rss className="w-4 h-auto opacity-40 hover:opacity-60" />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<SearchBar />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MemosHeader;
|
|
62
web/src/components/MobileHeader.tsx
Normal file
62
web/src/components/MobileHeader.tsx
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { useLocationStore, useMemoStore, useShortcutStore } from "../store/module";
|
||||||
|
import Icon from "./Icon";
|
||||||
|
import { toggleHeader } from "./Header";
|
||||||
|
import { toggleHomeSidebar } from "./HomeSidebar";
|
||||||
|
|
||||||
|
let prevRequestTimestamp = Date.now();
|
||||||
|
|
||||||
|
const MobileHeader = () => {
|
||||||
|
const locationStore = useLocationStore();
|
||||||
|
const memoStore = useMemoStore();
|
||||||
|
const shortcutStore = useShortcutStore();
|
||||||
|
const query = locationStore.state.query;
|
||||||
|
const shortcuts = shortcutStore.state.shortcuts;
|
||||||
|
const [titleText, setTitleText] = useState("MEMOS");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!query?.shortcutId) {
|
||||||
|
setTitleText("MEMOS");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shortcut = shortcutStore.getShortcutById(query?.shortcutId);
|
||||||
|
if (shortcut) {
|
||||||
|
setTitleText(shortcut.title);
|
||||||
|
}
|
||||||
|
}, [query, shortcuts]);
|
||||||
|
|
||||||
|
const handleTitleTextClick = useCallback(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - prevRequestTimestamp > 1 * 1000) {
|
||||||
|
prevRequestTimestamp = now;
|
||||||
|
memoStore.fetchMemos().catch(() => {
|
||||||
|
// do nth
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sticky top-0 pt-4 pb-1 mb-1 backdrop-blur-sm flex sm:hidden flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-10">
|
||||||
|
<div className="flex flex-row justify-start items-center mr-2 shrink-0 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="flex sm:hidden flex-row justify-center items-center w-6 h-6 mr-1 shrink-0 bg-transparent"
|
||||||
|
onClick={() => toggleHeader(true)}
|
||||||
|
>
|
||||||
|
<Icon.Menu className="w-5 h-auto dark:text-gray-200" />
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
className="font-bold text-lg leading-10 mr-1 text-ellipsis shrink-0 cursor-pointer overflow-hidden text-gray-700 dark:text-gray-200"
|
||||||
|
onClick={handleTitleTextClick}
|
||||||
|
>
|
||||||
|
{titleText}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row justify-end items-center pr-1">
|
||||||
|
<Icon.Search className="w-5 h-auto dark:text-gray-200" onClick={() => toggleHomeSidebar(true)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MobileHeader;
|
@ -104,7 +104,7 @@ const ResourcesDialog: React.FC<Props> = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text">{t("sidebar.resources")}</p>
|
<p className="title-text">{t("common.resources")}</p>
|
||||||
<button className="btn close-btn" onClick={destroy}>
|
<button className="btn close-btn" onClick={destroy}>
|
||||||
<Icon.X className="icon-img" />
|
<Icon.X className="icon-img" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -79,7 +79,7 @@ const ResourcesSelectorDialog: React.FC<Props> = (props: Props) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dialog-header-container">
|
<div className="dialog-header-container">
|
||||||
<p className="title-text">{t("sidebar.resources")}</p>
|
<p className="title-text">{t("common.resources")}</p>
|
||||||
<button className="btn close-btn" onClick={destroy}>
|
<button className="btn close-btn" onClick={destroy}>
|
||||||
<Icon.X className="icon-img" />
|
<Icon.X className="icon-img" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -86,7 +86,7 @@ const SearchBar = () => {
|
|||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="quickly-action-wrapper">
|
<div className="quickly-action-wrapper !hidden">
|
||||||
<div className="quickly-action-container">
|
<div className="quickly-action-container">
|
||||||
<p className="title-text">{t("search.quickly-filter").toUpperCase()}</p>
|
<p className="title-text">{t("search.quickly-filter").toUpperCase()}</p>
|
||||||
<div className="section-container types-container">
|
<div className="section-container types-container">
|
||||||
|
@ -148,7 +148,7 @@ const SystemSection = () => {
|
|||||||
</span>
|
</span>
|
||||||
<Button onClick={handleVacuumBtnClick}>{t("common.vacuum")}</Button>
|
<Button onClick={handleVacuumBtnClick}>{t("common.vacuum")}</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="title-text">{t("sidebar.setting")}</p>
|
<p className="title-text">{t("common.settings")}</p>
|
||||||
<div className="form-label">
|
<div className="form-label">
|
||||||
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
|
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
|
||||||
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
||||||
|
@ -1,93 +0,0 @@
|
|||||||
import { isUndefined } from "lodash-es";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { Link } from "react-router-dom";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useLocationStore, useUserStore } from "../store/module";
|
|
||||||
import showDailyReviewDialog from "./DailyReviewDialog";
|
|
||||||
import showResourcesDialog from "./ResourcesDialog";
|
|
||||||
import showSettingDialog from "./SettingDialog";
|
|
||||||
import UserBanner from "./UserBanner";
|
|
||||||
import UsageHeatMap from "./UsageHeatMap";
|
|
||||||
import ShortcutList from "./ShortcutList";
|
|
||||||
import TagList from "./TagList";
|
|
||||||
import "../less/siderbar.less";
|
|
||||||
|
|
||||||
const Sidebar = () => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const locationStore = useLocationStore();
|
|
||||||
const query = locationStore.state.query;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
toggleSidebar(false);
|
|
||||||
}, [query]);
|
|
||||||
|
|
||||||
const handleSettingBtnClick = () => {
|
|
||||||
showSettingDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="mask" onClick={() => toggleSidebar(false)}></div>
|
|
||||||
<aside className="sidebar-wrapper">
|
|
||||||
<UserBanner />
|
|
||||||
<UsageHeatMap />
|
|
||||||
<div className="w-full px-2 my-2 flex flex-col justify-start items-start shrink-0">
|
|
||||||
<button
|
|
||||||
className="leading-10 px-4 rounded-lg text-base dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
|
|
||||||
onClick={() => showDailyReviewDialog()}
|
|
||||||
>
|
|
||||||
<span className="mr-1">📅</span> {t("sidebar.daily-review")}
|
|
||||||
</button>
|
|
||||||
<Link
|
|
||||||
to="/explore"
|
|
||||||
className="leading-10 px-4 rounded-lg text-base dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
|
|
||||||
>
|
|
||||||
<span className="mr-1">🏂</span> {t("common.explore")}
|
|
||||||
</Link>
|
|
||||||
<button
|
|
||||||
className="leading-10 px-4 rounded-lg text-base dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
|
|
||||||
onClick={() => showResourcesDialog()}
|
|
||||||
>
|
|
||||||
<span className="mr-1">🗂️</span> {t("sidebar.resources")}
|
|
||||||
</button>
|
|
||||||
{!userStore.isVisitorMode() && (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
className="leading-10 px-4 rounded-lg text-base dark:text-gray-200 hover:bg-white hover:shadow dark:hover:bg-zinc-700"
|
|
||||||
onClick={handleSettingBtnClick}
|
|
||||||
>
|
|
||||||
<span className="mr-1">⚙️</span> {t("sidebar.setting")}
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!userStore.isVisitorMode() && (
|
|
||||||
<>
|
|
||||||
<ShortcutList />
|
|
||||||
<TagList />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</aside>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toggleSidebar = (show?: boolean) => {
|
|
||||||
const sidebarEl = document.body.querySelector(".sidebar-wrapper") as HTMLDivElement;
|
|
||||||
const maskEl = document.body.querySelector(".mask") as HTMLDivElement;
|
|
||||||
|
|
||||||
if (isUndefined(show)) {
|
|
||||||
show = !sidebarEl.classList.contains("show");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (show) {
|
|
||||||
sidebarEl.classList.add("show");
|
|
||||||
maskEl.classList.add("show");
|
|
||||||
} else {
|
|
||||||
sidebarEl.classList.remove("show");
|
|
||||||
maskEl.classList.remove("show");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Sidebar;
|
|
@ -83,7 +83,6 @@ const TagList = () => {
|
|||||||
{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} />
|
||||||
))}
|
))}
|
||||||
{tags.length <= 3 && <p className="tip-text">{t("tag-list.tip-text")}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMemoStore, useTagStore, useUserStore } from "../store/module";
|
import { useUserStore } from "../store/module";
|
||||||
import { getMemoStats } from "../helpers/api";
|
|
||||||
import * as utils from "../helpers/utils";
|
|
||||||
import Dropdown from "./common/Dropdown";
|
import Dropdown from "./common/Dropdown";
|
||||||
import showArchivedMemoDialog from "./ArchivedMemoDialog";
|
|
||||||
import showAboutSiteDialog from "./AboutSiteDialog";
|
import showAboutSiteDialog from "./AboutSiteDialog";
|
||||||
import UserAvatar from "./UserAvatar";
|
import UserAvatar from "./UserAvatar";
|
||||||
import showSettingDialog from "./SettingDialog";
|
import showSettingDialog from "./SettingDialog";
|
||||||
@ -12,14 +9,8 @@ import showSettingDialog from "./SettingDialog";
|
|||||||
const UserBanner = () => {
|
const UserBanner = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const memoStore = useMemoStore();
|
|
||||||
const tagStore = useTagStore();
|
|
||||||
const { user, owner } = userStore.state;
|
const { user, owner } = userStore.state;
|
||||||
const { memos } = memoStore.state;
|
|
||||||
const tags = tagStore.state.tags;
|
|
||||||
const [username, setUsername] = useState("Memos");
|
const [username, setUsername] = useState("Memos");
|
||||||
const [memoAmount, setMemoAmount] = useState(0);
|
|
||||||
const [createdDays, setCreatedDays] = useState(0);
|
|
||||||
const isVisitorMode = userStore.isVisitorMode();
|
const isVisitorMode = userStore.isVisitorMode();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -28,31 +19,15 @@ const UserBanner = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setUsername(owner.nickname || owner.username);
|
setUsername(owner.nickname || owner.username);
|
||||||
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(owner.createdTs)) / 1000 / 3600 / 24));
|
|
||||||
} else if (user) {
|
} else if (user) {
|
||||||
setUsername(user.nickname || user.username);
|
setUsername(user.nickname || user.username);
|
||||||
setCreatedDays(Math.ceil((Date.now() - utils.getTimeStampByDate(user.createdTs)) / 1000 / 3600 / 24));
|
|
||||||
}
|
}
|
||||||
}, [isVisitorMode, user, owner]);
|
}, [isVisitorMode, user, owner]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getMemoStats(userStore.getCurrentUserId())
|
|
||||||
.then(({ data: { data } }) => {
|
|
||||||
setMemoAmount(data.length);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
|
||||||
}, [memos]);
|
|
||||||
|
|
||||||
const handleMyAccountClick = () => {
|
const handleMyAccountClick = () => {
|
||||||
showSettingDialog("my-account");
|
showSettingDialog("my-account");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleArchivedBtnClick = () => {
|
|
||||||
showArchivedMemoDialog();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAboutBtnClick = () => {
|
const handleAboutBtnClick = () => {
|
||||||
showAboutSiteDialog();
|
showAboutSiteDialog();
|
||||||
};
|
};
|
||||||
@ -63,7 +38,6 @@ const UserBanner = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<div className="flex flex-row justify-between items-center relative w-full h-auto px-3 flex-nowrap shrink-0">
|
<div className="flex flex-row justify-between items-center relative w-full h-auto px-3 flex-nowrap shrink-0">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@ -88,12 +62,6 @@ const UserBanner = () => {
|
|||||||
>
|
>
|
||||||
<span className="mr-1">🤠</span> {t("setting.my-account")}
|
<span className="mr-1">🤠</span> {t("setting.my-account")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
className="w-full px-3 truncate text-left leading-10 cursor-pointer rounded dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-zinc-800"
|
|
||||||
onClick={handleArchivedBtnClick}
|
|
||||||
>
|
|
||||||
<span className="mr-1">🗃️</span> {t("sidebar.archived")}
|
|
||||||
</button>
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
@ -114,21 +82,6 @@ const UserBanner = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-between items-start w-full px-6 select-none shrink-0 pb-2">
|
|
||||||
<div className="flex flex-col justify-start items-start">
|
|
||||||
<span className="font-bold text-2xl opacity-80 leading-10 text-slate-600 dark:text-gray-300">{memoAmount}</span>
|
|
||||||
<span className="text-gray-400 text-xs font-mono">{t("amount-text.memo", { count: memoAmount })}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-start items-start">
|
|
||||||
<span className="font-bold text-2xl opacity-80 leading-10 text-slate-600 dark:text-gray-300">{tags.length}</span>
|
|
||||||
<span className="text-gray-400 text-xs font-mono">{t("amount-text.tag", { count: tags.length })}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col justify-start items-start">
|
|
||||||
<span className="font-bold text-2xl opacity-80 leading-10 text-slate-600 dark:text-gray-300">{createdDays}</span>
|
|
||||||
<span className="text-gray-400 text-xs font-mono">{t("amount-text.day", { count: createdDays })}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -17,11 +17,6 @@
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
@media screen and (min-width: 1024px) {
|
|
||||||
.ml-calc {
|
|
||||||
margin-left: calc(100vw - 100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
@apply flex flex-row justify-center items-center w-full h-full dark:bg-zinc-800;
|
@apply flex flex-row justify-center items-center w-full h-full dark:bg-zinc-800;
|
||||||
|
|
||||||
> .page-container {
|
> .page-container {
|
||||||
@apply w-80 max-w-full h-full py-4 flex flex-col justify-start items-center ml-calc;
|
@apply w-80 max-w-full h-full py-4 flex flex-col justify-start items-center;
|
||||||
|
|
||||||
> .auth-form-wrapper {
|
> .auth-form-wrapper {
|
||||||
@apply w-full py-4 grow flex flex-col justify-center items-center;
|
@apply w-full py-4 grow flex flex-col justify-center items-center;
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
.page-wrapper.explore {
|
|
||||||
@apply w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800;
|
|
||||||
|
|
||||||
> .page-container {
|
|
||||||
@apply relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-8;
|
|
||||||
|
|
||||||
> .page-header {
|
|
||||||
@apply sticky top-0 z-10 max-w-2xl w-full h-auto flex flex-row justify-between backdrop-blur-sm items-center px-4 sm:pr-6 pt-6 mb-2 ml-calc;
|
|
||||||
|
|
||||||
> .title-container {
|
|
||||||
@apply flex flex-row justify-start items-center;
|
|
||||||
|
|
||||||
> .logo-img {
|
|
||||||
@apply h-12 w-auto rounded-md mr-2;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .title-text {
|
|
||||||
@apply text-xl sm:text-4xl text-gray-700 dark:text-gray-200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .memos-wrapper {
|
|
||||||
@apply relative flex-grow max-w-2xl w-full h-auto flex flex-col justify-start items-start px-4 sm:pr-6 ml-calc;
|
|
||||||
|
|
||||||
> .memo-container {
|
|
||||||
@apply relative flex flex-col justify-start items-start w-full p-4 mt-2 bg-white dark:bg-zinc-700 rounded-lg border border-white dark:border-zinc-800 hover:border-gray-200 dark:hover:border-zinc-600;
|
|
||||||
|
|
||||||
&.pinned {
|
|
||||||
@apply border-gray-200 border-2 dark:border-zinc-600;
|
|
||||||
}
|
|
||||||
|
|
||||||
> .corner-container {
|
|
||||||
@apply absolute top-0 right-0 z-1;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
@apply rounded-tr-md absolute top-0 right-0 border-transparent border-t-green-600 border-r-green-600;
|
|
||||||
content: "";
|
|
||||||
border-width: 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .memo-header {
|
|
||||||
@apply mb-2 w-full flex flex-row justify-start items-center text-sm text-gray-400;
|
|
||||||
|
|
||||||
> .name-text {
|
|
||||||
@apply ml-2 hover:text-green-600 hover:underline;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
.sidebar-wrapper {
|
.header-wrapper {
|
||||||
@apply fixed sm:sticky top-0 z-30 sm:z-0 -translate-x-64 sm:translate-x-0 sm:flex flex-col justify-start items-start w-64 h-auto max-h-screen py-4 pl-2 bg-white dark:bg-zinc-800 sm:bg-transparent overflow-x-hidden overflow-y-auto transition-transform duration-300 overscroll-contain hide-scrollbar;
|
@apply fixed sm:sticky top-0 z-30 sm:z-0 -translate-x-64 sm:translate-x-0 sm:flex flex-col justify-start items-start w-56 h-full py-4 pl-2 bg-white dark:bg-zinc-800 sm:bg-transparent overflow-x-hidden overflow-y-auto transition-transform duration-300 overscroll-contain hide-scrollbar;
|
||||||
|
|
||||||
&.show {
|
&.show {
|
||||||
@apply translate-x-0 shadow-2xl sm:shadow-none;
|
@apply translate-x-0 shadow-2xl sm:shadow-none;
|
15
web/src/less/home-sidebar.less
Normal file
15
web/src/less/home-sidebar.less
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
.sidebar-wrapper {
|
||||||
|
@apply flex-shrink-0 fixed sm:sticky top-0 z-30 sm:z-0 translate-x-56 sm:translate-x-0 hidden md:flex flex-col justify-start items-start w-56 h-full py-4 bg-white dark:bg-zinc-800 sm:bg-transparent overflow-x-hidden overflow-y-auto transition-transform duration-300 overscroll-contain hide-scrollbar;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
@apply flex translate-x-0 right-0 shadow-2xl sm:shadow-none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mask {
|
||||||
|
@apply fixed top-0 left-0 w-full h-full bg-black opacity-0 transition-opacity duration-300 pointer-events-none z-20 sm:hidden;
|
||||||
|
|
||||||
|
&.show {
|
||||||
|
@apply opacity-60 pointer-events-auto;
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
.page-wrapper.home {
|
.page-wrapper.home {
|
||||||
@apply w-full h-full overflow-y-auto overflow-x-hidden bg-zinc-100 dark:bg-zinc-800;
|
@apply w-full h-full overflow-y-auto bg-zinc-100 dark:bg-zinc-800;
|
||||||
|
|
||||||
> .banner-wrapper {
|
> .banner-wrapper {
|
||||||
@apply w-full flex flex-col justify-start items-center;
|
@apply w-full flex flex-col justify-start items-center;
|
||||||
@ -8,28 +8,16 @@
|
|||||||
> .page-container {
|
> .page-container {
|
||||||
@apply relative w-full h-auto mx-auto flex flex-row justify-start sm:justify-center items-start;
|
@apply relative w-full h-auto mx-auto flex flex-row justify-start sm:justify-center items-start;
|
||||||
|
|
||||||
> .sidebar-wrapper {
|
> .header-wrapper {
|
||||||
@apply flex-shrink-0 h-full ml-calc;
|
@apply flex-shrink-0 h-full;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .memos-wrapper {
|
> .memos-wrapper {
|
||||||
@apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 sm:pr-6;
|
@apply relative flex-grow max-w-2xl w-full h-auto flex flex-col justify-start items-start px-2 sm:pt-4;
|
||||||
|
|
||||||
> .memos-editor-wrapper {
|
> .memos-editor-wrapper {
|
||||||
@apply w-full h-auto flex flex-col justify-start items-start bg-zinc-100 dark:bg-zinc-800 rounded-lg;
|
@apply w-full h-auto flex flex-col justify-start items-start bg-zinc-100 dark:bg-zinc-800 rounded-lg;
|
||||||
}
|
}
|
||||||
|
|
||||||
> .addition-btn-container {
|
|
||||||
@apply fixed bottom-12 left-1/2 -translate-x-1/2;
|
|
||||||
|
|
||||||
> .btn {
|
|
||||||
@apply bg-blue-600 dark:bg-blue-800 text-white dark:text-gray-200 px-4 py-2 rounded-3xl shadow-2xl hover:opacity-80;
|
|
||||||
|
|
||||||
> .icon {
|
|
||||||
@apply text-lg mr-1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
@apply relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-8;
|
@apply relative w-full min-h-full mx-auto flex flex-col justify-start items-center pb-8;
|
||||||
|
|
||||||
> .page-header {
|
> .page-header {
|
||||||
@apply sticky top-0 z-10 max-w-2xl w-full min-h-full flex flex-row justify-between items-center px-4 pt-6 mb-2 bg-zinc-100 dark:bg-zinc-800 ml-calc;
|
@apply sticky top-0 z-10 max-w-2xl w-full min-h-full flex flex-row justify-between items-center px-4 pt-6 mb-2 bg-zinc-100 dark:bg-zinc-800;
|
||||||
|
|
||||||
> .title-container {
|
> .title-container {
|
||||||
@apply flex flex-row justify-start items-center;
|
@apply flex flex-row justify-start items-center;
|
||||||
@ -35,7 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
> .memos-wrapper {
|
> .memos-wrapper {
|
||||||
@apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4 ml-calc;
|
@apply relative flex-grow max-w-2xl w-full min-h-full flex flex-col justify-start items-start px-4;
|
||||||
|
|
||||||
> .memo-container {
|
> .memo-container {
|
||||||
@apply flex flex-col justify-start items-start w-full p-4 mt-2 bg-white dark:bg-zinc-700 rounded-lg border border-white dark:border-zinc-800 hover:border-gray-200 dark:hover:border-zinc-700;
|
@apply flex flex-col justify-start items-start w-full p-4 mt-2 bg-white dark:bg-zinc-700 rounded-lg border border-white dark:border-zinc-800 hover:border-gray-200 dark:hover:border-zinc-700;
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
.memos-header-container {
|
|
||||||
@apply sticky top-0 pt-4 pb-1 mb-1 backdrop-blur-sm flex flex-row justify-between items-center w-full h-auto flex-nowrap shrink-0 z-10;
|
|
||||||
|
|
||||||
> .title-container {
|
|
||||||
@apply flex flex-row justify-start items-center mr-2 shrink-0 overflow-hidden;
|
|
||||||
|
|
||||||
> .action-btn {
|
|
||||||
@apply flex sm:hidden flex-row justify-center items-center w-6 h-6 mr-1 shrink-0 bg-transparent;
|
|
||||||
|
|
||||||
> .icon-img {
|
|
||||||
@apply w-5 h-auto dark:text-gray-200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .title-text {
|
|
||||||
@apply font-bold text-lg leading-10 mr-1 text-ellipsis shrink-0 cursor-pointer overflow-hidden text-gray-700 dark:text-gray-200;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .btns-container {
|
|
||||||
@apply flex flex-row justify-end items-center;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,5 @@
|
|||||||
.search-bar-container {
|
.search-bar-container {
|
||||||
@apply relative w-auto;
|
@apply relative w-full;
|
||||||
|
|
||||||
&:hover,
|
&:hover,
|
||||||
&:active {
|
&:active {
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"about": "About",
|
"about": "About",
|
||||||
|
"home": "Home",
|
||||||
|
"resources": "Resources",
|
||||||
|
"settings": "Settings",
|
||||||
|
"daily-review": "Daily Review",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"password": "Password",
|
"password": "Password",
|
||||||
"repeat-password-short": "Repeat",
|
|
||||||
"repeat-password": "Repeat the password",
|
|
||||||
"new-password": "New password",
|
|
||||||
"repeat-new-password": "Repeat the new password",
|
|
||||||
"avatar": "Avatar",
|
"avatar": "Avatar",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"nickname": "Nickname",
|
"nickname": "Nickname",
|
||||||
|
"new-password": "New password",
|
||||||
|
"repeat-new-password": "Repeat the new password",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"close": "Close",
|
"close": "Close",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
@ -57,12 +59,6 @@
|
|||||||
"host-tip": "You are registering as the Site Host.",
|
"host-tip": "You are registering as the Site Host.",
|
||||||
"not-host-tip": "If you don't have an account, please contact the site host."
|
"not-host-tip": "If you don't have an account, please contact the site host."
|
||||||
},
|
},
|
||||||
"sidebar": {
|
|
||||||
"daily-review": "Daily Review",
|
|
||||||
"resources": "Resources",
|
|
||||||
"setting": "Settings",
|
|
||||||
"archived": "Archived"
|
|
||||||
},
|
|
||||||
"daily-review": {
|
"daily-review": {
|
||||||
"oops-nothing": "Oops, there is nothing."
|
"oops-nothing": "Oops, there is nothing."
|
||||||
},
|
},
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
{
|
{
|
||||||
"common": {
|
"common": {
|
||||||
"about": "关于",
|
"about": "关于",
|
||||||
|
"home": "主页",
|
||||||
|
"resources": "资源库",
|
||||||
|
"settings": "设置",
|
||||||
|
"daily-review": "每日回顾",
|
||||||
"email": "邮箱",
|
"email": "邮箱",
|
||||||
"password": "密码",
|
"password": "密码",
|
||||||
"repeat-password-short": "重复密码",
|
|
||||||
"repeat-password": "重复密码",
|
|
||||||
"new-password": "新密码",
|
|
||||||
"repeat-new-password": "重复新密码",
|
|
||||||
"username": "用户名",
|
"username": "用户名",
|
||||||
"nickname": "昵称",
|
"nickname": "昵称",
|
||||||
|
"new-password": "新密码",
|
||||||
|
"repeat-new-password": "重复新密码",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"close": "关闭",
|
"close": "关闭",
|
||||||
"cancel": "退出",
|
"cancel": "退出",
|
||||||
@ -57,12 +59,6 @@
|
|||||||
"host-tip": "你正在注册为 Host 用户账号。",
|
"host-tip": "你正在注册为 Host 用户账号。",
|
||||||
"not-host-tip": "如果你没有账号,请联系站点 Host"
|
"not-host-tip": "如果你没有账号,请联系站点 Host"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
|
||||||
"daily-review": "每日回顾",
|
|
||||||
"resources": "资源库",
|
|
||||||
"setting": "设置",
|
|
||||||
"archived": "已归档"
|
|
||||||
},
|
|
||||||
"daily-review": {
|
"daily-review": {
|
||||||
"oops-nothing": "啊哦,空空荡荡。"
|
"oops-nothing": "啊哦,空空荡荡。"
|
||||||
},
|
},
|
||||||
|
@ -1,24 +1,21 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { useGlobalStore, useLocationStore, useMemoStore, useUserStore } from "../store/module";
|
import { useGlobalStore, useLocationStore, useMemoStore, useUserStore } from "../store/module";
|
||||||
|
import { TAG_REG } from "../labs/marked/parser";
|
||||||
import { DEFAULT_MEMO_LIMIT } from "../helpers/consts";
|
import { DEFAULT_MEMO_LIMIT } from "../helpers/consts";
|
||||||
import useLoading from "../hooks/useLoading";
|
import useLoading from "../hooks/useLoading";
|
||||||
import toastHelper from "../components/Toast";
|
import toastHelper from "../components/Toast";
|
||||||
import MemoContent from "../components/MemoContent";
|
|
||||||
import MemoResources from "../components/MemoResources";
|
|
||||||
import MemoFilter from "../components/MemoFilter";
|
|
||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
import { TAG_REG } from "../labs/marked/parser";
|
import MemoFilter from "../components/MemoFilter";
|
||||||
import "../less/explore.less";
|
import Memo from "../components/Memo";
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
memos: Memo[];
|
memos: Memo[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const Explore = () => {
|
const Explore = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const globalStore = useGlobalStore();
|
const globalStore = useGlobalStore();
|
||||||
const locationStore = useLocationStore();
|
const locationStore = useLocationStore();
|
||||||
@ -91,20 +88,6 @@ const Explore = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleMemoContentClick = async (e: React.MouseEvent) => {
|
|
||||||
const targetEl = e.target as HTMLElement;
|
|
||||||
|
|
||||||
if (targetEl.className === "tag-span") {
|
|
||||||
const tagName = targetEl.innerText.slice(1);
|
|
||||||
const currTagQuery = locationStore.getState().query?.tag;
|
|
||||||
if (currTagQuery === tagName) {
|
|
||||||
locationStore.setTagQuery(undefined);
|
|
||||||
} else {
|
|
||||||
locationStore.setTagQuery(tagName);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTitleClick = () => {
|
const handleTitleClick = () => {
|
||||||
if (user) {
|
if (user) {
|
||||||
navigate("/");
|
navigate("/");
|
||||||
@ -114,12 +97,11 @@ const Explore = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="page-wrapper explore">
|
<section className="w-full min-h-full flex flex-col justify-start items-center pb-8 bg-zinc-100 dark:bg-zinc-800">
|
||||||
<div className="page-container">
|
<div className="sticky top-0 z-10 max-w-2xl w-full h-auto flex flex-row justify-between backdrop-blur-sm items-center px-4 sm:pr-6 pt-6 mb-2">
|
||||||
<div className="page-header">
|
<div className="flex flex-row justify-start items-center cursor-pointer hover:opacity-80" onClick={handleTitleClick}>
|
||||||
<div className="title-container cursor-pointer hover:opacity-80" onClick={handleTitleClick}>
|
<img className="h-12 w-auto rounded-md mr-2" src={customizedProfile.logoUrl} alt="" />
|
||||||
<img className="logo-img" src={customizedProfile.logoUrl} alt="" />
|
<span className="text-xl sm:text-4xl text-gray-700 dark:text-gray-200">{customizedProfile.name}</span>
|
||||||
<span className="title-text">{customizedProfile.name}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row justify-end items-center">
|
<div className="flex flex-row justify-end items-center">
|
||||||
<a
|
<a
|
||||||
@ -133,39 +115,22 @@ const Explore = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!loadingState.isLoading && (
|
{!loadingState.isLoading && (
|
||||||
<main className="memos-wrapper">
|
<main className="relative flex-grow max-w-2xl w-full h-auto flex flex-col justify-start items-start px-4 sm:pr-6">
|
||||||
<MemoFilter />
|
<MemoFilter />
|
||||||
{sortedMemos.map((memo) => {
|
{sortedMemos.map((memo) => {
|
||||||
const createdAtStr = dayjs(memo.createdTs).locale(i18n.language).format("YYYY/MM/DD HH:mm:ss");
|
return <Memo key={`${memo.id}-${memo.createdTs}`} memo={memo} readonly={true} />;
|
||||||
return (
|
|
||||||
<div className={`memo-container ${memo.pinned ? "pinned" : ""}`} key={memo.id}>
|
|
||||||
{memo.pinned && <div className="corner-container"></div>}
|
|
||||||
<div className="memo-header">
|
|
||||||
<span className="time-text">{createdAtStr}</span>
|
|
||||||
<a className="name-text" href={`/u/${memo.creatorId}`}>
|
|
||||||
@{memo.creatorName}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<MemoContent className="memo-content" content={memo.content} onMemoContentClick={handleMemoContentClick} />
|
|
||||||
<MemoResources resourceList={memo.resourceList} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
{isComplete ? (
|
{isComplete ? (
|
||||||
state.memos.length === 0 ? (
|
state.memos.length === 0 ? (
|
||||||
<p className="w-full text-center mt-12 text-gray-600">{t("message.no-memos")}</p>
|
<p className="w-full text-center mt-12 text-gray-600">{t("message.no-memos")}</p>
|
||||||
) : null
|
) : null
|
||||||
) : (
|
) : (
|
||||||
<p
|
<p className="m-auto text-center mt-4 italic cursor-pointer text-gray-500 hover:text-green-600" onClick={handleFetchMoreClick}>
|
||||||
className="m-auto text-center mt-4 italic cursor-pointer text-gray-500 hover:text-green-600"
|
|
||||||
onClick={handleFetchMoreClick}
|
|
||||||
>
|
|
||||||
{t("memo-list.fetch-more")}
|
{t("memo-list.fetch-more")}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -3,12 +3,13 @@ import { useTranslation } from "react-i18next";
|
|||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
import { useGlobalStore, useUserStore } from "../store/module";
|
import { useGlobalStore, useUserStore } from "../store/module";
|
||||||
import toastHelper from "../components/Toast";
|
import toastHelper from "../components/Toast";
|
||||||
import Sidebar from "../components/Sidebar";
|
import Header from "../components/Header";
|
||||||
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";
|
||||||
import MemoList from "../components/MemoList";
|
import MemoList from "../components/MemoList";
|
||||||
import UpdateVersionBanner from "../components/UpdateVersionBanner";
|
import UpdateVersionBanner from "../components/UpdateVersionBanner";
|
||||||
|
import MobileHeader from "../components/MobileHeader";
|
||||||
|
import HomeSidebar from "../components/HomeSidebar";
|
||||||
import "../less/home.less";
|
import "../less/home.less";
|
||||||
|
|
||||||
function Home() {
|
function Home() {
|
||||||
@ -40,28 +41,16 @@ function Home() {
|
|||||||
<UpdateVersionBanner />
|
<UpdateVersionBanner />
|
||||||
</div>
|
</div>
|
||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
<Sidebar />
|
<Header />
|
||||||
<main className="memos-wrapper">
|
<main className="memos-wrapper">
|
||||||
<MemosHeader />
|
<MobileHeader />
|
||||||
<div className="memos-editor-wrapper">
|
<div className="memos-editor-wrapper">
|
||||||
{!userStore.isVisitorMode() && <MemoEditor />}
|
{!userStore.isVisitorMode() && <MemoEditor />}
|
||||||
<MemoFilter />
|
<MemoFilter />
|
||||||
</div>
|
</div>
|
||||||
<MemoList />
|
<MemoList />
|
||||||
{userStore.isVisitorMode() && (
|
|
||||||
<div className="addition-btn-container">
|
|
||||||
{user ? (
|
|
||||||
<button className="btn" onClick={() => (window.location.href = "/")}>
|
|
||||||
<span className="icon">🏠</span> {t("common.back-to-home")}
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<button className="btn" onClick={() => (window.location.href = "/auth")}>
|
|
||||||
<span className="icon">👉</span> {t("common.sign-in")}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
|
{!userStore.isVisitorMode() && <HomeSidebar />}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
Reference in New Issue
Block a user