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
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
This commit is contained in:
167
components/houses/HouseCard.tsx
Normal file
167
components/houses/HouseCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user