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>
pull/5231/head
Steven 1 week ago
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…
Cancel
Save