81 lines
3.1 KiB
Go
81 lines
3.1 KiB
Go
//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,
|
||
})
|
||
}
|