Skip to content

Commit 97cbaa6

Browse files
committed
Add stats admin page
1 parent 7d4636f commit 97cbaa6

File tree

4 files changed

+216
-2
lines changed

4 files changed

+216
-2
lines changed

src/lib/utils.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,3 +51,7 @@ export default function fileSizeFromUrl(url: string): Promise<number> {
5151
});
5252
});
5353
}
54+
55+
export function formatMinutes(mins: number | null) {
56+
return Math.floor((mins ?? 0) / 60) + 'h ' + ((mins ?? 0) % 60) + 'min';
57+
}

src/routes/dashboard/admin/admin/+page.svelte

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import Head from '$lib/components/Head.svelte';
3-
import { Users } from '@lucide/svelte';
3+
import { ChartLine, Users } from '@lucide/svelte';
44
55
let { data } = $props();
66
</script>
@@ -13,12 +13,21 @@
1313
<div class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6">
1414
<a
1515
class="themed-box flex flex-col items-center justify-center gap-2 p-3 shadow-xl transition-transform hover:scale-105"
16-
href="/dashboard/admin/admin/users"
16+
href="admin/users"
1717
>
1818
<div>
1919
<Users size={40} />
2020
</div>
2121
<p class="text-2xl font-bold">Users</p>
2222
</a>
23+
<a
24+
class="themed-box flex flex-col items-center justify-center gap-2 p-3 shadow-xl transition-transform hover:scale-105"
25+
href="admin/stats"
26+
>
27+
<div>
28+
<ChartLine size={40} />
29+
</div>
30+
<p class="text-2xl font-bold">Stats</p>
31+
</a>
2332
</div>
2433
</div>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { db } from '$lib/server/db/index.js';
2+
import { project, user, devlog } from '$lib/server/db/schema.js';
3+
import { error } from '@sveltejs/kit';
4+
import { count, eq, sql, and, ne, countDistinct } from 'drizzle-orm';
5+
6+
export async function load({ locals }) {
7+
if (!locals.user) {
8+
throw error(500);
9+
}
10+
if (!locals.user.hasAdmin) {
11+
throw error(403, { message: 'get out, peasant' });
12+
}
13+
14+
const [users] = await db
15+
.select({
16+
count: count(),
17+
total: {
18+
clay: sql<number>`sum(${user.clay})`,
19+
brick: sql<number>`sum(${user.brick})`,
20+
shopScore: sql<number>`sum(${user.shopScore})`,
21+
},
22+
average: {
23+
clay: sql<number>`avg(${user.clay})`,
24+
brick: sql<number>`avg(${user.brick})`,
25+
shopScore: sql<number>`avg(${user.shopScore})`,
26+
}
27+
})
28+
.from(user);
29+
30+
const [usersWithProjects] = await db
31+
.select({
32+
total: countDistinct(project.userId),
33+
shipped: sql<number>`count(distinct ${project.userId}) filter (where ${project.status} != 'building')`
34+
})
35+
.from(project)
36+
.where(eq(project.deleted, false));
37+
38+
const [projectCount] = await db
39+
.select({
40+
count: count(),
41+
building: sql<number>`count(*) filter (where ${project.status} = 'building')`,
42+
submitted: sql<number>`count(*) filter (where ${project.status} = 'submitted')`,
43+
t1_approved: sql<number>`count(*) filter (where ${project.status} = 't1_approved')`,
44+
printing: sql<number>`count(*) filter (where ${project.status} = 'printing')`,
45+
printed: sql<number>`count(*) filter (where ${project.status} = 'printed')`,
46+
finalized: sql<number>`count(*) filter (where ${project.status} = 'finalized')`,
47+
rejected: sql<number>`count(*) filter (where ${project.status} = 'rejected')`,
48+
rejected_locked: sql<number>`count(*) filter (where ${project.status} = 'rejected_locked')`
49+
})
50+
.from(project)
51+
.where(eq(project.deleted, false));
52+
53+
const [shippedProjectCount] = await db
54+
.select({
55+
count: count()
56+
})
57+
.from(project)
58+
.where(and(eq(project.deleted, false), ne(project.status, 'building')));
59+
60+
const [devlogs] = await db
61+
.select({
62+
count: count(),
63+
totalTime: sql<number>`sum(${devlog.timeSpent})`,
64+
timePerDevlog: sql<number>`avg(${devlog.timeSpent})`
65+
})
66+
.from(devlog)
67+
.where(eq(devlog.deleted, false));
68+
69+
return {
70+
users: users,
71+
project: projectCount,
72+
usersWithProjects,
73+
shippedProjectCount: shippedProjectCount.count,
74+
devlogs
75+
};
76+
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<script lang="ts">
2+
import DataCard from '$lib/components/DataCard.svelte';
3+
import Head from '$lib/components/Head.svelte';
4+
import { formatMinutes, projectStatuses } from '$lib/utils.js';
5+
import { CircleDollarSign, Clock, PencilRuler, User } from '@lucide/svelte';
6+
7+
let { data } = $props();
8+
</script>
9+
10+
<Head title="Stats" />
11+
12+
<div class="flex h-full flex-col">
13+
<h1 class="mt-5 mb-3 font-hero text-3xl font-medium">Stats</h1>
14+
15+
<div class="flex flex-col gap-5">
16+
<div class="flex flex-col gap-1">
17+
<h2 class="flex flex-row gap-2 text-2xl font-bold"><User size={28} />Users</h2>
18+
<div
19+
class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6"
20+
>
21+
<DataCard title="Total">
22+
<code>{data.users.count}</code>
23+
</DataCard>
24+
<DataCard title="With projects">
25+
<code>{data.usersWithProjects.total}</code>
26+
</DataCard>
27+
<DataCard title="With shipped projects">
28+
<code>{data.usersWithProjects.shipped}</code>
29+
</DataCard>
30+
</div>
31+
</div>
32+
33+
<div class="flex flex-col gap-1">
34+
<h2 class="flex flex-row gap-2 text-2xl font-bold"><PencilRuler size={28} />Projects</h2>
35+
<div
36+
class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6"
37+
>
38+
<DataCard title="Total">
39+
<code>{data.project.count}</code>
40+
</DataCard>
41+
<DataCard title="Devlog count">
42+
<code>{data.devlogs.count}</code>
43+
</DataCard>
44+
<DataCard title="Shipped projects">
45+
<code>{data.shippedProjectCount}</code>
46+
</DataCard>
47+
</div>
48+
<h3 class="mt-1 text-xl font-semibold">By status</h3>
49+
<div
50+
class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6"
51+
>
52+
<DataCard title={projectStatuses.building}>
53+
<code>{data.project.building}</code>
54+
</DataCard>
55+
<DataCard title={projectStatuses.submitted}>
56+
<code>{data.project.submitted}</code>
57+
</DataCard>
58+
<DataCard title={projectStatuses.t1_approved}>
59+
<code>{data.project.t1_approved}</code>
60+
</DataCard>
61+
<DataCard title={projectStatuses.printing}>
62+
<code>{data.project.printing}</code>
63+
</DataCard>
64+
<DataCard title={projectStatuses.printed}>
65+
<code>{data.project.printed}</code>
66+
</DataCard>
67+
<DataCard title={projectStatuses.finalized}>
68+
<code>{data.project.finalized}</code>
69+
</DataCard>
70+
<DataCard title={projectStatuses.rejected}>
71+
<code>{data.project.rejected}</code>
72+
</DataCard>
73+
<DataCard title={projectStatuses.rejected_locked}>
74+
<code>{data.project.rejected_locked}</code>
75+
</DataCard>
76+
</div>
77+
</div>
78+
79+
<div class="flex flex-col gap-1">
80+
<h2 class="flex flex-row gap-2 text-2xl font-bold"><Clock size={28} />Time</h2>
81+
<div
82+
class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6"
83+
>
84+
<DataCard title="Total">
85+
{formatMinutes(data.devlogs.totalTime)}
86+
</DataCard>
87+
<DataCard title="Average devlog time">
88+
{formatMinutes(data.devlogs.timePerDevlog)}
89+
</DataCard>
90+
</div>
91+
</div>
92+
93+
<div class="flex flex-col gap-1">
94+
<h2 class="flex flex-row gap-2 text-2xl font-bold"><CircleDollarSign size={28} />Currency</h2>
95+
<h3 class="text-xl font-semibold">Total</h3>
96+
<div
97+
class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6"
98+
>
99+
<DataCard title="Clay">
100+
<code>{data.users.total.clay}</code>
101+
</DataCard>
102+
<DataCard title="Bricks">
103+
<code>{data.users.total.brick}</code>
104+
</DataCard>
105+
<DataCard title="Market score">
106+
<code>{data.users.total.shopScore}</code>
107+
</DataCard>
108+
</div>
109+
<h3 class="mt-1 text-xl font-semibold">Average</h3>
110+
<div
111+
class="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6"
112+
>
113+
<DataCard title="Clay">
114+
<code>{Math.round(data.users.average.clay * 100) / 100}</code>
115+
</DataCard>
116+
<DataCard title="Bricks">
117+
<code>{Math.round(data.users.average.brick * 100) / 100}</code>
118+
</DataCard>
119+
<DataCard title="Market score">
120+
<code>{Math.round(data.users.average.shopScore * 100) / 100}</code>
121+
</DataCard>
122+
</div>
123+
</div>
124+
</div>
125+
</div>

0 commit comments

Comments
 (0)