Fitness dashboard v1
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
// Fitness dashboard range dropdowns: changing one reloads the page with that
|
||||
// chart's query parameter updated. Plain GET navigation — each range is a
|
||||
// distinct, bookmarkable view, so no postReplace/history handling is needed.
|
||||
document.addEventListener('change', function (e) {
|
||||
var sel = e.target.closest('[data-fitness-range]');
|
||||
if (!sel) return;
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set(sel.dataset.fitnessRange, sel.value);
|
||||
window.location.href = url.toString();
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
{{define "fitnessChart"}}
|
||||
<section class="fitness-chart panel">
|
||||
<div class="fitness-chart-header row space-between">
|
||||
<span class="caption">{{.Title}}</span>
|
||||
<select class="input fitness-range" data-fitness-range="{{.Param}}" aria-label="{{.Title}} time range">
|
||||
{{range .Options}}<option value="{{.Value}}"{{if .Selected}} selected{{end}}>{{.Label}}</option>{{end}}
|
||||
</select>
|
||||
</div>
|
||||
{{if .Empty}}
|
||||
<p class="fitness-empty is-empty">No data in this range.</p>
|
||||
{{else}}
|
||||
<svg class="fitness-svg" viewBox="0 0 {{.ViewW}} {{.ViewH}}" role="img" aria-label="{{.Title}}">
|
||||
{{range .YTicks}}
|
||||
<line class="chart-grid" x1="{{$.PlotX}}" y1="{{.Pos}}" x2="{{$.PlotR}}" y2="{{.Pos}}"/>
|
||||
<text class="chart-label" x="{{$.YLabelX}}" y="{{.Pos}}" text-anchor="end" dominant-baseline="middle">{{.Label}}</text>
|
||||
{{end}}
|
||||
{{range .XTicks}}
|
||||
<text class="chart-label" x="{{.Pos}}" y="{{$.XLabelY}}" text-anchor="{{.Anchor}}">{{.Label}}</text>
|
||||
{{end}}
|
||||
<line class="chart-axis" x1="{{.PlotX}}" y1="{{.PlotY}}" x2="{{.PlotX}}" y2="{{.PlotB}}"/>
|
||||
<line class="chart-axis" x1="{{.PlotX}}" y1="{{.PlotB}}" x2="{{.PlotR}}" y2="{{.PlotB}}"/>
|
||||
{{range .Bars}}
|
||||
<g class="chart-bar-group">
|
||||
<title>{{.Title}}</title>
|
||||
<rect class="chart-bar" x="{{.X}}" y="{{.Y}}" width="{{.W}}" height="{{.H}}"/>
|
||||
<line class="chart-net" x1="{{.X}}" y1="{{.NetY}}" x2="{{.X2}}" y2="{{.NetY}}"/>
|
||||
</g>
|
||||
{{end}}
|
||||
{{range .Lines}}
|
||||
<polyline class="chart-line" points="{{.}}"/>
|
||||
{{end}}
|
||||
{{range .Dots}}
|
||||
<circle class="chart-dot" cx="{{.X}}" cy="{{.Y}}" r="2.5"><title>{{.Title}}</title></circle>
|
||||
{{end}}
|
||||
{{if .Goal}}
|
||||
<line class="chart-goal" x1="{{.PlotX}}" y1="{{.Goal.Y}}" x2="{{.PlotR}}" y2="{{.Goal.Y}}"/>
|
||||
<text class="chart-goal-label" x="{{.PlotR}}" y="{{.Goal.LabelY}}" text-anchor="end">{{.Goal.Label}}</text>
|
||||
{{end}}
|
||||
</svg>
|
||||
{{end}}
|
||||
</section>
|
||||
{{end}}
|
||||
<div class="fitness-dash col">
|
||||
{{if .Notice}}
|
||||
<p class="muted">{{.Notice}}</p>
|
||||
{{else}}
|
||||
{{range .Charts}}{{template "fitnessChart" .}}{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
<script src="/_/fitness/fitness.js"></script>
|
||||
@@ -70,6 +70,7 @@ hr { border: none; border-top: var(--border-dashed); margin: var(--space-4) 0; }
|
||||
.gap-2 { gap: var(--space-2); }
|
||||
.gap-3 { gap: var(--space-3); }
|
||||
.gap-4 { gap: var(--space-4); }
|
||||
.space-between { justify-content: space-between; }
|
||||
.divider-dashed { border-bottom: var(--border-dashed); }
|
||||
|
||||
/* === Page layout ===
|
||||
@@ -654,6 +655,30 @@ aside.sidebar:empty { display: none; }
|
||||
.diary-cal-grid td.cal-current a { color: var(--primary-hover); }
|
||||
.btn-block.cal-current { color: var(--primary-hover); }
|
||||
|
||||
/* === Fitness dashboard ===
|
||||
Server-rendered inline SVG charts. Geometry comes precomputed from Go;
|
||||
colors and strokes are applied here via classes so the inline SVG follows
|
||||
the theme palette. */
|
||||
.fitness-chart { padding: var(--space-3); }
|
||||
.fitness-chart-header { margin-bottom: var(--space-2); }
|
||||
.fitness-range { width: auto; font-size: var(--font-sm); }
|
||||
.fitness-empty {
|
||||
border: var(--border-dashed);
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
padding: var(--space-5);
|
||||
}
|
||||
.fitness-svg { display: block; width: 100%; height: auto; }
|
||||
.fitness-svg .chart-grid { stroke: var(--bg-panel-hover); }
|
||||
.fitness-svg .chart-axis { stroke: var(--text-muted); }
|
||||
.fitness-svg .chart-label { fill: var(--text-muted); font-size: var(--font-xs); }
|
||||
.fitness-svg .chart-line { fill: none; stroke: var(--link); stroke-width: 1.5; }
|
||||
.fitness-svg .chart-dot { fill: var(--link); }
|
||||
.fitness-svg .chart-bar { fill: var(--secondary); }
|
||||
.fitness-svg .chart-net { stroke: var(--link); stroke-width: 1.5; }
|
||||
.fitness-svg .chart-goal { stroke: var(--primary-hover); stroke-dasharray: 4 3; }
|
||||
.fitness-svg .chart-goal-label { fill: var(--primary-hover); font-size: var(--font-xs); }
|
||||
|
||||
/* === Responsive === */
|
||||
@media (max-width: 1100px) {
|
||||
.page-wrap { grid-template-columns: 1fr; }
|
||||
|
||||
Reference in New Issue
Block a user