feat: update home layout (#1242)

This commit is contained in:
boojack
2023-03-04 13:49:53 +08:00
committed by GitHub
parent 9d4bb5b3af
commit 6ab58f294e
26 changed files with 610 additions and 721 deletions

View File

@ -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)}>

View 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;

View 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;

View File

@ -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()}`}

View File

@ -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;

View 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;

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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)} />

View File

@ -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;

View File

@ -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>
); );

View File

@ -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,72 +38,50 @@ 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" trigger={
trigger={ <div className="px-2 py-1 max-w-full flex flex-row justify-start items-center cursor-pointer rounded hover:shadow hover:bg-white dark:hover:bg-zinc-700">
<div className="px-2 py-1 max-w-full flex flex-row justify-start items-center cursor-pointer rounded hover:shadow hover:bg-white dark:hover:bg-zinc-700"> <UserAvatar avatarUrl={user?.avatarUrl} />
<UserAvatar avatarUrl={user?.avatarUrl} /> <span className="px-1 text-lg font-medium text-slate-800 dark:text-gray-200 shrink truncate">{username}</span>
<span className="px-1 text-lg font-medium text-slate-800 dark:text-gray-200 shrink truncate">{username}</span> {!isVisitorMode && user?.role === "HOST" ? (
{!isVisitorMode && user?.role === "HOST" ? ( <span className="text-xs px-1 bg-blue-600 dark:bg-blue-800 rounded text-white dark:text-gray-200 shadow">MOD</span>
<span className="text-xs px-1 bg-blue-600 dark:bg-blue-800 rounded text-white dark:text-gray-200 shadow">MOD</span> ) : null}
) : null} </div>
</div> }
} actionsClassName="min-w-[128px] max-w-full"
actionsClassName="min-w-[128px] max-w-full" positionClassName="top-full mt-2"
positionClassName="top-full mt-2" actions={
actions={ <>
<> {!userStore.isVisitorMode() && (
{!userStore.isVisitorMode() && ( <>
<>
<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={handleMyAccountClick}
>
<span className="mr-1">🤠</span> {t("setting.my-account")}
</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
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={handleAboutBtnClick}
>
<span className="mr-1">🏂</span> {t("common.about")}
</button>
{!userStore.isVisitorMode() && (
<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" 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={handleSignOutBtnClick} onClick={handleMyAccountClick}
> >
<span className="mr-1">👋</span> {t("common.sign-out")} <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"
</div> onClick={handleAboutBtnClick}
<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="mr-1">🏂</span> {t("common.about")}
<span className="font-bold text-2xl opacity-80 leading-10 text-slate-600 dark:text-gray-300">{memoAmount}</span> </button>
<span className="text-gray-400 text-xs font-mono">{t("amount-text.memo", { count: memoAmount })}</span> {!userStore.isVisitorMode() && (
</div> <button
<div className="flex flex-col justify-start items-start"> 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"
<span className="font-bold text-2xl opacity-80 leading-10 text-slate-600 dark:text-gray-300">{tags.length}</span> onClick={handleSignOutBtnClick}
<span className="text-gray-400 text-xs font-mono">{t("amount-text.tag", { count: tags.length })}</span> >
</div> <span className="mr-1">👋</span> {t("common.sign-out")}
<div className="flex flex-col justify-start items-start"> </button>
<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> />
</> </div>
); );
}; };

View File

@ -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 {

View File

@ -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;

View File

@ -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;
}
}
}
}
}
}

View File

@ -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;

View 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;
}
}

View File

@ -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;
}
}
}
} }
} }
} }

View File

@ -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;

View File

@ -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;
}
}

View File

@ -1,5 +1,5 @@
.search-bar-container { .search-bar-container {
@apply relative w-auto; @apply relative w-full;
&:hover, &:hover,
&:active { &:active {

View File

@ -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."
}, },

View File

@ -1,271 +1,267 @@
{ {
"common": { "common": {
"about": "关于", "about": "关于",
"email": "邮箱", "home": "主页",
"password": "密码", "resources": "资源库",
"repeat-password-short": "重复密码", "settings": "设置",
"repeat-password": "重复密码", "daily-review": "每日回顾",
"new-password": "新密码", "email": "邮箱",
"repeat-new-password": "重复新密码", "password": "密码",
"username": "用户名", "username": "用户名",
"nickname": "昵称", "nickname": "昵称",
"save": "保存", "new-password": "新密码",
"close": "关闭", "repeat-new-password": "重复新密码",
"cancel": "退出", "save": "保存",
"create": "创建", "close": "关闭",
"change": "修改", "cancel": "退出",
"confirm": "确定", "create": "创建",
"reset": "重置", "change": "修改",
"language": "语言", "confirm": "确定",
"version": "版本", "reset": "重置",
"pin": "置顶", "language": "语言",
"unpin": "取消置顶", "version": "版本",
"edit": "编辑", "pin": "置顶",
"restore": "恢复", "unpin": "取消置顶",
"delete": "删除", "edit": "编辑",
"null": "", "restore": "恢复",
"share": "分享", "delete": "删除",
"archive": "归档", "null": "",
"basic": "基础", "share": "分享",
"admin": "管理员", "archive": "归档",
"explore": "探索", "basic": "基础",
"sign-in": "登录", "admin": "管理员",
"sign-up": "注册", "explore": "探索",
"sign-out": "退出登录", "sign-in": "登录",
"back-to-home": "回到主页", "sign-up": "注册",
"type": "类型", "sign-out": "退出登录",
"shortcuts": "捷径", "back-to-home": "回到主页",
"title": "标题", "type": "类型",
"filter": "过滤器", "shortcuts": "捷径",
"tags": "全部标签", "title": "标题",
"yourself": "你自己", "filter": "过滤器",
"archived-at": "归档于", "tags": "全部标签",
"changed": "已更改", "yourself": "你自己",
"update-on": "更新于", "archived-at": "归档于",
"fold": "折叠", "changed": "已更改",
"expand": "展开", "update-on": "更新于",
"image": "图片", "fold": "折叠",
"link": "链接", "expand": "展开",
"vacuum": "清理", "image": "图片",
"select": "选择", "link": "链接",
"database": "数据库", "vacuum": "清理",
"avatar": "头像" "select": "选择",
}, "database": "数据库",
"slogan": "一个可用于知识管理和社交网络的开源、自托管的备忘录中心。", "avatar": "头像"
"auth": { },
"signup-as-host": "注册为 Host", "slogan": "一个可用于知识管理和社交网络的开源、自托管的备忘录中心。",
"host-tip": "你正在注册为 Host 用户账号。", "auth": {
"not-host-tip": "如果你没有账号,请联系站点 Host" "signup-as-host": "注册为 Host",
}, "host-tip": "你正在注册为 Host 用户账号。",
"sidebar": { "not-host-tip": "如果你没有账号,请联系站点 Host"
"daily-review": "每日回顾", },
"resources": "资源库", "daily-review": {
"setting": "设置", "oops-nothing": "啊哦,空空荡荡。"
"archived": "已归档" },
}, "resources": {
"daily-review": { "description": "查看在 Memo 中的静态资源。例如:图片",
"oops-nothing": "啊哦,空空荡荡。" "no-resources": "没有资源",
}, "fetching-data": "请求数据中...",
"resources": { "upload": "上传",
"description": "查看在 Memo 中的静态资源。例如:图片", "preview": "预览",
"no-resources": "没有资源", "copy-link": "拷贝链接",
"fetching-data": "请求数据中...", "delete-resource": "删除资源",
"upload": "上传", "warning-text": "确定删除这个资源么?此操作不可逆❗",
"preview": "预览", "linked-amount": "链接的 Memo 数量",
"copy-link": "拷贝链接", "rename": "重命名",
"delete-resource": "删除资源", "clear": "清除",
"warning-text": "确定删除这资源么?此操作不可逆❗", "warning-text-unused": "确定删除这些无用资源么?此操作不可逆❗",
"linked-amount": "链接的 Memo 数量", "no-unused-resources": "无可删除的资源",
"rename": "重命名", "name": "资源名称"
"clear": "清除", },
"warning-text-unused": "确定删除这些无用资源么?此操作不可逆❗", "archived": {
"no-unused-resources": "无可删除的资源", "archived-memos": "已归档的 Memo",
"name": "资源名称" "no-archived-memos": "没有归档的 Memo",
}, "fetching-data": "请求数据中..."
"archived": { },
"archived-memos": "已归档的 Memo", "editor": {
"no-archived-memos": "没有归档的 Memo", "editing": "编辑中...",
"fetching-data": "请求数据中..." "cancel-edit": "退出编辑",
}, "save": "记下",
"editor": { "placeholder": "现在的想法是...",
"editing": "编辑中...", "only-image-supported": "仅支持图片文件。",
"cancel-edit": "退出编辑", "cant-empty": "内容不能为空",
"save": "记下", "local": "本地上传",
"placeholder": "现在的想法是...", "resources": "资源库"
"only-image-supported": "仅支持图片文件。", },
"cant-empty": "内容不能为空", "memo": {
"local": "本地上传", "view-detail": "查看详情",
"resources": "资源库" "copy": "复制",
}, "visibility": {
"memo": { "private": "仅自己可见",
"view-detail": "查看详情", "protected": "登录用户可见",
"copy": "复制", "public": "所有人可见",
"visibility": { "disabled": "公共memos已禁用"
"private": "仅自己可见",
"protected": "登录用户可见",
"public": "所有人可见",
"disabled": "公共memos已禁用"
}
},
"memo-list": {
"fetching-data": "请求数据中...",
"fetch-more": "点击加载更多"
},
"shortcut-list": {
"shortcut-title": "捷径标题",
"create-shortcut": "创建捷径",
"edit-shortcut": "编辑捷径",
"eligible-memo": "符合条件的 Memo",
"fill-previous": "请填写之前的过滤值",
"title-required": "标题是必填项",
"value-required": "过滤值是必填项"
},
"filter": {
"new-filter": "新建过滤器",
"type": {
"tag": "标签",
"type": "类型",
"text": "文本",
"display-time": "显示时间",
"visibility": "可见性"
},
"operator": {
"contains": "包含",
"not-contains": "不包含",
"is": "是",
"is-not": "不是",
"before": "早于",
"after": "晚于"
},
"value": {
"not-tagged": "无标签",
"linked": "包含链接"
},
"text-placeholder": "以 ^ 开头使用正则表达式"
},
"tag-list": {
"tip-text": "输入`#tag `来创建标签"
},
"search": {
"quickly-filter": "快速过滤"
},
"setting": {
"my-account": "我的账号",
"preference": "偏好设置",
"storage": "存储设置",
"member": "成员",
"member-list": "成员列表",
"system": "系统",
"account-section": {
"title": "账号信息",
"update-information": "更新个人信息",
"change-password": "修改密码"
},
"preference-section": {
"theme": "主题",
"default-memo-visibility": "默认 Memo 可见性",
"enable-folding-memo": "开启折叠 Memo",
"enable-double-click": "开启双击编辑",
"editor-font-style": "编辑器字体样式",
"mobile-editor-style": "移动端编辑器样式",
"default-memo-sort-option": "Memo 显示时间",
"created_ts": "创建时间",
"updated_ts": "更新时间"
},
"storage-section": {
"storage-services-list": "存储服务列表",
"create-a-service": "新建服务",
"update-a-service": "更新服务",
"delete-storage": "删除存储服务",
"warning-text": "确定删除这个存储服务么?此操作不可逆❗"
},
"member-section": {
"create-a-member": "创建成员"
},
"system-section": {
"server-name": "服务名称",
"customize-server": {
"title": "自定义服务",
"default": "默认为 memos",
"icon-url": "图标 URL"
},
"database-file-size": "数据库文件大小",
"allow-user-signup": "允许用户注册",
"additional-style": "自定义样式",
"additional-script": "自定义脚本",
"additional-style-placeholder": "自定义 CSS 代码",
"additional-script-placeholder": "自定义 JavaScript 代码",
"disable-public-memos": "禁用公共memos"
},
"apperance-option": {
"system": "跟随系统",
"light": "总是浅色",
"dark": "总是深色"
},
"sso": "SSO"
},
"amount-text": {
"memo_other": "MEMOS",
"tag_other": "TAGS",
"day_other": "DAYS",
"memo_one": "MEMO",
"tag_one": "TAG",
"day_one": "DAY"
},
"message": {
"no-memos": "没有 Memo 了 🌃",
"memos-ready": "所有 Memo 已就绪 🎉",
"restored-successfully": "恢复成功",
"memo-updated-datetime": "Memo 创建日期时间已更改。",
"invalid-created-datetime": "创建的日期时间无效。",
"change-memo-created-time": "更改 Memo 创建时间",
"memo-not-found": "找不到 Memo。",
"fill-all": "请填写所有字段。",
"password-not-match": "密码不一致。",
"new-password-not-match": "新密码不匹配。",
"image-load-failed": "图片加载失败",
"fill-form": "请填写此表单",
"login-failed": "登录失败",
"signup-failed": "注册失败",
"user-not-found": "未找到用户",
"password-changed": "密码已修改",
"private-only": "此 Memo 仅自己可见",
"copied": "已复制",
"succeed-copy-content": "复制内容到剪贴板成功。",
"succeed-copy-code": "复制代码到剪贴板成功。",
"succeed-copy-link": "复制链接到剪贴板成功。",
"change-resource-filename": "更改资源文件名",
"resource-filename-updated": "资源文件名更改成功。",
"invalid-resource-filename": "无效的资源文件名",
"click-to-save-the-image": "点击保存图片",
"generating-the-screenshot": "正在生成图片...",
"count-selected-resources": "所选资源总数",
"too-short": "过短",
"too-long": "过长",
"not-allow-space": "不允许包含空格",
"not-allow-chinese": "不允许包含中文",
"succeed-update-additional-style": "更新附加样式成功",
"succeed-copy-resource-link": "复制资源链接到剪贴板成功",
"succeed-update-customized-profile": "更新自定义配置文件成功",
"succeed-update-additional-script": "更新附加脚本成功",
"update-succeed": "更新成功",
"page-not-found": "404 - 未找到网页 😥",
"succeed-vacuum-database": "清理数据库成功"
},
"days": {
"monday": "星期一",
"mon": "一",
"tuesday": "星期二",
"tue": "二",
"wednesday": "星期三",
"wed": "三",
"thursday": "星期四",
"thu": "四",
"friday": "星期五",
"fri": "五",
"saturday": "星期六",
"sat": "六",
"sunday": "星期天",
"sun": "日"
} }
},
"memo-list": {
"fetching-data": "请求数据中...",
"fetch-more": "点击加载更多"
},
"shortcut-list": {
"shortcut-title": "捷径标题",
"create-shortcut": "创建捷径",
"edit-shortcut": "编辑捷径",
"eligible-memo": "符合条件的 Memo",
"fill-previous": "请填写之前的过滤值",
"title-required": "标题是必填项",
"value-required": "过滤值是必填项"
},
"filter": {
"new-filter": "新建过滤器",
"type": {
"tag": "标签",
"type": "类型",
"text": "文本",
"display-time": "显示时间",
"visibility": "可见性"
},
"operator": {
"contains": "包含",
"not-contains": "不包含",
"is": "是",
"is-not": "不是",
"before": "早于",
"after": "晚于"
},
"value": {
"not-tagged": "无标签",
"linked": "包含链接"
},
"text-placeholder": "以 ^ 开头使用正则表达式"
},
"tag-list": {
"tip-text": "输入`#tag `来创建标签"
},
"search": {
"quickly-filter": "快速过滤"
},
"setting": {
"my-account": "我的账号",
"preference": "偏好设置",
"storage": "存储设置",
"member": "成员",
"member-list": "成员列表",
"system": "系统",
"account-section": {
"title": "账号信息",
"update-information": "更新个人信息",
"change-password": "修改密码"
},
"preference-section": {
"theme": "主题",
"default-memo-visibility": "默认 Memo 可见性",
"enable-folding-memo": "开启折叠 Memo",
"enable-double-click": "开启双击编辑",
"editor-font-style": "编辑器字体样式",
"mobile-editor-style": "移动端编辑器样式",
"default-memo-sort-option": "Memo 显示时间",
"created_ts": "创建时间",
"updated_ts": "更新时间"
},
"storage-section": {
"storage-services-list": "存储服务列表",
"create-a-service": "新建服务",
"update-a-service": "更新服务",
"delete-storage": "删除存储服务",
"warning-text": "确定删除这个存储服务么?此操作不可逆❗"
},
"member-section": {
"create-a-member": "创建成员"
},
"system-section": {
"server-name": "服务名称",
"customize-server": {
"title": "自定义服务",
"default": "默认为 memos",
"icon-url": "图标 URL"
},
"database-file-size": "数据库文件大小",
"allow-user-signup": "允许用户注册",
"additional-style": "自定义样式",
"additional-script": "自定义脚本",
"additional-style-placeholder": "自定义 CSS 代码",
"additional-script-placeholder": "自定义 JavaScript 代码",
"disable-public-memos": "禁用公共memos"
},
"apperance-option": {
"system": "跟随系统",
"light": "总是浅色",
"dark": "总是深色"
},
"sso": "SSO"
},
"amount-text": {
"memo_other": "MEMOS",
"tag_other": "TAGS",
"day_other": "DAYS",
"memo_one": "MEMO",
"tag_one": "TAG",
"day_one": "DAY"
},
"message": {
"no-memos": "没有 Memo 了 🌃",
"memos-ready": "所有 Memo 已就绪 🎉",
"restored-successfully": "恢复成功",
"memo-updated-datetime": "Memo 创建日期时间已更改。",
"invalid-created-datetime": "创建的日期时间无效。",
"change-memo-created-time": "更改 Memo 创建时间",
"memo-not-found": "找不到 Memo。",
"fill-all": "请填写所有字段。",
"password-not-match": "密码不一致。",
"new-password-not-match": "新密码不匹配。",
"image-load-failed": "图片加载失败",
"fill-form": "请填写此表单",
"login-failed": "登录失败",
"signup-failed": "注册失败",
"user-not-found": "未找到用户",
"password-changed": "密码已修改",
"private-only": "此 Memo 仅自己可见",
"copied": "已复制",
"succeed-copy-content": "复制内容到剪贴板成功。",
"succeed-copy-code": "复制代码到剪贴板成功。",
"succeed-copy-link": "复制链接到剪贴板成功。",
"change-resource-filename": "更改资源文件名",
"resource-filename-updated": "资源文件名更改成功。",
"invalid-resource-filename": "无效的资源文件名",
"click-to-save-the-image": "点击保存图片",
"generating-the-screenshot": "正在生成图片...",
"count-selected-resources": "所选资源总数",
"too-short": "过短",
"too-long": "过长",
"not-allow-space": "不允许包含空格",
"not-allow-chinese": "不允许包含中文",
"succeed-update-additional-style": "更新附加样式成功",
"succeed-copy-resource-link": "复制资源链接到剪贴板成功",
"succeed-update-customized-profile": "更新自定义配置文件成功",
"succeed-update-additional-script": "更新附加脚本成功",
"update-succeed": "更新成功",
"page-not-found": "404 - 未找到网页 😥",
"succeed-vacuum-database": "清理数据库成功"
},
"days": {
"monday": "星期一",
"mon": "一",
"tuesday": "星期二",
"tue": "二",
"wednesday": "星期三",
"wed": "三",
"thursday": "星期四",
"thu": "四",
"friday": "星期五",
"fri": "五",
"saturday": "星期六",
"sat": "六",
"sunday": "星期天",
"sun": "日"
}
} }

View File

@ -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,58 +97,40 @@ 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 className="flex flex-row justify-center items-center h-12 w-12 border rounded-full hover:opacity-80 hover:shadow dark:text-white "
className="flex flex-row justify-center items-center h-12 w-12 border rounded-full hover:opacity-80 hover:shadow dark:text-white " href="/explore/rss.xml"
href="/explore/rss.xml" target="_blank"
target="_blank" rel="noreferrer"
rel="noreferrer" >
> <Icon.Rss className="w-7 h-auto opacity-60" />
<Icon.Rss className="w-7 h-auto opacity-60" /> </a>
</a>
</div>
</div> </div>
{!loadingState.isLoading && (
<main className="memos-wrapper">
<MemoFilter />
{sortedMemos.map((memo) => {
const createdAtStr = dayjs(memo.createdTs).locale(i18n.language).format("YYYY/MM/DD HH:mm:ss");
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 ? (
state.memos.length === 0 ? (
<p className="w-full text-center mt-12 text-gray-600">{t("message.no-memos")}</p>
) : null
) : (
<p
className="m-auto text-center mt-4 italic cursor-pointer text-gray-500 hover:text-green-600"
onClick={handleFetchMoreClick}
>
{t("memo-list.fetch-more")}
</p>
)}
</main>
)}
</div> </div>
{!loadingState.isLoading && (
<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 />
{sortedMemos.map((memo) => {
return <Memo key={`${memo.id}-${memo.createdTs}`} memo={memo} readonly={true} />;
})}
{isComplete ? (
state.memos.length === 0 ? (
<p className="w-full text-center mt-12 text-gray-600">{t("message.no-memos")}</p>
) : null
) : (
<p className="m-auto text-center mt-4 italic cursor-pointer text-gray-500 hover:text-green-600" onClick={handleFetchMoreClick}>
{t("memo-list.fetch-more")}
</p>
)}
</main>
)}
</section> </section>
); );
}; };

View File

@ -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>
); );