feat: implement Weather Workshop feature with models, UI components, and session management for enhanced team visibility and personal well-being tracking
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m16s
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m16s
This commit is contained in:
273
src/components/weather/WeatherCard.tsx
Normal file
273
src/components/weather/WeatherCard.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useTransition } from 'react';
|
||||
import { createOrUpdateWeatherEntry } from '@/actions/weather';
|
||||
import { Avatar } from '@/components/ui/Avatar';
|
||||
import { Textarea } from '@/components/ui/Textarea';
|
||||
|
||||
const WEATHER_EMOJIS = [
|
||||
{ emoji: '', label: 'Aucun' },
|
||||
{ emoji: '☀️', label: 'Soleil' },
|
||||
{ emoji: '🌤️', label: 'Soleil derrière nuage' },
|
||||
{ emoji: '⛅', label: 'Soleil et nuages' },
|
||||
{ emoji: '☁️', label: 'Nuages' },
|
||||
{ emoji: '🌦️', label: 'Soleil et pluie' },
|
||||
{ emoji: '🌧️', label: 'Pluie' },
|
||||
{ emoji: '⛈️', label: 'Orage et pluie' },
|
||||
{ emoji: '🌩️', label: 'Éclair' },
|
||||
{ emoji: '❄️', label: 'Neige' },
|
||||
{ emoji: '🌨️', label: 'Neige qui tombe' },
|
||||
{ emoji: '🌪️', label: 'Tornade' },
|
||||
{ emoji: '🌫️', label: 'Brouillard' },
|
||||
{ emoji: '🌈', label: 'Arc-en-ciel' },
|
||||
{ emoji: '🌊', label: 'Vague' },
|
||||
{ emoji: '🔥', label: 'Feu' },
|
||||
{ emoji: '💨', label: 'Vent' },
|
||||
{ emoji: '⭐', label: 'Étoile' },
|
||||
{ emoji: '🌟', label: 'Étoile brillante' },
|
||||
{ emoji: '✨', label: 'Étincelles' },
|
||||
];
|
||||
|
||||
interface WeatherEntry {
|
||||
id: string;
|
||||
userId: string;
|
||||
performanceEmoji: string | null;
|
||||
moralEmoji: string | null;
|
||||
fluxEmoji: string | null;
|
||||
valueCreationEmoji: string | null;
|
||||
notes: string | null;
|
||||
user: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface WeatherCardProps {
|
||||
sessionId: string;
|
||||
currentUserId: string;
|
||||
entry: WeatherEntry;
|
||||
canEdit: boolean;
|
||||
}
|
||||
|
||||
export function WeatherCard({ sessionId, currentUserId, entry, canEdit }: WeatherCardProps) {
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [notes, setNotes] = useState(entry.notes || '');
|
||||
const [performanceEmoji, setPerformanceEmoji] = useState(entry.performanceEmoji || null);
|
||||
const [moralEmoji, setMoralEmoji] = useState(entry.moralEmoji || null);
|
||||
const [fluxEmoji, setFluxEmoji] = useState(entry.fluxEmoji || null);
|
||||
const [valueCreationEmoji, setValueCreationEmoji] = useState(entry.valueCreationEmoji || null);
|
||||
|
||||
const isCurrentUser = entry.userId === currentUserId;
|
||||
const canEditThis = canEdit && isCurrentUser;
|
||||
|
||||
function handleEmojiChange(axis: 'performance' | 'moral' | 'flux' | 'valueCreation', emoji: string | null) {
|
||||
if (!canEditThis) return;
|
||||
|
||||
// Calculate new values
|
||||
const newPerformanceEmoji = axis === 'performance' ? emoji : performanceEmoji;
|
||||
const newMoralEmoji = axis === 'moral' ? emoji : moralEmoji;
|
||||
const newFluxEmoji = axis === 'flux' ? emoji : fluxEmoji;
|
||||
const newValueCreationEmoji = axis === 'valueCreation' ? emoji : valueCreationEmoji;
|
||||
|
||||
// Update local state immediately
|
||||
if (axis === 'performance') {
|
||||
setPerformanceEmoji(emoji);
|
||||
} else if (axis === 'moral') {
|
||||
setMoralEmoji(emoji);
|
||||
} else if (axis === 'flux') {
|
||||
setFluxEmoji(emoji);
|
||||
} else if (axis === 'valueCreation') {
|
||||
setValueCreationEmoji(emoji);
|
||||
}
|
||||
|
||||
// Save to server with new values
|
||||
startTransition(async () => {
|
||||
await createOrUpdateWeatherEntry(sessionId, {
|
||||
performanceEmoji: newPerformanceEmoji,
|
||||
moralEmoji: newMoralEmoji,
|
||||
fluxEmoji: newFluxEmoji,
|
||||
valueCreationEmoji: newValueCreationEmoji,
|
||||
notes,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function handleNotesChange(newNotes: string) {
|
||||
if (!canEditThis) return;
|
||||
setNotes(newNotes);
|
||||
}
|
||||
|
||||
function handleNotesBlur() {
|
||||
if (!canEditThis) return;
|
||||
startTransition(async () => {
|
||||
await createOrUpdateWeatherEntry(sessionId, {
|
||||
performanceEmoji,
|
||||
moralEmoji,
|
||||
fluxEmoji,
|
||||
valueCreationEmoji,
|
||||
notes,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// For current user without entry, we need to get user info from somewhere
|
||||
// For now, we'll use a placeholder - in real app, you'd pass user info as prop
|
||||
const user = entry.user;
|
||||
|
||||
return (
|
||||
<tr className={`border-b border-border ${isPending ? 'opacity-50' : ''}`}>
|
||||
{/* User column */}
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar email={user.email} name={user.name} size={32} />
|
||||
<span className="text-sm font-medium text-foreground">
|
||||
{user.name || user.email || 'Vous'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{/* Performance */}
|
||||
<td className="px-4 py-3">
|
||||
{canEditThis ? (
|
||||
<div className="relative">
|
||||
<select
|
||||
value={performanceEmoji || ''}
|
||||
onChange={(e) => handleEmojiChange('performance', e.target.value || null)}
|
||||
className="w-full appearance-none rounded-lg border border-border bg-card px-3 py-2.5 pr-10 text-lg text-foreground transition-colors hover:bg-card-hover focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
>
|
||||
{WEATHER_EMOJIS.map(({ emoji, label }) => (
|
||||
<option key={emoji || 'none'} value={emoji}>
|
||||
{emoji} {label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
||||
<svg
|
||||
className="h-4 w-4 text-muted"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-2xl text-center">{performanceEmoji || '-'}</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Moral */}
|
||||
<td className="px-4 py-3">
|
||||
{canEditThis ? (
|
||||
<div className="relative">
|
||||
<select
|
||||
value={moralEmoji || ''}
|
||||
onChange={(e) => handleEmojiChange('moral', e.target.value || null)}
|
||||
className="w-full appearance-none rounded-lg border border-border bg-card px-3 py-2.5 pr-10 text-lg text-foreground transition-colors hover:bg-card-hover focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
>
|
||||
{WEATHER_EMOJIS.map(({ emoji, label }) => (
|
||||
<option key={emoji || 'none'} value={emoji}>
|
||||
{emoji} {label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
||||
<svg
|
||||
className="h-4 w-4 text-muted"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-2xl text-center">{moralEmoji || '-'}</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Flux */}
|
||||
<td className="px-4 py-3">
|
||||
{canEditThis ? (
|
||||
<div className="relative">
|
||||
<select
|
||||
value={fluxEmoji || ''}
|
||||
onChange={(e) => handleEmojiChange('flux', e.target.value || null)}
|
||||
className="w-full appearance-none rounded-lg border border-border bg-card px-3 py-2.5 pr-10 text-lg text-foreground transition-colors hover:bg-card-hover focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
>
|
||||
{WEATHER_EMOJIS.map(({ emoji, label }) => (
|
||||
<option key={emoji || 'none'} value={emoji}>
|
||||
{emoji} {label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
||||
<svg
|
||||
className="h-4 w-4 text-muted"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-2xl text-center">{fluxEmoji || '-'}</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Création de valeur */}
|
||||
<td className="px-4 py-3">
|
||||
{canEditThis ? (
|
||||
<div className="relative">
|
||||
<select
|
||||
value={valueCreationEmoji || ''}
|
||||
onChange={(e) => handleEmojiChange('valueCreation', e.target.value || null)}
|
||||
className="w-full appearance-none rounded-lg border border-border bg-card px-3 py-2.5 pr-10 text-lg text-foreground transition-colors hover:bg-card-hover focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20"
|
||||
>
|
||||
{WEATHER_EMOJIS.map(({ emoji, label }) => (
|
||||
<option key={emoji || 'none'} value={emoji}>
|
||||
{emoji} {label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="pointer-events-none absolute right-3 top-1/2 -translate-y-1/2">
|
||||
<svg
|
||||
className="h-4 w-4 text-muted"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-2xl text-center">{valueCreationEmoji || '-'}</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
{/* Notes */}
|
||||
<td className="px-4 py-3 min-w-[300px]">
|
||||
{canEditThis ? (
|
||||
<Textarea
|
||||
value={notes}
|
||||
onChange={(e) => handleNotesChange(e.target.value)}
|
||||
onBlur={handleNotesBlur}
|
||||
placeholder="Notes globales..."
|
||||
className="min-h-[120px] w-full resize-y"
|
||||
rows={5}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-sm text-foreground whitespace-pre-wrap min-h-[120px]">
|
||||
{notes || '-'}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user