mirror of https://github.com/usememos/memos.git
refactor(web): use Radix Checkbox and remove memoTypeStats
- Replace native input with Radix UI Checkbox in TaskListItem for better accessibility and consistent styling - Remove memoTypeStats tracking and display (link count, todo count, code count) - Remove StatCard component and related type definitions - Simplify statistics to only track activity calendar data and tags 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8f136ffa75
commit
b7215f46a6
|
|
@ -1,4 +1,5 @@
|
|||
import { useContext } from "react";
|
||||
import { useContext, useRef } from "react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { memoStore } from "@/store";
|
||||
import { toggleTaskAtIndex } from "@/utils/markdown-manipulation";
|
||||
import { MemoContentContext } from "./MemoContentContext";
|
||||
|
|
@ -20,19 +21,16 @@ interface TaskListItemProps extends React.InputHTMLAttributes<HTMLInputElement>
|
|||
|
||||
export const TaskListItem: React.FC<TaskListItemProps> = ({ checked, ...props }) => {
|
||||
const context = useContext(MemoContentContext);
|
||||
const checkboxRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation();
|
||||
|
||||
const handleChange = async (newChecked: boolean) => {
|
||||
// Don't update if readonly or no memo context
|
||||
if (context.readonly || !context.memoName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newChecked = e.target.checked;
|
||||
|
||||
// Find the task index by walking up the DOM
|
||||
const listItem = e.target.closest("li.task-list-item");
|
||||
const listItem = checkboxRef.current?.closest("li.task-list-item");
|
||||
if (!listItem) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -78,5 +76,7 @@ export const TaskListItem: React.FC<TaskListItemProps> = ({ checked, ...props })
|
|||
|
||||
// Override the disabled prop from remark-gfm (which defaults to true)
|
||||
// We want interactive checkboxes, only disabled when readonly
|
||||
return <input {...props} type="checkbox" checked={checked} disabled={context.readonly} onChange={handleChange} />;
|
||||
return (
|
||||
<Checkbox ref={checkboxRef} checked={checked} disabled={context.readonly} onCheckedChange={handleChange} className={props.className} />
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,55 +0,0 @@
|
|||
import { cloneElement, isValidElement } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { StatCardProps } from "@/types/statistics";
|
||||
|
||||
export const StatCard = ({ icon, label, count, onClick, tooltip, className }: StatCardProps) => {
|
||||
const iconNode = isValidElement(icon)
|
||||
? cloneElement(icon, {
|
||||
className: cn("h-3.5 w-3.5", icon.props.className),
|
||||
})
|
||||
: icon;
|
||||
|
||||
const countNode = (() => {
|
||||
if (typeof count === "number" || typeof count === "string") {
|
||||
return <span className="text-foreground/80">{count}</span>;
|
||||
}
|
||||
if (isValidElement(count)) {
|
||||
return cloneElement(count, {
|
||||
className: cn("text-foreground/80", count.props.className),
|
||||
});
|
||||
}
|
||||
return <span className="text-foreground/80">{count}</span>;
|
||||
})();
|
||||
|
||||
const button = (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-md border border-border/40 bg-background/80 px-1 pr-2 py-0.5 text-sm leading-none text-muted-foreground transition-colors",
|
||||
"hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring/70 focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<span className="flex h-5 w-5 items-center justify-center text-muted-foreground/80">{iconNode}</span>
|
||||
<span className="truncate text-sm text-foreground/70">{label}</span>
|
||||
<span className="ml-1 flex items-center text-sm">{countNode}</span>
|
||||
</button>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{tooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,17 +1,10 @@
|
|||
import dayjs from "dayjs";
|
||||
import { CheckCircleIcon, Code2Icon, LinkIcon, ListTodoIcon, BookmarkIcon } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState, useCallback } from "react";
|
||||
import { matchPath, useLocation } from "react-router-dom";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import { Routes } from "@/router";
|
||||
import { userStore } from "@/store";
|
||||
import memoFilterStore, { FilterFactor } from "@/store/memoFilter";
|
||||
import memoFilterStore from "@/store/memoFilter";
|
||||
import type { StatisticsData } from "@/types/statistics";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import ActivityCalendar from "../ActivityCalendar";
|
||||
import { MonthNavigator } from "./MonthNavigator";
|
||||
import { StatCard } from "./StatCard";
|
||||
|
||||
export type StatisticsViewContext = "home" | "explore" | "archived" | "profile";
|
||||
|
||||
|
|
@ -31,13 +24,8 @@ interface Props {
|
|||
}
|
||||
|
||||
const StatisticsView = observer((props: Props) => {
|
||||
const { context = "home", statisticsData } = props;
|
||||
const t = useTranslate();
|
||||
const location = useLocation();
|
||||
const currentUser = useCurrentUser();
|
||||
|
||||
const { memoTypeStats, activityStats } = statisticsData;
|
||||
|
||||
const { statisticsData } = props;
|
||||
const { activityStats } = statisticsData;
|
||||
const [selectedDate] = useState(new Date());
|
||||
const [visibleMonthString, setVisibleMonthString] = useState(dayjs().format("YYYY-MM"));
|
||||
|
||||
|
|
@ -46,18 +34,6 @@ const StatisticsView = observer((props: Props) => {
|
|||
memoFilterStore.addFilter({ factor: "displayTime", value: date });
|
||||
}, []);
|
||||
|
||||
const handleFilterClick = useCallback((factor: FilterFactor, value: string = "") => {
|
||||
memoFilterStore.addFilter({ factor, value });
|
||||
}, []);
|
||||
|
||||
const isRootPath = matchPath(Routes.ROOT, location.pathname);
|
||||
const hasPinnedMemos = currentUser && (userStore.state.currentUserStats?.pinnedMemos || []).length > 0;
|
||||
|
||||
// Determine if we should show the pinned stat card
|
||||
// Only show on home page (root path) for the current user with pinned memos
|
||||
// Don't show on explore page since it's global
|
||||
const shouldShowPinned = context === "home" && isRootPath && hasPinnedMemos;
|
||||
|
||||
return (
|
||||
<div className="group w-full mt-2 space-y-1 text-muted-foreground animate-fade-in">
|
||||
<MonthNavigator visibleMonth={visibleMonthString} onMonthChange={setVisibleMonthString} />
|
||||
|
|
@ -70,49 +46,6 @@ const StatisticsView = observer((props: Props) => {
|
|||
onClick={handleCalendarClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 w-full flex flex-wrap items-center gap-2">
|
||||
{shouldShowPinned && (
|
||||
<StatCard
|
||||
icon={<BookmarkIcon className="opacity-70" />}
|
||||
label={t("common.pinned")}
|
||||
count={userStore.state.currentUserStats!.pinnedMemos.length}
|
||||
onClick={() => handleFilterClick("pinned")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StatCard
|
||||
icon={<LinkIcon className="opacity-70" />}
|
||||
label={t("memo.links")}
|
||||
count={memoTypeStats.linkCount}
|
||||
onClick={() => handleFilterClick("property.hasLink")}
|
||||
/>
|
||||
|
||||
<StatCard
|
||||
icon={memoTypeStats.undoCount > 0 ? <ListTodoIcon className="opacity-70" /> : <CheckCircleIcon className="opacity-70" />}
|
||||
label={t("memo.to-do")}
|
||||
count={
|
||||
memoTypeStats.undoCount > 0 ? (
|
||||
<div className="text-sm flex flex-row items-start justify-center">
|
||||
<span className="truncate">{memoTypeStats.todoCount - memoTypeStats.undoCount}</span>
|
||||
<span className="font-mono opacity-50">/</span>
|
||||
<span className="truncate">{memoTypeStats.todoCount}</span>
|
||||
</div>
|
||||
) : (
|
||||
memoTypeStats.todoCount
|
||||
)
|
||||
}
|
||||
onClick={() => handleFilterClick("property.hasTaskList")}
|
||||
tooltip={memoTypeStats.undoCount > 0 ? "Done / Total" : undefined}
|
||||
/>
|
||||
|
||||
<StatCard
|
||||
icon={<Code2Icon className="opacity-70" />}
|
||||
label={t("memo.code")}
|
||||
count={memoTypeStats.codeCount}
|
||||
onClick={() => handleFilterClick("property.hasCode")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ import { countBy } from "lodash-es";
|
|||
import { useEffect, useState } from "react";
|
||||
import { memoServiceClient } from "@/grpcweb";
|
||||
import { State } from "@/types/proto/api/v1/common";
|
||||
import { UserStats_MemoTypeStats } from "@/types/proto/api/v1/user_service";
|
||||
import type { StatisticsData } from "@/types/statistics";
|
||||
|
||||
export interface FilteredMemoStats {
|
||||
|
|
@ -47,7 +46,6 @@ export interface FilteredMemoStats {
|
|||
export const useFilteredMemoStats = (filter?: string, state: State = State.NORMAL, orderBy?: string): FilteredMemoStats => {
|
||||
const [data, setData] = useState<FilteredMemoStats>({
|
||||
statistics: {
|
||||
memoTypeStats: UserStats_MemoTypeStats.fromPartial({}),
|
||||
activityStats: {},
|
||||
},
|
||||
tags: {},
|
||||
|
|
@ -69,7 +67,6 @@ export const useFilteredMemoStats = (filter?: string, state: State = State.NORMA
|
|||
});
|
||||
|
||||
// Compute statistics and tags from fetched memos
|
||||
const memoTypeStats = UserStats_MemoTypeStats.fromPartial({});
|
||||
const displayTimeList: Date[] = [];
|
||||
const tagCount: Record<string, number> = {};
|
||||
|
||||
|
|
@ -86,24 +83,6 @@ export const useFilteredMemoStats = (filter?: string, state: State = State.NORMA
|
|||
tagCount[tag] = (tagCount[tag] || 0) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Count memo properties
|
||||
if (memo.property) {
|
||||
if (memo.property.hasLink) {
|
||||
memoTypeStats.linkCount += 1;
|
||||
}
|
||||
if (memo.property.hasTaskList) {
|
||||
memoTypeStats.todoCount += 1;
|
||||
// Check if there are undone tasks
|
||||
const undoneMatches = memo.content.match(/- \[ \]/g);
|
||||
if (undoneMatches && undoneMatches.length > 0) {
|
||||
memoTypeStats.undoCount += 1;
|
||||
}
|
||||
}
|
||||
if (memo.property.hasCode) {
|
||||
memoTypeStats.codeCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -111,7 +90,7 @@ export const useFilteredMemoStats = (filter?: string, state: State = State.NORMA
|
|||
const activityStats = countBy(displayTimeList.map((date) => dayjs(date).format("YYYY-MM-DD")));
|
||||
|
||||
setData({
|
||||
statistics: { memoTypeStats, activityStats },
|
||||
statistics: { activityStats },
|
||||
tags: tagCount,
|
||||
loading: false,
|
||||
});
|
||||
|
|
@ -119,7 +98,6 @@ export const useFilteredMemoStats = (filter?: string, state: State = State.NORMA
|
|||
console.error("Failed to fetch memos for statistics:", error);
|
||||
setData({
|
||||
statistics: {
|
||||
memoTypeStats: UserStats_MemoTypeStats.fromPartial({}),
|
||||
activityStats: {},
|
||||
},
|
||||
tags: {},
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
import { UserStats_MemoTypeStats } from "@/types/proto/api/v1/user_service";
|
||||
|
||||
export interface ActivityData {
|
||||
date: string;
|
||||
count: number;
|
||||
|
|
@ -11,19 +9,6 @@ export interface CalendarDay {
|
|||
date?: string;
|
||||
}
|
||||
|
||||
export interface StatCardData {
|
||||
id: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
count: number;
|
||||
filter: {
|
||||
factor: string;
|
||||
value?: string;
|
||||
};
|
||||
tooltip?: string;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export interface StatisticsViewProps {
|
||||
className?: string;
|
||||
}
|
||||
|
|
@ -40,16 +25,6 @@ export interface ActivityCalendarProps {
|
|||
onClick?: (date: string) => void;
|
||||
}
|
||||
|
||||
export interface StatCardProps {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
count: number | React.ReactNode;
|
||||
onClick: () => void;
|
||||
tooltip?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface StatisticsData {
|
||||
memoTypeStats: UserStats_MemoTypeStats;
|
||||
activityStats: Record<string, number>;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue