Compare commits
2 Commits
2c7a346cde
...
85ee812ab1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85ee812ab1 | ||
|
|
cb02b494f4 |
30
.env
30
.env
@@ -5,8 +5,28 @@
|
|||||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
||||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||||
|
|
||||||
DATABASE_URL="file:./data/dev.db"
|
# DATABASE_URL="file:./data/dev.db"
|
||||||
AUTH_SECRET="your-secret-key-change-this-in-production"
|
# AUTH_SECRET="your-secret-key-change-this-in-production"
|
||||||
AUTH_URL="http://localhost:3000"
|
# AUTH_URL="http://localhost:3000"
|
||||||
PRISMA_DATA_PATH="/Users/julien.froidefond/Sites/DAIS/public/got-gaming/data"
|
# PRISMA_DATA_PATH="/Users/julien.froidefond/Sites/DAIS/public/got-gaming/data"
|
||||||
UPLOADS_PATH="/Users/julien.froidefond/Sites/DAIS/public/got-gaming/public/uploads"
|
# UPLOADS_PATH="/Users/julien.froidefond/Sites/DAIS/public/got-gaming/public/uploads"
|
||||||
|
|
||||||
|
# NextAuth Configuration
|
||||||
|
NEXTAUTH_SECRET=change-this-secret-in-production
|
||||||
|
NEXTAUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# PostgreSQL Configuration
|
||||||
|
POSTGRES_USER=gotgaming
|
||||||
|
POSTGRES_PASSWORD=change-this-in-production
|
||||||
|
POSTGRES_DB=gotgaming
|
||||||
|
POSTGRES_HOST=localhost
|
||||||
|
POSTGRES_PORT=5433
|
||||||
|
|
||||||
|
# Database URL (construite automatiquement si non définie)
|
||||||
|
# Si vous définissez cette variable, elle sera utilisée telle quelle
|
||||||
|
# Sinon, elle sera construite à partir des variables POSTGRES_* ci-dessus
|
||||||
|
# DATABASE_URL=postgresql://gotgaming:change-this-in-production@got-postgres:5432/gotgaming?schema=public
|
||||||
|
|
||||||
|
# Docker Volumes (optionnel)
|
||||||
|
POSTGRES_DATA_PATH=./data/postgres
|
||||||
|
UPLOADS_PATH=./public/uploads
|
||||||
|
|||||||
19
.env.example
Normal file
19
.env.example
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# NextAuth Configuration
|
||||||
|
NEXTAUTH_SECRET=change-this-secret-in-production
|
||||||
|
NEXTAUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# PostgreSQL Configuration
|
||||||
|
POSTGRES_USER=gotgaming
|
||||||
|
POSTGRES_PASSWORD=change-this-in-production
|
||||||
|
POSTGRES_DB=gotgaming
|
||||||
|
POSTGRES_HOST=got-postgres
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
|
||||||
|
# Database URL (construite automatiquement si non définie)
|
||||||
|
# Si vous définissez cette variable, elle sera utilisée telle quelle
|
||||||
|
# Sinon, elle sera construite à partir des variables POSTGRES_* ci-dessus
|
||||||
|
# DATABASE_URL=postgresql://gotgaming:change-this-in-production@got-postgres:5432/gotgaming?schema=public
|
||||||
|
|
||||||
|
# Docker Volumes (optionnel)
|
||||||
|
POSTGRES_DATA_PATH=./data/postgres
|
||||||
|
UPLOADS_PATH=./public/uploads
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,6 +25,7 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
|
.env
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
|
|||||||
@@ -24,17 +24,36 @@ docker-compose logs -f
|
|||||||
|
|
||||||
## Variables d'environnement
|
## Variables d'environnement
|
||||||
|
|
||||||
Créez un fichier `.env` à la racine du projet avec les variables suivantes :
|
Créez un fichier `.env` à la racine du projet à partir du template `.env.example` :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Puis modifiez les valeurs dans `.env` selon votre configuration :
|
||||||
|
|
||||||
```env
|
```env
|
||||||
|
# NextAuth Configuration
|
||||||
NEXTAUTH_SECRET=your-secret-key-here
|
NEXTAUTH_SECRET=your-secret-key-here
|
||||||
NEXTAUTH_URL=http://localhost:3000
|
NEXTAUTH_URL=http://localhost:3000
|
||||||
|
|
||||||
|
# PostgreSQL Configuration
|
||||||
POSTGRES_USER=gotgaming
|
POSTGRES_USER=gotgaming
|
||||||
POSTGRES_PASSWORD=change-this-in-production
|
POSTGRES_PASSWORD=change-this-in-production
|
||||||
POSTGRES_DB=gotgaming
|
POSTGRES_DB=gotgaming
|
||||||
DATABASE_URL=postgresql://gotgaming:change-this-in-production@got-postgres:5432/gotgaming?schema=public
|
|
||||||
|
# Database URL (optionnel - construite automatiquement si non définie)
|
||||||
|
# DATABASE_URL=postgresql://gotgaming:change-this-in-production@got-postgres:5432/gotgaming?schema=public
|
||||||
|
|
||||||
|
# Docker Volumes (optionnel)
|
||||||
|
POSTGRES_DATA_PATH=./data/postgres
|
||||||
|
UPLOADS_PATH=./public/uploads
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Important** :
|
||||||
|
- Le fichier `.env` est ignoré par Git (ne pas commiter vos secrets)
|
||||||
|
- Si vous changez `POSTGRES_PASSWORD` après la première initialisation, vous devrez soit réinitialiser la base, soit changer le mot de passe manuellement dans PostgreSQL
|
||||||
|
|
||||||
## Volumes persistants
|
## Volumes persistants
|
||||||
|
|
||||||
### Base de données PostgreSQL
|
### Base de données PostgreSQL
|
||||||
|
|||||||
48
actions/houses/create.ts
Normal file
48
actions/houses/create.ts
Normal 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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
173
actions/houses/invitations.ts
Normal file
173
actions/houses/invitations.ts
Normal 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
163
actions/houses/requests.ts
Normal 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
114
actions/houses/update.ts
Normal 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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
51
app/api/houses/[houseId]/invitations/route.ts
Normal file
51
app/api/houses/[houseId]/invitations/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
48
app/api/houses/[houseId]/requests/route.ts
Normal file
48
app/api/houses/[houseId]/requests/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
59
app/api/houses/[houseId]/route.ts
Normal file
59
app/api/houses/[houseId]/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
48
app/api/houses/my-house/route.ts
Normal file
48
app/api/houses/my-house/route.ts
Normal 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
87
app/api/houses/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
36
app/api/invitations/route.ts
Normal file
36
app/api/invitations/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
17
app/api/leaderboard/houses/route.ts
Normal file
17
app/api/leaderboard/houses/route.ts
Normal 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
146
app/houses/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
167
components/houses/HouseCard.tsx
Normal file
167
components/houses/HouseCard.tsx
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import Card from "@/components/ui/Card";
|
||||||
|
import Button from "@/components/ui/Button";
|
||||||
|
import Avatar from "@/components/ui/Avatar";
|
||||||
|
import { requestToJoin } from "@/actions/houses/requests";
|
||||||
|
import { useTransition } from "react";
|
||||||
|
import Alert from "@/components/ui/Alert";
|
||||||
|
|
||||||
|
interface House {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
creator: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
avatar: string | null;
|
||||||
|
};
|
||||||
|
memberships?: Array<{
|
||||||
|
id: string;
|
||||||
|
role: string;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
avatar: string | null;
|
||||||
|
score?: number;
|
||||||
|
level?: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
_count?: {
|
||||||
|
memberships: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HouseCardProps {
|
||||||
|
house: House;
|
||||||
|
onRequestSent?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HouseCard({ house, onRequestSent }: HouseCardProps) {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const isMember = house.memberships?.some(
|
||||||
|
(m) => m.user.id === session?.user?.id
|
||||||
|
);
|
||||||
|
const memberCount = house._count?.memberships || house.memberships?.length || 0;
|
||||||
|
|
||||||
|
const handleRequestToJoin = () => {
|
||||||
|
if (!session?.user?.id) return;
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
setSuccess(null);
|
||||||
|
|
||||||
|
startTransition(async () => {
|
||||||
|
const result = await requestToJoin(house.id);
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
setSuccess("Demande envoyée avec succès");
|
||||||
|
onRequestSent?.();
|
||||||
|
} else {
|
||||||
|
setError(result.error || "Erreur lors de l'envoi de la demande");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="p-4 sm:p-6">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4 mb-4">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="text-xl font-bold mb-2 break-words" style={{ color: "var(--foreground)" }}>
|
||||||
|
{house.name}
|
||||||
|
</h3>
|
||||||
|
{house.description && (
|
||||||
|
<p className="text-sm mb-2 break-words" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
{house.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-wrap items-center gap-2 sm:gap-4 text-xs" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
<span>Créée par {house.creator.username}</span>
|
||||||
|
<span className="hidden sm:inline">•</span>
|
||||||
|
<span>{memberCount} membre{memberCount > 1 ? "s" : ""}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Alert variant="error" className="mb-4">
|
||||||
|
{error}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{success && (
|
||||||
|
<Alert variant="success" className="mb-4">
|
||||||
|
{success}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{session?.user?.id && !isMember && (
|
||||||
|
<Button
|
||||||
|
onClick={handleRequestToJoin}
|
||||||
|
disabled={isPending}
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
className="w-full sm:w-auto"
|
||||||
|
>
|
||||||
|
{isPending ? "Envoi..." : "Demander à rejoindre"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isMember && (
|
||||||
|
<div className="text-xs mb-4" style={{ color: "var(--success)" }}>
|
||||||
|
✓ Vous êtes membre
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Members List */}
|
||||||
|
{house.memberships && house.memberships.length > 0 && (
|
||||||
|
<div className="mt-4 pt-4 border-t" style={{ borderColor: "var(--border)" }}>
|
||||||
|
<h4 className="text-xs font-bold uppercase tracking-wider mb-3" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
Membres ({house.memberships.length})
|
||||||
|
</h4>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{house.memberships.map((membership) => (
|
||||||
|
<div
|
||||||
|
key={membership.id}
|
||||||
|
className="flex items-center gap-2 p-2 rounded"
|
||||||
|
style={{ backgroundColor: "var(--card-hover)" }}
|
||||||
|
title={`${membership.user.username} (${membership.role})${membership.user.score !== undefined ? ` - ${membership.user.score} pts` : ""}`}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
src={membership.user.avatar}
|
||||||
|
username={membership.user.username}
|
||||||
|
size="sm"
|
||||||
|
className="flex-shrink-0"
|
||||||
|
borderClassName="border-pixel-gold/30"
|
||||||
|
/>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-xs font-semibold truncate" style={{ color: "var(--foreground)" }}>
|
||||||
|
{membership.user.username}
|
||||||
|
</span>
|
||||||
|
{membership.role === "OWNER" && (
|
||||||
|
<span className="text-[10px] uppercase" style={{ color: "var(--accent)" }}>
|
||||||
|
👑
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{membership.user.score !== undefined && membership.user.level !== undefined && (
|
||||||
|
<div className="text-[10px]" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
{membership.user.score} pts • Lv.{membership.user.level}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
90
components/houses/HouseForm.tsx
Normal file
90
components/houses/HouseForm.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
327
components/houses/HouseManagement.tsx
Normal file
327
components/houses/HouseManagement.tsx
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useTransition } from "react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import Card from "@/components/ui/Card";
|
||||||
|
import Button from "@/components/ui/Button";
|
||||||
|
import SectionTitle from "@/components/ui/SectionTitle";
|
||||||
|
import HouseForm from "./HouseForm";
|
||||||
|
import RequestList from "./RequestList";
|
||||||
|
import Alert from "@/components/ui/Alert";
|
||||||
|
import { deleteHouse, leaveHouse } from "@/actions/houses/update";
|
||||||
|
import { inviteUser } from "@/actions/houses/invitations";
|
||||||
|
|
||||||
|
interface House {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string | null;
|
||||||
|
creator: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
avatar: string | null;
|
||||||
|
};
|
||||||
|
memberships?: Array<{
|
||||||
|
id: string;
|
||||||
|
role: string;
|
||||||
|
user: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
avatar: string | null;
|
||||||
|
score?: number;
|
||||||
|
level?: number;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface User {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
avatar: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HouseManagementProps {
|
||||||
|
house: House | null;
|
||||||
|
users?: User[];
|
||||||
|
requests?: Array<{
|
||||||
|
id: string;
|
||||||
|
requester: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
avatar: string | null;
|
||||||
|
};
|
||||||
|
status: string;
|
||||||
|
createdAt: string;
|
||||||
|
}>;
|
||||||
|
onUpdate?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Request {
|
||||||
|
id: string;
|
||||||
|
requester: {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
avatar: string | null;
|
||||||
|
};
|
||||||
|
status: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HouseManagement({
|
||||||
|
house,
|
||||||
|
users = [],
|
||||||
|
requests: initialRequests = [],
|
||||||
|
onUpdate,
|
||||||
|
}: HouseManagementProps) {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
|
const [showInviteForm, setShowInviteForm] = useState(false);
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState("");
|
||||||
|
const [requests, setRequests] = useState<Request[]>(initialRequests);
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const userRole = house?.memberships?.find(
|
||||||
|
(m) => m.user.id === session?.user?.id
|
||||||
|
)?.role;
|
||||||
|
const isOwner = userRole === "OWNER";
|
||||||
|
const isAdmin = userRole === "ADMIN" || isOwner;
|
||||||
|
const pendingRequests = requests.filter((r) => r.status === "PENDING");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchRequests = async () => {
|
||||||
|
if (!house || !isAdmin) return;
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/houses/${house.id}/requests?status=PENDING`);
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
setRequests(data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching requests:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchRequests();
|
||||||
|
}, [house?.id, isAdmin]);
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (!house || !confirm("Êtes-vous sûr de vouloir supprimer cette maison ?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
startTransition(async () => {
|
||||||
|
const result = await deleteHouse(house.id);
|
||||||
|
if (result.success) {
|
||||||
|
onUpdate?.();
|
||||||
|
} else {
|
||||||
|
setError(result.error || "Erreur lors de la suppression");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLeave = () => {
|
||||||
|
if (!house || !confirm("Êtes-vous sûr de vouloir quitter cette maison ?")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
startTransition(async () => {
|
||||||
|
const result = await leaveHouse(house.id);
|
||||||
|
if (result.success) {
|
||||||
|
onUpdate?.();
|
||||||
|
} else {
|
||||||
|
setError(result.error || "Erreur lors de la sortie");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInvite = () => {
|
||||||
|
if (!house || !selectedUserId) return;
|
||||||
|
|
||||||
|
setError(null);
|
||||||
|
setSuccess(null);
|
||||||
|
|
||||||
|
startTransition(async () => {
|
||||||
|
const result = await inviteUser(house.id, selectedUserId);
|
||||||
|
if (result.success) {
|
||||||
|
setSuccess("Invitation envoyée");
|
||||||
|
setShowInviteForm(false);
|
||||||
|
setSelectedUserId("");
|
||||||
|
onUpdate?.();
|
||||||
|
} else {
|
||||||
|
setError(result.error || "Erreur lors de l'envoi de l'invitation");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const availableUsers = users.filter(
|
||||||
|
(u) =>
|
||||||
|
u.id !== session?.user?.id &&
|
||||||
|
!house?.memberships?.some((m) => m.user.id === u.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!house) {
|
||||||
|
return (
|
||||||
|
<Card className="p-6">
|
||||||
|
<SectionTitle>Ma Maison</SectionTitle>
|
||||||
|
<p className="text-sm mb-4" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
Vous n'êtes membre d'aucune maison pour le moment.
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card className="p-4 sm:p-6">
|
||||||
|
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4 mb-4">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<SectionTitle>{house.name}</SectionTitle>
|
||||||
|
{house.description && (
|
||||||
|
<p className="text-sm mt-2 break-words" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
{house.description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-2 sm:flex-nowrap">
|
||||||
|
{isAdmin && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
onClick={() => setIsEditing(!isEditing)}
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
className="flex-1 sm:flex-none"
|
||||||
|
>
|
||||||
|
{isEditing ? "Annuler" : "Modifier"}
|
||||||
|
</Button>
|
||||||
|
{isOwner && (
|
||||||
|
<Button onClick={handleDelete} variant="danger" size="sm" className="flex-1 sm:flex-none">
|
||||||
|
Supprimer
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!isOwner && (
|
||||||
|
<Button onClick={handleLeave} variant="danger" size="sm" className="flex-1 sm:flex-none">
|
||||||
|
Quitter
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error && <Alert variant="error" className="mb-4">{error}</Alert>}
|
||||||
|
{success && <Alert variant="success" className="mb-4">{success}</Alert>}
|
||||||
|
|
||||||
|
{isEditing ? (
|
||||||
|
<HouseForm
|
||||||
|
house={house}
|
||||||
|
onSuccess={() => {
|
||||||
|
setIsEditing(false);
|
||||||
|
onUpdate?.();
|
||||||
|
}}
|
||||||
|
onCancel={() => setIsEditing(false)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<h3 className="font-bold mb-3" style={{ color: "var(--foreground)" }}>
|
||||||
|
Membres ({house.memberships?.length ?? 0})
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{(house.memberships || []).map((membership) => (
|
||||||
|
<div
|
||||||
|
key={membership.id}
|
||||||
|
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-2 rounded"
|
||||||
|
style={{ backgroundColor: "var(--card-hover)" }}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||||
|
{membership.user.avatar && (
|
||||||
|
<img
|
||||||
|
src={membership.user.avatar}
|
||||||
|
alt={membership.user.username}
|
||||||
|
className="w-8 h-8 rounded-full flex-shrink-0"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="min-w-0">
|
||||||
|
<span className="font-semibold block sm:inline" style={{ color: "var(--foreground)" }}>
|
||||||
|
{membership.user.username}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs block sm:inline sm:ml-2" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
({membership.user.score} pts - Niveau {membership.user.level})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs uppercase flex-shrink-0" style={{ color: "var(--accent)" }}>
|
||||||
|
{membership.role}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isAdmin && (
|
||||||
|
<div className="mt-4">
|
||||||
|
{showInviteForm ? (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<select
|
||||||
|
value={selectedUserId}
|
||||||
|
onChange={(e) => setSelectedUserId(e.target.value)}
|
||||||
|
className="w-full p-2 rounded border"
|
||||||
|
style={{
|
||||||
|
backgroundColor: "var(--input)",
|
||||||
|
borderColor: "var(--border)",
|
||||||
|
color: "var(--foreground)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="">Sélectionner un utilisateur</option>
|
||||||
|
{availableUsers.map((user) => (
|
||||||
|
<option key={user.id} value={user.id}>
|
||||||
|
{user.username}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleInvite}
|
||||||
|
disabled={!selectedUserId || isPending}
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
{isPending ? "Envoi..." : "Inviter"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setShowInviteForm(false);
|
||||||
|
setSelectedUserId("");
|
||||||
|
}}
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Annuler
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
onClick={() => setShowInviteForm(true)}
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Inviter un utilisateur
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{isAdmin && pendingRequests.length > 0 && (
|
||||||
|
<Card className="p-4 sm:p-6">
|
||||||
|
<SectionTitle>Demandes d'adhésion</SectionTitle>
|
||||||
|
<RequestList requests={pendingRequests} onUpdate={onUpdate} />
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
236
components/houses/HousesSection.tsx
Normal file
236
components/houses/HousesSection.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
123
components/houses/InvitationList.tsx
Normal file
123
components/houses/InvitationList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
119
components/houses/RequestList.tsx
Normal file
119
components/houses/RequestList.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ services:
|
|||||||
- "3040:3000"
|
- "3040:3000"
|
||||||
environment:
|
environment:
|
||||||
- NODE_ENV=production
|
- NODE_ENV=production
|
||||||
|
- POSTGRES_USER=${POSTGRES_USER:-gotgaming}
|
||||||
|
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-change-this-in-production}
|
||||||
|
- POSTGRES_DB=${POSTGRES_DB:-gotgaming}
|
||||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-gotgaming}:${POSTGRES_PASSWORD:-change-this-in-production}@got-postgres:5432/${POSTGRES_DB:-gotgaming}?schema=public
|
- DATABASE_URL=postgresql://${POSTGRES_USER:-gotgaming}:${POSTGRES_PASSWORD:-change-this-in-production}@got-postgres:5432/${POSTGRES_DB:-gotgaming}?schema=public
|
||||||
- NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000}
|
- NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000}
|
||||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-change-this-secret-in-production}
|
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-change-this-secret-in-production}
|
||||||
|
|||||||
@@ -2,8 +2,31 @@ import { PrismaClient } from "@/prisma/generated/prisma/client";
|
|||||||
import { PrismaPg } from "@prisma/adapter-pg";
|
import { PrismaPg } from "@prisma/adapter-pg";
|
||||||
import { Pool } from "pg";
|
import { Pool } from "pg";
|
||||||
|
|
||||||
|
// Construire DATABASE_URL si elle n'est pas définie, en utilisant les variables individuelles
|
||||||
|
let databaseUrl = process.env.DATABASE_URL;
|
||||||
|
|
||||||
|
if (!databaseUrl) {
|
||||||
|
const user = process.env.POSTGRES_USER || "gotgaming";
|
||||||
|
const password = process.env.POSTGRES_PASSWORD || "change-this-in-production";
|
||||||
|
const host = process.env.POSTGRES_HOST || "got-postgres";
|
||||||
|
const port = process.env.POSTGRES_PORT || "5432";
|
||||||
|
const db = process.env.POSTGRES_DB || "gotgaming";
|
||||||
|
|
||||||
|
// Encoder le mot de passe pour l'URL
|
||||||
|
const encodedPassword = encodeURIComponent(password);
|
||||||
|
databaseUrl = `postgresql://${user}:${encodedPassword}@${host}:${port}/${db}?schema=public`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof databaseUrl !== "string") {
|
||||||
|
throw new Error("DATABASE_URL must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger l'URL de connexion (masquer le mot de passe pour la sécurité)
|
||||||
|
const logUrl = databaseUrl.replace(/:\/\/[^:]+:[^@]+@/, "://***:***@");
|
||||||
|
console.log(`[Prisma] Connecting to PostgreSQL: ${logUrl}`);
|
||||||
|
|
||||||
const pool = new Pool({
|
const pool = new Pool({
|
||||||
connectionString: process.env.DATABASE_URL,
|
connectionString: databaseUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
const adapter = new PrismaPg(pool);
|
const adapter = new PrismaPg(pool);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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 */
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
120
prisma/migrations/20251217131946_add_houses_system/migration.sql
Normal file
120
prisma/migrations/20251217131946_add_houses_system/migration.sql
Normal 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;
|
||||||
|
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
174
prisma/seed.ts
174
prisma/seed.ts
@@ -7,8 +7,35 @@ import { PrismaPg } from "@prisma/adapter-pg";
|
|||||||
import { Pool } from "pg";
|
import { Pool } from "pg";
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
|
|
||||||
|
// Construire DATABASE_URL si elle n'est pas définie (même logique que lib/prisma.ts)
|
||||||
|
let databaseUrl = process.env.DATABASE_URL;
|
||||||
|
|
||||||
|
if (!databaseUrl) {
|
||||||
|
const user = process.env.POSTGRES_USER || "gotgaming";
|
||||||
|
const password = process.env.POSTGRES_PASSWORD || "change-this-in-production";
|
||||||
|
// Si on est dans Docker, utiliser le nom du service, sinon localhost avec le port externe
|
||||||
|
const host =
|
||||||
|
process.env.POSTGRES_HOST ||
|
||||||
|
(process.env.DOCKER_ENV ? "got-postgres" : "localhost");
|
||||||
|
const port =
|
||||||
|
process.env.POSTGRES_PORT || (process.env.DOCKER_ENV ? "5432" : "5433");
|
||||||
|
const db = process.env.POSTGRES_DB || "gotgaming";
|
||||||
|
|
||||||
|
// Encoder le mot de passe pour l'URL
|
||||||
|
const encodedPassword = encodeURIComponent(password);
|
||||||
|
databaseUrl = `postgresql://${user}:${encodedPassword}@${host}:${port}/${db}?schema=public`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof databaseUrl !== "string") {
|
||||||
|
throw new Error("DATABASE_URL must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger l'URL de connexion (masquer le mot de passe pour la sécurité)
|
||||||
|
const logUrl = databaseUrl.replace(/:\/\/[^:]+:[^@]+@/, "://***:***@");
|
||||||
|
console.log(`[Seed] Connecting to PostgreSQL: ${logUrl}`);
|
||||||
|
|
||||||
const pool = new Pool({
|
const pool = new Pool({
|
||||||
connectionString: process.env.DATABASE_URL,
|
connectionString: databaseUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
const adapter = new PrismaPg(pool);
|
const adapter = new PrismaPg(pool);
|
||||||
@@ -211,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()
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
893
services/houses/house.service.ts
Normal file
893
services/houses/house.service.ts
Normal 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();
|
||||||
|
|
||||||
@@ -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
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user