Skip to content

Commit 7d44ce3

Browse files
committed
feat(StatsSideRoute): Add date filters for stats route
1 parent d4aa8ad commit 7d44ce3

5 files changed

Lines changed: 134 additions & 15 deletions

File tree

src/app/router.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ import { SearchRoute } from "./routes/search/search-route"
88
import { SettingsRoute } from "./routes/settings/settings-route"
99
import { SettingsSideRoute } from "./routes/settings/settings-side-route"
1010
import { StatsRoute } from "./routes/stats/stats-route"
11+
import { StatsSideRoute } from "./routes/stats/stats-side-route"
1112

1213
const AppRouter = () => (
1314
<AppLayout
1415
sideContent={
1516
<Switch>
1617
<Route path="/settings/*" component={SettingsSideRoute} />
1718
<Route path="/settings" component={SettingsSideRoute} />
19+
<Route path="/stats" component={StatsSideRoute} />
1820
<Route path="/" component={MainSideRoute} />
1921
</Switch>
2022
}

src/app/routes/stats/stats-route.tsx

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
1-
import { Dispatch, Fragment, useState } from "react"
1+
import { Dispatch, Fragment, useMemo, useState } from "react"
22

33
import { ClockPlus } from "lucide-react"
44

55
import { ContextInfo } from "components/ui/context-info"
6-
import { timeEntriesData, TimeEntry } from "data/time-entries"
6+
import { TimeEntry } from "data/time-entries"
77
import { useAtomValue } from "lib/yaasl"
88
import { cn } from "utils/cn"
99
import { vstack } from "utils/styles"
1010
import { timeHelpers } from "utils/time-helpers"
1111

1212
import { getTimeStats, TimeStats } from "./get-time-stats"
13+
import { filteredStatsEntries } from "./stats-side-route"
1314
import { TimeStatsChart } from "./time-stats-chart"
1415

1516
const getEntries = <TObj extends object>(obj: TObj) =>
@@ -128,13 +129,18 @@ const StatsModeHeader = ({
128129
)
129130

130131
const StatsCharts = ({ mode }: { mode: Mode }) => {
131-
const timeEntries = useAtomValue(timeEntriesData)
132-
133-
const data = {
134-
weekday: () => getDayStats(timeEntries),
135-
month: () => getMonthStats(timeEntries),
136-
year: () => getYearStats(timeEntries),
137-
}[mode]()
132+
const filtered = useAtomValue(filteredStatsEntries)
133+
134+
const data = useMemo(() => {
135+
switch (mode) {
136+
case "weekday":
137+
return getDayStats(filtered)
138+
case "month":
139+
return getMonthStats(filtered)
140+
case "year":
141+
return getYearStats(filtered)
142+
}
143+
}, [mode, filtered])
138144

139145
const tick = {
140146
weekday: weekdayTick,
@@ -156,8 +162,8 @@ const StatsCharts = ({ mode }: { mode: Mode }) => {
156162
icon={ClockPlus}
157163
label="Insufficient data"
158164
>
159-
You will need to add more data first, to be able to see stats here.
160-
(at least 2 {mode}s)
165+
You will need to provide more data, to be able to see stats here. (at
166+
least 2 {mode}s)
161167
</ContextInfo>
162168
</div>
163169
)
@@ -166,23 +172,23 @@ const StatsCharts = ({ mode }: { mode: Mode }) => {
166172
return (
167173
<div className={cn(vstack({ gap: 8 }), "w-full max-w-xl gap-10 *:h-48")}>
168174
<TimeStatsChart
169-
caption="Start time"
175+
caption="Start time (avg)"
170176
timeStats={data}
171177
type="start"
172178
dotLabel={({ y }) => timeHelpers.fromMinutes(y)}
173179
tickLabel={tick}
174180
transformX={transform}
175181
/>
176182
<TimeStatsChart
177-
caption="End time"
183+
caption="End time (avg)"
178184
timeStats={data}
179185
type="end"
180186
dotLabel={({ y }) => timeHelpers.fromMinutes(y)}
181187
tickLabel={tick}
182188
transformX={transform}
183189
/>
184190
<TimeStatsChart
185-
caption="Work time"
191+
caption="Work time (avg)"
186192
timeStats={data}
187193
type="total"
188194
dotLabel={({ y }) => `${(y / 60).toFixed(1)}h`}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { Button } from "components/ui/button"
2+
import { DateInput } from "components/ui/date-input"
3+
import { Divider } from "components/ui/divider"
4+
import { InputLabel } from "components/ui/input-label"
5+
import { timeEntriesData } from "data/time-entries"
6+
import { createAtom, createSelector, useAtomValue } from "lib/yaasl"
7+
import { dateHelpers } from "utils/date-helpers"
8+
import { getLocale } from "utils/get-locale"
9+
10+
interface StatsFilter {
11+
name: string
12+
start: string
13+
end: string
14+
}
15+
16+
const createYearFilter = (year: number) => ({
17+
name: `${year}`,
18+
start: `${year}-01-01`,
19+
end: `${year}-12-31`,
20+
})
21+
22+
const year = new Date().getFullYear()
23+
const statsFilterData = createAtom<StatsFilter>({
24+
name: "stats-filter",
25+
defaultValue: createYearFilter(year),
26+
})
27+
28+
export const filteredStatsEntries = createSelector(
29+
[timeEntriesData, statsFilterData],
30+
(entries, { start, end }) =>
31+
Object.fromEntries(
32+
Object.entries(entries).filter(([date]) =>
33+
dateHelpers.isInRange(date, start, end)
34+
)
35+
)
36+
)
37+
38+
export const StatsSideRoute = () => {
39+
const selectedFilter = useAtomValue(statsFilterData)
40+
const entries = useAtomValue(timeEntriesData)
41+
const dates = Object.keys(entries)
42+
.sort()
43+
.map(date => new Date(date))
44+
45+
const years = [...new Set(dates.map(date => date.getFullYear()))].sort(
46+
(a, b) => b - a
47+
)
48+
49+
const matchingEntries = Object.values(
50+
useAtomValue(filteredStatsEntries)
51+
).flat()
52+
53+
const predefinedFilters: StatsFilter[] = [
54+
{
55+
name: "All",
56+
start: dateHelpers.stringify(dates[0]!),
57+
end: dateHelpers.stringify(dates.at(-1)!),
58+
},
59+
...years.map(createYearFilter),
60+
]
61+
62+
return (
63+
<div>
64+
<InputLabel label="Predefined Filters" />
65+
{predefinedFilters.map(filter => (
66+
<Button
67+
key={filter.name}
68+
onClick={() => statsFilterData.set(filter)}
69+
active={
70+
selectedFilter.start === filter.start &&
71+
selectedFilter.end === filter.end
72+
}
73+
>
74+
{filter.name}
75+
</Button>
76+
))}
77+
78+
<Divider color="gentle" className="my-4" />
79+
80+
<InputLabel label="Start date">
81+
<DateInput
82+
locale={getLocale()}
83+
value={selectedFilter.start}
84+
onChange={start =>
85+
statsFilterData.set(state => ({ ...state, start, name: "Custom" }))
86+
}
87+
max={dateHelpers.today()}
88+
/>
89+
</InputLabel>
90+
91+
<InputLabel label="End date">
92+
<DateInput
93+
locale={getLocale()}
94+
value={selectedFilter.end}
95+
onChange={end =>
96+
statsFilterData.set(state => ({ ...state, end, name: "Custom" }))
97+
}
98+
max={dateHelpers.today()}
99+
/>
100+
</InputLabel>
101+
102+
<Divider color="gentle" className="my-4" />
103+
104+
<InputLabel label={`${matchingEntries.length} Matching time entries`} />
105+
</div>
106+
)
107+
}

src/components/ui/input-label.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { vstack } from "utils/styles"
99
interface InputLabelProps extends ClassNameProp {
1010
label: string
1111
htmlFor?: string
12-
children: ((id: string) => ReactNode) | ReactNode
12+
children?: ((id: string) => ReactNode) | ReactNode
1313
}
1414

1515
export const InputLabel = ({

src/utils/date-helpers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ const stringify = (date: Date | ParsedDate) => {
3030

3131
const today = () => stringify(new Date())
3232

33+
const isInRange = (date: string, start: string, end: string) =>
34+
[date, start, end].sort()[1] === date
35+
3336
export const dateHelpers = {
3437
today,
3538
parse,
3639
stringify,
3740
daysSince,
41+
isInRange,
3842
}

0 commit comments

Comments
 (0)