Skip to content

Commit fa636e7

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

5 files changed

Lines changed: 132 additions & 9 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: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
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"
66
import { timeEntriesData, TimeEntry } from "data/time-entries"
77
import { useAtomValue } from "lib/yaasl"
88
import { cn } from "utils/cn"
9+
import { dateHelpers } from "utils/date-helpers"
910
import { vstack } from "utils/styles"
1011
import { timeHelpers } from "utils/time-helpers"
1112

1213
import { getTimeStats, TimeStats } from "./get-time-stats"
14+
import { statsFilterData } from "./stats-side-route"
1315
import { TimeStatsChart } from "./time-stats-chart"
1416

1517
const getEntries = <TObj extends object>(obj: TObj) =>
@@ -129,12 +131,28 @@ const StatsModeHeader = ({
129131

130132
const StatsCharts = ({ mode }: { mode: Mode }) => {
131133
const timeEntries = useAtomValue(timeEntriesData)
134+
const statsFilter = useAtomValue(statsFilterData)
135+
136+
const filtered = useMemo(
137+
() =>
138+
Object.fromEntries(
139+
Object.entries(timeEntries).filter(([date]) =>
140+
dateHelpers.isInRange(date, statsFilter.start, statsFilter.end)
141+
)
142+
),
143+
[timeEntries, statsFilter]
144+
)
132145

133-
const data = {
134-
weekday: () => getDayStats(timeEntries),
135-
month: () => getMonthStats(timeEntries),
136-
year: () => getYearStats(timeEntries),
137-
}[mode]()
146+
const data = useMemo(() => {
147+
switch (mode) {
148+
case "weekday":
149+
return getDayStats(filtered)
150+
case "month":
151+
return getMonthStats(filtered)
152+
case "year":
153+
return getYearStats(filtered)
154+
}
155+
}, [mode, filtered])
138156

139157
const tick = {
140158
weekday: weekdayTick,
@@ -156,8 +174,8 @@ const StatsCharts = ({ mode }: { mode: Mode }) => {
156174
icon={ClockPlus}
157175
label="Insufficient data"
158176
>
159-
You will need to add more data first, to be able to see stats here.
160-
(at least 2 {mode}s)
177+
You will need to provide more data, to be able to see stats here. (at
178+
least 2 {mode}s)
161179
</ContextInfo>
162180
</div>
163181
)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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, 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+
export const statsFilterData = createAtom<StatsFilter>({
24+
name: "stats-filter",
25+
defaultValue: createYearFilter(year),
26+
})
27+
28+
export const StatsSideRoute = () => {
29+
const selectedFilter = useAtomValue(statsFilterData)
30+
const entries = useAtomValue(timeEntriesData)
31+
const dates = Object.keys(entries)
32+
.sort()
33+
.map(date => new Date(date))
34+
35+
const years = [...new Set(dates.map(date => date.getFullYear()))].sort(
36+
(a, b) => b - a
37+
)
38+
39+
const matchingEntries = Object.entries(entries).flatMap(([date, entries]) =>
40+
dateHelpers.isInRange(date, selectedFilter.start, selectedFilter.end)
41+
? entries
42+
: []
43+
)
44+
45+
const predefinedFilters: StatsFilter[] = [
46+
{
47+
name: "All",
48+
start: dateHelpers.stringify(dates[0]!),
49+
end: dateHelpers.stringify(dates.at(-1)!),
50+
},
51+
...years.map(createYearFilter),
52+
]
53+
54+
return (
55+
<div>
56+
<InputLabel label="Predefined Filters" />
57+
{predefinedFilters.map(filter => (
58+
<Button
59+
key={filter.name}
60+
onClick={() => statsFilterData.set(filter)}
61+
active={
62+
selectedFilter.start === filter.start &&
63+
selectedFilter.end === filter.end
64+
}
65+
>
66+
{filter.name}
67+
</Button>
68+
))}
69+
70+
<Divider color="gentle" className="my-4" />
71+
72+
<InputLabel label="Start date">
73+
<DateInput
74+
locale={getLocale()}
75+
value={selectedFilter.start}
76+
onChange={start =>
77+
statsFilterData.set(state => ({ ...state, start, name: "Custom" }))
78+
}
79+
max={dateHelpers.today()}
80+
/>
81+
</InputLabel>
82+
83+
<InputLabel label="End date">
84+
<DateInput
85+
locale={getLocale()}
86+
value={selectedFilter.end}
87+
onChange={end =>
88+
statsFilterData.set(state => ({ ...state, end, name: "Custom" }))
89+
}
90+
max={dateHelpers.today()}
91+
/>
92+
</InputLabel>
93+
94+
<Divider color="gentle" className="my-4" />
95+
96+
<InputLabel label={`${matchingEntries.length} Matching time entries`} />
97+
</div>
98+
)
99+
}

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)