Skip to content

Commit 2b1d752

Browse files
Merge pull request #22 from MichaelHolley/feature/9-statistics
Feature/9 statistics
2 parents 2079985 + 5a5663b commit 2b1d752

File tree

8 files changed

+137
-23
lines changed

8 files changed

+137
-23
lines changed

.github/workflows/CI.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: CI
22

33
on:
44
pull_request:
5-
branches: [master]
5+
branches: [main]
66

77
jobs:
88
build:
@@ -19,7 +19,6 @@ jobs:
1919
- uses: pnpm/action-setup@v4
2020
name: Install pnpm & dependencies
2121
with:
22-
version: 9
2322
run_install: true
2423

2524
- name: Build production bundle

src/lib/components/Habit/HabitOverviewItemComponent.svelte renamed to src/lib/components/Habit/OverviewItemComponent.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import { enhance } from '$app/forms';
33
import { Prisma } from '@prisma/client';
44
import dayjs from 'dayjs';
5-
import HabitActivityHistory from './HabitActivityHistoryComponent.svelte';
5+
import HabitActivityHistory from './HistoryComponent.svelte';
66
77
let { habit } = $props();
88
</script>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<script lang="ts">
2+
import dayjs from 'dayjs';
3+
const {
4+
summary
5+
}: {
6+
summary: {
7+
longest: string[];
8+
current: string[];
9+
completionRate: number;
10+
mostActive: { day: string; count: number } | undefined;
11+
};
12+
} = $props();
13+
</script>
14+
15+
<div class="flex flex-row flex-wrap items-center justify-center gap-4 rounded-lg bg-base-200 p-4">
16+
<div>
17+
<div class="stat place-items-center">
18+
<div class="stat-title">Longest Streak</div>
19+
<div class="stat-value">{summary.longest.length}</div>
20+
{#if summary.longest.length > 0}
21+
<div class="stat-desc">
22+
Starting from {dayjs(summary.longest[0]).format('DD MMM YYYY')}
23+
</div>
24+
{/if}
25+
</div>
26+
</div>
27+
<div>
28+
<div class="stat place-items-center">
29+
<div class="stat-title">Current Streak</div>
30+
<div class="stat-value">{summary.current.length}</div>
31+
{#if summary.current.length > 0}
32+
<div class="stat-desc">
33+
Starting from {dayjs(summary.current[0]).format('DD MMM YYYY')}
34+
</div>
35+
{/if}
36+
</div>
37+
</div>
38+
<div>
39+
<div class="stat place-items-center">
40+
<div class="stat-title">Completion Rate</div>
41+
<div class="stat-value">{Math.floor(summary.completionRate * 100)}%</div>
42+
<div class="stat-desc">Active days since starting</div>
43+
</div>
44+
</div>
45+
<div>
46+
<div class="stat place-items-center">
47+
<div class="stat-title">Most Active</div>
48+
<div class="stat-value">{summary.mostActive?.day}</div>
49+
<div class="stat-desc">with {summary.mostActive?.count} days</div>
50+
</div>
51+
</div>
52+
</div>

src/routes/(app)/+page.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
2-
import HabitOverviewItem from '$lib/components/Habit/HabitOverviewItemComponent.svelte';
3-
import LastXDays from '$lib/components/Habit/HabitSummaryComponent.svelte';
2+
import HabitOverviewItem from '$lib/components/Habit/OverviewItemComponent.svelte';
3+
import LastXDays from '$lib/components/Habit/LastDaysOverviewComponent.svelte';
44
import type { PageServerData } from './$types';
55
66
let { data }: { data: PageServerData } = $props();

src/routes/(app)/[id]/+page.server.ts

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { deleteHabit, getHabitForUser, updateDates } from '$lib/server/habit';
2-
import { Prisma } from '@prisma/client';
2+
import { Prisma, type Habit } from '@prisma/client';
33
import { redirect } from '@sveltejs/kit';
44
import type { Actions, PageServerLoad } from './$types';
55
import dayjs from 'dayjs';
@@ -15,7 +15,9 @@ export const load: PageServerLoad = async (event) => {
1515
return redirect(302, '/');
1616
}
1717

18-
return { habit: habit };
18+
const summary = getSummaryForHabit(habit);
19+
20+
return { habit: habit, summary: summary };
1921
};
2022

2123
export const actions: Actions = {
@@ -67,3 +69,62 @@ export const actions: Actions = {
6769
}
6870
}
6971
};
72+
73+
const getSummaryForHabit = (habit: Habit) => {
74+
const dates = habit.dates as Prisma.JsonArray as string[];
75+
return getDatesData(dates);
76+
};
77+
78+
const getDatesData = (
79+
dates: string[]
80+
): {
81+
longest: string[];
82+
current: string[];
83+
completionRate: number;
84+
mostActive: { day: string; count: number } | undefined;
85+
} => {
86+
const sortedDates = [...new Set(dates)]
87+
.map((date) => dayjs(date))
88+
.sort((a, b) => a.valueOf() - b.valueOf());
89+
90+
const weekdayMap = new Map<string, number>();
91+
92+
if (sortedDates.length === 0)
93+
return {
94+
longest: [],
95+
current: [],
96+
completionRate: 0,
97+
mostActive: undefined
98+
};
99+
100+
let currentStreak = [sortedDates[0]];
101+
let maxStreak = [sortedDates[0]];
102+
103+
for (let i = 0; i < sortedDates.length; i++) {
104+
const weekday = sortedDates[i].format('dddd');
105+
weekdayMap.set(weekday, (weekdayMap.get(weekday) || 0) + 1);
106+
107+
if (i === 0) continue;
108+
109+
if (sortedDates[i].diff(sortedDates[i - 1], 'day') === 1) {
110+
currentStreak.push(sortedDates[i]);
111+
if (currentStreak.length > maxStreak.length) {
112+
maxStreak = [...currentStreak];
113+
}
114+
} else {
115+
currentStreak = [sortedDates[i]];
116+
}
117+
}
118+
119+
const daysSinceFirstDate = dayjs().diff(sortedDates[0], 'day') + 1;
120+
121+
const weekdayMapArray = Array.from(weekdayMap.entries());
122+
weekdayMapArray.sort((a, b) => b[1] - a[1]);
123+
124+
return {
125+
longest: maxStreak.map((date) => date.format('YYYY-MM-DD')),
126+
current: currentStreak.map((date) => date.format('YYYY-MM-DD')),
127+
completionRate: sortedDates.length / daysSinceFirstDate,
128+
mostActive: { day: weekdayMapArray[0][0], count: weekdayMapArray[0][1] }
129+
};
130+
};

src/routes/(app)/[id]/+page.svelte

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,32 @@
11
<script lang="ts">
22
import { enhance } from '$app/forms';
3-
import HabitActivityHistory from '$lib/components/Habit/HabitActivityHistoryComponent.svelte';
3+
import HabitActivityHistory from '$lib/components/Habit/HistoryComponent.svelte';
44
import NavigateBackButton from '$lib/components/NavigateBackButtonComponent.svelte';
55
import dayjs from 'dayjs';
66
import type { PageData } from './$types';
77
import { Prisma } from '@prisma/client';
8+
import SummaryComponent from '$lib/components/Habit/SummaryComponent.svelte';
89
910
let { data }: { data: PageData } = $props();
1011
let deleteModal: HTMLDialogElement;
1112
</script>
1213

13-
<div class="mb-3">
14-
<NavigateBackButton backUrl="/" />
15-
</div>
16-
17-
<div class="mb-3">
14+
<div>
15+
<div class="mb-3">
16+
<NavigateBackButton backUrl="/" />
17+
</div>
1818
<h2 class="text-3xl">{data.habit?.title}</h2>
1919
<p class="text-xs text-neutral-400">{data.habit?.description}</p>
20-
</div>
21-
<div class="timestamp-grid grid gap-x-3 text-xs text-neutral-400">
22-
<p>Created:</p>
23-
<p>
24-
{dayjs(data.habit.createdAt).format('DD MMM YYYY - HH:mm')}
25-
</p>
26-
<p>Updated:</p>
27-
<p>
28-
{dayjs(data.habit.updatedAt).format('DD MMM YYYY - HH:mm')}
29-
</p>
20+
<div class="timestamp-grid grid gap-x-3 text-xs text-neutral-400">
21+
<p>Created:</p>
22+
<p>
23+
{dayjs(data.habit.createdAt).format('DD MMM YYYY - HH:mm')}
24+
</p>
25+
<p>Updated:</p>
26+
<p>
27+
{dayjs(data.habit.updatedAt).format('DD MMM YYYY - HH:mm')}
28+
</p>
29+
</div>
3030
</div>
3131

3232
<div class="my-6 flex flex-row gap-3">
@@ -62,6 +62,8 @@
6262
</form>
6363
</div>
6464

65+
<SummaryComponent summary={data.summary} />
66+
6567
<dialog id="delete_modal" bind:this={deleteModal} class="modal modal-bottom sm:modal-middle">
6668
<div class="modal-box">
6769
<form method="dialog">

0 commit comments

Comments
 (0)