Add house leaderboard feature: Integrate house leaderboard functionality in LeaderboardPage and LeaderboardSection components. Update userStatsService to fetch house leaderboard data, and enhance UI to display house rankings, scores, and member details. Update Prisma schema to include house-related models and relationships, and seed database with initial house data.
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled

This commit is contained in:
Julien Froidefond
2025-12-17 13:35:18 +01:00
parent cb02b494f4
commit 85ee812ab1
36 changed files with 5422 additions and 13 deletions

View File

@@ -0,0 +1,167 @@
"use client";
import { useState } from "react";
import { useSession } from "next-auth/react";
import Card from "@/components/ui/Card";
import Button from "@/components/ui/Button";
import Avatar from "@/components/ui/Avatar";
import { requestToJoin } from "@/actions/houses/requests";
import { useTransition } from "react";
import Alert from "@/components/ui/Alert";
interface House {
id: string;
name: string;
description: string | null;
creator: {
id: string;
username: string;
avatar: string | null;
};
memberships?: Array<{
id: string;
role: string;
user: {
id: string;
username: string;
avatar: string | null;
score?: number;
level?: number;
};
}>;
_count?: {
memberships: number;
};
}
interface HouseCardProps {
house: House;
onRequestSent?: () => void;
}
export default function HouseCard({ house, onRequestSent }: HouseCardProps) {
const { data: session } = useSession();
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const isMember = house.memberships?.some(
(m) => m.user.id === session?.user?.id
);
const memberCount = house._count?.memberships || house.memberships?.length || 0;
const handleRequestToJoin = () => {
if (!session?.user?.id) return;
setError(null);
setSuccess(null);
startTransition(async () => {
const result = await requestToJoin(house.id);
if (result.success) {
setSuccess("Demande envoyée avec succès");
onRequestSent?.();
} else {
setError(result.error || "Erreur lors de l'envoi de la demande");
}
});
};
return (
<Card className="p-4 sm:p-6">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4 mb-4">
<div className="flex-1 min-w-0">
<h3 className="text-xl font-bold mb-2 break-words" style={{ color: "var(--foreground)" }}>
{house.name}
</h3>
{house.description && (
<p className="text-sm mb-2 break-words" style={{ color: "var(--muted-foreground)" }}>
{house.description}
</p>
)}
<div className="flex flex-wrap items-center gap-2 sm:gap-4 text-xs" style={{ color: "var(--muted-foreground)" }}>
<span>Créée par {house.creator.username}</span>
<span className="hidden sm:inline"></span>
<span>{memberCount} membre{memberCount > 1 ? "s" : ""}</span>
</div>
</div>
</div>
{error && (
<Alert variant="error" className="mb-4">
{error}
</Alert>
)}
{success && (
<Alert variant="success" className="mb-4">
{success}
</Alert>
)}
{session?.user?.id && !isMember && (
<Button
onClick={handleRequestToJoin}
disabled={isPending}
variant="primary"
size="sm"
className="w-full sm:w-auto"
>
{isPending ? "Envoi..." : "Demander à rejoindre"}
</Button>
)}
{isMember && (
<div className="text-xs mb-4" style={{ color: "var(--success)" }}>
Vous êtes membre
</div>
)}
{/* Members List */}
{house.memberships && house.memberships.length > 0 && (
<div className="mt-4 pt-4 border-t" style={{ borderColor: "var(--border)" }}>
<h4 className="text-xs font-bold uppercase tracking-wider mb-3" style={{ color: "var(--muted-foreground)" }}>
Membres ({house.memberships.length})
</h4>
<div className="flex flex-wrap gap-2">
{house.memberships.map((membership) => (
<div
key={membership.id}
className="flex items-center gap-2 p-2 rounded"
style={{ backgroundColor: "var(--card-hover)" }}
title={`${membership.user.username} (${membership.role})${membership.user.score !== undefined ? ` - ${membership.user.score} pts` : ""}`}
>
<Avatar
src={membership.user.avatar}
username={membership.user.username}
size="sm"
className="flex-shrink-0"
borderClassName="border-pixel-gold/30"
/>
<div className="min-w-0">
<div className="flex items-center gap-1">
<span className="text-xs font-semibold truncate" style={{ color: "var(--foreground)" }}>
{membership.user.username}
</span>
{membership.role === "OWNER" && (
<span className="text-[10px] uppercase" style={{ color: "var(--accent)" }}>
👑
</span>
)}
</div>
{membership.user.score !== undefined && membership.user.level !== undefined && (
<div className="text-[10px]" style={{ color: "var(--muted-foreground)" }}>
{membership.user.score} pts Lv.{membership.user.level}
</div>
)}
</div>
</div>
))}
</div>
</div>
)}
</Card>
);
}

View File

@@ -0,0 +1,90 @@
"use client";
import { useState, useTransition } from "react";
import Button from "@/components/ui/Button";
import Input from "@/components/ui/Input";
import Textarea from "@/components/ui/Textarea";
import Alert from "@/components/ui/Alert";
import { createHouse } from "@/actions/houses/create";
import { updateHouse } from "@/actions/houses/update";
interface HouseFormProps {
house?: {
id: string;
name: string;
description: string | null;
};
onSuccess?: () => void;
onCancel?: () => void;
}
export default function HouseForm({
house,
onSuccess,
onCancel,
}: HouseFormProps) {
const [name, setName] = useState(house?.name || "");
const [description, setDescription] = useState(house?.description || "");
const [error, setError] = useState<string | null>(null);
const [isPending, startTransition] = useTransition();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setError(null);
startTransition(async () => {
const result = house
? await updateHouse(house.id, { name, description: description || null })
: await createHouse({ name, description: description || null });
if (result.success) {
onSuccess?.();
} else {
setError(result.error || "Une erreur est survenue");
}
});
};
return (
<form onSubmit={handleSubmit} className="space-y-4">
{error && <Alert variant="error">{error}</Alert>}
<Input
label="Nom de la maison"
value={name}
onChange={(e) => setName(e.target.value)}
required
minLength={3}
maxLength={50}
disabled={isPending}
/>
<Textarea
label="Description (optionnelle)"
value={description}
onChange={(e) => setDescription(e.target.value)}
maxLength={500}
disabled={isPending}
rows={4}
/>
<div className="flex flex-col sm:flex-row gap-2">
<Button type="submit" disabled={isPending} variant="primary" className="w-full sm:w-auto">
{isPending ? "Enregistrement..." : house ? "Modifier" : "Créer"}
</Button>
{onCancel && (
<Button
type="button"
onClick={onCancel}
disabled={isPending}
variant="secondary"
className="w-full sm:w-auto"
>
Annuler
</Button>
)}
</div>
</form>
);
}

View File

@@ -0,0 +1,327 @@
"use client";
import { useState, useEffect, useTransition } from "react";
import { useSession } from "next-auth/react";
import Card from "@/components/ui/Card";
import Button from "@/components/ui/Button";
import SectionTitle from "@/components/ui/SectionTitle";
import HouseForm from "./HouseForm";
import RequestList from "./RequestList";
import Alert from "@/components/ui/Alert";
import { deleteHouse, leaveHouse } from "@/actions/houses/update";
import { inviteUser } from "@/actions/houses/invitations";
interface House {
id: string;
name: string;
description: string | null;
creator: {
id: string;
username: string;
avatar: string | null;
};
memberships?: Array<{
id: string;
role: string;
user: {
id: string;
username: string;
avatar: string | null;
score?: number;
level?: number;
};
}>;
}
interface User {
id: string;
username: string;
avatar: string | null;
}
interface HouseManagementProps {
house: House | null;
users?: User[];
requests?: Array<{
id: string;
requester: {
id: string;
username: string;
avatar: string | null;
};
status: string;
createdAt: string;
}>;
onUpdate?: () => void;
}
interface Request {
id: string;
requester: {
id: string;
username: string;
avatar: string | null;
};
status: string;
createdAt: string;
}
export default function HouseManagement({
house,
users = [],
requests: initialRequests = [],
onUpdate,
}: HouseManagementProps) {
const { data: session } = useSession();
const [isEditing, setIsEditing] = useState(false);
const [showInviteForm, setShowInviteForm] = useState(false);
const [selectedUserId, setSelectedUserId] = useState("");
const [requests, setRequests] = useState<Request[]>(initialRequests);
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null);
const userRole = house?.memberships?.find(
(m) => m.user.id === session?.user?.id
)?.role;
const isOwner = userRole === "OWNER";
const isAdmin = userRole === "ADMIN" || isOwner;
const pendingRequests = requests.filter((r) => r.status === "PENDING");
useEffect(() => {
const fetchRequests = async () => {
if (!house || !isAdmin) return;
try {
const response = await fetch(`/api/houses/${house.id}/requests?status=PENDING`);
if (response.ok) {
const data = await response.json();
setRequests(data);
}
} catch (error) {
console.error("Error fetching requests:", error);
}
};
fetchRequests();
}, [house?.id, isAdmin]);
const handleDelete = () => {
if (!house || !confirm("Êtes-vous sûr de vouloir supprimer cette maison ?")) {
return;
}
setError(null);
startTransition(async () => {
const result = await deleteHouse(house.id);
if (result.success) {
onUpdate?.();
} else {
setError(result.error || "Erreur lors de la suppression");
}
});
};
const handleLeave = () => {
if (!house || !confirm("Êtes-vous sûr de vouloir quitter cette maison ?")) {
return;
}
setError(null);
startTransition(async () => {
const result = await leaveHouse(house.id);
if (result.success) {
onUpdate?.();
} else {
setError(result.error || "Erreur lors de la sortie");
}
});
};
const handleInvite = () => {
if (!house || !selectedUserId) return;
setError(null);
setSuccess(null);
startTransition(async () => {
const result = await inviteUser(house.id, selectedUserId);
if (result.success) {
setSuccess("Invitation envoyée");
setShowInviteForm(false);
setSelectedUserId("");
onUpdate?.();
} else {
setError(result.error || "Erreur lors de l'envoi de l'invitation");
}
});
};
const availableUsers = users.filter(
(u) =>
u.id !== session?.user?.id &&
!house?.memberships?.some((m) => m.user.id === u.id)
);
if (!house) {
return (
<Card className="p-6">
<SectionTitle>Ma Maison</SectionTitle>
<p className="text-sm mb-4" style={{ color: "var(--muted-foreground)" }}>
Vous n'êtes membre d'aucune maison pour le moment.
</p>
</Card>
);
}
return (
<div className="space-y-6">
<Card className="p-4 sm:p-6">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4 mb-4">
<div className="flex-1 min-w-0">
<SectionTitle>{house.name}</SectionTitle>
{house.description && (
<p className="text-sm mt-2 break-words" style={{ color: "var(--muted-foreground)" }}>
{house.description}
</p>
)}
</div>
<div className="flex flex-wrap gap-2 sm:flex-nowrap">
{isAdmin && (
<>
<Button
onClick={() => setIsEditing(!isEditing)}
variant="secondary"
size="sm"
className="flex-1 sm:flex-none"
>
{isEditing ? "Annuler" : "Modifier"}
</Button>
{isOwner && (
<Button onClick={handleDelete} variant="danger" size="sm" className="flex-1 sm:flex-none">
Supprimer
</Button>
)}
</>
)}
{!isOwner && (
<Button onClick={handleLeave} variant="danger" size="sm" className="flex-1 sm:flex-none">
Quitter
</Button>
)}
</div>
</div>
{error && <Alert variant="error" className="mb-4">{error}</Alert>}
{success && <Alert variant="success" className="mb-4">{success}</Alert>}
{isEditing ? (
<HouseForm
house={house}
onSuccess={() => {
setIsEditing(false);
onUpdate?.();
}}
onCancel={() => setIsEditing(false)}
/>
) : (
<div>
<h3 className="font-bold mb-3" style={{ color: "var(--foreground)" }}>
Membres ({house.memberships?.length ?? 0})
</h3>
<div className="space-y-2">
{(house.memberships || []).map((membership) => (
<div
key={membership.id}
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-2 rounded"
style={{ backgroundColor: "var(--card-hover)" }}
>
<div className="flex items-center gap-2 min-w-0 flex-1">
{membership.user.avatar && (
<img
src={membership.user.avatar}
alt={membership.user.username}
className="w-8 h-8 rounded-full flex-shrink-0"
/>
)}
<div className="min-w-0">
<span className="font-semibold block sm:inline" style={{ color: "var(--foreground)" }}>
{membership.user.username}
</span>
<span className="text-xs block sm:inline sm:ml-2" style={{ color: "var(--muted-foreground)" }}>
({membership.user.score} pts - Niveau {membership.user.level})
</span>
</div>
</div>
<span className="text-xs uppercase flex-shrink-0" style={{ color: "var(--accent)" }}>
{membership.role}
</span>
</div>
))}
</div>
{isAdmin && (
<div className="mt-4">
{showInviteForm ? (
<div className="space-y-2">
<select
value={selectedUserId}
onChange={(e) => setSelectedUserId(e.target.value)}
className="w-full p-2 rounded border"
style={{
backgroundColor: "var(--input)",
borderColor: "var(--border)",
color: "var(--foreground)",
}}
>
<option value="">Sélectionner un utilisateur</option>
{availableUsers.map((user) => (
<option key={user.id} value={user.id}>
{user.username}
</option>
))}
</select>
<div className="flex gap-2">
<Button
onClick={handleInvite}
disabled={!selectedUserId || isPending}
variant="primary"
size="sm"
>
{isPending ? "Envoi..." : "Inviter"}
</Button>
<Button
onClick={() => {
setShowInviteForm(false);
setSelectedUserId("");
}}
variant="secondary"
size="sm"
>
Annuler
</Button>
</div>
</div>
) : (
<Button
onClick={() => setShowInviteForm(true)}
variant="primary"
size="sm"
>
Inviter un utilisateur
</Button>
)}
</div>
)}
</div>
)}
</Card>
{isAdmin && pendingRequests.length > 0 && (
<Card className="p-4 sm:p-6">
<SectionTitle>Demandes d'adhésion</SectionTitle>
<RequestList requests={pendingRequests} onUpdate={onUpdate} />
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,236 @@
"use client";
import { useState, useEffect } from "react";
import { useSession } from "next-auth/react";
import Card from "@/components/ui/Card";
import Button from "@/components/ui/Button";
import SectionTitle from "@/components/ui/SectionTitle";
import BackgroundSection from "@/components/ui/BackgroundSection";
import HouseCard from "./HouseCard";
import HouseForm from "./HouseForm";
import HouseManagement from "./HouseManagement";
import InvitationList from "./InvitationList";
import Input from "@/components/ui/Input";
interface House {
id: string;
name: string;
description: string | null;
creator: {
id: string;
username: string;
avatar: string | null;
};
memberships?: Array<{
id: string;
role: string;
user: {
id: string;
username: string;
avatar: string | null;
score?: number;
level?: number;
};
}>;
_count?: {
memberships: number;
};
}
interface User {
id: string;
username: string;
avatar: string | null;
}
interface HousesSectionProps {
initialHouses?: House[];
initialMyHouse?: House | null;
initialUsers?: User[];
initialInvitations?: Array<{
id: string;
house: {
id: string;
name: string;
};
inviter: {
id: string;
username: string;
avatar: string | null;
};
status: string;
createdAt: string;
}>;
backgroundImage: string;
}
export default function HousesSection({
initialHouses = [],
initialMyHouse = null,
initialUsers = [],
initialInvitations = [],
backgroundImage,
}: HousesSectionProps) {
const { data: session } = useSession();
const [houses, setHouses] = useState<House[]>(initialHouses);
const [myHouse, setMyHouse] = useState<House | null>(initialMyHouse);
const [invitations, setInvitations] = useState(initialInvitations);
const [showCreateForm, setShowCreateForm] = useState(false);
const [searchTerm, setSearchTerm] = useState("");
const fetchHouses = async () => {
try {
const params = new URLSearchParams();
if (searchTerm) {
params.append("search", searchTerm);
}
params.append("include", "members,creator");
const response = await fetch(`/api/houses?${params}`);
if (response.ok) {
const data = await response.json();
setHouses(data);
}
} catch (error) {
console.error("Error fetching houses:", error);
}
};
const fetchMyHouse = async () => {
try {
const response = await fetch("/api/houses/my-house");
if (response.ok) {
const data = await response.json();
setMyHouse(data);
} else if (response.status === 404) {
setMyHouse(null);
}
} catch (error) {
console.error("Error fetching my house:", error);
}
};
const fetchInvitations = async () => {
try {
const response = await fetch("/api/invitations?status=PENDING");
if (response.ok) {
const data = await response.json();
setInvitations(data);
}
} catch (error) {
console.error("Error fetching invitations:", error);
}
};
useEffect(() => {
if (searchTerm) {
const timeout = setTimeout(() => {
fetchHouses();
}, 300);
return () => clearTimeout(timeout);
} else {
fetchHouses();
}
}, [searchTerm]);
const handleUpdate = () => {
fetchMyHouse();
fetchHouses();
fetchInvitations();
};
const filteredHouses = houses.filter((house) => {
if (!myHouse) return true;
return house.id !== myHouse.id;
});
return (
<BackgroundSection backgroundImage={backgroundImage}>
{/* Title Section */}
<SectionTitle
variant="gradient"
size="lg"
subtitle="Rejoignez une maison ou créez la vôtre"
className="mb-12 overflow-hidden"
>
MAISONS
</SectionTitle>
<div className="space-y-4 sm:space-y-6">
{session?.user && (
<>
{invitations.length > 0 && (
<Card className="p-4 sm:p-6">
<SectionTitle>Mes Invitations</SectionTitle>
<InvitationList invitations={invitations} onUpdate={handleUpdate} />
</Card>
)}
<Card className="p-4 sm:p-6">
<SectionTitle>Ma Maison</SectionTitle>
{myHouse ? (
<HouseManagement
house={myHouse}
users={initialUsers}
onUpdate={handleUpdate}
/>
) : (
<div>
{showCreateForm ? (
<HouseForm
onSuccess={() => {
setShowCreateForm(false);
handleUpdate();
}}
onCancel={() => setShowCreateForm(false)}
/>
) : (
<div>
<p className="text-sm mb-4 break-words" style={{ color: "var(--muted-foreground)" }}>
Vous n'êtes membre d'aucune maison. Créez-en une ou demandez à rejoindre une maison existante.
</p>
<Button
onClick={() => setShowCreateForm(true)}
variant="primary"
className="w-full sm:w-auto"
>
Créer une maison
</Button>
</div>
)}
</div>
)}
</Card>
</>
)}
<Card className="p-4 sm:p-6">
<SectionTitle>Toutes les Maisons</SectionTitle>
<div className="mb-4">
<Input
placeholder="Rechercher une maison..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{filteredHouses.length === 0 ? (
<p className="text-sm" style={{ color: "var(--muted-foreground)" }}>
Aucune maison trouvée
</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{filteredHouses.map((house) => (
<HouseCard
key={house.id}
house={house}
onRequestSent={handleUpdate}
/>
))}
</div>
)}
</Card>
</div>
</BackgroundSection>
);
}

View File

@@ -0,0 +1,123 @@
"use client";
import { useState, useTransition } from "react";
import Card from "@/components/ui/Card";
import Button from "@/components/ui/Button";
import {
acceptInvitation,
rejectInvitation,
} from "@/actions/houses/invitations";
import Alert from "@/components/ui/Alert";
interface Invitation {
id: string;
house: {
id: string;
name: string;
};
inviter: {
id: string;
username: string;
avatar: string | null;
};
status: string;
createdAt: string;
}
interface InvitationListProps {
invitations: Invitation[];
onUpdate?: () => void;
}
export default function InvitationList({
invitations,
onUpdate,
}: InvitationListProps) {
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const handleAccept = (invitationId: string) => {
setError(null);
startTransition(async () => {
const result = await acceptInvitation(invitationId);
if (result.success) {
onUpdate?.();
} else {
setError(result.error || "Erreur lors de l'acceptation");
}
});
};
const handleReject = (invitationId: string) => {
setError(null);
startTransition(async () => {
const result = await rejectInvitation(invitationId);
if (result.success) {
onUpdate?.();
} else {
setError(result.error || "Erreur lors du refus");
}
});
};
if (invitations.length === 0) {
return (
<p className="text-sm" style={{ color: "var(--muted-foreground)" }}>
Aucune invitation en attente
</p>
);
}
return (
<div className="space-y-4">
{error && <Alert variant="error">{error}</Alert>}
{invitations.map((invitation) => (
<Card key={invitation.id} className="p-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
<div className="flex-1 min-w-0">
<h4 className="font-bold mb-1 break-words" style={{ color: "var(--foreground)" }}>
Invitation de {invitation.inviter.username}
</h4>
<p className="text-sm mb-2 break-words" style={{ color: "var(--muted-foreground)" }}>
Pour rejoindre la maison <strong>{invitation.house.name}</strong>
</p>
</div>
{invitation.status === "PENDING" && (
<div className="flex gap-2 sm:flex-nowrap">
<Button
onClick={() => handleAccept(invitation.id)}
disabled={isPending}
variant="success"
size="sm"
className="flex-1 sm:flex-none"
>
Accepter
</Button>
<Button
onClick={() => handleReject(invitation.id)}
disabled={isPending}
variant="danger"
size="sm"
className="flex-1 sm:flex-none"
>
Refuser
</Button>
</div>
)}
{invitation.status === "ACCEPTED" && (
<span className="text-xs flex-shrink-0" style={{ color: "var(--success)" }}>
Acceptée
</span>
)}
{invitation.status === "REJECTED" && (
<span className="text-xs flex-shrink-0" style={{ color: "var(--destructive)" }}>
Refusée
</span>
)}
</div>
</Card>
))}
</div>
);
}

View File

@@ -0,0 +1,119 @@
"use client";
import { useState, useTransition } from "react";
import Card from "@/components/ui/Card";
import Button from "@/components/ui/Button";
import {
acceptRequest,
rejectRequest,
} from "@/actions/houses/requests";
import Alert from "@/components/ui/Alert";
interface Request {
id: string;
requester: {
id: string;
username: string;
avatar: string | null;
};
status: string;
createdAt: string;
}
interface RequestListProps {
requests: Request[];
onUpdate?: () => void;
}
export default function RequestList({
requests,
onUpdate,
}: RequestListProps) {
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const handleAccept = (requestId: string) => {
setError(null);
startTransition(async () => {
const result = await acceptRequest(requestId);
if (result.success) {
onUpdate?.();
} else {
setError(result.error || "Erreur lors de l'acceptation");
}
});
};
const handleReject = (requestId: string) => {
setError(null);
startTransition(async () => {
const result = await rejectRequest(requestId);
if (result.success) {
onUpdate?.();
} else {
setError(result.error || "Erreur lors du refus");
}
});
};
if (requests.length === 0) {
return (
<p className="text-sm" style={{ color: "var(--muted-foreground)" }}>
Aucune demande en attente
</p>
);
}
return (
<div className="space-y-4">
{error && <Alert variant="error">{error}</Alert>}
{requests.map((request) => (
<Card key={request.id} className="p-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3">
<div className="flex-1 min-w-0">
<h4 className="font-bold mb-1 break-words" style={{ color: "var(--foreground)" }}>
{request.requester.username}
</h4>
<p className="text-sm break-words" style={{ color: "var(--muted-foreground)" }}>
souhaite rejoindre votre maison
</p>
</div>
{request.status === "PENDING" && (
<div className="flex gap-2 sm:flex-nowrap">
<Button
onClick={() => handleAccept(request.id)}
disabled={isPending}
variant="success"
size="sm"
className="flex-1 sm:flex-none"
>
Accepter
</Button>
<Button
onClick={() => handleReject(request.id)}
disabled={isPending}
variant="danger"
size="sm"
className="flex-1 sm:flex-none"
>
Refuser
</Button>
</div>
)}
{request.status === "ACCEPTED" && (
<span className="text-xs flex-shrink-0" style={{ color: "var(--success)" }}>
Acceptée
</span>
)}
{request.status === "REJECTED" && (
<span className="text-xs flex-shrink-0" style={{ color: "var(--destructive)" }}>
Refusée
</span>
)}
</div>
</Card>
))}
</div>
);
}