This commit is contained in:
2025-08-17 17:04:27 -04:00
parent f9f23f2d3f
commit 2eed0d9021
72 changed files with 2713 additions and 100 deletions

View File

@@ -0,0 +1,35 @@
package stats
import (
"fmt"
g "maragu.dev/gomponents"
h "maragu.dev/gomponents/html"
)
type InfoCardData struct {
Title string
Size int64
Link string
}
func InfoCard(d InfoCardData) g.Node {
cardContent := h.Div(
g.Attr("class", "flex gap-4 w-full p-4 bg-white shadow-lg dark:bg-gray-700 rounded"),
h.Div(
g.Attr("class", "flex flex-col justify-around w-full text-sm"),
h.P(g.Attr("class", "text-2xl font-bold"), g.Text(fmt.Sprint(d.Size))),
h.P(g.Attr("class", "text-sm text-gray-400"), g.Text(d.Title)),
),
)
if d.Link == "" {
return h.Div(g.Attr("class", "w-full"), cardContent)
}
return h.A(
g.Attr("class", "w-full"),
h.Href(d.Link),
cardContent,
)
}

View File

@@ -0,0 +1,130 @@
package stats
import (
"fmt"
g "maragu.dev/gomponents"
h "maragu.dev/gomponents/html"
)
type LeaderboardItem struct {
UserID string
Value string
}
type LeaderboardData struct {
Name string
All []LeaderboardItem
Year []LeaderboardItem
Month []LeaderboardItem
Week []LeaderboardItem
}
func LeaderboardCard(l LeaderboardData) g.Node {
orderedItems := map[string][]LeaderboardItem{
"All": l.All,
"Year": l.Year,
"Month": l.Month,
"Week": l.Week,
}
var allNodes []g.Node
for key, items := range orderedItems {
// Get Top Reader Nodes
topReaders := items[:min(len(items), 3)]
var topReaderNodes []g.Node
for idx, reader := range topReaders {
border := ""
if idx > 0 {
border = " border-t border-gray-200"
}
topReaderNodes = append(topReaderNodes, h.Div(
g.Attr("class", "flex items-center justify-between pt-2 pb-2 text-sm"+border),
h.Div(h.P(g.Text(reader.UserID))),
h.Div(g.Attr("class", "flex items-end font-bold"), g.Text(reader.Value)),
))
}
allNodes = append(allNodes, g.Group([]g.Node{
h.Div(
g.Attr("class", "flex items-end my-6 space-x-2 hidden peer-checked/"+key+":block"),
g.If(len(items) == 0,
h.P(g.Attr("class", "text-5xl font-bold text-black dark:text-white"), g.Text("N/A")),
),
g.If(len(items) > 0,
h.P(g.Attr("class", "text-5xl font-bold text-black dark:text-white"), g.Text(items[0].UserID)),
),
),
h.Div(
g.Attr("class", "hidden dark:text-white peer-checked/"+key+":block"),
g.Group(topReaderNodes),
),
}))
}
return h.Div(
g.Attr("class", "w-full"),
h.Div(
g.Attr("class", "flex flex-col justify-between h-full w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded"),
h.Div(
h.Div(
g.Attr("class", "flex justify-between"),
h.P(
g.Attr("class", "text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500"),
g.Textf("%s Leaderboard", l.Name),
),
h.Div(
g.Attr("class", "flex gap-2 text-xs text-gray-400 items-center"),
h.Label(
g.Attr("for", fmt.Sprintf("all-%s", l.Name)),
g.Attr("class", "cursor-pointer hover:text-black dark:hover:text-white"),
g.Text("all"),
),
h.Label(
g.Attr("for", fmt.Sprintf("year-%s", l.Name)),
g.Attr("class", "cursor-pointer hover:text-black dark:hover:text-white"),
g.Text("year"),
),
h.Label(
g.Attr("for", fmt.Sprintf("month-%s", l.Name)),
g.Attr("class", "cursor-pointer hover:text-black dark:hover:text-white"),
g.Text("month"),
),
h.Label(
g.Attr("for", fmt.Sprintf("week-%s", l.Name)),
g.Attr("class", "cursor-pointer hover:text-black dark:hover:text-white"),
g.Text("week"),
),
),
),
),
h.Input(
g.Attr("type", "radio"),
g.Attr("name", fmt.Sprintf("options-%s", l.Name)),
g.Attr("id", fmt.Sprintf("all-%s", l.Name)),
g.Attr("class", "hidden peer/All"),
g.Attr("checked", ""),
),
h.Input(
g.Attr("type", "radio"),
g.Attr("name", fmt.Sprintf("options-%s", l.Name)),
g.Attr("id", fmt.Sprintf("year-%s", l.Name)),
g.Attr("class", "hidden peer/Year"),
),
h.Input(
g.Attr("type", "radio"),
g.Attr("name", fmt.Sprintf("options-%s", l.Name)),
g.Attr("id", fmt.Sprintf("month-%s", l.Name)),
g.Attr("class", "hidden peer/Month"),
),
h.Input(
g.Attr("type", "radio"),
g.Attr("name", fmt.Sprintf("options-%s", l.Name)),
g.Attr("id", fmt.Sprintf("week-%s", l.Name)),
g.Attr("class", "hidden peer/Week"),
),
g.Group(allNodes),
),
)
}

View File

@@ -0,0 +1,61 @@
package stats
import (
"fmt"
g "maragu.dev/gomponents"
h "maragu.dev/gomponents/html"
"reichard.io/antholume/database"
"reichard.io/antholume/graph"
)
func MonthlyChart(dailyStats []database.GetDailyReadStatsRow) g.Node {
graphData := buildSVGGraphData(dailyStats, 800, 70)
return h.Div(
g.Attr("class", "relative"),
h.SVG(
g.Attr("viewBox", fmt.Sprintf("26 0 755 %d", graphData.Height)),
g.Attr("preserveAspectRatio", "none"),
g.Attr("width", "100%"),
g.Attr("height", "6em"),
g.El("path",
g.Attr("fill", "#316BBE"),
g.Attr("fill-opacity", "0.5"),
g.Attr("stroke", "none"),
g.Attr("d", graphData.BezierPath+" "+graphData.BezierFill),
),
g.El("path",
g.Attr("fill", "none"),
g.Attr("stroke", "#316BBE"),
g.Attr("d", graphData.BezierPath),
),
),
h.Div(
g.Attr("class", "flex absolute w-full h-full top-0"),
g.Attr("style", "width: calc(100%*31/30); transform: translateX(-50%); left: 50%"),
g.Group(g.Map(dailyStats, func(d database.GetDailyReadStatsRow) g.Node {
return h.Div(
g.Attr("onclick", ""),
g.Attr("class", "opacity-0 hover:opacity-100 w-full"),
g.Attr("style", "background: linear-gradient(rgba(128, 128, 128, 0.5), rgba(128, 128, 128, 0.5)) no-repeat center/2px 100%"),
h.Div(
g.Attr("class", "flex flex-col items-center p-2 rounded absolute top-3 dark:text-white text-xs pointer-events-none"),
g.Attr("style", "transform: translateX(-50%); background-color: rgba(128, 128, 128, 0.2); left: 50%"),
h.Span(g.Text(d.Date)),
h.Span(g.Textf("%d minutes", d.MinutesRead)),
),
)
})),
),
)
}
// buildSVGGraphData builds SVGGraphData from the provided stats, width and height.
func buildSVGGraphData(inputData []database.GetDailyReadStatsRow, svgWidth int, svgHeight int) graph.SVGGraphData {
var intData []int64
for _, item := range inputData {
intData = append(intData, item.MinutesRead)
}
return graph.GetSVGGraphData(intData, svgWidth, svgHeight)
}

View File

@@ -0,0 +1,65 @@
package stats
import (
g "maragu.dev/gomponents"
h "maragu.dev/gomponents/html"
"reichard.io/antholume/database"
)
func StreakCard(s database.UserStreak) g.Node {
return h.Div(
g.Attr("class", "w-full"),
h.Div(
g.Attr("class", "relative w-full px-4 py-6 bg-white shadow-lg dark:bg-gray-700 rounded"),
h.P(
g.Attr("class", "text-sm font-semibold text-gray-700 border-b border-gray-200 w-max dark:text-white dark:border-gray-500"),
g.If(s.Window == "WEEK", g.Text("Weekly Read Streak")),
g.If(s.Window != "WEEK", g.Text("Daily Read Streak")),
),
h.Div(
g.Attr("class", "flex items-end my-6 space-x-2"),
h.P(
g.Attr("class", "text-5xl font-bold text-black dark:text-white"),
g.Textf("%d", s.CurrentStreak),
),
),
h.Div(
g.Attr("class", "dark:text-white"),
h.Div(
g.Attr("class", "flex items-center justify-between pb-2 mb-2 text-sm border-b border-gray-200"),
h.Div(
h.P(
g.If(s.Window == "WEEK", g.Text("Current Weekly Streak")),
g.If(s.Window != "WEEK", g.Text("Current Daily Streak")),
),
h.Div(
g.Attr("class", "flex items-end text-sm text-gray-400"),
g.Textf("%s ➞ %s", s.CurrentStreakStartDate, s.CurrentStreakEndDate),
),
),
h.Div(
g.Attr("class", "flex items-end font-bold"),
g.Textf("%d", s.CurrentStreak),
),
),
h.Div(
g.Attr("class", "flex items-center justify-between pb-2 mb-2 text-sm"),
h.Div(
h.P(
g.If(s.Window == "WEEK", g.Text("Best Weekly Streak")),
g.If(s.Window != "WEEK", g.Text("Best Daily Streak")),
),
h.Div(
g.Attr("class", "flex items-end text-sm text-gray-400"),
g.Textf("%s ➞ %s", s.MaxStreakStartDate, s.MaxStreakEndDate),
),
),
h.Div(
g.Attr("class", "flex items-end font-bold"),
g.Textf("%d", s.MaxStreak),
),
),
),
),
)
}