257 lines
7.8 KiB
Plaintext
257 lines
7.8 KiB
Plaintext
|
package pages
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"reichard.io/antholume/database"
|
||
|
"reichard.io/antholume/graph"
|
||
|
"reichard.io/antholume/ngtemplates/common"
|
||
|
)
|
||
|
|
||
|
type UserMetadata struct {
|
||
|
DocumentCount int
|
||
|
ActivityCount int
|
||
|
ProgressCount int
|
||
|
DeviceCount int
|
||
|
}
|
||
|
|
||
|
type UserStatistics struct {
|
||
|
WPM map[string][]UserStatisticEntry
|
||
|
Duration map[string][]UserStatisticEntry
|
||
|
Words map[string][]UserStatisticEntry
|
||
|
}
|
||
|
|
||
|
type UserStatisticEntry struct {
|
||
|
UserID string
|
||
|
Value string
|
||
|
}
|
||
|
|
||
|
templ Home(
|
||
|
settings common.Settings,
|
||
|
dailyReadSVG graph.SVGGraphData,
|
||
|
userStreaks []database.UserStreak,
|
||
|
userStatistics UserStatistics,
|
||
|
userMetadata UserMetadata,
|
||
|
) {
|
||
|
@layout(settings, "Home WAT") {
|
||
|
<div class="flex flex-col gap-4">
|
||
|
<div class="w-full">
|
||
|
@DailyReadChart(dailyReadSVG)
|
||
|
</div>
|
||
|
<div class="grid grid-cols-2 gap-4 md:grid-cols-4">
|
||
|
@InfoCard("Documents", userMetadata.DocumentCount, "./documents")
|
||
|
@InfoCard("Activity Records", userMetadata.ActivityCount, "./activity")
|
||
|
@InfoCard("Progress Records", userMetadata.ProgressCount, "./progress")
|
||
|
@InfoCard("Devices", userMetadata.DeviceCount, "")
|
||
|
</div>
|
||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||
|
for _, item := range userStreaks {
|
||
|
@StreakCard(item)
|
||
|
}
|
||
|
</div>
|
||
|
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||
|
@LeaderboardCard("WPM", userStatistics.WPM)
|
||
|
@LeaderboardCard("Duration", userStatistics.Duration)
|
||
|
@LeaderboardCard("Words", userStatistics.Words)
|
||
|
</div>
|
||
|
</div>
|
||
|
}
|
||
|
}
|
||
|
|
||
|
templ infoCardInner(name string, metric int) {
|
||
|
<div class="flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||
|
<div class="flex flex-col justify-around dark:text-white w-full text-sm">
|
||
|
<p class="text-2xl font-bold text-black dark:text-white">{ fmt.Sprint(metric) }</p>
|
||
|
<p class="text-sm text-gray-400">{ name }</p>
|
||
|
</div>
|
||
|
</div>
|
||
|
}
|
||
|
|
||
|
templ InfoCard(name string, metric int, link string) {
|
||
|
if link != "" {
|
||
|
<a href={ templ.SafeURL(link) } class="w-full">
|
||
|
@infoCardInner(name, metric)
|
||
|
</a>
|
||
|
} else {
|
||
|
<div class="w-full">
|
||
|
@infoCardInner(name, metric)
|
||
|
</div>
|
||
|
}
|
||
|
}
|
||
|
|
||
|
templ LeaderboardCard(name string, stats map[string][]UserStatisticEntry) {
|
||
|
<div class="w-full">
|
||
|
<div class="flex flex-col justify-between h-full w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||
|
<div>
|
||
|
<div class="flex justify-between">
|
||
|
<p class="text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
||
|
{ name } Leaderboard
|
||
|
</p>
|
||
|
<div class="flex gap-2 text-xs text-gray-400 items-center">
|
||
|
<label
|
||
|
for={ fmt.Sprintf("all-%s", name) }
|
||
|
class="cursor-pointer hover:text-black dark:hover:text-white"
|
||
|
>all</label>
|
||
|
<label
|
||
|
for={ fmt.Sprintf("year-%s", name) }
|
||
|
class="cursor-pointer hover:text-black dark:hover:text-white"
|
||
|
>year</label>
|
||
|
<label
|
||
|
for={ fmt.Sprintf("month-%s", name) }
|
||
|
class="cursor-pointer hover:text-black dark:hover:text-white"
|
||
|
>month</label>
|
||
|
<label
|
||
|
for={ fmt.Sprintf("week-%s", name) }
|
||
|
class="cursor-pointer hover:text-black dark:hover:text-white"
|
||
|
>week</label>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
<input
|
||
|
type="radio"
|
||
|
name={ fmt.Sprintf("options-%s", name) }
|
||
|
id={ fmt.Sprintf("all-%s", name) }
|
||
|
class="hidden peer/All"
|
||
|
checked
|
||
|
/>
|
||
|
<input
|
||
|
type="radio"
|
||
|
name={ fmt.Sprintf("options-%s", name) }
|
||
|
id={ fmt.Sprintf("year-%s", name) }
|
||
|
class="hidden peer/Year"
|
||
|
/>
|
||
|
<input
|
||
|
type="radio"
|
||
|
name={ fmt.Sprintf("options-%s", name) }
|
||
|
id={ fmt.Sprintf("month-%s", name) }
|
||
|
class="hidden peer/Month"
|
||
|
/>
|
||
|
<input
|
||
|
type="radio"
|
||
|
name={ fmt.Sprintf("options-%s", name) }
|
||
|
id={ fmt.Sprintf("week-%s", name) }
|
||
|
class="hidden peer/Week"
|
||
|
/>
|
||
|
for name, data := range stats {
|
||
|
<div class={ "flex items-end my-6 space-x-2 hidden", fmt.Sprintf("peer-checked/%s:block", name) }>
|
||
|
if len(data) == 0 {
|
||
|
<p class="text-5xl font-bold text-black dark:text-white">N/A</p>
|
||
|
} else {
|
||
|
<p class="text-5xl font-bold text-black dark:text-white">{ data[0].UserID }</p>
|
||
|
}
|
||
|
</div>
|
||
|
<div class={ "hidden dark:text-white", fmt.Sprintf("peer-checked/%s:block", name) }>
|
||
|
for idx, item := range data {
|
||
|
if idx == 0 {
|
||
|
<div class="flex items-center justify-between pt-2 pb-2 text-sm">
|
||
|
<div>
|
||
|
<p>{ item.UserID }</p>
|
||
|
</div>
|
||
|
<div class="flex items-end font-bold">{ item.Value }</div>
|
||
|
</div>
|
||
|
} else if idx < 3 {
|
||
|
<div class="flex items-center justify-between pt-2 pb-2 text-sm border-t border-gray-200">
|
||
|
<div>
|
||
|
<p>{ item.UserID }</p>
|
||
|
</div>
|
||
|
<div class="flex items-end font-bold">{ item.Value }</div>
|
||
|
</div>
|
||
|
}
|
||
|
}
|
||
|
</div>
|
||
|
}
|
||
|
</div>
|
||
|
</div>
|
||
|
}
|
||
|
|
||
|
templ StreakCard(streak database.UserStreak) {
|
||
|
<div class="w-full">
|
||
|
<div class="relative w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded">
|
||
|
<p class="text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
||
|
if streak.Window == "WEEK" {
|
||
|
Weekly Read Streak
|
||
|
} else {
|
||
|
Daily Read Streak
|
||
|
}
|
||
|
</p>
|
||
|
<div class="flex items-end my-6 space-x-2">
|
||
|
<p class="text-5xl font-bold text-black dark:text-white">
|
||
|
{ fmt.Sprint(streak.CurrentStreak) }
|
||
|
</p>
|
||
|
</div>
|
||
|
<div class="dark:text-white">
|
||
|
<div class="flex items-center justify-between pb-2 mb-2 text-sm border-b border-gray-200">
|
||
|
<div>
|
||
|
<p>
|
||
|
if streak.Window == "WEEK" {
|
||
|
Current Read Streak
|
||
|
} else {
|
||
|
Current Read Streak
|
||
|
}
|
||
|
</p>
|
||
|
<div class="flex items-end text-sm text-gray-400">
|
||
|
{ streak.CurrentStreakStartDate } ➞ { streak.CurrentStreakEndDate }
|
||
|
</div>
|
||
|
</div>
|
||
|
<div class="flex items-end font-bold">{ fmt.Sprint(streak.CurrentStreak) }</div>
|
||
|
</div>
|
||
|
<div class="flex items-center justify-between pb-2 mb-2 text-sm">
|
||
|
<div>
|
||
|
<p>
|
||
|
if streak.Window == "WEEK" {
|
||
|
Best Weekly Streak
|
||
|
} else {
|
||
|
Best Daily Streak
|
||
|
}
|
||
|
</p>
|
||
|
<div class="flex items-end text-sm text-gray-400">
|
||
|
{ streak.MaxStreakStartDate } ➞ { streak.MaxStreakEndDate }
|
||
|
</div>
|
||
|
</div>
|
||
|
<div class="flex items-end font-bold">{ fmt.Sprint(streak.MaxStreak) }</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
}
|
||
|
|
||
|
templ DailyReadChart(dailyReadSVG graph.SVGGraphData) {
|
||
|
<div class="relative w-full bg-white shadow-lg dark:bg-gray-700 rounded">
|
||
|
<p class="absolute top-3 left-5 text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500">
|
||
|
Daily Read Totals
|
||
|
</p>
|
||
|
<div class="relative">
|
||
|
<svg viewBox={ fmt.Sprintf("26 0 755 %d", dailyReadSVG.Height) } preserveAspectRatio="none" width="100%" height="6em">
|
||
|
<!-- Bezier Line Graph -->
|
||
|
<path
|
||
|
fill="#316BBE"
|
||
|
fill-opacity="0.5"
|
||
|
stroke="none"
|
||
|
d={ fmt.Sprintf("%s %s", dailyReadSVG.BezierPath, dailyReadSVG.BezierFill) }
|
||
|
></path>
|
||
|
<path fill="none" stroke="#316BBE" d={ dailyReadSVG.BezierPath }></path>
|
||
|
</svg>
|
||
|
<div
|
||
|
class="flex absolute w-full h-full top-0"
|
||
|
style="width: calc(100%*31/30); transform: translateX(-50%); left: 50%"
|
||
|
>
|
||
|
<!-- Required for iOS "Hover" Events (onclick) -->
|
||
|
for _, item := range dailyReadSVG.LinePoints {
|
||
|
<div
|
||
|
onclick
|
||
|
class="opacity-0 hover:opacity-100 w-full"
|
||
|
style="background: linear-gradient(rgba(128, 128, 128, 0.5), rgba(128, 128, 128, 0.5)) no-repeat center/2px 100%"
|
||
|
>
|
||
|
<div
|
||
|
class="flex flex-col items-center p-2 rounded absolute top-3 dark:text-white text-xs pointer-events-none"
|
||
|
style="transform: translateX(-50%); background-color: rgba(128, 128, 128, 0.2); left: 50%"
|
||
|
>
|
||
|
<span>{ item.Data.Label }</span>
|
||
|
<span>{ fmt.Sprint(item.Data.Value) } minutes</span>
|
||
|
</div>
|
||
|
</div>
|
||
|
}
|
||
|
</div>
|
||
|
</div>
|
||
|
</div>
|
||
|
}
|