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:
327
components/houses/HouseManagement.tsx
Normal file
327
components/houses/HouseManagement.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user