feat: integrate user team retrieval into session components, enhancing sharing functionality and user experience across motivators, sessions, weekly check-ins, and year reviews
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m35s

This commit is contained in:
Julien Froidefond
2026-02-17 14:47:43 +01:00
parent 4d04d3ede8
commit d05157d498
19 changed files with 345 additions and 892 deletions

View File

@@ -1,12 +1,13 @@
'use client';
import { useState, useTransition } from 'react';
import Link from 'next/link';
import { Modal } from '@/components/ui/Modal';
import { Input } from '@/components/ui/Input';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Avatar } from '@/components/ui/Avatar';
import { shareSessionAction, removeShareAction } from '@/actions/share';
import { getTeamMembersForShare, type TeamWithMembers } from '@/lib/share-utils';
import type { ShareRole } from '@prisma/client';
interface ShareUser {
@@ -19,39 +20,79 @@ interface Share {
id: string;
role: ShareRole;
user: ShareUser;
createdAt: Date;
createdAt?: Date;
}
type ShareTab = 'teamMember' | 'team' | 'email';
interface ShareModalProps {
isOpen: boolean;
onClose: () => void;
sessionId: string;
title: string;
sessionSubtitle?: string;
sessionTitle: string;
shares: Share[];
isOwner: boolean;
userTeams?: TeamWithMembers[];
currentUserId?: string;
onShareWithEmail: (email: string, role: ShareRole) => Promise<{ success: boolean; error?: string }>;
onShareWithTeam?: (teamId: string, role: ShareRole) => Promise<{ success: boolean; error?: string }>;
onRemoveShare: (userId: string) => Promise<unknown>;
helpText?: React.ReactNode;
}
const SELECT_STYLE =
'appearance-none rounded-lg border border-border bg-card px-3 py-2.5 pr-10 text-sm text-foreground transition-colors hover:bg-card-hover focus:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20';
export function ShareModal({
isOpen,
onClose,
sessionId,
title,
sessionSubtitle,
sessionTitle,
shares,
isOwner,
userTeams = [],
currentUserId = '',
onShareWithEmail,
onShareWithTeam,
onRemoveShare,
helpText,
}: ShareModalProps) {
const teamMembers = getTeamMembersForShare(userTeams, currentUserId);
const hasTeamShare = !!onShareWithTeam;
const [shareType, setShareType] = useState<ShareTab>('teamMember');
const [email, setEmail] = useState('');
const [teamId, setTeamId] = useState('');
const [selectedMemberId, setSelectedMemberId] = useState('');
const [role, setRole] = useState<ShareRole>('EDITOR');
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const resetForm = () => {
setEmail('');
setTeamId('');
setSelectedMemberId('');
};
async function handleShare(e: React.FormEvent) {
e.preventDefault();
setError(null);
startTransition(async () => {
const result = await shareSessionAction(sessionId, email, role);
let result: { success: boolean; error?: string };
if (shareType === 'team' && onShareWithTeam) {
result = await onShareWithTeam(teamId, role);
} else {
const targetEmail =
shareType === 'teamMember'
? teamMembers.find((m) => m.id === selectedMemberId)?.email ?? ''
: email;
result = await onShareWithEmail(targetEmail, role);
}
if (result.success) {
setEmail('');
resetForm();
} else {
setError(result.error || 'Erreur lors du partage');
}
@@ -60,53 +101,172 @@ export function ShareModal({
async function handleRemove(userId: string) {
startTransition(async () => {
await removeShareAction(sessionId, userId);
await onRemoveShare(userId);
});
}
const tabs: { value: ShareTab; label: string; icon: string }[] = [
{ value: 'teamMember', label: 'Membre', icon: '👥' },
...(hasTeamShare ? [{ value: 'team' as ShareTab, label: 'Équipe', icon: '🏢' }] : []),
{ value: 'email', label: 'Email', icon: '👤' },
];
return (
<Modal isOpen={isOpen} onClose={onClose} title="Partager la session">
<Modal isOpen={isOpen} onClose={onClose} title={title}>
<div className="space-y-6">
{/* Session info */}
<div>
<p className="text-sm text-muted">Session</p>
{sessionSubtitle && <p className="text-sm text-muted">{sessionSubtitle}</p>}
<p className="font-medium text-foreground">{sessionTitle}</p>
</div>
{/* Share form (only for owner) */}
{isOwner && (
<form onSubmit={handleShare} className="space-y-4">
<div className="flex gap-2">
<Input
type="email"
placeholder="Email de l'utilisateur"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="flex-1"
required
/>
<select
value={role}
onChange={(e) => setRole(e.target.value as ShareRole)}
className="rounded-lg border border-border bg-input px-3 py-2 text-sm text-foreground"
>
<option value="EDITOR">Éditeur</option>
<option value="VIEWER">Lecteur</option>
</select>
<div className="flex gap-2 border-b border-border pb-3 flex-wrap">
{tabs.map((tab) => (
<button
key={tab.value}
type="button"
onClick={() => {
setShareType(tab.value);
resetForm();
}}
className={`flex-1 min-w-0 rounded-lg px-4 py-2 text-sm font-medium transition-colors ${
shareType === tab.value
? 'bg-primary text-primary-foreground'
: 'bg-card-hover text-muted hover:text-foreground'
}`}
>
{tab.icon} {tab.label}
</button>
))}
</div>
{shareType === 'email' && (
<div className="flex gap-2">
<Input
type="email"
placeholder="Email de l'utilisateur"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="flex-1"
required
/>
<select value={role} onChange={(e) => setRole(e.target.value as ShareRole)} className={SELECT_STYLE}>
<option value="EDITOR">Éditeur</option>
<option value="VIEWER">Lecteur</option>
</select>
</div>
)}
{shareType === 'teamMember' && (
<div className="space-y-2">
{teamMembers.length === 0 ? (
<p className="text-sm text-muted">
Vous n&apos;êtes membre d&apos;aucune équipe ou vos équipes n&apos;ont pas d&apos;autres membres.
Créez une équipe depuis la page{' '}
<Link href="/teams" className="text-primary hover:underline">
Équipes
</Link>
.
</p>
) : (
<div className="flex gap-2">
<div className="relative flex-1">
<select
value={selectedMemberId}
onChange={(e) => setSelectedMemberId(e.target.value)}
className={`w-full ${SELECT_STYLE}`}
required
>
<option value="">Sélectionner un membre</option>
{teamMembers.map((m) => (
<option key={m.id} value={m.id}>
{m.name || m.email} {m.name && `(${m.email})`}
</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>
<select value={role} onChange={(e) => setRole(e.target.value as ShareRole)} className={SELECT_STYLE}>
<option value="EDITOR">Éditeur</option>
<option value="VIEWER">Lecteur</option>
</select>
</div>
)}
</div>
)}
{shareType === 'team' && hasTeamShare && (
<div className="space-y-2">
{userTeams.length === 0 ? (
<p className="text-sm text-muted">
Vous n&apos;êtes membre d&apos;aucune équipe. Créez une équipe depuis la page{' '}
<Link href="/teams" className="text-primary hover:underline">
Équipes
</Link>
.
</p>
) : (
<>
<div className="relative">
<select
value={teamId}
onChange={(e) => setTeamId(e.target.value)}
className={`w-full ${SELECT_STYLE}`}
required
>
<option value="">Sélectionner une équipe</option>
{userTeams.map((team) => (
<option key={team.id} value={team.id}>
{team.name} {team.userRole === 'ADMIN' && '(Admin)'}
</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="relative">
<select value={role} onChange={(e) => setRole(e.target.value as ShareRole)} className={`w-full ${SELECT_STYLE}`}>
<option value="EDITOR">Éditeur</option>
<option value="VIEWER">Lecteur</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>
)}
{error && <p className="text-sm text-destructive">{error}</p>}
<Button type="submit" disabled={isPending || !email} className="w-full">
{isPending ? 'Partage...' : 'Partager'}
<Button
type="submit"
disabled={
isPending ||
(shareType === 'email' && !email) ||
(shareType === 'teamMember' && !selectedMemberId) ||
(shareType === 'team' && !teamId)
}
className="w-full"
>
{isPending ? 'Partage...' : shareType === 'team' ? "Partager à l'équipe" : 'Partager'}
</Button>
</form>
)}
{/* Current shares */}
<div className="space-y-2">
<p className="text-sm font-medium text-foreground">Collaborateurs ({shares.length})</p>
{shares.length === 0 ? (
<p className="text-sm text-muted">Aucun collaborateur pour le moment</p>
) : (
@@ -125,7 +285,6 @@ export function ShareModal({
{share.user.name && <p className="text-xs text-muted">{share.user.email}</p>}
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant={share.role === 'EDITOR' ? 'primary' : 'default'}>
{share.role === 'EDITOR' ? 'Éditeur' : 'Lecteur'}
@@ -137,12 +296,7 @@ export function ShareModal({
className="rounded p-1 text-muted hover:bg-destructive/10 hover:text-destructive"
title="Retirer l'accès"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
className="h-4 w-4"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" className="h-4 w-4">
<path
fillRule="evenodd"
d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.52.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z"
@@ -158,14 +312,7 @@ export function ShareModal({
)}
</div>
{/* Help text */}
<div className="rounded-lg bg-primary/5 p-3">
<p className="text-xs text-muted">
<strong>Éditeur</strong> : peut modifier les items et actions
<br />
<strong>Lecteur</strong> : peut uniquement consulter
</p>
</div>
{helpText && <div className="rounded-lg bg-primary/5 p-3">{helpText}</div>}
</div>
</Modal>
);