Skip to content

Commit

Permalink
feat: adding more stats
Browse files Browse the repository at this point in the history
  • Loading branch information
tomgobich committed Jan 5, 2025
1 parent 95ae52e commit 4920004
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 56 deletions.
60 changes: 28 additions & 32 deletions app/actions/dashboard/get_dashboard_counts.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,47 @@
import { MonthlyStat } from '#actions/stats/get_monthly'
import PostStats from '#actions/stats/post_stats'
import UserStats from '#actions/stats/user_stats'
import Collection from '#models/collection'
import Post from '#models/post'
import Taxonomy from '#models/taxonomy'

export interface GetDashboardCountsContract {
posts: BigInt
postSeconds: BigInt
series: BigInt
topics: BigInt
posts: {
total: BigInt
monthly: MonthlyStat[]
}
users: {
total: bigint
total: BigInt
monthly: MonthlyStat[]
}
completedLessons: {
total: BigInt
monthly: MonthlyStat[]
}
watchSeconds: {
total: BigInt
monthly: MonthlyStat[]
}
postSeconds: BigInt
}

export default class GetDashboardCounts {
static async handle(): Promise<GetDashboardCountsContract> {
return {
posts: await this.#countPublishedPosts(),
postSeconds: await this.#countPostSeconds(),
series: await this.#countSeries(),
topics: await this.#countTopics(),
posts: {
total: await PostStats.getTotalPublished(),
monthly: await PostStats.getMonthlyPublished(),
},
users: {
total: await UserStats.getTotal(),
monthly: await UserStats.getMonthlyRegistrations(),
},
completedLessons: {
total: await PostStats.getLessonsUsersHaveCompleted(),
monthly: await PostStats.getMonthlyLessonsUsersHaveCompleted(),
},
watchSeconds: {
total: await PostStats.getLessonsWatchDuration(),
monthly: await PostStats.getMonthlyLessonsWatchDuration(),
},
postSeconds: await PostStats.getTotalPostSeconds(),
}
}

static async #countPublishedPosts() {
return Post.query()
.apply((scope) => scope.published())
.getCount()
}

static async #countPostSeconds() {
return Post.query()
.apply((scope) => scope.published())
.getSum('video_seconds')
}

static async #countSeries() {
return Collection.query().wherePublic().whereNull('parentId').getCount()
}

static async #countTopics() {
return Taxonomy.query().getCount()
}
}
60 changes: 60 additions & 0 deletions app/actions/stats/post_stats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import db from '@adonisjs/lucid/services/db'
import { DateTime } from 'luxon'
import GetMonthly from './get_monthly.js'
import Post from '#models/post'
import States from '#enums/states'
import Progress from '#models/progress'

export default class PostStats {
static async getTotalPublished() {
return Post.query()
.apply((scope) => scope.published())
.getCount()
}

static async getMonthlyPublished(
startDate: DateTime<true> = DateTime.now().minus({ year: 1 }).startOf('month')
) {
const query = db
.from('posts')
.where('state_id', States.PUBLIC)
.where('publish_at', '<=', DateTime.now().toSQL())

return GetMonthly.count(query, { startDate, monthlyColumn: 'publish_at' })
}

static async getTotalPostSeconds() {
return Post.query()
.apply((scope) => scope.published())
.getSum('video_seconds')
}

static async getLessonsUsersHaveCompleted() {
return Progress.query().where('isCompleted', true).getCount()
}

static async getMonthlyLessonsUsersHaveCompleted(
startDate: DateTime<true> = DateTime.now().minus({ year: 1 }).startOf('month')
) {
const query = db.from('progresses').where('is_completed', true)

return GetMonthly.count(query, {
startDate,
monthlyColumn: 'updated_at',
})
}

static async getLessonsWatchDuration() {
return Progress.query().getSum('watch_seconds')
}

static async getMonthlyLessonsWatchDuration(
startDate: DateTime<true> = DateTime.now().minus({ year: 1 }).startOf('month')
) {
return GetMonthly.sum(db.from('progresses'), {
startDate,
monthlyColumn: 'updated_at',
aggregateColumn: 'watch_seconds',
})
}
}
2 changes: 1 addition & 1 deletion app/actions/stats/user_stats.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import User from '#models/user'
import db from '@adonisjs/lucid/services/db'
import { DateTime } from 'luxon'
import GetMonthly, { MonthlyStat } from './get_monthly.js'
import GetMonthly from './get_monthly.js'

export default class UserStats {
static async getTotal() {
Expand Down
109 changes: 86 additions & 23 deletions inertia/pages/dashboard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@
import { Head } from '@inertiajs/vue3'
import type { GetDashboardCountsContract } from '#actions/dashboard/get_dashboard_counts'
import { secondsToTimestring, toLocaleString } from '~/lib/utils'
import { nextTick, onMounted, ref } from 'vue'
import { onMounted, ref } from 'vue'
import AreaChart from '~/components/ui/chart-area/AreaChart.vue'
defineProps<{
counts: GetDashboardCountsContract
}>()
const analytics = ref<HTMLIFrameElement | null>(null)
const activeStat = ref('users')
onMounted(() => {
setTimeout(() => {
Expand All @@ -27,7 +28,11 @@ onMounted(() => {

<div class="shadow-lg mb-6 rounded-lg overflow-hidden">
<div class="grid gap-4 md:grid-cols-3 md:gap-0 lg:grid-cols-5 bg-white">
<Card class="-mr-px rounded-none rounded-tl-lg border-b-0 shadow-none">
<Card
class="-mr-px rounded-none rounded-tl-lg cursor-pointer"
:class="{ 'border-b-0 shadow-none': activeStat === 'users' }"
@click="activeStat = 'users'"
>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium"> Users </CardTitle>
<DollarSign class="h-4 w-4 text-muted-foreground" />
Expand All @@ -38,63 +43,121 @@ onMounted(() => {
</div>
</CardContent>
</Card>
<Card class="-mr-px rounded-none">
<Card
class="-mr-px rounded-none cursor-pointer"
:class="{ 'border-b-0 shadow-none': activeStat === 'posts' }"
@click="activeStat = 'posts'"
>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium"> Posts Published </CardTitle>
<DollarSign class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">
{{ toLocaleString(counts.posts) }}
{{ toLocaleString(counts.posts.total) }}
</div>
</CardContent>
</Card>
<Card class="-mr-px rounded-none">
<Card
class="-mr-px rounded-none cursor-pointer"
:class="{ 'border-b-0 shadow-none': activeStat === 'completedLessons' }"
@click="activeStat = 'completedLessons'"
>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium"> Total Video Runtime </CardTitle>
<CardTitle class="text-sm font-medium"> User Completed Lessons </CardTitle>
<DollarSign class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">
{{ secondsToTimestring(counts.postSeconds) }}
{{ toLocaleString(counts.completedLessons.total) }}
</div>
</CardContent>
</Card>
<Card class="-mr-px rounded-none">
<Card
class="-mr-px rounded-none cursor-pointer"
:class="{ 'border-b-0 shadow-none': activeStat === 'watchSeconds' }"
@click="activeStat = 'watchSeconds'"
>
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium"> Active/Completed Series </CardTitle>
<CardTitle class="text-sm font-medium"> User Watch Duration </CardTitle>
<DollarSign class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">
{{ toLocaleString(counts.series) }}
{{ secondsToTimestring(counts.watchSeconds.total) }}
</div>
</CardContent>
</Card>
<Card class="rounded-none rounded-tr-lg">
<Card class="-mr-px rounded-none">
<CardHeader class="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle class="text-sm font-medium"> Topics Covered </CardTitle>
<CardTitle class="text-sm font-medium"> Total Video Runtime </CardTitle>
<DollarSign class="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div class="text-2xl font-bold">
{{ toLocaleString(counts.topics) }}
{{ secondsToTimestring(counts.postSeconds) }}
</div>
</CardContent>
</Card>
</div>

<div class="bg-white border border-t-0 rounded-b-lg py-4 -m-x-px">
<AreaChart
class="w-full h-[225px]"
index="month"
:show-x-axis="true"
:show-y-axis="true"
:show-legend="false"
:data="counts.users.monthly"
:categories="['total']"
:colors="['#6574cd']"
/>
<div v-if="activeStat === 'users'">
<h3 class="px-4 pb-2 text-xs uppercase tracking-wide font-semibold">New Users</h3>
<AreaChart
class="w-full h-[225px]"
index="month"
:show-x-axis="true"
:show-y-axis="true"
:show-legend="false"
:data="counts.users.monthly"
:categories="['total']"
:colors="['#6574cd']"
/>
</div>
<div v-else-if="activeStat === 'posts'">
<h3 class="px-4 pb-2 text-xs uppercase tracking-wide font-semibold">Published Posts</h3>
<AreaChart
class="w-full h-[225px]"
index="month"
:show-x-axis="true"
:show-y-axis="true"
:show-legend="false"
:data="counts.posts.monthly"
:categories="['total']"
:colors="['#6574cd']"
/>
</div>
<div v-else-if="activeStat === 'completedLessons'">
<h3 class="px-4 pb-2 text-xs uppercase tracking-wide font-semibold">
Lessons Completed by Users
</h3>
<AreaChart
class="w-full h-[225px]"
index="month"
:show-x-axis="true"
:show-y-axis="true"
:show-legend="false"
:data="counts.completedLessons.monthly"
:categories="['total']"
:colors="['#6574cd']"
/>
</div>
<div v-else-if="activeStat === 'watchSeconds'">
<h3 class="px-4 pb-2 text-xs uppercase tracking-wide font-semibold">
Seconds Watched by Users
</h3>
<AreaChart
class="w-full h-[225px]"
index="month"
:show-x-axis="true"
:show-y-axis="true"
:show-legend="false"
:data="counts.watchSeconds.monthly"
:categories="['total']"
:colors="['#6574cd']"
/>
</div>
</div>
</div>

Expand Down

0 comments on commit 4920004

Please sign in to comment.