Files
kjol/go/cmd/kjol-website/app/server_counter.go

81 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//go:build !(js && wasm)
package app
import (
"strconv"
"time"
. "kjol/vdom"
ui "kjol/webui"
)
// ServerCounter is a SERVER component — note it's written exactly like a client
// component (same builders, signals, On handlers). The //gowasm:server directive
// makes the build generate a client stub so calling ServerCounter() on the
// frontend is identical to calling any component; the state and this render run
// on the server (its chart SVG is drawn there by webui.ChartSVG), and clicks
// round-trip over /rsc.
//
// The chart plots the counter value against the wall-clock time of each click
// (milliseconds since the first click), so spacing clicks out spreads the data
// points along the x-axis. Because the component is stateless on the server, the
// click points live in a signal that round-trips with the rest of its state
// (a plain slice would reset on every request).
//
//gowasm:server
func ServerCounter() func() *VNode {
count := NewSignal(0)
points := NewSignal([]clickPoint{})
bump := func(delta int) {
count.Set(count.Get() + delta)
points.Set(append(points.Get(), clickPoint{T: time.Now().UnixMilli(), V: count.Get()}))
}
// No card of its own: the component draws bare content and lets the caller frame it.
// The docs page already puts it in a demo panel, and a card inside a card gives you
// two borders and two shadows around the same thing.
return func() *VNode {
return Div(
Div(Attr("class", "flex items-center gap-2 mb-3"),
Span(Attr("class", "text-ink-soft"), Text("Server counter: ")),
Strong(Attr("class", "badge inline-flex items-center rounded-full bg-green-700 px-2.5 py-0.5 text-sm font-semibold text-white"), Text(strconv.Itoa(count.Get()))),
Div(Attr("class", "ml-auto flex gap-1"),
ui.Button(ui.ButtonProps{Color: ui.ButtonSecondary, Small: true, Text: "", OnClick: func() { bump(-1) }}),
ui.Button(ui.ButtonProps{Color: ui.ButtonGreen, Small: true, Text: "+", OnClick: func() { bump(1) }}),
),
),
Div(Attr("class", "rounded-default border border-line bg-surface p-2 overflow-auto"),
Raw(clickChartSVG(points.Get()))),
)
}
}
// clickPoint records one click: its wall-clock time and the resulting counter
// value. Exported fields so the signal's JSON snapshot round-trips it.
type clickPoint struct {
T int64 // click time, Unix milliseconds
V int // counter value after the click
}
// clickChartSVG plots the counter value at each click as a line, drawn on the server by
// webui.ChartSVG — the same chart geometry the client kit uses, rendered to a static SVG
// string (no controller, no hover) because a server component's output is HTML.
func clickChartSVG(points []clickPoint) string {
if len(points) == 0 {
return `<span class="text-ink-muted">Click + / to plot the counter over each click.</span>`
}
vals := make([]float64, len(points))
labels := make([]string, len(points))
for i, p := range points {
vals[i] = float64(p.V)
labels[i] = strconv.Itoa(i + 1)
}
return ui.ChartSVG(ui.ChartProps{
Kind: ui.ChartLine,
Labels: labels,
Series: []ui.ChartSeries{{Name: "Counter", Data: vals, Color: "var(--color-chart-4)"}},
Height: 240,
NoTooltip: true,
})
}