Fixes the inconsistency where switching the memo list to update_time
left the activity heatmap aggregating by created_time. The heatmap
now follows the same time basis as the list it sits next to.
Backend
- UserStats gains memo_updated_timestamps (additive proto field, tag 8).
- GetUserStats and ListAllUserStats populate it alongside the existing
memo_created_timestamps. No DB migration; memo.updated_ts already
exists on every row.
Frontend
- useFilteredMemoStats reads timeBasis from ViewContext and selects
the matching timestamp source.
- StatisticsView and MonthNavigator forward timeBasis through to
MonthCalendar / YearCalendar so tooltip text matches the basis
("X memos in DATE" vs "X memos updated on DATE").
- Falls back to memoCreatedTimestamps when an old server returns an
empty memoUpdatedTimestamps array (detected by length divergence,
since protobuf-es deserializes missing repeated fields as []).
Tests
- Backend: TestGetUserStats_MemoUpdatedTimestamps verifies the field
is populated and reflects post-creation updates.
- Frontend: filtered-memo-stats covers create/update source switching
and the old-server fallback path; activity-calendar-tooltip covers
basis-aware label selection.
Spec and implementation plan committed under docs/superpowers/.
40 lines
1.2 KiB
TypeScript
40 lines
1.2 KiB
TypeScript
import dayjs from "dayjs";
|
|
import { useState } from "react";
|
|
import { calculateMaxCount, MonthCalendar } from "@/components/ActivityCalendar";
|
|
import { useDateFilterNavigation } from "@/hooks";
|
|
import type { StatisticsData } from "@/types/statistics";
|
|
import { MonthNavigator } from "./MonthNavigator";
|
|
|
|
interface Props {
|
|
statisticsData: StatisticsData;
|
|
}
|
|
|
|
const StatisticsView = (props: Props) => {
|
|
const { statisticsData } = props;
|
|
const { activityStats, timeBasis } = statisticsData;
|
|
const navigateToDateFilter = useDateFilterNavigation();
|
|
const [visibleMonthString, setVisibleMonthString] = useState(dayjs().format("YYYY-MM"));
|
|
|
|
return (
|
|
<div className="group w-full mt-2 flex flex-col text-muted-foreground animate-fade-in">
|
|
<MonthNavigator
|
|
visibleMonth={visibleMonthString}
|
|
onMonthChange={setVisibleMonthString}
|
|
activityStats={activityStats}
|
|
timeBasis={timeBasis}
|
|
/>
|
|
|
|
<div className="w-full animate-scale-in">
|
|
<MonthCalendar
|
|
month={visibleMonthString}
|
|
data={activityStats}
|
|
maxCount={calculateMaxCount(activityStats)}
|
|
onClick={navigateToDateFilter}
|
|
timeBasis={timeBasis}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default StatisticsView;
|