mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
chore: update navigator
This commit is contained in:
@@ -1,12 +1,10 @@
|
||||
import dayjs from "dayjs";
|
||||
import { isEqual } from "lodash-es";
|
||||
import toast from "react-hot-toast";
|
||||
import { cn } from "@/utils";
|
||||
|
||||
// Helper function to convert Date to local datetime string.
|
||||
const toLocalDateTimeString = (date: Date | undefined): string => {
|
||||
if (!date) return "";
|
||||
return dayjs(date).format("YYYY-MM-DDTHH:mm:ss");
|
||||
return date?.toLocaleString() || "";
|
||||
};
|
||||
|
||||
interface Props {
|
||||
@@ -20,7 +18,7 @@ const DateTimeInput: React.FC<Props> = ({ value, originalValue, onChange }) => {
|
||||
<input
|
||||
type="text"
|
||||
className={cn(
|
||||
"w-auto px-1 bg-transparent rounded text-xs transition-all",
|
||||
"px-1 bg-transparent rounded text-xs transition-all",
|
||||
"border-transparent outline-none focus:border-gray-300 dark:focus:border-zinc-700",
|
||||
!isEqual(value, originalValue) && "border-gray-300 dark:border-zinc-700",
|
||||
"border",
|
||||
|
@@ -1,50 +1,25 @@
|
||||
import { last } from "lodash-es";
|
||||
import { Globe2Icon, HomeIcon } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { matchPath, NavLink, useLocation } from "react-router-dom";
|
||||
import { matchPath, useLocation } from "react-router-dom";
|
||||
import useDebounce from "react-use/lib/useDebounce";
|
||||
import SearchBar from "@/components/SearchBar";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { Routes } from "@/router";
|
||||
import { memoStore, userStore } from "@/store/v2";
|
||||
import { cn } from "@/utils";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import MemoFilters from "../MemoFilters";
|
||||
import StatisticsView from "../StatisticsView";
|
||||
import ShortcutsSection from "./ShortcutsSection";
|
||||
import TagsSection from "./TagsSection";
|
||||
|
||||
interface NavLinkItem {
|
||||
id: string;
|
||||
path: string;
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const HomeSidebar = observer((props: Props) => {
|
||||
const t = useTranslate();
|
||||
const location = useLocation();
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
const homeNavLink: NavLinkItem = {
|
||||
id: "header-home",
|
||||
path: Routes.ROOT,
|
||||
title: t("common.home"),
|
||||
icon: <HomeIcon className="w-4 h-auto opacity-70 shrink-0" />,
|
||||
};
|
||||
const exploreNavLink: NavLinkItem = {
|
||||
id: "header-explore",
|
||||
path: Routes.EXPLORE,
|
||||
title: t("common.explore"),
|
||||
icon: <Globe2Icon className="w-4 h-auto opacity-70 shrink-0" />,
|
||||
};
|
||||
|
||||
const navLinks: NavLinkItem[] = currentUser ? [homeNavLink, exploreNavLink] : [exploreNavLink];
|
||||
|
||||
useDebounce(
|
||||
async () => {
|
||||
let parent: string | undefined = undefined;
|
||||
@@ -65,30 +40,7 @@ const HomeSidebar = observer((props: Props) => {
|
||||
return (
|
||||
<aside className={cn("relative w-full h-full overflow-auto flex flex-col justify-start items-start", props.className)}>
|
||||
<SearchBar />
|
||||
<div className="mt-2 w-full space-y-1">
|
||||
{navLinks.map((navLink) => (
|
||||
<NavLink
|
||||
key={navLink.id}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"w-full px-2 rounded-xl border flex flex-row items-center justify-between text-sm text-zinc-600 dark:text-gray-400 hover:bg-white hover:border-gray-200 dark:hover:border-zinc-700 dark:hover:bg-zinc-800",
|
||||
isActive ? "bg-white drop-shadow-sm dark:bg-zinc-800 border-gray-200 dark:border-zinc-700" : "border-transparent",
|
||||
)
|
||||
}
|
||||
to={navLink.path}
|
||||
viewTransition
|
||||
>
|
||||
<div className="flex flex-row items-center">
|
||||
{navLink.icon}
|
||||
<span className="ml-2 truncate leading-8">{navLink.title}</span>
|
||||
</div>
|
||||
{navLink.path === Routes.ROOT && currentUser && userStore.state.currentUserStats && (
|
||||
<span className="font-mono text-xs opacity-80">{userStore.state.currentUserStats.totalMemoCount}</span>
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
<div className="px-2 w-full">
|
||||
<div className="mt-1 px-1 w-full">
|
||||
<StatisticsView />
|
||||
<MemoFilters />
|
||||
{currentUser && <ShortcutsSection />}
|
||||
|
@@ -548,7 +548,7 @@ const MemoEditor = observer((props: Props) => {
|
||||
|
||||
{/* Show memo metadata if memoName is provided */}
|
||||
{memoName && (
|
||||
<div className="w-full mb-4 text-xs leading-5 px-4 opacity-60 font-mono text-gray-500 dark:text-zinc-500">
|
||||
<div className="w-full -mt-1 mb-4 text-xs leading-5 px-4 opacity-60 font-mono text-gray-500 dark:text-zinc-500">
|
||||
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-0.5 items-center">
|
||||
{!isEqual(createTime, updateTime) && (
|
||||
<>
|
||||
|
@@ -1,12 +1,11 @@
|
||||
import { Tooltip } from "@mui/joy";
|
||||
import { BellIcon, PaperclipIcon, SettingsIcon, UserCircleIcon } from "lucide-react";
|
||||
import { EarthIcon, LibraryIcon, PaperclipIcon, UserCircleIcon } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect } from "react";
|
||||
import { NavLink } from "react-router-dom";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { Routes } from "@/router";
|
||||
import { userStore } from "@/store/v2";
|
||||
import { Inbox_Status } from "@/types/proto/api/v1/inbox_service";
|
||||
import { cn } from "@/utils";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import BrandBanner from "./BrandBanner";
|
||||
@@ -28,7 +27,6 @@ const Navigation = observer((props: Props) => {
|
||||
const { collapsed, className } = props;
|
||||
const t = useTranslate();
|
||||
const currentUser = useCurrentUser();
|
||||
const hasUnreadInbox = userStore.state.inboxes.some((inbox) => inbox.status === Inbox_Status.UNREAD);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentUser) {
|
||||
@@ -38,31 +36,24 @@ const Navigation = observer((props: Props) => {
|
||||
userStore.fetchInboxes();
|
||||
}, []);
|
||||
|
||||
const homeNavLink: NavLinkItem = {
|
||||
id: "header-memos",
|
||||
path: Routes.ROOT,
|
||||
title: t("common.memos"),
|
||||
icon: <LibraryIcon className="w-6 h-auto opacity-70 shrink-0" />,
|
||||
};
|
||||
const exploreNavLink: NavLinkItem = {
|
||||
id: "header-explore",
|
||||
path: Routes.EXPLORE,
|
||||
title: t("common.explore"),
|
||||
icon: <EarthIcon className="w-6 h-auto opacity-70 shrink-0" />,
|
||||
};
|
||||
const resourcesNavLink: NavLinkItem = {
|
||||
id: "header-resources",
|
||||
path: Routes.RESOURCES,
|
||||
title: t("common.resources"),
|
||||
icon: <PaperclipIcon className="w-6 h-auto opacity-70 shrink-0" />,
|
||||
};
|
||||
const inboxNavLink: NavLinkItem = {
|
||||
id: "header-inbox",
|
||||
path: Routes.INBOX,
|
||||
title: t("common.inbox"),
|
||||
icon: (
|
||||
<>
|
||||
<div className="relative">
|
||||
<BellIcon className="w-6 h-auto opacity-70 shrink-0" />
|
||||
{hasUnreadInbox && <div className="absolute top-0 left-5 w-2 h-2 rounded-full bg-blue-500"></div>}
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
};
|
||||
const settingNavLink: NavLinkItem = {
|
||||
id: "header-setting",
|
||||
path: Routes.SETTING,
|
||||
title: t("common.settings"),
|
||||
icon: <SettingsIcon className="w-6 h-auto opacity-70 shrink-0" />,
|
||||
};
|
||||
const signInNavLink: NavLinkItem = {
|
||||
id: "header-auth",
|
||||
path: Routes.AUTH,
|
||||
@@ -70,7 +61,7 @@ const Navigation = observer((props: Props) => {
|
||||
icon: <UserCircleIcon className="w-6 h-auto opacity-70 shrink-0" />,
|
||||
};
|
||||
|
||||
const navLinks: NavLinkItem[] = currentUser ? [resourcesNavLink, inboxNavLink, settingNavLink] : [signInNavLink];
|
||||
const navLinks: NavLinkItem[] = currentUser ? [homeNavLink, exploreNavLink, resourcesNavLink] : [exploreNavLink, signInNavLink];
|
||||
|
||||
return (
|
||||
<header
|
||||
@@ -80,7 +71,7 @@ const Navigation = observer((props: Props) => {
|
||||
)}
|
||||
>
|
||||
<div className="w-full px-1 py-1 flex flex-col justify-start items-start space-y-2 overflow-auto hide-scrollbar shrink">
|
||||
<NavLink className="mb-2" to={currentUser ? Routes.ROOT : Routes.EXPLORE}>
|
||||
<NavLink className="mb-2 cursor-default" to={currentUser ? Routes.ROOT : Routes.EXPLORE}>
|
||||
<BrandBanner collapsed={collapsed} />
|
||||
</NavLink>
|
||||
{navLinks.map((navLink) => (
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import dayjs from "dayjs";
|
||||
import { CalendarIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import i18n from "@/i18n";
|
||||
import type { MonthNavigatorProps } from "@/types/statistics";
|
||||
|
||||
@@ -16,15 +16,14 @@ export const MonthNavigator = ({ visibleMonth, onMonthChange }: MonthNavigatorPr
|
||||
|
||||
return (
|
||||
<div className="w-full mb-1 flex flex-row justify-between items-center gap-1">
|
||||
<div className="relative text-sm inline-flex flex-row items-center w-auto gap-2 dark:text-gray-400">
|
||||
<CalendarIcon className="w-4 h-auto opacity-70 ml-px" />
|
||||
<span className="relative text-sm dark:text-gray-400">
|
||||
{currentMonth.toLocaleString(i18n.language, { year: "numeric", month: "long" })}
|
||||
</div>
|
||||
</span>
|
||||
<div className="flex justify-end items-center shrink-0 gap-1">
|
||||
<button className="p-1 cursor-pointer hover:opacity-80 transition-opacity" onClick={handlePrevMonth} aria-label="Previous month">
|
||||
<button className="cursor-pointer hover:opacity-80 transition-opacity" onClick={handlePrevMonth} aria-label="Previous month">
|
||||
<ChevronLeftIcon className="w-5 h-auto shrink-0 opacity-40" />
|
||||
</button>
|
||||
<button className="p-1 cursor-pointer hover:opacity-80 transition-opacity" onClick={handleNextMonth} aria-label="Next month">
|
||||
<button className="cursor-pointer hover:opacity-80 transition-opacity" onClick={handleNextMonth} aria-label="Next month">
|
||||
<ChevronRightIcon className="w-5 h-auto shrink-0 opacity-40" />
|
||||
</button>
|
||||
</div>
|
||||
|
@@ -14,9 +14,9 @@ export const StatCard = ({ icon, label, count, onClick, tooltip, className }: St
|
||||
>
|
||||
<div className="w-auto flex justify-start items-center mr-1">
|
||||
{icon}
|
||||
<span className="block text-sm">{label}</span>
|
||||
<span className="block text-xs opacity-80">{label}</span>
|
||||
</div>
|
||||
<span className="text-sm truncate">{count}</span>
|
||||
<span className="text-xs truncate opacity-80">{count}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@@ -49,7 +49,7 @@ const StatisticsView = observer(() => {
|
||||
<div className="pt-1 w-full flex flex-row justify-start items-center gap-1 flex-wrap">
|
||||
{isRootPath && hasPinnedMemos && (
|
||||
<StatCard
|
||||
icon={<BookmarkIcon className="w-4 h-auto mr-1" />}
|
||||
icon={<BookmarkIcon className="w-3 h-auto mr-1 opacity-70" />}
|
||||
label={t("common.pinned")}
|
||||
count={userStore.state.currentUserStats!.pinnedMemos.length}
|
||||
onClick={() => handleFilterClick("pinned")}
|
||||
@@ -57,7 +57,7 @@ const StatisticsView = observer(() => {
|
||||
)}
|
||||
|
||||
<StatCard
|
||||
icon={<LinkIcon className="w-4 h-auto mr-1" />}
|
||||
icon={<LinkIcon className="w-3 h-auto mr-1 opacity-70" />}
|
||||
label={t("memo.links")}
|
||||
count={memoTypeStats.linkCount}
|
||||
onClick={() => handleFilterClick("property.hasLink")}
|
||||
@@ -65,7 +65,11 @@ const StatisticsView = observer(() => {
|
||||
|
||||
<StatCard
|
||||
icon={
|
||||
memoTypeStats.undoCount > 0 ? <ListTodoIcon className="w-4 h-auto mr-1" /> : <CheckCircleIcon className="w-4 h-auto mr-1" />
|
||||
memoTypeStats.undoCount > 0 ? (
|
||||
<ListTodoIcon className="w-3 h-auto mr-1 opacity-70" />
|
||||
) : (
|
||||
<CheckCircleIcon className="w-3 h-auto mr-1 opacity-70" />
|
||||
)
|
||||
}
|
||||
label={t("memo.to-do")}
|
||||
count={
|
||||
@@ -84,7 +88,7 @@ const StatisticsView = observer(() => {
|
||||
/>
|
||||
|
||||
<StatCard
|
||||
icon={<Code2Icon className="w-4 h-auto mr-1" />}
|
||||
icon={<Code2Icon className="w-3 h-auto mr-1 opacity-70" />}
|
||||
label={t("memo.code")}
|
||||
count={memoTypeStats.codeCount}
|
||||
onClick={() => handleFilterClick("property.hasCode")}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Dropdown, Menu, MenuButton, MenuItem } from "@mui/joy";
|
||||
import { ArchiveIcon, LogOutIcon, User2Icon, SquareUserIcon } from "lucide-react";
|
||||
import { ArchiveIcon, LogOutIcon, User2Icon, SquareUserIcon, SettingsIcon, BellIcon } from "lucide-react";
|
||||
import { authServiceClient } from "@/grpcweb";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
@@ -45,7 +45,7 @@ const UserBanner = (props: Props) => {
|
||||
)}
|
||||
</div>
|
||||
</MenuButton>
|
||||
<Menu placement="bottom-start" style={{ zIndex: "9999" }}>
|
||||
<Menu size="sm" placement="bottom-start" style={{ zIndex: "9999" }}>
|
||||
<MenuItem onClick={() => navigateTo(`/u/${encodeURIComponent(currentUser.username)}`)}>
|
||||
<SquareUserIcon className="w-4 h-auto opacity-60" />
|
||||
<span className="truncate">{t("common.profile")}</span>
|
||||
@@ -54,6 +54,14 @@ const UserBanner = (props: Props) => {
|
||||
<ArchiveIcon className="w-4 h-auto opacity-60" />
|
||||
<span className="truncate">{t("common.archived")}</span>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => navigateTo(Routes.INBOX)}>
|
||||
<BellIcon className="w-4 h-auto opacity-60" />
|
||||
<span className="truncate">{t("common.inbox")}</span>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => navigateTo(Routes.SETTING)}>
|
||||
<SettingsIcon className="w-4 h-auto opacity-60" />
|
||||
<span className="truncate">{t("common.settings")}</span>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleSignOut}>
|
||||
<LogOutIcon className="w-4 h-auto opacity-60" />
|
||||
<span className="truncate">{t("common.sign-out")}</span>
|
||||
|
@@ -1,50 +1,20 @@
|
||||
import dayjs from "dayjs";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useMemo } from "react";
|
||||
import MemoView from "@/components/MemoView";
|
||||
import MobileHeader from "@/components/MobileHeader";
|
||||
import PagedMemoList from "@/components/PagedMemoList";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import useResponsiveWidth from "@/hooks/useResponsiveWidth";
|
||||
import { viewStore } from "@/store/v2";
|
||||
import memoFilterStore from "@/store/v2/memoFilter";
|
||||
import { Direction, State } from "@/types/proto/api/v1/common";
|
||||
import { Memo } from "@/types/proto/api/v1/memo_service";
|
||||
|
||||
const Explore = observer(() => {
|
||||
const user = useCurrentUser();
|
||||
|
||||
const memoListFilter = useMemo(() => {
|
||||
const conditions = [];
|
||||
const contentSearch: string[] = [];
|
||||
const tagSearch: string[] = [];
|
||||
for (const filter of memoFilterStore.filters) {
|
||||
if (filter.factor === "contentSearch") {
|
||||
contentSearch.push(`"${filter.value}"`);
|
||||
} else if (filter.factor === "tagSearch") {
|
||||
tagSearch.push(`"${filter.value}"`);
|
||||
} else if (filter.factor === "property.hasLink") {
|
||||
conditions.push(`has_link == true`);
|
||||
} else if (filter.factor === "property.hasTaskList") {
|
||||
conditions.push(`has_task_list == true`);
|
||||
} else if (filter.factor === "property.hasCode") {
|
||||
conditions.push(`has_code == true`);
|
||||
} else if (filter.factor === "displayTime") {
|
||||
const filterDate = new Date(filter.value);
|
||||
const filterUtcTimestamp = filterDate.getTime() + filterDate.getTimezoneOffset() * 60 * 1000;
|
||||
const timestampAfter = filterUtcTimestamp / 1000;
|
||||
conditions.push(`display_time_after == ${timestampAfter}`);
|
||||
conditions.push(`display_time_before == ${timestampAfter + 60 * 60 * 24}`);
|
||||
}
|
||||
}
|
||||
if (contentSearch.length > 0) {
|
||||
conditions.push(`content_search == [${contentSearch.join(", ")}]`);
|
||||
}
|
||||
if (tagSearch.length > 0) {
|
||||
conditions.push(`tag_search == [${tagSearch.join(", ")}]`);
|
||||
}
|
||||
return conditions.join(" && ");
|
||||
}, [user, memoFilterStore.filters, viewStore.state.orderByTimeAsc]);
|
||||
const { md } = useResponsiveWidth();
|
||||
|
||||
return (
|
||||
<section className="@container w-full max-w-5xl min-h-full flex flex-col justify-start items-center sm:pt-3 md:pt-6 pb-8">
|
||||
{!md && <MobileHeader />}
|
||||
<div className="w-full px-4 sm:px-6">
|
||||
<PagedMemoList
|
||||
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showCreator showVisibility compact />}
|
||||
listSort={(memos: Memo[]) =>
|
||||
@@ -57,8 +27,9 @@ const Explore = observer(() => {
|
||||
)
|
||||
}
|
||||
direction={viewStore.state.orderByTimeAsc ? Direction.ASC : Direction.DESC}
|
||||
oldFilter={memoListFilter}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
});
|
||||
|
||||
|
@@ -84,14 +84,6 @@ const router = createBrowserRouter([
|
||||
path: "",
|
||||
element: <Home />,
|
||||
},
|
||||
{
|
||||
path: Routes.EXPLORE,
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Explore />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: Routes.ARCHIVED,
|
||||
element: (
|
||||
@@ -110,6 +102,14 @@ const router = createBrowserRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: Routes.EXPLORE,
|
||||
element: (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Explore />
|
||||
</Suspense>
|
||||
),
|
||||
},
|
||||
{
|
||||
path: Routes.RESOURCES,
|
||||
element: (
|
||||
|
Reference in New Issue
Block a user