Piattaforme riservate: ruolo dedicato e port della dashboard Stress Index
Il ruolo `campus` diventa `piattaforme` e non apre più solo la sezione Biohacking: copre tutte le aree riservate, con migrazione guardata sugli utenti esistenti. Nell'header la vecchia voce Biohacking lascia il posto a un menu Piattaforme con i due link, visibile solo a chi ha il ruolo, e /piattaforme diventa la pagina di atterraggio. Stress Index è portata dentro il sito: le nove viste dell'area professionisti diventano pagine Astro sotto /piattaforme/stress-index, con lo stile del sito al posto del design system del prototipo. React resta solo dove serve davvero — grafici recharts e scheda cliente — il resto è Astro con un filo di JS. I dati sono ancora quelli dimostrativi, raccolti in lib/stress-index/data.ts. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// Andamento medio dello studio, con selettore di periodo. L'aggregazione media stress e
|
||||
// recupero di tutti i clienti sugli ultimi N rilievi settimanali.
|
||||
import { useState } from 'react';
|
||||
import DataChart from './DataChart';
|
||||
import type { Measurement } from '../../../lib/stress-index/data';
|
||||
|
||||
const PERIODS = [
|
||||
{ key: '30g', label: '30 giorni', points: 5 },
|
||||
{ key: '90g', label: '90 giorni', points: 13 },
|
||||
{ key: '6m', label: '6 mesi', points: 20 },
|
||||
{ key: 'tutto', label: 'Tutto', points: 20 },
|
||||
] as const;
|
||||
|
||||
interface Props { measurementsByClient: Record<string, Measurement[]> }
|
||||
|
||||
function aggregate(byClient: Record<string, Measurement[]>, points: number) {
|
||||
const out: { date: string; stress: number; recovery: number }[] = [];
|
||||
for (let fromEnd = points; fromEnd >= 1; fromEnd--) {
|
||||
let stress = 0, recovery = 0, count = 0, date = '';
|
||||
for (const series of Object.values(byClient)) {
|
||||
const point = series[series.length - fromEnd];
|
||||
if (!point) continue;
|
||||
stress += point.stress;
|
||||
recovery += point.recovery;
|
||||
count += 1;
|
||||
date = point.date;
|
||||
}
|
||||
if (count) out.push({ date, stress: Math.round(stress / count), recovery: Math.round(recovery / count) });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export default function AndamentoChart({ measurementsByClient }: Props) {
|
||||
const [period, setPeriod] = useState<string>('90g');
|
||||
const active = PERIODS.find((p) => p.key === period) ?? PERIODS[1];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="si-chips">
|
||||
{PERIODS.map((p) => (
|
||||
<button key={p.key} type="button" className="si-chip" aria-pressed={p.key === period} onClick={() => setPeriod(p.key)}>
|
||||
{p.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<DataChart
|
||||
data={aggregate(measurementsByClient, active.points)}
|
||||
xKey="date"
|
||||
series={[
|
||||
{ key: 'stress', label: 'Stress medio' },
|
||||
{ key: 'recovery', label: 'Recupero medio' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Grafico generico della piattaforma (isola React: recharts ha bisogno del DOM).
|
||||
// I colori sono quelli del sito, non quelli del prototipo originale.
|
||||
import {
|
||||
ResponsiveContainer, LineChart, BarChart, Line, Bar, CartesianGrid, XAxis, YAxis, Tooltip, Legend,
|
||||
} from 'recharts';
|
||||
|
||||
export interface ChartSeries { key: string; label: string; color?: string }
|
||||
|
||||
interface Props {
|
||||
type?: 'line' | 'bar';
|
||||
data: Record<string, string | number>[];
|
||||
xKey: string;
|
||||
series: ChartSeries[];
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const COLORS = ['#9c8b70', '#3a2929', '#6f8f6a', '#c08a3e', '#b05a4e'];
|
||||
const AXIS = '#8a8a8a';
|
||||
|
||||
export default function DataChart({ type = 'line', data, xKey, series, height = 260 }: Props) {
|
||||
if (!data.length) return <p className="si-chart__empty">Nessun dato nel periodo selezionato.</p>;
|
||||
const Chart = type === 'bar' ? BarChart : LineChart;
|
||||
|
||||
return (
|
||||
<div className="si-chart" style={{ height }}>
|
||||
<ResponsiveContainer width="100%" height={height}>
|
||||
<Chart data={data} margin={{ top: 6, right: 8, bottom: 0, left: -18 }}>
|
||||
<CartesianGrid stroke="#e4dfd7" strokeDasharray="3 3" vertical={false} />
|
||||
<XAxis dataKey={xKey} stroke={AXIS} fontSize={11} tickLine={false} />
|
||||
<YAxis stroke={AXIS} fontSize={11} tickLine={false} axisLine={false} />
|
||||
<Tooltip
|
||||
contentStyle={{ border: '1px solid #e4dfd7', borderRadius: 0, fontSize: 12 }}
|
||||
labelStyle={{ color: '#2b2b2b', fontWeight: 600 }}
|
||||
/>
|
||||
{series.length > 1 && <Legend iconType="plainline" wrapperStyle={{ fontSize: 12 }} />}
|
||||
{series.map((s, i) =>
|
||||
type === 'bar' ? (
|
||||
<Bar key={s.key} dataKey={s.key} name={s.label} fill={s.color ?? COLORS[i % COLORS.length]} />
|
||||
) : (
|
||||
<Line
|
||||
key={s.key} type="monotone" dataKey={s.key} name={s.label} dot={false} strokeWidth={2.2}
|
||||
stroke={s.color ?? COLORS[i % COLORS.length]}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</Chart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user