Compare commits
3 Commits
db7a0cef96
...
313ad53e2e
| Author | SHA1 | Date | |
|---|---|---|---|
| 313ad53e2e | |||
| 8bff21bede | |||
| 4aea17124e |
@@ -325,7 +325,7 @@ export default function DesignSystemPage() {
|
|||||||
<Card id="disclosure-dropdown" className="p-6">
|
<Card id="disclosure-dropdown" className="p-6">
|
||||||
<h2 className="mb-4 text-xl font-semibold text-foreground">Disclosure & Dropdown</h2>
|
<h2 className="mb-4 text-xl font-semibold text-foreground">Disclosure & Dropdown</h2>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Disclosure title="Panneau pliable" subtitle="Composant Disclosure">
|
<Disclosure icon="ℹ️" title="Panneau pliable" subtitle="Composant Disclosure">
|
||||||
<p className="text-sm text-muted">Contenu du panneau.</p>
|
<p className="text-sm text-muted">Contenu du panneau.</p>
|
||||||
</Disclosure>
|
</Disclosure>
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
|
|||||||
@@ -40,6 +40,16 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
|
|||||||
getUserTeams(authSession.user.id),
|
getUserTeams(authSession.user.id),
|
||||||
getWeatherSessionsHistory(authSession.user.id),
|
getWeatherSessionsHistory(authSession.user.id),
|
||||||
]);
|
]);
|
||||||
|
const currentHistoryIndex = history.findIndex((point) => point.sessionId === session.id);
|
||||||
|
const previousTeamAverages =
|
||||||
|
currentHistoryIndex > 0
|
||||||
|
? {
|
||||||
|
performance: history[currentHistoryIndex - 1].performance,
|
||||||
|
moral: history[currentHistoryIndex - 1].moral,
|
||||||
|
flux: history[currentHistoryIndex - 1].flux,
|
||||||
|
valueCreation: history[currentHistoryIndex - 1].valueCreation,
|
||||||
|
}
|
||||||
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto max-w-7xl px-4">
|
<main className="mx-auto max-w-7xl px-4">
|
||||||
@@ -54,12 +64,6 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
|
|||||||
badges={<Badge variant="primary">{session.entries.length} membres</Badge>}
|
badges={<Badge variant="primary">{session.entries.length} membres</Badge>}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Info sur les catégories */}
|
|
||||||
<WeatherInfoPanel />
|
|
||||||
|
|
||||||
{/* Évolution dans le temps */}
|
|
||||||
<WeatherTrendChart data={history} currentSessionId={session.id} />
|
|
||||||
|
|
||||||
{/* Live Wrapper + Board */}
|
{/* Live Wrapper + Board */}
|
||||||
<WeatherLiveWrapper
|
<WeatherLiveWrapper
|
||||||
sessionId={session.id}
|
sessionId={session.id}
|
||||||
@@ -70,7 +74,7 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
|
|||||||
canEdit={session.canEdit}
|
canEdit={session.canEdit}
|
||||||
userTeams={userTeams}
|
userTeams={userTeams}
|
||||||
>
|
>
|
||||||
<WeatherAverageBar entries={session.entries} />
|
<WeatherAverageBar entries={session.entries} previousAverages={previousTeamAverages} />
|
||||||
<WeatherBoard
|
<WeatherBoard
|
||||||
sessionId={session.id}
|
sessionId={session.id}
|
||||||
currentUserId={authSession.user.id}
|
currentUserId={authSession.user.id}
|
||||||
@@ -84,6 +88,8 @@ export default async function WeatherSessionPage({ params }: WeatherSessionPageP
|
|||||||
canEdit={session.canEdit}
|
canEdit={session.canEdit}
|
||||||
previousEntries={Object.fromEntries(previousEntries)}
|
previousEntries={Object.fromEntries(previousEntries)}
|
||||||
/>
|
/>
|
||||||
|
<WeatherInfoPanel className="mt-6 mb-6" />
|
||||||
|
<WeatherTrendChart data={history} currentSessionId={session.id} />
|
||||||
</WeatherLiveWrapper>
|
</WeatherLiveWrapper>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { ReactNode, useId, useState } from 'react';
|
import { ReactNode, useId, useState } from 'react';
|
||||||
|
|
||||||
interface DisclosureProps {
|
interface DisclosureProps {
|
||||||
|
icon?: ReactNode;
|
||||||
title: ReactNode;
|
title: ReactNode;
|
||||||
subtitle?: ReactNode;
|
subtitle?: ReactNode;
|
||||||
defaultOpen?: boolean;
|
defaultOpen?: boolean;
|
||||||
@@ -11,6 +12,7 @@ interface DisclosureProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Disclosure({
|
export function Disclosure({
|
||||||
|
icon,
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
defaultOpen = false,
|
defaultOpen = false,
|
||||||
@@ -21,29 +23,41 @@ export function Disclosure({
|
|||||||
const contentId = useId();
|
const contentId = useId();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`rounded-lg border border-border bg-card-hover ${className}`}>
|
<div className={`overflow-hidden rounded-xl border border-border bg-card ${className}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIsOpen((prev) => !prev)}
|
onClick={() => setIsOpen((prev) => !prev)}
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
aria-controls={contentId}
|
aria-controls={contentId}
|
||||||
className="flex w-full items-center justify-between px-4 py-2.5 text-left transition-colors hover:bg-card"
|
className={`
|
||||||
|
flex w-full items-center justify-between gap-3 px-4 py-3 text-left transition-colors
|
||||||
|
${isOpen ? 'bg-card-hover/60' : 'hover:bg-card-hover/60'}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
|
<div className="min-w-0 flex flex-1 items-start gap-2">
|
||||||
|
{icon && (
|
||||||
|
<span className="inline-flex h-5 w-5 shrink-0 items-center justify-center self-center text-base leading-none">
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="text-sm font-semibold text-foreground">{title}</div>
|
<div className="text-sm font-medium text-foreground">{title}</div>
|
||||||
{subtitle && <div className="text-xs text-muted">{subtitle}</div>}
|
{subtitle && <div className="mt-0.5 text-xs text-muted">{subtitle}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-card-hover text-muted">
|
||||||
<svg
|
<svg
|
||||||
className={`h-4 w-4 shrink-0 text-muted transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
className={`h-4 w-4 transition-transform ${isOpen ? 'rotate-180' : ''}`}
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
>
|
>
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div id={contentId} className="border-t border-border px-4 py-3">
|
<div id={contentId} className="border-t border-border bg-card px-4 py-3">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// src/components/weather/WeatherAverageBar.tsx
|
// src/components/weather/WeatherAverageBar.tsx
|
||||||
import { getAverageEmoji } from '@/lib/weather-utils';
|
import { getAverageEmoji, getEmojiScore } from '@/lib/weather-utils';
|
||||||
|
|
||||||
interface WeatherEntry {
|
interface WeatherEntry {
|
||||||
performanceEmoji: string | null;
|
performanceEmoji: string | null;
|
||||||
@@ -10,22 +10,93 @@ interface WeatherEntry {
|
|||||||
|
|
||||||
interface WeatherAverageBarProps {
|
interface WeatherAverageBarProps {
|
||||||
entries: WeatherEntry[];
|
entries: WeatherEntry[];
|
||||||
|
previousAverages?: {
|
||||||
|
performance: number | null;
|
||||||
|
moral: number | null;
|
||||||
|
flux: number | null;
|
||||||
|
valueCreation: number | null;
|
||||||
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AXES = [
|
const AXES = [
|
||||||
{ key: 'performanceEmoji' as const, label: 'Performance' },
|
{ key: 'performanceEmoji' as const, scoreKey: 'performance' as const, label: 'Performance' },
|
||||||
{ key: 'moralEmoji' as const, label: 'Moral' },
|
{ key: 'moralEmoji' as const, scoreKey: 'moral' as const, label: 'Moral' },
|
||||||
{ key: 'fluxEmoji' as const, label: 'Flux' },
|
{ key: 'fluxEmoji' as const, scoreKey: 'flux' as const, label: 'Flux' },
|
||||||
{ key: 'valueCreationEmoji' as const, label: 'Création de valeur' },
|
{
|
||||||
|
key: 'valueCreationEmoji' as const,
|
||||||
|
scoreKey: 'valueCreation' as const,
|
||||||
|
label: 'Création de valeur',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function WeatherAverageBar({ entries }: WeatherAverageBarProps) {
|
function getAverageScore(emojis: (string | null)[]): number | null {
|
||||||
|
const scores = emojis.map(getEmojiScore).filter((score): score is number => score !== null);
|
||||||
|
if (scores.length === 0) return null;
|
||||||
|
return scores.reduce((sum, score) => sum + score, 0) / scores.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function AverageEvolutionIndicator({
|
||||||
|
currentScore,
|
||||||
|
previousScore,
|
||||||
|
}: {
|
||||||
|
currentScore: number | null;
|
||||||
|
previousScore: number | null | undefined;
|
||||||
|
}) {
|
||||||
|
if (currentScore === null || previousScore === null || previousScore === undefined) return null;
|
||||||
|
const delta = currentScore - previousScore;
|
||||||
|
|
||||||
|
if (delta < 0) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[10px] font-bold"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'color-mix(in srgb, var(--success) 18%, transparent)',
|
||||||
|
color: 'var(--success)',
|
||||||
|
}}
|
||||||
|
title="En progression"
|
||||||
|
>
|
||||||
|
↑
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (delta > 0) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[10px] font-bold"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'color-mix(in srgb, var(--destructive) 18%, transparent)',
|
||||||
|
color: 'var(--destructive)',
|
||||||
|
}}
|
||||||
|
title="En baisse"
|
||||||
|
>
|
||||||
|
↓
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[10px] font-bold"
|
||||||
|
style={{
|
||||||
|
backgroundColor: 'color-mix(in srgb, var(--muted) 15%, transparent)',
|
||||||
|
color: 'var(--muted)',
|
||||||
|
}}
|
||||||
|
title="Stable"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WeatherAverageBar({ entries, previousAverages }: WeatherAverageBarProps) {
|
||||||
if (entries.length === 0) return null;
|
if (entries.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center gap-3 mb-4">
|
<div className="flex flex-wrap items-center gap-3 mb-4">
|
||||||
<span className="text-xs font-medium text-muted uppercase tracking-wide">Moyenne équipe</span>
|
<span className="text-xs font-medium text-muted uppercase tracking-wide">Moyenne équipe</span>
|
||||||
{AXES.map(({ key, label }) => {
|
{AXES.map(({ key, scoreKey, label }) => {
|
||||||
|
const currentScore = getAverageScore(entries.map((e) => e[key]));
|
||||||
const avg = getAverageEmoji(entries.map((e) => e[key]));
|
const avg = getAverageEmoji(entries.map((e) => e[key]));
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -34,6 +105,10 @@ export function WeatherAverageBar({ entries }: WeatherAverageBarProps) {
|
|||||||
>
|
>
|
||||||
<span className="text-lg leading-none">{avg ?? '—'}</span>
|
<span className="text-lg leading-none">{avg ?? '—'}</span>
|
||||||
<span className="text-xs text-muted">{label}</span>
|
<span className="text-xs text-muted">{label}</span>
|
||||||
|
<AverageEvolutionIndicator
|
||||||
|
currentScore={currentScore}
|
||||||
|
previousScore={previousAverages?.[scoreKey]}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -2,9 +2,13 @@
|
|||||||
|
|
||||||
import { Disclosure } from '@/components/ui';
|
import { Disclosure } from '@/components/ui';
|
||||||
|
|
||||||
export function WeatherInfoPanel() {
|
interface WeatherInfoPanelProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WeatherInfoPanel({ className = 'mb-6' }: WeatherInfoPanelProps) {
|
||||||
return (
|
return (
|
||||||
<Disclosure title="Les 4 axes de la météo personnelle" className="mb-6">
|
<Disclosure icon="ℹ️" title="Les 4 axes de la météo personnelle" className={className}>
|
||||||
<div className="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-4">
|
<div className="grid gap-2.5 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-0.5 text-xs font-medium text-foreground">☀️ Performance</p>
|
<p className="mb-0.5 text-xs font-medium text-foreground">☀️ Performance</p>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import type { WeatherHistoryPoint } from '@/services/weather';
|
|||||||
interface WeatherTrendChartProps {
|
interface WeatherTrendChartProps {
|
||||||
data: WeatherHistoryPoint[];
|
data: WeatherHistoryPoint[];
|
||||||
currentSessionId?: string;
|
currentSessionId?: string;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const INDICATORS = [
|
const INDICATORS = [
|
||||||
@@ -63,7 +64,11 @@ function buildPath(
|
|||||||
|
|
||||||
const Y_TICKS = [1, 5, 10, 14, 19];
|
const Y_TICKS = [1, 5, 10, 14, 19];
|
||||||
|
|
||||||
export function WeatherTrendChart({ data, currentSessionId }: WeatherTrendChartProps) {
|
export function WeatherTrendChart({
|
||||||
|
data,
|
||||||
|
currentSessionId,
|
||||||
|
className = 'mb-6',
|
||||||
|
}: WeatherTrendChartProps) {
|
||||||
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
const [hoveredIdx, setHoveredIdx] = useState<number | null>(null);
|
||||||
|
|
||||||
if (data.length < 2) return null;
|
if (data.length < 2) return null;
|
||||||
@@ -72,7 +77,8 @@ export function WeatherTrendChart({ data, currentSessionId }: WeatherTrendChartP
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Disclosure
|
<Disclosure
|
||||||
className="mb-6"
|
icon="📈"
|
||||||
|
className={className}
|
||||||
title={
|
title={
|
||||||
<>
|
<>
|
||||||
Évolution dans le temps
|
Évolution dans le temps
|
||||||
|
|||||||
Reference in New Issue
Block a user