chore: update navigator

pull/4745/head
Johnny 3 weeks ago
parent 663e56747f
commit 3343dc73f7

@ -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,64 +1,35 @@
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 (
<PagedMemoList
renderer={(memo: Memo) => <MemoView key={`${memo.name}-${memo.updateTime}`} memo={memo} showCreator showVisibility compact />}
listSort={(memos: Memo[]) =>
memos
.filter((memo) => memo.state === State.NORMAL)
.sort((a, b) =>
viewStore.state.orderByTimeAsc
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
)
}
direction={viewStore.state.orderByTimeAsc ? Direction.ASC : Direction.DESC}
oldFilter={memoListFilter}
/>
<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[]) =>
memos
.filter((memo) => memo.state === State.NORMAL)
.sort((a, b) =>
viewStore.state.orderByTimeAsc
? dayjs(a.displayTime).unix() - dayjs(b.displayTime).unix()
: dayjs(b.displayTime).unix() - dayjs(a.displayTime).unix(),
)
}
direction={viewStore.state.orderByTimeAsc ? Direction.ASC : Direction.DESC}
/>
</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: (

Loading…
Cancel
Save