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

48
actions/houses/create.ts Normal file
View File

@@ -0,0 +1,48 @@
"use server";
import { revalidatePath } from "next/cache";
import { auth } from "@/lib/auth";
import { houseService } from "@/services/houses/house.service";
import {
ValidationError,
ConflictError,
} from "@/services/errors";
export async function createHouse(data: {
name: string;
description?: string | null;
}) {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
error: "Vous devez être connecté pour créer une maison",
};
}
const house = await houseService.createHouse({
name: data.name,
description: data.description,
creatorId: session.user.id,
});
revalidatePath("/houses");
revalidatePath("/profile");
return { success: true, message: "Maison créée avec succès", data: house };
} catch (error) {
console.error("Create house error:", error);
if (error instanceof ValidationError || error instanceof ConflictError) {
return { success: false, error: error.message };
}
return {
success: false,
error: "Une erreur est survenue lors de la création de la maison",
};
}
}

View File

@@ -0,0 +1,173 @@
"use server";
import { revalidatePath } from "next/cache";
import { auth } from "@/lib/auth";
import { houseService } from "@/services/houses/house.service";
import {
ValidationError,
ConflictError,
ForbiddenError,
NotFoundError,
} from "@/services/errors";
export async function inviteUser(houseId: string, inviteeId: string) {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
error: "Vous devez être connecté",
};
}
const invitation = await houseService.inviteUser({
houseId,
inviterId: session.user.id,
inviteeId,
});
revalidatePath("/houses");
revalidatePath(`/houses/${houseId}`);
return {
success: true,
message: "Invitation envoyée",
data: invitation,
};
} catch (error) {
console.error("Invite user error:", error);
if (
error instanceof ValidationError ||
error instanceof ConflictError ||
error instanceof ForbiddenError
) {
return { success: false, error: error.message };
}
return {
success: false,
error: "Une erreur est survenue lors de l'envoi de l'invitation",
};
}
}
export async function acceptInvitation(invitationId: string) {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
error: "Vous devez être connecté",
};
}
const membership = await houseService.acceptInvitation(
invitationId,
session.user.id
);
revalidatePath("/houses");
revalidatePath("/profile");
revalidatePath("/invitations");
return {
success: true,
message: "Invitation acceptée",
data: membership,
};
} catch (error) {
console.error("Accept invitation error:", error);
if (
error instanceof ValidationError ||
error instanceof ConflictError ||
error instanceof ForbiddenError ||
error instanceof NotFoundError
) {
return { success: false, error: error.message };
}
return {
success: false,
error: "Une erreur est survenue lors de l'acceptation de l'invitation",
};
}
}
export async function rejectInvitation(invitationId: string) {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
error: "Vous devez être connecté",
};
}
await houseService.rejectInvitation(invitationId, session.user.id);
revalidatePath("/houses");
revalidatePath("/invitations");
return { success: true, message: "Invitation refusée" };
} catch (error) {
console.error("Reject invitation error:", error);
if (
error instanceof ConflictError ||
error instanceof ForbiddenError ||
error instanceof NotFoundError
) {
return { success: false, error: error.message };
}
return {
success: false,
error: "Une erreur est survenue lors du refus de l'invitation",
};
}
}
export async function cancelInvitation(invitationId: string) {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
error: "Vous devez être connecté",
};
}
// Récupérer l'invitation pour obtenir le houseId avant de l'annuler
const invitation = await houseService.getInvitationById(invitationId);
await houseService.cancelInvitation(invitationId, session.user.id);
revalidatePath("/houses");
if (invitation?.houseId) {
revalidatePath(`/houses/${invitation.houseId}`);
}
return { success: true, message: "Invitation annulée" };
} catch (error) {
console.error("Cancel invitation error:", error);
if (
error instanceof ConflictError ||
error instanceof ForbiddenError ||
error instanceof NotFoundError
) {
return { success: false, error: error.message };
}
return {
success: false,
error: "Une erreur est survenue lors de l'annulation de l'invitation",
};
}
}

163
actions/houses/requests.ts Normal file
View File

@@ -0,0 +1,163 @@
"use server";
import { revalidatePath } from "next/cache";
import { auth } from "@/lib/auth";
import { houseService } from "@/services/houses/house.service";
import {
ValidationError,
ConflictError,
ForbiddenError,
NotFoundError,
} from "@/services/errors";
export async function requestToJoin(houseId: string) {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
error: "Vous devez être connecté",
};
}
const request = await houseService.requestToJoin({
houseId,
requesterId: session.user.id,
});
revalidatePath("/houses");
revalidatePath(`/houses/${houseId}`);
return {
success: true,
message: "Demande envoyée",
data: request,
};
} catch (error) {
console.error("Request to join error:", error);
if (
error instanceof ValidationError ||
error instanceof ConflictError
) {
return { success: false, error: error.message };
}
return {
success: false,
error: "Une erreur est survenue lors de l'envoi de la demande",
};
}
}
export async function acceptRequest(requestId: string) {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
error: "Vous devez être connecté",
};
}
const membership = await houseService.acceptRequest(
requestId,
session.user.id
);
revalidatePath("/houses");
revalidatePath("/profile");
return {
success: true,
message: "Demande acceptée",
data: membership,
};
} catch (error) {
console.error("Accept request error:", error);
if (
error instanceof ConflictError ||
error instanceof ForbiddenError ||
error instanceof NotFoundError
) {
return { success: false, error: error.message };
}
return {
success: false,
error: "Une erreur est survenue lors de l'acceptation de la demande",
};
}
}
export async function rejectRequest(requestId: string) {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
error: "Vous devez être connecté",
};
}
await houseService.rejectRequest(requestId, session.user.id);
revalidatePath("/houses");
return { success: true, message: "Demande refusée" };
} catch (error) {
console.error("Reject request error:", error);
if (
error instanceof ConflictError ||
error instanceof ForbiddenError ||
error instanceof NotFoundError
) {
return { success: false, error: error.message };
}
return {
success: false,
error: "Une erreur est survenue lors du refus de la demande",
};
}
}
export async function cancelRequest(requestId: string) {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
error: "Vous devez être connecté",
};
}
await houseService.cancelRequest(requestId, session.user.id);
revalidatePath("/houses");
return { success: true, message: "Demande annulée" };
} catch (error) {
console.error("Cancel request error:", error);
if (
error instanceof ConflictError ||
error instanceof ForbiddenError ||
error instanceof NotFoundError
) {
return { success: false, error: error.message };
}
return {
success: false,
error: "Une erreur est survenue lors de l'annulation de la demande",
};
}
}

114
actions/houses/update.ts Normal file
View File

@@ -0,0 +1,114 @@
"use server";
import { revalidatePath } from "next/cache";
import { auth } from "@/lib/auth";
import { houseService } from "@/services/houses/house.service";
import {
ValidationError,
ConflictError,
ForbiddenError,
} from "@/services/errors";
export async function updateHouse(
houseId: string,
data: {
name?: string;
description?: string | null;
}
) {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
error: "Vous devez être connecté",
};
}
const house = await houseService.updateHouse(houseId, session.user.id, data);
revalidatePath("/houses");
revalidatePath(`/houses/${houseId}`);
return { success: true, message: "Maison mise à jour", data: house };
} catch (error) {
console.error("Update house error:", error);
if (
error instanceof ValidationError ||
error instanceof ConflictError ||
error instanceof ForbiddenError
) {
return { success: false, error: error.message };
}
return {
success: false,
error: "Une erreur est survenue lors de la mise à jour de la maison",
};
}
}
export async function deleteHouse(houseId: string) {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
error: "Vous devez être connecté",
};
}
await houseService.deleteHouse(houseId, session.user.id);
revalidatePath("/houses");
revalidatePath("/profile");
return { success: true, message: "Maison supprimée" };
} catch (error) {
console.error("Delete house error:", error);
if (error instanceof ForbiddenError) {
return { success: false, error: error.message };
}
return {
success: false,
error: "Une erreur est survenue lors de la suppression de la maison",
};
}
}
export async function leaveHouse(houseId: string) {
try {
const session = await auth();
if (!session?.user?.id) {
return {
success: false,
error: "Vous devez être connecté",
};
}
await houseService.leaveHouse(houseId, session.user.id);
revalidatePath("/houses");
revalidatePath("/profile");
return { success: true, message: "Vous avez quitté la maison" };
} catch (error) {
console.error("Leave house error:", error);
if (error instanceof ForbiddenError) {
return { success: false, error: error.message };
}
return {
success: false,
error: "Une erreur est survenue lors de la sortie de la maison",
};
}
}

View File

@@ -0,0 +1,51 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { houseService } from "@/services/houses/house.service";
export async function GET(
request: Request,
{ params }: { params: Promise<{ houseId: string }> }
) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ error: "Vous devez être connecté" },
{ status: 401 }
);
}
const { houseId } = await params;
// Vérifier que l'utilisateur est membre de la maison
const isMember = await houseService.isUserMemberOfHouse(
session.user.id,
houseId
);
if (!isMember) {
return NextResponse.json(
{ error: "Vous devez être membre de cette maison" },
{ status: 403 }
);
}
const { searchParams } = new URL(request.url);
const status = searchParams.get("status") as "PENDING" | "ACCEPTED" | "REJECTED" | "CANCELLED" | null;
const invitations = await houseService.getHouseInvitations(
houseId,
status || undefined
);
return NextResponse.json(invitations);
} catch (error) {
console.error("Error fetching house invitations:", error);
return NextResponse.json(
{ error: "Erreur lors de la récupération des invitations" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,48 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { houseService } from "@/services/houses/house.service";
export async function GET(
request: Request,
{ params }: { params: Promise<{ houseId: string }> }
) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ error: "Vous devez être connecté" },
{ status: 401 }
);
}
const { houseId } = await params;
// Vérifier que l'utilisateur est propriétaire ou admin
const isAuthorized = await houseService.isUserOwnerOrAdmin(
session.user.id,
houseId
);
if (!isAuthorized) {
return NextResponse.json(
{ error: "Vous n'avez pas les permissions pour voir les demandes" },
{ status: 403 }
);
}
const { searchParams } = new URL(request.url);
const status = searchParams.get("status") as "PENDING" | "ACCEPTED" | "REJECTED" | "CANCELLED" | null;
const requests = await houseService.getHouseRequests(houseId, status || undefined);
return NextResponse.json(requests);
} catch (error) {
console.error("Error fetching house requests:", error);
return NextResponse.json(
{ error: "Erreur lors de la récupération des demandes" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,59 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { houseService } from "@/services/houses/house.service";
export async function GET(
request: Request,
{ params }: { params: Promise<{ houseId: string }> }
) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ error: "Vous devez être connecté" },
{ status: 401 }
);
}
const { houseId } = await params;
const house = await houseService.getHouseById(houseId, {
memberships: {
include: {
user: {
select: {
id: true,
username: true,
avatar: true,
score: true,
level: true,
},
},
},
},
creator: {
select: {
id: true,
username: true,
avatar: true,
},
},
});
if (!house) {
return NextResponse.json(
{ error: "Maison non trouvée" },
{ status: 404 }
);
}
return NextResponse.json(house);
} catch (error) {
console.error("Error fetching house:", error);
return NextResponse.json(
{ error: "Erreur lors de la récupération de la maison" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,48 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { houseService } from "@/services/houses/house.service";
export async function GET() {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ error: "Vous devez être connecté" },
{ status: 401 }
);
}
const house = await houseService.getUserHouse(session.user.id, {
memberships: {
include: {
user: {
select: {
id: true,
username: true,
avatar: true,
score: true,
level: true,
},
},
},
},
creator: {
select: {
id: true,
username: true,
avatar: true,
},
},
});
return NextResponse.json(house);
} catch (error) {
console.error("Error fetching user house:", error);
return NextResponse.json(
{ error: "Erreur lors de la récupération de votre maison" },
{ status: 500 }
);
}
}

87
app/api/houses/route.ts Normal file
View File

@@ -0,0 +1,87 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { houseService } from "@/services/houses/house.service";
export async function GET(request: Request) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ error: "Vous devez être connecté" },
{ status: 401 }
);
}
const { searchParams } = new URL(request.url);
const search = searchParams.get("search");
const include = searchParams.get("include")?.split(",") || [];
const includeOptions: {
memberships?: {
include: {
user: {
select: {
id: boolean;
username: boolean;
avatar: boolean;
score?: boolean;
level?: boolean;
};
};
};
};
creator?: {
select: {
id: boolean;
username: boolean;
avatar: boolean;
};
};
} = {};
if (include.includes("members")) {
includeOptions.memberships = {
include: {
user: {
select: {
id: true,
username: true,
avatar: true,
score: true,
level: true,
},
},
},
};
}
if (include.includes("creator")) {
includeOptions.creator = {
select: {
id: true,
username: true,
avatar: true,
},
};
}
let houses;
if (search) {
houses = await houseService.searchHouses(search, {
include: includeOptions,
});
} else {
houses = await houseService.getAllHouses({
include: includeOptions,
});
}
return NextResponse.json(houses);
} catch (error) {
console.error("Error fetching houses:", error);
return NextResponse.json(
{ error: "Erreur lors de la récupération des maisons" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,36 @@
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { houseService } from "@/services/houses/house.service";
export async function GET(request: Request) {
try {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json(
{ error: "Vous devez être connecté" },
{ status: 401 }
);
}
const { searchParams } = new URL(request.url);
const statusParam = searchParams.get("status");
const status = statusParam && ["PENDING", "ACCEPTED", "REJECTED", "CANCELLED"].includes(statusParam)
? (statusParam as "PENDING" | "ACCEPTED" | "REJECTED" | "CANCELLED")
: undefined;
const invitations = await houseService.getUserInvitations(
session.user.id,
status
);
return NextResponse.json(invitations);
} catch (error) {
console.error("Error fetching invitations:", error);
return NextResponse.json(
{ error: "Erreur lors de la récupération des invitations" },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,17 @@
import { NextResponse } from "next/server";
import { userStatsService } from "@/services/users/user-stats.service";
export async function GET() {
try {
const leaderboard = await userStatsService.getHouseLeaderboard(10);
return NextResponse.json(leaderboard);
} catch (error) {
console.error("Error fetching house leaderboard:", error);
return NextResponse.json(
{ error: "Erreur lors de la récupération du leaderboard des maisons" },
{ status: 500 }
);
}
}

146
app/houses/page.tsx Normal file
View File

@@ -0,0 +1,146 @@
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth";
import { getBackgroundImage } from "@/lib/preferences";
import NavigationWrapper from "@/components/navigation/NavigationWrapper";
import HousesSection from "@/components/houses/HousesSection";
import { houseService } from "@/services/houses/house.service";
import { userService } from "@/services/users/user.service";
export const dynamic = "force-dynamic";
export default async function HousesPage() {
const session = await auth();
if (!session?.user?.id) {
redirect("/login");
}
const [housesData, myHouseData, invitationsData, users, backgroundImage] = await Promise.all([
// Récupérer les maisons
houseService.getAllHouses({
include: {
memberships: {
include: {
user: {
select: {
id: true,
username: true,
avatar: true,
score: true,
level: true,
},
},
},
orderBy: [
{ role: "asc" }, // OWNER, ADMIN, MEMBER
{ user: { score: "desc" } }, // Puis par score décroissant
],
},
creator: {
select: {
id: true,
username: true,
avatar: true,
},
},
},
}),
// Récupérer la maison de l'utilisateur
houseService.getUserHouse(session.user.id, {
memberships: {
include: {
user: {
select: {
id: true,
username: true,
avatar: true,
score: true,
level: true,
},
},
},
},
creator: {
select: {
id: true,
username: true,
avatar: true,
},
},
}),
// Récupérer les invitations de l'utilisateur
houseService.getUserInvitations(session.user.id, "PENDING"),
// Récupérer tous les utilisateurs pour les invitations
userService.getAllUsers({
select: {
id: true,
username: true,
avatar: true,
},
}),
getBackgroundImage("challenges", "/got-2.jpg"),
]);
// Sérialiser les données pour le client
const houses = (housesData as any[]).map((house: any) => ({
id: house.id,
name: house.name,
description: house.description,
creator: house.creator || { id: house.creatorId, username: "Unknown", avatar: null },
memberships: (house.memberships || []).map((m: any) => ({
id: m.id,
role: m.role,
user: {
id: m.user.id,
username: m.user.username,
avatar: m.user.avatar,
score: m.user.score ?? 0,
level: m.user.level ?? 1,
},
})),
}));
const myHouse = myHouseData
? {
id: myHouseData.id,
name: myHouseData.name,
description: myHouseData.description,
creator: (myHouseData as any).creator || { id: (myHouseData as any).creatorId, username: "Unknown", avatar: null },
memberships: ((myHouseData as any).memberships || []).map((m: any) => ({
id: m.id,
role: m.role,
user: {
id: m.user.id,
username: m.user.username,
avatar: m.user.avatar,
score: m.user.score ?? 0,
level: m.user.level ?? 1,
},
})),
}
: null;
const invitations = invitationsData.map((inv: any) => ({
id: inv.id,
house: {
id: inv.house.id,
name: inv.house.name,
},
inviter: inv.inviter,
status: inv.status,
createdAt: inv.createdAt.toISOString(),
}));
return (
<main className="min-h-screen bg-black relative">
<NavigationWrapper />
<HousesSection
initialHouses={houses}
initialMyHouse={myHouse}
initialUsers={users}
initialInvitations={invitations}
backgroundImage={backgroundImage}
/>
</main>
);
}

View File

@@ -7,8 +7,9 @@ export const dynamic = "force-dynamic";
export default async function LeaderboardPage() { export default async function LeaderboardPage() {
// Paralléliser les appels DB // Paralléliser les appels DB
const [leaderboard, backgroundImage] = await Promise.all([ const [leaderboard, houseLeaderboard, backgroundImage] = await Promise.all([
userStatsService.getLeaderboard(10), userStatsService.getLeaderboard(10),
userStatsService.getHouseLeaderboard(10),
getBackgroundImage("leaderboard", "/leaderboard-bg.jpg"), getBackgroundImage("leaderboard", "/leaderboard-bg.jpg"),
]); ]);
@@ -17,6 +18,7 @@ export default async function LeaderboardPage() {
<NavigationWrapper /> <NavigationWrapper />
<LeaderboardSection <LeaderboardSection
leaderboard={leaderboard} leaderboard={leaderboard}
houseLeaderboard={houseLeaderboard}
backgroundImage={backgroundImage} backgroundImage={backgroundImage}
/> />
</main> </main>

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>
);
}

View File

@@ -26,8 +26,29 @@ interface LeaderboardEntry {
characterClass?: CharacterClass | null; characterClass?: CharacterClass | null;
} }
interface HouseMember {
id: string;
username: string;
avatar: string | null;
score: number;
level: number;
role: string;
}
interface HouseLeaderboardEntry {
rank: number;
houseId: string;
houseName: string;
totalScore: number;
memberCount: number;
averageScore: number;
description: string | null;
members: HouseMember[];
}
interface LeaderboardSectionProps { interface LeaderboardSectionProps {
leaderboard: LeaderboardEntry[]; leaderboard: LeaderboardEntry[];
houseLeaderboard: HouseLeaderboardEntry[];
backgroundImage: string; backgroundImage: string;
} }
@@ -38,11 +59,15 @@ const formatScore = (score: number): string => {
export default function LeaderboardSection({ export default function LeaderboardSection({
leaderboard, leaderboard,
houseLeaderboard,
backgroundImage, backgroundImage,
}: LeaderboardSectionProps) { }: LeaderboardSectionProps) {
const [selectedEntry, setSelectedEntry] = useState<LeaderboardEntry | null>( const [selectedEntry, setSelectedEntry] = useState<LeaderboardEntry | null>(
null null
); );
const [selectedHouse, setSelectedHouse] = useState<HouseLeaderboardEntry | null>(
null
);
return ( return (
<BackgroundSection backgroundImage={backgroundImage}> <BackgroundSection backgroundImage={backgroundImage}>
@@ -56,7 +81,7 @@ export default function LeaderboardSection({
LEADERBOARD LEADERBOARD
</SectionTitle> </SectionTitle>
{/* Leaderboard Table */} {/* Players Leaderboard Table */}
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto"> <div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
{/* Header */} {/* Header */}
<div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300"> <div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300">
@@ -143,6 +168,90 @@ export default function LeaderboardSection({
</div> </div>
</div> </div>
{/* House Leaderboard Table */}
<div className="mt-12">
<SectionTitle
variant="gradient"
size="md"
subtitle="Top Houses"
className="mb-8 overflow-hidden"
>
MAISONS
</SectionTitle>
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg backdrop-blur-sm overflow-x-auto">
{/* Header */}
<div className="bg-gray-900/80 border-b border-pixel-gold/30 grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 font-bold text-[10px] sm:text-xs uppercase tracking-widest text-gray-300">
<div className="col-span-2 sm:col-span-1 text-center">Rank</div>
<div className="col-span-5 sm:col-span-6">Maison</div>
<div className="col-span-3 text-right">Score Total</div>
<div className="col-span-2 text-right">Membres</div>
</div>
{/* Entries */}
<div className="divide-y divide-pixel-gold/10 overflow-visible">
{houseLeaderboard.map((house) => (
<div
key={house.houseId}
className={`grid grid-cols-12 gap-2 sm:gap-4 p-2 sm:p-4 hover:bg-gray-900/50 transition relative cursor-pointer ${
house.rank <= 3
? "bg-gradient-to-r from-pixel-gold/10 via-pixel-gold/5 to-transparent"
: "bg-black/40"
}`}
onClick={() => setSelectedHouse(house)}
>
{/* Rank */}
<div className="col-span-2 sm:col-span-1 flex items-center justify-center">
<span
className={`inline-flex items-center justify-center w-8 h-8 sm:w-10 sm:h-10 rounded-full font-bold text-xs sm:text-sm ${
house.rank === 1
? "bg-gradient-to-br from-pixel-gold to-orange-500 text-black shadow-lg shadow-pixel-gold/50"
: house.rank === 2
? "bg-gradient-to-br from-gray-400 to-gray-500 text-black"
: house.rank === 3
? "bg-gradient-to-br from-orange-700 to-orange-800 text-white"
: "bg-gray-900 text-gray-400 border border-gray-800"
}`}
>
{house.rank}
</span>
</div>
{/* House Name */}
<div className="col-span-5 sm:col-span-6 flex items-center gap-2 sm:gap-3 min-w-0">
<div className="flex items-center gap-1 sm:gap-2 min-w-0">
<span
className={`font-bold text-xs sm:text-sm break-words ${
house.rank <= 3 ? "text-pixel-gold" : "text-white"
}`}
>
{house.houseName}
</span>
{house.rank <= 3 && (
<span className="text-pixel-gold text-xs"></span>
)}
</div>
</div>
{/* Total Score */}
<div className="col-span-3 flex items-center justify-end">
<span className="font-mono text-gray-300 text-xs sm:text-sm">
{formatScore(house.totalScore)}
</span>
</div>
{/* Member Count */}
<div className="col-span-2 flex items-center justify-end">
<span className="font-bold text-gray-400 text-xs sm:text-sm">
{house.memberCount}
</span>
</div>
</div>
))}
</div>
</div>
</div>
{/* Footer Info */} {/* Footer Info */}
<div className="mt-8 text-center"> <div className="mt-8 text-center">
<p className="text-gray-500 text-sm"> <p className="text-gray-500 text-sm">
@@ -151,6 +260,112 @@ export default function LeaderboardSection({
<p className="text-gray-600 text-xs mt-2">Rankings update every hour</p> <p className="text-gray-600 text-xs mt-2">Rankings update every hour</p>
</div> </div>
{/* House Modal */}
{selectedHouse && (
<Modal
isOpen={!!selectedHouse}
onClose={() => setSelectedHouse(null)}
size="md"
>
<div className="p-4 sm:p-8">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<h2 className="text-xl sm:text-3xl font-bold text-pixel-gold uppercase tracking-wider break-words">
{selectedHouse.houseName}
</h2>
<CloseButton onClick={() => setSelectedHouse(null)} size="md" />
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-4 mb-6">
<Card variant="default" className="p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Rank
</div>
<div className="text-2xl font-bold text-pixel-gold">
#{selectedHouse.rank}
</div>
</Card>
<Card variant="default" className="p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Score Total
</div>
<div className="text-2xl font-bold text-pixel-gold">
{formatScore(selectedHouse.totalScore)}
</div>
</Card>
<Card variant="default" className="p-4">
<div className="text-xs text-gray-400 uppercase tracking-widest mb-1">
Membres
</div>
<div className="text-2xl font-bold text-pixel-gold">
{selectedHouse.memberCount}
</div>
</Card>
</div>
{/* Members List */}
<div className="border-t border-pixel-gold/30 pt-6 mb-6">
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-4 font-bold">
Membres ({selectedHouse.memberCount})
</div>
<div className="space-y-2">
{selectedHouse.members.map((member) => (
<div
key={member.id}
className="flex items-center justify-between p-3 rounded"
style={{ backgroundColor: "var(--card-hover)" }}
>
<div className="flex items-center gap-3">
<Avatar
src={member.avatar}
username={member.username}
size="sm"
className="flex-shrink-0"
borderClassName="border-pixel-gold/30"
/>
<div>
<div className="flex items-center gap-2">
<span className="font-semibold text-sm" style={{ color: "var(--foreground)" }}>
{member.username}
</span>
<span className="text-xs uppercase" style={{ color: "var(--accent)" }}>
{member.role}
</span>
</div>
<div className="text-xs" style={{ color: "var(--muted-foreground)" }}>
Niveau {member.level}
</div>
</div>
</div>
<div className="text-right">
<div className="font-mono text-sm font-bold" style={{ color: "var(--foreground)" }}>
{formatScore(member.score)}
</div>
<div className="text-xs" style={{ color: "var(--muted-foreground)" }}>
points
</div>
</div>
</div>
))}
</div>
</div>
{/* Description */}
{selectedHouse.description && (
<div className="border-t border-pixel-gold/30 pt-6">
<div className="text-xs text-pixel-gold uppercase tracking-widest mb-3 font-bold">
Description
</div>
<p className="text-gray-200 leading-relaxed whitespace-pre-wrap break-words">
{selectedHouse.description}
</p>
</div>
)}
</div>
</Modal>
)}
{/* Character Modal */} {/* Character Modal */}
{selectedEntry && ( {selectedEntry && (
<Modal <Modal

View File

@@ -118,7 +118,22 @@ export default function Navigation({
LEADERBOARD LEADERBOARD
</Link> </Link>
{isAuthenticated && ( {isAuthenticated && (
<ChallengeBadge initialCount={initialActiveChallengesCount} /> <>
<Link
href="/houses"
className="transition text-xs font-normal uppercase tracking-widest"
style={{ color: "var(--foreground)" }}
onMouseEnter={(e) =>
(e.currentTarget.style.color = "var(--accent-color)")
}
onMouseLeave={(e) =>
(e.currentTarget.style.color = "var(--foreground)")
}
>
MAISONS
</Link>
<ChallengeBadge initialCount={initialActiveChallengesCount} />
</>
)} )}
{isAdmin && ( {isAdmin && (
<Link <Link
@@ -279,10 +294,26 @@ export default function Navigation({
LEADERBOARD LEADERBOARD
</Link> </Link>
{isAuthenticated && ( {isAuthenticated && (
<ChallengeBadge <>
initialCount={initialActiveChallengesCount} <Link
onNavigate={() => setIsMenuOpen(false)} href="/houses"
/> onClick={() => setIsMenuOpen(false)}
className="transition text-xs font-normal uppercase tracking-widest py-2"
style={{ color: "var(--foreground)" }}
onMouseEnter={(e) =>
(e.currentTarget.style.color = "var(--accent-color)")
}
onMouseLeave={(e) =>
(e.currentTarget.style.color = "var(--foreground)")
}
>
MAISONS
</Link>
<ChallengeBadge
initialCount={initialActiveChallengesCount}
onNavigate={() => setIsMenuOpen(false)}
/>
</>
)} )}
{isAdmin && ( {isAdmin && (
<Link <Link

View File

@@ -52,3 +52,23 @@ export type SitePreferences = Prisma.SitePreferencesModel
* *
*/ */
export type Challenge = Prisma.ChallengeModel export type Challenge = Prisma.ChallengeModel
/**
* Model House
*
*/
export type House = Prisma.HouseModel
/**
* Model HouseMembership
*
*/
export type HouseMembership = Prisma.HouseMembershipModel
/**
* Model HouseInvitation
*
*/
export type HouseInvitation = Prisma.HouseInvitationModel
/**
* Model HouseRequest
*
*/
export type HouseRequest = Prisma.HouseRequestModel

View File

@@ -74,3 +74,23 @@ export type SitePreferences = Prisma.SitePreferencesModel
* *
*/ */
export type Challenge = Prisma.ChallengeModel export type Challenge = Prisma.ChallengeModel
/**
* Model House
*
*/
export type House = Prisma.HouseModel
/**
* Model HouseMembership
*
*/
export type HouseMembership = Prisma.HouseMembershipModel
/**
* Model HouseInvitation
*
*/
export type HouseInvitation = Prisma.HouseInvitationModel
/**
* Model HouseRequest
*
*/
export type HouseRequest = Prisma.HouseRequestModel

View File

@@ -270,6 +270,57 @@ export type DateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> _max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
} }
export type EnumHouseRoleFilter<$PrismaModel = never> = {
equals?: $Enums.HouseRole | Prisma.EnumHouseRoleFieldRefInput<$PrismaModel>
in?: $Enums.HouseRole[] | Prisma.ListEnumHouseRoleFieldRefInput<$PrismaModel>
notIn?: $Enums.HouseRole[] | Prisma.ListEnumHouseRoleFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumHouseRoleFilter<$PrismaModel> | $Enums.HouseRole
}
export type EnumHouseRoleWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.HouseRole | Prisma.EnumHouseRoleFieldRefInput<$PrismaModel>
in?: $Enums.HouseRole[] | Prisma.ListEnumHouseRoleFieldRefInput<$PrismaModel>
notIn?: $Enums.HouseRole[] | Prisma.ListEnumHouseRoleFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumHouseRoleWithAggregatesFilter<$PrismaModel> | $Enums.HouseRole
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumHouseRoleFilter<$PrismaModel>
_max?: Prisma.NestedEnumHouseRoleFilter<$PrismaModel>
}
export type EnumInvitationStatusFilter<$PrismaModel = never> = {
equals?: $Enums.InvitationStatus | Prisma.EnumInvitationStatusFieldRefInput<$PrismaModel>
in?: $Enums.InvitationStatus[] | Prisma.ListEnumInvitationStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.InvitationStatus[] | Prisma.ListEnumInvitationStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumInvitationStatusFilter<$PrismaModel> | $Enums.InvitationStatus
}
export type EnumInvitationStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.InvitationStatus | Prisma.EnumInvitationStatusFieldRefInput<$PrismaModel>
in?: $Enums.InvitationStatus[] | Prisma.ListEnumInvitationStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.InvitationStatus[] | Prisma.ListEnumInvitationStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumInvitationStatusWithAggregatesFilter<$PrismaModel> | $Enums.InvitationStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumInvitationStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumInvitationStatusFilter<$PrismaModel>
}
export type EnumRequestStatusFilter<$PrismaModel = never> = {
equals?: $Enums.RequestStatus | Prisma.EnumRequestStatusFieldRefInput<$PrismaModel>
in?: $Enums.RequestStatus[] | Prisma.ListEnumRequestStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RequestStatus[] | Prisma.ListEnumRequestStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRequestStatusFilter<$PrismaModel> | $Enums.RequestStatus
}
export type EnumRequestStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RequestStatus | Prisma.EnumRequestStatusFieldRefInput<$PrismaModel>
in?: $Enums.RequestStatus[] | Prisma.ListEnumRequestStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RequestStatus[] | Prisma.ListEnumRequestStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRequestStatusWithAggregatesFilter<$PrismaModel> | $Enums.RequestStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRequestStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumRequestStatusFilter<$PrismaModel>
}
export type NestedStringFilter<$PrismaModel = never> = { export type NestedStringFilter<$PrismaModel = never> = {
equals?: string | Prisma.StringFieldRefInput<$PrismaModel> equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel> in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
@@ -539,4 +590,55 @@ export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
_max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel> _max?: Prisma.NestedDateTimeNullableFilter<$PrismaModel>
} }
export type NestedEnumHouseRoleFilter<$PrismaModel = never> = {
equals?: $Enums.HouseRole | Prisma.EnumHouseRoleFieldRefInput<$PrismaModel>
in?: $Enums.HouseRole[] | Prisma.ListEnumHouseRoleFieldRefInput<$PrismaModel>
notIn?: $Enums.HouseRole[] | Prisma.ListEnumHouseRoleFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumHouseRoleFilter<$PrismaModel> | $Enums.HouseRole
}
export type NestedEnumHouseRoleWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.HouseRole | Prisma.EnumHouseRoleFieldRefInput<$PrismaModel>
in?: $Enums.HouseRole[] | Prisma.ListEnumHouseRoleFieldRefInput<$PrismaModel>
notIn?: $Enums.HouseRole[] | Prisma.ListEnumHouseRoleFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumHouseRoleWithAggregatesFilter<$PrismaModel> | $Enums.HouseRole
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumHouseRoleFilter<$PrismaModel>
_max?: Prisma.NestedEnumHouseRoleFilter<$PrismaModel>
}
export type NestedEnumInvitationStatusFilter<$PrismaModel = never> = {
equals?: $Enums.InvitationStatus | Prisma.EnumInvitationStatusFieldRefInput<$PrismaModel>
in?: $Enums.InvitationStatus[] | Prisma.ListEnumInvitationStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.InvitationStatus[] | Prisma.ListEnumInvitationStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumInvitationStatusFilter<$PrismaModel> | $Enums.InvitationStatus
}
export type NestedEnumInvitationStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.InvitationStatus | Prisma.EnumInvitationStatusFieldRefInput<$PrismaModel>
in?: $Enums.InvitationStatus[] | Prisma.ListEnumInvitationStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.InvitationStatus[] | Prisma.ListEnumInvitationStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumInvitationStatusWithAggregatesFilter<$PrismaModel> | $Enums.InvitationStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumInvitationStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumInvitationStatusFilter<$PrismaModel>
}
export type NestedEnumRequestStatusFilter<$PrismaModel = never> = {
equals?: $Enums.RequestStatus | Prisma.EnumRequestStatusFieldRefInput<$PrismaModel>
in?: $Enums.RequestStatus[] | Prisma.ListEnumRequestStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RequestStatus[] | Prisma.ListEnumRequestStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRequestStatusFilter<$PrismaModel> | $Enums.RequestStatus
}
export type NestedEnumRequestStatusWithAggregatesFilter<$PrismaModel = never> = {
equals?: $Enums.RequestStatus | Prisma.EnumRequestStatusFieldRefInput<$PrismaModel>
in?: $Enums.RequestStatus[] | Prisma.ListEnumRequestStatusFieldRefInput<$PrismaModel>
notIn?: $Enums.RequestStatus[] | Prisma.ListEnumRequestStatusFieldRefInput<$PrismaModel>
not?: Prisma.NestedEnumRequestStatusWithAggregatesFilter<$PrismaModel> | $Enums.RequestStatus
_count?: Prisma.NestedIntFilter<$PrismaModel>
_min?: Prisma.NestedEnumRequestStatusFilter<$PrismaModel>
_max?: Prisma.NestedEnumRequestStatusFilter<$PrismaModel>
}

View File

@@ -52,3 +52,32 @@ export const ChallengeStatus = {
} as const } as const
export type ChallengeStatus = (typeof ChallengeStatus)[keyof typeof ChallengeStatus] export type ChallengeStatus = (typeof ChallengeStatus)[keyof typeof ChallengeStatus]
export const HouseRole = {
OWNER: 'OWNER',
ADMIN: 'ADMIN',
MEMBER: 'MEMBER'
} as const
export type HouseRole = (typeof HouseRole)[keyof typeof HouseRole]
export const InvitationStatus = {
PENDING: 'PENDING',
ACCEPTED: 'ACCEPTED',
REJECTED: 'REJECTED',
CANCELLED: 'CANCELLED'
} as const
export type InvitationStatus = (typeof InvitationStatus)[keyof typeof InvitationStatus]
export const RequestStatus = {
PENDING: 'PENDING',
ACCEPTED: 'ACCEPTED',
REJECTED: 'REJECTED',
CANCELLED: 'CANCELLED'
} as const
export type RequestStatus = (typeof RequestStatus)[keyof typeof RequestStatus]

File diff suppressed because one or more lines are too long

View File

@@ -390,7 +390,11 @@ export const ModelName = {
EventRegistration: 'EventRegistration', EventRegistration: 'EventRegistration',
EventFeedback: 'EventFeedback', EventFeedback: 'EventFeedback',
SitePreferences: 'SitePreferences', SitePreferences: 'SitePreferences',
Challenge: 'Challenge' Challenge: 'Challenge',
House: 'House',
HouseMembership: 'HouseMembership',
HouseInvitation: 'HouseInvitation',
HouseRequest: 'HouseRequest'
} as const } as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName] export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -406,7 +410,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
omit: GlobalOmitOptions omit: GlobalOmitOptions
} }
meta: { meta: {
modelProps: "user" | "userPreferences" | "event" | "eventRegistration" | "eventFeedback" | "sitePreferences" | "challenge" modelProps: "user" | "userPreferences" | "event" | "eventRegistration" | "eventFeedback" | "sitePreferences" | "challenge" | "house" | "houseMembership" | "houseInvitation" | "houseRequest"
txIsolationLevel: TransactionIsolationLevel txIsolationLevel: TransactionIsolationLevel
} }
model: { model: {
@@ -928,6 +932,302 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
} }
} }
} }
House: {
payload: Prisma.$HousePayload<ExtArgs>
fields: Prisma.HouseFieldRefs
operations: {
findUnique: {
args: Prisma.HouseFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HousePayload> | null
}
findUniqueOrThrow: {
args: Prisma.HouseFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HousePayload>
}
findFirst: {
args: Prisma.HouseFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HousePayload> | null
}
findFirstOrThrow: {
args: Prisma.HouseFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HousePayload>
}
findMany: {
args: Prisma.HouseFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HousePayload>[]
}
create: {
args: Prisma.HouseCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HousePayload>
}
createMany: {
args: Prisma.HouseCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.HouseCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HousePayload>[]
}
delete: {
args: Prisma.HouseDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HousePayload>
}
update: {
args: Prisma.HouseUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HousePayload>
}
deleteMany: {
args: Prisma.HouseDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.HouseUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.HouseUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HousePayload>[]
}
upsert: {
args: Prisma.HouseUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HousePayload>
}
aggregate: {
args: Prisma.HouseAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateHouse>
}
groupBy: {
args: Prisma.HouseGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.HouseGroupByOutputType>[]
}
count: {
args: Prisma.HouseCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.HouseCountAggregateOutputType> | number
}
}
}
HouseMembership: {
payload: Prisma.$HouseMembershipPayload<ExtArgs>
fields: Prisma.HouseMembershipFieldRefs
operations: {
findUnique: {
args: Prisma.HouseMembershipFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseMembershipPayload> | null
}
findUniqueOrThrow: {
args: Prisma.HouseMembershipFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseMembershipPayload>
}
findFirst: {
args: Prisma.HouseMembershipFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseMembershipPayload> | null
}
findFirstOrThrow: {
args: Prisma.HouseMembershipFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseMembershipPayload>
}
findMany: {
args: Prisma.HouseMembershipFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseMembershipPayload>[]
}
create: {
args: Prisma.HouseMembershipCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseMembershipPayload>
}
createMany: {
args: Prisma.HouseMembershipCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.HouseMembershipCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseMembershipPayload>[]
}
delete: {
args: Prisma.HouseMembershipDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseMembershipPayload>
}
update: {
args: Prisma.HouseMembershipUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseMembershipPayload>
}
deleteMany: {
args: Prisma.HouseMembershipDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.HouseMembershipUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.HouseMembershipUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseMembershipPayload>[]
}
upsert: {
args: Prisma.HouseMembershipUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseMembershipPayload>
}
aggregate: {
args: Prisma.HouseMembershipAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateHouseMembership>
}
groupBy: {
args: Prisma.HouseMembershipGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.HouseMembershipGroupByOutputType>[]
}
count: {
args: Prisma.HouseMembershipCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.HouseMembershipCountAggregateOutputType> | number
}
}
}
HouseInvitation: {
payload: Prisma.$HouseInvitationPayload<ExtArgs>
fields: Prisma.HouseInvitationFieldRefs
operations: {
findUnique: {
args: Prisma.HouseInvitationFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseInvitationPayload> | null
}
findUniqueOrThrow: {
args: Prisma.HouseInvitationFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseInvitationPayload>
}
findFirst: {
args: Prisma.HouseInvitationFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseInvitationPayload> | null
}
findFirstOrThrow: {
args: Prisma.HouseInvitationFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseInvitationPayload>
}
findMany: {
args: Prisma.HouseInvitationFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseInvitationPayload>[]
}
create: {
args: Prisma.HouseInvitationCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseInvitationPayload>
}
createMany: {
args: Prisma.HouseInvitationCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.HouseInvitationCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseInvitationPayload>[]
}
delete: {
args: Prisma.HouseInvitationDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseInvitationPayload>
}
update: {
args: Prisma.HouseInvitationUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseInvitationPayload>
}
deleteMany: {
args: Prisma.HouseInvitationDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.HouseInvitationUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.HouseInvitationUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseInvitationPayload>[]
}
upsert: {
args: Prisma.HouseInvitationUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseInvitationPayload>
}
aggregate: {
args: Prisma.HouseInvitationAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateHouseInvitation>
}
groupBy: {
args: Prisma.HouseInvitationGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.HouseInvitationGroupByOutputType>[]
}
count: {
args: Prisma.HouseInvitationCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.HouseInvitationCountAggregateOutputType> | number
}
}
}
HouseRequest: {
payload: Prisma.$HouseRequestPayload<ExtArgs>
fields: Prisma.HouseRequestFieldRefs
operations: {
findUnique: {
args: Prisma.HouseRequestFindUniqueArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseRequestPayload> | null
}
findUniqueOrThrow: {
args: Prisma.HouseRequestFindUniqueOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseRequestPayload>
}
findFirst: {
args: Prisma.HouseRequestFindFirstArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseRequestPayload> | null
}
findFirstOrThrow: {
args: Prisma.HouseRequestFindFirstOrThrowArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseRequestPayload>
}
findMany: {
args: Prisma.HouseRequestFindManyArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseRequestPayload>[]
}
create: {
args: Prisma.HouseRequestCreateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseRequestPayload>
}
createMany: {
args: Prisma.HouseRequestCreateManyArgs<ExtArgs>
result: BatchPayload
}
createManyAndReturn: {
args: Prisma.HouseRequestCreateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseRequestPayload>[]
}
delete: {
args: Prisma.HouseRequestDeleteArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseRequestPayload>
}
update: {
args: Prisma.HouseRequestUpdateArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseRequestPayload>
}
deleteMany: {
args: Prisma.HouseRequestDeleteManyArgs<ExtArgs>
result: BatchPayload
}
updateMany: {
args: Prisma.HouseRequestUpdateManyArgs<ExtArgs>
result: BatchPayload
}
updateManyAndReturn: {
args: Prisma.HouseRequestUpdateManyAndReturnArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseRequestPayload>[]
}
upsert: {
args: Prisma.HouseRequestUpsertArgs<ExtArgs>
result: runtime.Types.Utils.PayloadToResult<Prisma.$HouseRequestPayload>
}
aggregate: {
args: Prisma.HouseRequestAggregateArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.AggregateHouseRequest>
}
groupBy: {
args: Prisma.HouseRequestGroupByArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.HouseRequestGroupByOutputType>[]
}
count: {
args: Prisma.HouseRequestCountArgs<ExtArgs>
result: runtime.Types.Utils.Optional<Prisma.HouseRequestCountAggregateOutputType> | number
}
}
}
} }
} & { } & {
other: { other: {
@@ -1078,6 +1378,54 @@ export const ChallengeScalarFieldEnum = {
export type ChallengeScalarFieldEnum = (typeof ChallengeScalarFieldEnum)[keyof typeof ChallengeScalarFieldEnum] export type ChallengeScalarFieldEnum = (typeof ChallengeScalarFieldEnum)[keyof typeof ChallengeScalarFieldEnum]
export const HouseScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description',
creatorId: 'creatorId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type HouseScalarFieldEnum = (typeof HouseScalarFieldEnum)[keyof typeof HouseScalarFieldEnum]
export const HouseMembershipScalarFieldEnum = {
id: 'id',
houseId: 'houseId',
userId: 'userId',
role: 'role',
joinedAt: 'joinedAt'
} as const
export type HouseMembershipScalarFieldEnum = (typeof HouseMembershipScalarFieldEnum)[keyof typeof HouseMembershipScalarFieldEnum]
export const HouseInvitationScalarFieldEnum = {
id: 'id',
houseId: 'houseId',
inviterId: 'inviterId',
inviteeId: 'inviteeId',
status: 'status',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type HouseInvitationScalarFieldEnum = (typeof HouseInvitationScalarFieldEnum)[keyof typeof HouseInvitationScalarFieldEnum]
export const HouseRequestScalarFieldEnum = {
id: 'id',
houseId: 'houseId',
requesterId: 'requesterId',
status: 'status',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type HouseRequestScalarFieldEnum = (typeof HouseRequestScalarFieldEnum)[keyof typeof HouseRequestScalarFieldEnum]
export const SortOrder = { export const SortOrder = {
asc: 'asc', asc: 'asc',
desc: 'desc' desc: 'desc'
@@ -1213,6 +1561,48 @@ export type ListEnumChallengeStatusFieldRefInput<$PrismaModel> = FieldRefInputTy
/**
* Reference to a field of type 'HouseRole'
*/
export type EnumHouseRoleFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'HouseRole'>
/**
* Reference to a field of type 'HouseRole[]'
*/
export type ListEnumHouseRoleFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'HouseRole[]'>
/**
* Reference to a field of type 'InvitationStatus'
*/
export type EnumInvitationStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'InvitationStatus'>
/**
* Reference to a field of type 'InvitationStatus[]'
*/
export type ListEnumInvitationStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'InvitationStatus[]'>
/**
* Reference to a field of type 'RequestStatus'
*/
export type EnumRequestStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RequestStatus'>
/**
* Reference to a field of type 'RequestStatus[]'
*/
export type ListEnumRequestStatusFieldRefInput<$PrismaModel> = FieldRefInputType<$PrismaModel, 'RequestStatus[]'>
/** /**
* Reference to a field of type 'Float' * Reference to a field of type 'Float'
*/ */
@@ -1328,6 +1718,10 @@ export type GlobalOmitConfig = {
eventFeedback?: Prisma.EventFeedbackOmit eventFeedback?: Prisma.EventFeedbackOmit
sitePreferences?: Prisma.SitePreferencesOmit sitePreferences?: Prisma.SitePreferencesOmit
challenge?: Prisma.ChallengeOmit challenge?: Prisma.ChallengeOmit
house?: Prisma.HouseOmit
houseMembership?: Prisma.HouseMembershipOmit
houseInvitation?: Prisma.HouseInvitationOmit
houseRequest?: Prisma.HouseRequestOmit
} }
/* Types for Logging */ /* Types for Logging */

View File

@@ -57,7 +57,11 @@ export const ModelName = {
EventRegistration: 'EventRegistration', EventRegistration: 'EventRegistration',
EventFeedback: 'EventFeedback', EventFeedback: 'EventFeedback',
SitePreferences: 'SitePreferences', SitePreferences: 'SitePreferences',
Challenge: 'Challenge' Challenge: 'Challenge',
House: 'House',
HouseMembership: 'HouseMembership',
HouseInvitation: 'HouseInvitation',
HouseRequest: 'HouseRequest'
} as const } as const
export type ModelName = (typeof ModelName)[keyof typeof ModelName] export type ModelName = (typeof ModelName)[keyof typeof ModelName]
@@ -187,6 +191,54 @@ export const ChallengeScalarFieldEnum = {
export type ChallengeScalarFieldEnum = (typeof ChallengeScalarFieldEnum)[keyof typeof ChallengeScalarFieldEnum] export type ChallengeScalarFieldEnum = (typeof ChallengeScalarFieldEnum)[keyof typeof ChallengeScalarFieldEnum]
export const HouseScalarFieldEnum = {
id: 'id',
name: 'name',
description: 'description',
creatorId: 'creatorId',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type HouseScalarFieldEnum = (typeof HouseScalarFieldEnum)[keyof typeof HouseScalarFieldEnum]
export const HouseMembershipScalarFieldEnum = {
id: 'id',
houseId: 'houseId',
userId: 'userId',
role: 'role',
joinedAt: 'joinedAt'
} as const
export type HouseMembershipScalarFieldEnum = (typeof HouseMembershipScalarFieldEnum)[keyof typeof HouseMembershipScalarFieldEnum]
export const HouseInvitationScalarFieldEnum = {
id: 'id',
houseId: 'houseId',
inviterId: 'inviterId',
inviteeId: 'inviteeId',
status: 'status',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type HouseInvitationScalarFieldEnum = (typeof HouseInvitationScalarFieldEnum)[keyof typeof HouseInvitationScalarFieldEnum]
export const HouseRequestScalarFieldEnum = {
id: 'id',
houseId: 'houseId',
requesterId: 'requesterId',
status: 'status',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
} as const
export type HouseRequestScalarFieldEnum = (typeof HouseRequestScalarFieldEnum)[keyof typeof HouseRequestScalarFieldEnum]
export const SortOrder = { export const SortOrder = {
asc: 'asc', asc: 'asc',
desc: 'desc' desc: 'desc'

View File

@@ -15,4 +15,8 @@ export type * from './models/EventRegistration'
export type * from './models/EventFeedback' export type * from './models/EventFeedback'
export type * from './models/SitePreferences' export type * from './models/SitePreferences'
export type * from './models/Challenge' export type * from './models/Challenge'
export type * from './models/House'
export type * from './models/HouseMembership'
export type * from './models/HouseInvitation'
export type * from './models/HouseRequest'
export type * from './commonInputTypes' export type * from './commonInputTypes'

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,120 @@
-- CreateEnum
CREATE TYPE "HouseRole" AS ENUM ('OWNER', 'ADMIN', 'MEMBER');
-- CreateEnum
CREATE TYPE "InvitationStatus" AS ENUM ('PENDING', 'ACCEPTED', 'REJECTED', 'CANCELLED');
-- CreateEnum
CREATE TYPE "RequestStatus" AS ENUM ('PENDING', 'ACCEPTED', 'REJECTED', 'CANCELLED');
-- CreateTable
CREATE TABLE "House" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"creatorId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "House_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HouseMembership" (
"id" TEXT NOT NULL,
"houseId" TEXT NOT NULL,
"userId" TEXT NOT NULL,
"role" "HouseRole" NOT NULL DEFAULT 'MEMBER',
"joinedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "HouseMembership_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HouseInvitation" (
"id" TEXT NOT NULL,
"houseId" TEXT NOT NULL,
"inviterId" TEXT NOT NULL,
"inviteeId" TEXT NOT NULL,
"status" "InvitationStatus" NOT NULL DEFAULT 'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HouseInvitation_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "HouseRequest" (
"id" TEXT NOT NULL,
"houseId" TEXT NOT NULL,
"requesterId" TEXT NOT NULL,
"status" "RequestStatus" NOT NULL DEFAULT 'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "HouseRequest_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "House_creatorId_idx" ON "House"("creatorId");
-- CreateIndex
CREATE INDEX "House_name_idx" ON "House"("name");
-- CreateIndex
CREATE UNIQUE INDEX "HouseMembership_houseId_userId_key" ON "HouseMembership"("houseId", "userId");
-- CreateIndex
CREATE INDEX "HouseMembership_houseId_idx" ON "HouseMembership"("houseId");
-- CreateIndex
CREATE INDEX "HouseMembership_userId_idx" ON "HouseMembership"("userId");
-- CreateIndex
CREATE UNIQUE INDEX "HouseInvitation_houseId_inviteeId_key" ON "HouseInvitation"("houseId", "inviteeId");
-- CreateIndex
CREATE INDEX "HouseInvitation_houseId_idx" ON "HouseInvitation"("houseId");
-- CreateIndex
CREATE INDEX "HouseInvitation_inviteeId_idx" ON "HouseInvitation"("inviteeId");
-- CreateIndex
CREATE INDEX "HouseInvitation_status_idx" ON "HouseInvitation"("status");
-- CreateIndex
CREATE UNIQUE INDEX "HouseRequest_houseId_requesterId_key" ON "HouseRequest"("houseId", "requesterId");
-- CreateIndex
CREATE INDEX "HouseRequest_houseId_idx" ON "HouseRequest"("houseId");
-- CreateIndex
CREATE INDEX "HouseRequest_requesterId_idx" ON "HouseRequest"("requesterId");
-- CreateIndex
CREATE INDEX "HouseRequest_status_idx" ON "HouseRequest"("status");
-- AddForeignKey
ALTER TABLE "House" ADD CONSTRAINT "House_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HouseMembership" ADD CONSTRAINT "HouseMembership_houseId_fkey" FOREIGN KEY ("houseId") REFERENCES "House"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HouseMembership" ADD CONSTRAINT "HouseMembership_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HouseInvitation" ADD CONSTRAINT "HouseInvitation_houseId_fkey" FOREIGN KEY ("houseId") REFERENCES "House"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HouseInvitation" ADD CONSTRAINT "HouseInvitation_inviterId_fkey" FOREIGN KEY ("inviterId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HouseInvitation" ADD CONSTRAINT "HouseInvitation_inviteeId_fkey" FOREIGN KEY ("inviteeId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HouseRequest" ADD CONSTRAINT "HouseRequest_houseId_fkey" FOREIGN KEY ("houseId") REFERENCES "House"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "HouseRequest" ADD CONSTRAINT "HouseRequest_requesterId_fkey" FOREIGN KEY ("requesterId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -31,6 +31,11 @@ model User {
challengesAsChallenged Challenge[] @relation("Challenged") challengesAsChallenged Challenge[] @relation("Challenged")
challengesAsAdmin Challenge[] @relation("AdminValidator") challengesAsAdmin Challenge[] @relation("AdminValidator")
challengesAsWinner Challenge[] @relation("ChallengeWinner") challengesAsWinner Challenge[] @relation("ChallengeWinner")
houseMemberships HouseMembership[]
houseInvitationsSent HouseInvitation[] @relation("Inviter")
houseInvitationsReceived HouseInvitation[] @relation("Invitee")
houseRequestsSent HouseRequest[] @relation("Requester")
housesCreated House[] @relation("HouseCreator")
@@index([score]) @@index([score])
@@index([email]) @@index([email])
@@ -166,3 +171,87 @@ model Challenge {
@@index([status]) @@index([status])
@@index([adminId]) @@index([adminId])
} }
model House {
id String @id @default(cuid())
name String
description String?
creatorId String
creator User @relation("HouseCreator", fields: [creatorId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
memberships HouseMembership[]
invitations HouseInvitation[]
requests HouseRequest[]
@@index([creatorId])
@@index([name])
}
model HouseMembership {
id String @id @default(cuid())
houseId String
userId String
role HouseRole @default(MEMBER)
joinedAt DateTime @default(now())
house House @relation(fields: [houseId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([houseId, userId])
@@index([houseId])
@@index([userId])
}
model HouseInvitation {
id String @id @default(cuid())
houseId String
inviterId String // Utilisateur qui envoie l'invitation
inviteeId String // Utilisateur invité
status InvitationStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
house House @relation(fields: [houseId], references: [id], onDelete: Cascade)
inviter User @relation("Inviter", fields: [inviterId], references: [id], onDelete: Cascade)
invitee User @relation("Invitee", fields: [inviteeId], references: [id], onDelete: Cascade)
@@unique([houseId, inviteeId])
@@index([houseId])
@@index([inviteeId])
@@index([status])
}
model HouseRequest {
id String @id @default(cuid())
houseId String
requesterId String // Utilisateur qui demande à rejoindre
status RequestStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
house House @relation(fields: [houseId], references: [id], onDelete: Cascade)
requester User @relation("Requester", fields: [requesterId], references: [id], onDelete: Cascade)
@@unique([houseId, requesterId])
@@index([houseId])
@@index([requesterId])
@@index([status])
}
enum HouseRole {
OWNER
ADMIN
MEMBER
}
enum InvitationStatus {
PENDING
ACCEPTED
REJECTED
CANCELLED
}
enum RequestStatus {
PENDING
ACCEPTED
REJECTED
CANCELLED
}

View File

@@ -238,7 +238,150 @@ async function main() {
}) })
); );
console.log("Seed completed:", { admin, users, events }); // Créer les maisons Game of Thrones
const housesData = [
{
name: "Maison Stark",
description:
"Winter is Coming. La Maison Stark de Winterfell règne sur le Nord depuis des millénaires. Fiers, loyaux et honorables, les Stark sont connus pour leur sens de la justice et leur connexion avec les anciens dieux. Leur devise rappelle que l'hiver approche toujours.",
creatorId: users[0].id, // DragonSlayer99
},
{
name: "Maison Lannister",
description:
"Hear Me Roar. La Maison Lannister de Castral Roc est la plus riche des Sept Royaumes. Célèbres pour leur ruse, leur ambition et leur devise 'Un Lannister paie toujours ses dettes', ils contrôlent les terres de l'Ouest avec une main de fer.",
creatorId: users[1].id, // MineMaster
},
{
name: "Maison Targaryen",
description:
"Fire and Blood. Les Targaryen sont les derniers descendants des seigneurs dragons de Valyria. Maîtres des dragons et des flammes, ils ont conquis les Sept Royaumes il y a trois cents ans. Leur sang de feu coule dans leurs veines.",
creatorId: users[2].id, // CraftKing
},
{
name: "Maison Baratheon",
description:
"Ours is the Fury. La Maison Baratheon de Port-Réal règne sur les Terres de l'Orage. Fondée par Orys Baratheon, compagnon d'Aegon le Conquérant, cette maison est connue pour sa force, sa détermination et sa fureur au combat.",
creatorId: users[3].id, // ForestWalker
},
{
name: "Maison Tyrell",
description:
"Growing Strong. La Maison Tyrell de Hautjardin contrôle le Bief, la région la plus fertile des Sept Royaumes. Maîtres de l'agriculture et du commerce, ils sont réputés pour leur richesse, leur diplomatie et leur capacité à faire fleurir même les terres les plus arides.",
creatorId: users[4].id, // HolyGuardian
},
{
name: "Maison Martell",
description:
"Unbowed, Unbent, Unbroken. La Maison Martell de Dorne n'a jamais été conquise. Fiers et indépendants, les Martell gouvernent les terres du Sud avec sagesse. Leur résilience légendaire et leur refus de se soumettre font d'eux des alliés redoutables.",
creatorId: users[5].id, // TechSmith
},
{
name: "Maison Greyjoy",
description:
"We Do Not Sow. La Maison Greyjoy des Îles de Fer règne sur les mers. Fiers guerriers et pillards redoutés, les Greyjoy ne cultivent pas la terre mais prennent ce qu'ils veulent par la force. Leur devise reflète leur nature de conquérants des océans.",
creatorId: admin.id, // Admin crée seulement cette maison
},
];
// Supprimer toutes les maisons existantes avant de les recréer
await prisma.house.deleteMany({});
// Créer les maisons avec leurs membres
// On doit créer les maisons séquentiellement pour éviter les conflits de membres multiples
const houses = [];
const usedUserIds = new Set<string>();
for (let index = 0; index < housesData.length; index++) {
const houseData = housesData[index];
// Vérifier si le créateur est déjà utilisé
if (usedUserIds.has(houseData.creatorId)) {
// Trouver un utilisateur disponible
const availableUser = users.find((u) => !usedUserIds.has(u.id));
if (!availableUser) {
console.warn(
`Pas d'utilisateur disponible pour ${houseData.name}, utilisation de l'admin`
);
// Utiliser l'admin seulement si vraiment nécessaire
if (!usedUserIds.has(admin.id)) {
houseData.creatorId = admin.id;
} else {
console.error(
`Impossible de créer ${houseData.name}, tous les utilisateurs sont déjà dans une maison`
);
continue;
}
} else {
houseData.creatorId = availableUser.id;
}
}
const house = await prisma.house.create({
data: {
name: houseData.name,
description: houseData.description,
creatorId: houseData.creatorId,
memberships: {
create: {
userId: houseData.creatorId,
role: "OWNER",
},
},
},
});
usedUserIds.add(houseData.creatorId);
// Ajouter quelques membres supplémentaires pour certaines maisons
if (index === 0 && users.length > 1 && !usedUserIds.has(users[1].id)) {
// Stark : ajouter un membre
await prisma.houseMembership.create({
data: {
houseId: house.id,
userId: users[1].id,
role: "MEMBER",
},
});
usedUserIds.add(users[1].id);
}
if (index === 1 && users.length > 2 && !usedUserIds.has(users[2].id)) {
// Lannister : ajouter deux membres
await prisma.houseMembership.create({
data: {
houseId: house.id,
userId: users[2].id,
role: "MEMBER",
},
});
usedUserIds.add(users[2].id);
if (users.length > 3 && !usedUserIds.has(users[3].id)) {
await prisma.houseMembership.create({
data: {
houseId: house.id,
userId: users[3].id,
role: "ADMIN",
},
});
usedUserIds.add(users[3].id);
}
}
if (index === 2 && users.length > 4 && !usedUserIds.has(users[4].id)) {
// Targaryen : ajouter un membre
await prisma.houseMembership.create({
data: {
houseId: house.id,
userId: users[4].id,
role: "MEMBER",
},
});
usedUserIds.add(users[4].id);
}
houses.push(house);
}
console.log("Seed completed:", { admin, users, events, houses });
} }
main() main()

View File

@@ -34,3 +34,10 @@ export class ConflictError extends BusinessError {
this.name = "ConflictError"; this.name = "ConflictError";
} }
} }
export class ForbiddenError extends BusinessError {
constructor(message: string) {
super(message, "FORBIDDEN");
this.name = "ForbiddenError";
}
}

View File

@@ -0,0 +1,893 @@
import { prisma } from "../database";
import type {
House,
HouseMembership,
HouseInvitation,
HouseRequest,
HouseRole,
InvitationStatus,
RequestStatus,
Prisma,
} from "@/prisma/generated/prisma/client";
import {
ValidationError,
NotFoundError,
ConflictError,
ForbiddenError,
} from "../errors";
const HOUSE_NAME_MIN_LENGTH = 3;
const HOUSE_NAME_MAX_LENGTH = 50;
const HOUSE_DESCRIPTION_MAX_LENGTH = 500;
export interface CreateHouseInput {
name: string;
description?: string | null;
creatorId: string;
}
export interface UpdateHouseInput {
name?: string;
description?: string | null;
}
export interface InviteUserInput {
houseId: string;
inviterId: string;
inviteeId: string;
}
export interface RequestToJoinInput {
houseId: string;
requesterId: string;
}
/**
* Service de gestion des maisons
*/
export class HouseService {
/**
* Récupère une maison par son ID
*/
async getHouseById(
id: string,
include?: Prisma.HouseInclude
): Promise<House | null> {
return prisma.house.findUnique({
where: { id },
include,
});
}
/**
* Récupère toutes les maisons avec pagination
*/
async getAllHouses(options?: {
skip?: number;
take?: number;
include?: Prisma.HouseInclude;
orderBy?: Prisma.HouseOrderByWithRelationInput;
}): Promise<House[]> {
return prisma.house.findMany({
skip: options?.skip,
take: options?.take,
include: options?.include,
orderBy: options?.orderBy || { createdAt: "desc" },
});
}
/**
* Recherche des maisons par nom
*/
async searchHouses(
searchTerm: string,
options?: {
skip?: number;
take?: number;
include?: Prisma.HouseInclude;
}
): Promise<House[]> {
return prisma.house.findMany({
where: {
name: {
contains: searchTerm,
mode: "insensitive",
},
},
skip: options?.skip,
take: options?.take,
include: options?.include,
orderBy: { createdAt: "desc" },
});
}
/**
* Récupère les maisons d'un utilisateur
*/
async getUserHouses(
userId: string,
include?: Prisma.HouseInclude
): Promise<House[]> {
const memberships = await prisma.houseMembership.findMany({
where: { userId },
include: {
house: {
include: include,
},
},
});
return memberships.map((m) => m.house);
}
/**
* Récupère la maison d'un utilisateur (s'il en a une)
*/
async getUserHouse(
userId: string,
include?: Prisma.HouseInclude
): Promise<House | null> {
const membership = await prisma.houseMembership.findFirst({
where: { userId },
include: {
house: {
include: include,
},
},
orderBy: { joinedAt: "asc" }, // Première maison jointe
});
return membership?.house || null;
}
/**
* Vérifie si un utilisateur est membre d'une maison
*/
async isUserMemberOfHouse(
userId: string,
houseId: string
): Promise<boolean> {
const membership = await prisma.houseMembership.findUnique({
where: {
houseId_userId: {
houseId,
userId,
},
},
});
return !!membership;
}
/**
* Vérifie si un utilisateur est propriétaire ou admin d'une maison
*/
async isUserOwnerOrAdmin(
userId: string,
houseId: string
): Promise<boolean> {
const membership = await prisma.houseMembership.findUnique({
where: {
houseId_userId: {
houseId,
userId,
},
},
});
return membership?.role === "OWNER" || membership?.role === "ADMIN";
}
/**
* Vérifie si un utilisateur est propriétaire d'une maison
*/
async isUserOwner(userId: string, houseId: string): Promise<boolean> {
const membership = await prisma.houseMembership.findUnique({
where: {
houseId_userId: {
houseId,
userId,
},
},
});
return membership?.role === "OWNER";
}
/**
* Récupère le rôle d'un utilisateur dans une maison
*/
async getUserRole(
userId: string,
houseId: string
): Promise<HouseRole | null> {
const membership = await prisma.houseMembership.findUnique({
where: {
houseId_userId: {
houseId,
userId,
},
},
});
return membership?.role || null;
}
/**
* Récupère les membres d'une maison
*/
async getHouseMembers(
houseId: string,
include?: Prisma.HouseMembershipInclude
): Promise<HouseMembership[]> {
return prisma.houseMembership.findMany({
where: { houseId },
include,
orderBy: [
{ role: "asc" }, // OWNER, ADMIN, MEMBER
{ joinedAt: "asc" },
],
});
}
/**
* Crée une nouvelle maison
*/
async createHouse(data: CreateHouseInput): Promise<House> {
// Validation
if (!data.name || data.name.trim().length === 0) {
throw new ValidationError("Le nom de la maison est requis", "name");
}
if (
data.name.length < HOUSE_NAME_MIN_LENGTH ||
data.name.length > HOUSE_NAME_MAX_LENGTH
) {
throw new ValidationError(
`Le nom de la maison doit contenir entre ${HOUSE_NAME_MIN_LENGTH} et ${HOUSE_NAME_MAX_LENGTH} caractères`,
"name"
);
}
if (data.description && data.description.length > HOUSE_DESCRIPTION_MAX_LENGTH) {
throw new ValidationError(
`La description ne peut pas dépasser ${HOUSE_DESCRIPTION_MAX_LENGTH} caractères`,
"description"
);
}
// Vérifier si l'utilisateur est déjà dans une maison
const existingMembership = await prisma.houseMembership.findFirst({
where: { userId: data.creatorId },
});
if (existingMembership) {
throw new ConflictError(
"Vous êtes déjà membre d'une maison. Vous devez quitter votre maison actuelle avant d'en créer une nouvelle."
);
}
// Vérifier si le nom est déjà pris
const existingHouse = await prisma.house.findFirst({
where: {
name: {
equals: data.name.trim(),
mode: "insensitive",
},
},
});
if (existingHouse) {
throw new ConflictError("Ce nom de maison est déjà utilisé");
}
// Créer la maison et ajouter le créateur comme OWNER
return prisma.house.create({
data: {
name: data.name.trim(),
description: data.description?.trim() || null,
creatorId: data.creatorId,
memberships: {
create: {
userId: data.creatorId,
role: "OWNER",
},
},
},
});
}
/**
* Met à jour une maison
*/
async updateHouse(
houseId: string,
userId: string,
data: UpdateHouseInput
): Promise<House> {
// Vérifier que l'utilisateur est propriétaire ou admin
const isAuthorized = await this.isUserOwnerOrAdmin(userId, houseId);
if (!isAuthorized) {
throw new ForbiddenError(
"Vous n'avez pas les permissions pour modifier cette maison"
);
}
const updateData: Prisma.HouseUpdateInput = {};
if (data.name !== undefined) {
if (!data.name || data.name.trim().length === 0) {
throw new ValidationError("Le nom de la maison est requis", "name");
}
if (
data.name.length < HOUSE_NAME_MIN_LENGTH ||
data.name.length > HOUSE_NAME_MAX_LENGTH
) {
throw new ValidationError(
`Le nom de la maison doit contenir entre ${HOUSE_NAME_MIN_LENGTH} et ${HOUSE_NAME_MAX_LENGTH} caractères`,
"name"
);
}
// Vérifier si le nom est déjà pris par une autre maison
const existingHouse = await prisma.house.findFirst({
where: {
name: {
equals: data.name.trim(),
mode: "insensitive",
},
NOT: { id: houseId },
},
});
if (existingHouse) {
throw new ConflictError("Ce nom de maison est déjà utilisé");
}
updateData.name = data.name.trim();
}
if (data.description !== undefined) {
if (data.description && data.description.length > HOUSE_DESCRIPTION_MAX_LENGTH) {
throw new ValidationError(
`La description ne peut pas dépasser ${HOUSE_DESCRIPTION_MAX_LENGTH} caractères`,
"description"
);
}
updateData.description = data.description?.trim() || null;
}
return prisma.house.update({
where: { id: houseId },
data: updateData,
});
}
/**
* Supprime une maison
*/
async deleteHouse(houseId: string, userId: string): Promise<void> {
// Vérifier que l'utilisateur est propriétaire
const isOwner = await this.isUserOwner(userId, houseId);
if (!isOwner) {
throw new ForbiddenError(
"Seul le propriétaire peut supprimer la maison"
);
}
await prisma.house.delete({
where: { id: houseId },
});
}
/**
* Invite un utilisateur à rejoindre une maison
*/
async inviteUser(data: InviteUserInput): Promise<HouseInvitation> {
// Vérifier que l'inviteur est membre de la maison
const isMember = await this.isUserMemberOfHouse(
data.inviterId,
data.houseId
);
if (!isMember) {
throw new ForbiddenError(
"Vous devez être membre de la maison pour inviter quelqu'un"
);
}
// Vérifier que l'invité n'est pas déjà membre
const isAlreadyMember = await this.isUserMemberOfHouse(
data.inviteeId,
data.houseId
);
if (isAlreadyMember) {
throw new ConflictError("Cet utilisateur est déjà membre de la maison");
}
// Vérifier qu'il n'y a pas déjà une invitation en attente
const existingInvitation = await prisma.houseInvitation.findUnique({
where: {
houseId_inviteeId: {
houseId: data.houseId,
inviteeId: data.inviteeId,
},
},
});
if (existingInvitation && existingInvitation.status === "PENDING") {
throw new ConflictError(
"Une invitation est déjà en attente pour cet utilisateur"
);
}
// Vérifier que l'invité n'est pas déjà dans une autre maison
const existingMembership = await prisma.houseMembership.findFirst({
where: { userId: data.inviteeId },
});
if (existingMembership) {
throw new ConflictError(
"Cet utilisateur est déjà membre d'une autre maison"
);
}
// Créer l'invitation
return prisma.houseInvitation.create({
data: {
houseId: data.houseId,
inviterId: data.inviterId,
inviteeId: data.inviteeId,
status: "PENDING",
},
});
}
/**
* Accepte une invitation
*/
async acceptInvitation(
invitationId: string,
userId: string
): Promise<HouseMembership> {
const invitation = await prisma.houseInvitation.findUnique({
where: { id: invitationId },
});
if (!invitation) {
throw new NotFoundError("Invitation");
}
if (invitation.inviteeId !== userId) {
throw new ForbiddenError("Cette invitation ne vous est pas destinée");
}
if (invitation.status !== "PENDING") {
throw new ConflictError("Cette invitation n'est plus valide");
}
// Vérifier que l'utilisateur n'est pas déjà dans une maison
const existingMembership = await prisma.houseMembership.findFirst({
where: { userId },
});
if (existingMembership) {
throw new ConflictError(
"Vous êtes déjà membre d'une maison. Vous devez quitter votre maison actuelle avant d'accepter cette invitation."
);
}
// Créer le membership et mettre à jour l'invitation
return prisma.$transaction(async (tx) => {
const membership = await tx.houseMembership.create({
data: {
houseId: invitation.houseId,
userId: invitation.inviteeId,
role: "MEMBER",
},
});
await tx.houseInvitation.update({
where: { id: invitationId },
data: { status: "ACCEPTED" },
});
// Annuler toutes les autres invitations en attente pour cet utilisateur
await tx.houseInvitation.updateMany({
where: {
inviteeId: userId,
status: "PENDING",
id: { not: invitationId },
},
data: { status: "CANCELLED" },
});
// Annuler toutes les demandes en attente pour cet utilisateur
await tx.houseRequest.updateMany({
where: {
requesterId: userId,
status: "PENDING",
},
data: { status: "CANCELLED" },
});
return membership;
});
}
/**
* Refuse une invitation
*/
async rejectInvitation(invitationId: string, userId: string): Promise<void> {
const invitation = await prisma.houseInvitation.findUnique({
where: { id: invitationId },
});
if (!invitation) {
throw new NotFoundError("Invitation");
}
if (invitation.inviteeId !== userId) {
throw new ForbiddenError("Cette invitation ne vous est pas destinée");
}
if (invitation.status !== "PENDING") {
throw new ConflictError("Cette invitation n'est plus valide");
}
await prisma.houseInvitation.update({
where: { id: invitationId },
data: { status: "REJECTED" },
});
}
/**
* Annule une invitation (par l'inviteur)
*/
async cancelInvitation(invitationId: string, userId: string): Promise<void> {
const invitation = await prisma.houseInvitation.findUnique({
where: { id: invitationId },
});
if (!invitation) {
throw new NotFoundError("Invitation");
}
if (invitation.inviterId !== userId) {
throw new ForbiddenError(
"Vous ne pouvez annuler que vos propres invitations"
);
}
if (invitation.status !== "PENDING") {
throw new ConflictError("Cette invitation n'est plus valide");
}
await prisma.houseInvitation.update({
where: { id: invitationId },
data: { status: "CANCELLED" },
});
}
/**
* Demande à rejoindre une maison
*/
async requestToJoin(data: RequestToJoinInput): Promise<HouseRequest> {
// Vérifier que l'utilisateur n'est pas déjà membre
const isMember = await this.isUserMemberOfHouse(
data.requesterId,
data.houseId
);
if (isMember) {
throw new ConflictError("Vous êtes déjà membre de cette maison");
}
// Vérifier que l'utilisateur n'est pas déjà dans une autre maison
const existingMembership = await prisma.houseMembership.findFirst({
where: { userId: data.requesterId },
});
if (existingMembership) {
throw new ConflictError(
"Vous êtes déjà membre d'une maison. Vous devez quitter votre maison actuelle avant de faire une demande."
);
}
// Vérifier qu'il n'y a pas déjà une demande en attente
const existingRequest = await prisma.houseRequest.findUnique({
where: {
houseId_requesterId: {
houseId: data.houseId,
requesterId: data.requesterId,
},
},
});
if (existingRequest && existingRequest.status === "PENDING") {
throw new ConflictError(
"Une demande est déjà en attente pour cette maison"
);
}
// Créer la demande
return prisma.houseRequest.create({
data: {
houseId: data.houseId,
requesterId: data.requesterId,
status: "PENDING",
},
});
}
/**
* Accepte une demande d'adhésion
*/
async acceptRequest(
requestId: string,
userId: string
): Promise<HouseMembership> {
const request = await prisma.houseRequest.findUnique({
where: { id: requestId },
include: { house: true },
});
if (!request) {
throw new NotFoundError("Demande");
}
// Vérifier que l'utilisateur est propriétaire ou admin de la maison
const isAuthorized = await this.isUserOwnerOrAdmin(
userId,
request.houseId
);
if (!isAuthorized) {
throw new ForbiddenError(
"Vous n'avez pas les permissions pour accepter cette demande"
);
}
if (request.status !== "PENDING") {
throw new ConflictError("Cette demande n'est plus valide");
}
// Vérifier que le demandeur n'est pas déjà dans une maison
const existingMembership = await prisma.houseMembership.findFirst({
where: { userId: request.requesterId },
});
if (existingMembership) {
throw new ConflictError(
"Cet utilisateur est déjà membre d'une autre maison"
);
}
// Créer le membership et mettre à jour la demande
return prisma.$transaction(async (tx) => {
const membership = await tx.houseMembership.create({
data: {
houseId: request.houseId,
userId: request.requesterId,
role: "MEMBER",
},
});
await tx.houseRequest.update({
where: { id: requestId },
data: { status: "ACCEPTED" },
});
// Annuler toutes les autres demandes en attente pour cet utilisateur
await tx.houseRequest.updateMany({
where: {
requesterId: request.requesterId,
status: "PENDING",
id: { not: requestId },
},
data: { status: "CANCELLED" },
});
// Annuler toutes les invitations en attente pour cet utilisateur
await tx.houseInvitation.updateMany({
where: {
inviteeId: request.requesterId,
status: "PENDING",
},
data: { status: "CANCELLED" },
});
return membership;
});
}
/**
* Refuse une demande d'adhésion
*/
async rejectRequest(requestId: string, userId: string): Promise<void> {
const request = await prisma.houseRequest.findUnique({
where: { id: requestId },
});
if (!request) {
throw new NotFoundError("Demande");
}
// Vérifier que l'utilisateur est propriétaire ou admin de la maison
const isAuthorized = await this.isUserOwnerOrAdmin(
userId,
request.houseId
);
if (!isAuthorized) {
throw new ForbiddenError(
"Vous n'avez pas les permissions pour refuser cette demande"
);
}
if (request.status !== "PENDING") {
throw new ConflictError("Cette demande n'est plus valide");
}
await prisma.houseRequest.update({
where: { id: requestId },
data: { status: "REJECTED" },
});
}
/**
* Annule une demande (par le demandeur)
*/
async cancelRequest(requestId: string, userId: string): Promise<void> {
const request = await prisma.houseRequest.findUnique({
where: { id: requestId },
});
if (!request) {
throw new NotFoundError("Demande");
}
if (request.requesterId !== userId) {
throw new ForbiddenError(
"Vous ne pouvez annuler que vos propres demandes"
);
}
if (request.status !== "PENDING") {
throw new ConflictError("Cette demande n'est plus valide");
}
await prisma.houseRequest.update({
where: { id: requestId },
data: { status: "CANCELLED" },
});
}
/**
* Quitte une maison
*/
async leaveHouse(houseId: string, userId: string): Promise<void> {
const membership = await prisma.houseMembership.findUnique({
where: {
houseId_userId: {
houseId,
userId,
},
},
});
if (!membership) {
throw new NotFoundError("Membre");
}
// Le propriétaire ne peut pas quitter sa maison
if (membership.role === "OWNER") {
throw new ForbiddenError(
"Le propriétaire ne peut pas quitter sa maison. Vous devez d'abord transférer la propriété ou supprimer la maison."
);
}
await prisma.houseMembership.delete({
where: {
houseId_userId: {
houseId,
userId,
},
},
});
}
/**
* Récupère les invitations reçues par un utilisateur
*/
async getUserInvitations(
userId: string,
status?: InvitationStatus
): Promise<HouseInvitation[]> {
return prisma.houseInvitation.findMany({
where: {
inviteeId: userId,
...(status && { status }),
},
include: {
house: true,
inviter: {
select: {
id: true,
username: true,
avatar: true,
},
},
},
orderBy: { createdAt: "desc" },
});
}
/**
* Récupère les demandes d'une maison
*/
async getHouseRequests(
houseId: string,
status?: RequestStatus
): Promise<HouseRequest[]> {
return prisma.houseRequest.findMany({
where: {
houseId,
...(status && { status }),
},
include: {
requester: {
select: {
id: true,
username: true,
avatar: true,
},
},
},
orderBy: { createdAt: "desc" },
});
}
/**
* Récupère les invitations envoyées par une maison
*/
async getHouseInvitations(
houseId: string,
status?: InvitationStatus
): Promise<HouseInvitation[]> {
return prisma.houseInvitation.findMany({
where: {
houseId,
...(status && { status }),
},
include: {
invitee: {
select: {
id: true,
username: true,
avatar: true,
},
},
inviter: {
select: {
id: true,
username: true,
avatar: true,
},
},
},
orderBy: { createdAt: "desc" },
});
}
/**
* Récupère une invitation par son ID (avec seulement houseId)
*/
async getInvitationById(
id: string
): Promise<{ houseId: string } | null> {
return prisma.houseInvitation.findUnique({
where: { id },
select: { houseId: true },
});
}
}
export const houseService = new HouseService();

View File

@@ -27,6 +27,26 @@ export interface LeaderboardEntry {
characterClass: CharacterClass | null; characterClass: CharacterClass | null;
} }
export interface HouseMember {
id: string;
username: string;
avatar: string | null;
score: number;
level: number;
role: string;
}
export interface HouseLeaderboardEntry {
rank: number;
houseId: string;
houseName: string;
totalScore: number;
memberCount: number;
averageScore: number;
description: string | null;
members: HouseMember[];
}
/** /**
* Service de gestion des statistiques utilisateur * Service de gestion des statistiques utilisateur
*/ */
@@ -64,6 +84,72 @@ export class UserStatsService {
})); }));
} }
/**
* Récupère le leaderboard par maison
*/
async getHouseLeaderboard(limit: number = 10): Promise<HouseLeaderboardEntry[]> {
// Récupérer toutes les maisons avec leurs membres et leurs scores
const houses = await prisma.house.findMany({
include: {
memberships: {
include: {
user: {
select: {
id: true,
username: true,
avatar: true,
score: true,
level: true,
},
},
},
orderBy: [
{ role: "asc" }, // OWNER, ADMIN, MEMBER
{ user: { score: "desc" } }, // Puis par score décroissant
],
},
},
});
// Calculer le score total et la moyenne pour chaque maison
const houseStats = houses
.map((house) => {
const memberScores = house.memberships.map((m) => m.user.score);
const totalScore = memberScores.reduce((sum, score) => sum + score, 0);
const memberCount = house.memberships.length;
const averageScore = memberCount > 0 ? Math.floor(totalScore / memberCount) : 0;
// Mapper les membres avec leurs détails
const members: HouseMember[] = house.memberships.map((membership) => ({
id: membership.user.id,
username: membership.user.username,
avatar: membership.user.avatar,
score: membership.user.score,
level: membership.user.level,
role: membership.role,
}));
return {
houseId: house.id,
houseName: house.name,
totalScore,
memberCount,
averageScore,
description: house.description,
members,
};
})
.filter((house) => house.memberCount > 0) // Exclure les maisons sans membres
.sort((a, b) => b.totalScore - a.totalScore) // Trier par score total décroissant
.slice(0, limit) // Limiter le nombre de résultats
.map((house, index) => ({
rank: index + 1,
...house,
}));
return houseStats;
}
/** /**
* Met à jour les statistiques d'un utilisateur * Met à jour les statistiques d'un utilisateur
*/ */