Compare commits
20 Commits
2c7a346cde
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c47bf916c | ||
|
|
9bcafe54d3 | ||
|
|
14c767cfc0 | ||
|
|
82069c74bc | ||
|
|
a062f5573b | ||
|
|
6e7c5d3eaf | ||
|
|
5dc178543e | ||
|
|
881b8149e5 | ||
|
|
d6a1e21e9f | ||
|
|
0b56d625ec | ||
|
|
f5dab3cb95 | ||
|
|
1b82bd9ee6 | ||
|
|
12bc44e3ac | ||
|
|
4a415f79e0 | ||
|
|
a62e61a314 | ||
|
|
91460930a4 | ||
|
|
fdedc1cf65 | ||
|
|
4fcf34c9aa | ||
|
|
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.
|
||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
||||
|
||||
DATABASE_URL="file:./data/dev.db"
|
||||
AUTH_SECRET="your-secret-key-change-this-in-production"
|
||||
AUTH_URL="http://localhost:3000"
|
||||
PRISMA_DATA_PATH="/Users/julien.froidefond/Sites/DAIS/public/got-gaming/data"
|
||||
UPLOADS_PATH="/Users/julien.froidefond/Sites/DAIS/public/got-gaming/public/uploads"
|
||||
# DATABASE_URL="file:./data/dev.db"
|
||||
# AUTH_SECRET="your-secret-key-change-this-in-production"
|
||||
# AUTH_URL="http://localhost:3000"
|
||||
# PRISMA_DATA_PATH="/Users/julien.froidefond/Sites/DAIS/public/got-gaming/data"
|
||||
# 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@localhost:5433/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*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
|
||||
30
Dockerfile
30
Dockerfile
@@ -19,7 +19,9 @@ RUN corepack enable && corepack prepare pnpm@latest --activate
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
|
||||
ENV DATABASE_URL="postgresql://user:pass@localhost:5432/db"
|
||||
# ARG pour DATABASE_URL au build (valeur factice par défaut, car prisma generate n'a pas besoin de vraie DB)
|
||||
ARG DATABASE_URL_BUILD="postgresql://user:pass@localhost:5432/db"
|
||||
ENV DATABASE_URL=$DATABASE_URL_BUILD
|
||||
RUN pnpm prisma generate
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
@@ -45,15 +47,20 @@ COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||
COPY --from=builder /app/package.json ./package.json
|
||||
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
COPY --from=builder /app/next.config.js ./next.config.js
|
||||
COPY --from=builder /app/prisma ./prisma
|
||||
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
|
||||
|
||||
ENV DATABASE_URL="postgresql://user:pass@localhost:5432/db"
|
||||
# Copier le répertoire prisma complet (schema + migrations)
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
|
||||
# Copier prisma.config.ts (nécessaire pour Prisma 7)
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/prisma.config.ts ./prisma.config.ts
|
||||
|
||||
# Installer seulement les dépendances de production puis générer Prisma Client
|
||||
# ARG pour DATABASE_URL au build (valeur factice par défaut, car prisma generate n'a pas besoin de vraie DB)
|
||||
# Au runtime, DATABASE_URL sera définie par docker-compose.yml (voir ligne 41)
|
||||
ARG DATABASE_URL_BUILD="postgresql://user:pass@localhost:5432/db"
|
||||
ENV DATABASE_URL=$DATABASE_URL_BUILD
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
|
||||
pnpm install --frozen-lockfile --prod && \
|
||||
pnpm dlx prisma generate
|
||||
# Ne pas définir ENV DATABASE_URL ici - elle sera définie par docker-compose.yml au runtime
|
||||
|
||||
# Create uploads directories
|
||||
RUN mkdir -p /app/public/uploads /app/public/uploads/backgrounds && \
|
||||
@@ -63,7 +70,18 @@ RUN echo '#!/bin/sh' > /app/entrypoint.sh && \
|
||||
echo 'set -e' >> /app/entrypoint.sh && \
|
||||
echo 'mkdir -p /app/public/uploads' >> /app/entrypoint.sh && \
|
||||
echo 'mkdir -p /app/public/uploads/backgrounds' >> /app/entrypoint.sh && \
|
||||
echo 'pnpm dlx prisma migrate deploy || true' >> /app/entrypoint.sh && \
|
||||
echo 'if [ -z "$DATABASE_URL" ]; then' >> /app/entrypoint.sh && \
|
||||
echo ' echo "ERROR: DATABASE_URL is not set"' >> /app/entrypoint.sh && \
|
||||
echo ' exit 1' >> /app/entrypoint.sh && \
|
||||
echo 'fi' >> /app/entrypoint.sh && \
|
||||
echo 'export DATABASE_URL' >> /app/entrypoint.sh && \
|
||||
echo 'cd /app' >> /app/entrypoint.sh && \
|
||||
echo 'echo "Applying migrations..."' >> /app/entrypoint.sh && \
|
||||
echo 'if ! pnpm dlx prisma migrate deploy; then' >> /app/entrypoint.sh && \
|
||||
echo ' echo "Migration failed. Attempting to resolve failed migration..."' >> /app/entrypoint.sh && \
|
||||
echo ' pnpm dlx prisma migrate resolve --applied 20251217101717_init_postgres 2>/dev/null || true' >> /app/entrypoint.sh && \
|
||||
echo ' pnpm dlx prisma migrate deploy || echo "WARNING: Some migrations may need manual resolution"' >> /app/entrypoint.sh && \
|
||||
echo 'fi' >> /app/entrypoint.sh && \
|
||||
echo 'exec pnpm start' >> /app/entrypoint.sh && \
|
||||
chmod +x /app/entrypoint.sh && \
|
||||
chown nextjs:nodejs /app/entrypoint.sh
|
||||
|
||||
@@ -24,17 +24,36 @@ docker-compose logs -f
|
||||
|
||||
## 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
|
||||
# NextAuth Configuration
|
||||
NEXTAUTH_SECRET=your-secret-key-here
|
||||
NEXTAUTH_URL=http://localhost:3000
|
||||
|
||||
# PostgreSQL Configuration
|
||||
POSTGRES_USER=gotgaming
|
||||
POSTGRES_PASSWORD=change-this-in-production
|
||||
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
|
||||
|
||||
### Base de données PostgreSQL
|
||||
|
||||
139
actions/admin/houses.ts
Normal file
139
actions/admin/houses.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { houseService } from "@/services/houses/house.service";
|
||||
import { Role } from "@/prisma/generated/prisma/client";
|
||||
import {
|
||||
ValidationError,
|
||||
NotFoundError,
|
||||
ConflictError,
|
||||
ForbiddenError,
|
||||
} from "@/services/errors";
|
||||
|
||||
function checkAdminAccess() {
|
||||
return async () => {
|
||||
const session = await auth();
|
||||
if (!session?.user || session.user.role !== Role.ADMIN) {
|
||||
throw new Error("Accès refusé");
|
||||
}
|
||||
return session;
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateHouse(
|
||||
houseId: string,
|
||||
data: {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
}
|
||||
) {
|
||||
try {
|
||||
await checkAdminAccess()();
|
||||
|
||||
// L'admin peut modifier n'importe quelle maison sans vérifier les permissions normales
|
||||
// On utilise directement le service mais on bypass les vérifications de propriétaire/admin
|
||||
const house = await houseService.getHouseById(houseId);
|
||||
if (!house) {
|
||||
return { success: false, error: "Maison non trouvée" };
|
||||
}
|
||||
|
||||
// Utiliser le service avec le creatorId pour bypass les vérifications
|
||||
const updatedHouse = await houseService.updateHouse(
|
||||
houseId,
|
||||
house.creatorId, // Utiliser le creatorId pour bypass
|
||||
data
|
||||
);
|
||||
|
||||
revalidatePath("/admin");
|
||||
revalidatePath("/houses");
|
||||
|
||||
return { success: true, data: updatedHouse };
|
||||
} catch (error) {
|
||||
console.error("Error updating house:", error);
|
||||
|
||||
if (error instanceof ValidationError) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
if (error instanceof ConflictError) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
if (error instanceof Error && error.message === "Accès refusé") {
|
||||
return { success: false, error: "Accès refusé" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Erreur lors de la mise à jour de la maison",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteHouse(houseId: string) {
|
||||
try {
|
||||
await checkAdminAccess()();
|
||||
|
||||
const house = await houseService.getHouseById(houseId);
|
||||
if (!house) {
|
||||
return { success: false, error: "Maison non trouvée" };
|
||||
}
|
||||
|
||||
// L'admin peut supprimer n'importe quelle maison
|
||||
// On utilise le creatorId pour bypass les vérifications
|
||||
await houseService.deleteHouse(houseId, house.creatorId);
|
||||
|
||||
revalidatePath("/admin");
|
||||
revalidatePath("/houses");
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("Error deleting house:", error);
|
||||
|
||||
if (error instanceof NotFoundError) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
if (error instanceof ForbiddenError) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
if (error instanceof Error && error.message === "Accès refusé") {
|
||||
return { success: false, error: "Accès refusé" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Erreur lors de la suppression de la maison",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeMember(houseId: string, memberId: string) {
|
||||
try {
|
||||
await checkAdminAccess()();
|
||||
|
||||
// L'admin peut retirer n'importe quel membre (sauf le propriétaire)
|
||||
await houseService.removeMemberAsAdmin(houseId, memberId);
|
||||
|
||||
revalidatePath("/admin");
|
||||
revalidatePath("/houses");
|
||||
|
||||
return { success: true, message: "Membre retiré de la maison" };
|
||||
} catch (error) {
|
||||
console.error("Error removing member:", error);
|
||||
|
||||
if (error instanceof NotFoundError) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
if (error instanceof ForbiddenError) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
if (error instanceof Error && error.message === "Accès refusé") {
|
||||
return { success: false, error: "Accès refusé" };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Erreur lors du retrait du membre",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,13 @@ export async function updateSitePreferences(data: {
|
||||
eventsBackground?: string | null;
|
||||
leaderboardBackground?: string | null;
|
||||
challengesBackground?: string | null;
|
||||
profileBackground?: string | null;
|
||||
houseBackground?: string | null;
|
||||
eventRegistrationPoints?: number;
|
||||
eventFeedbackPoints?: number;
|
||||
houseJoinPoints?: number;
|
||||
houseLeavePoints?: number;
|
||||
houseCreatePoints?: number;
|
||||
}) {
|
||||
try {
|
||||
await checkAdminAccess()();
|
||||
@@ -31,8 +36,13 @@ export async function updateSitePreferences(data: {
|
||||
eventsBackground: data.eventsBackground,
|
||||
leaderboardBackground: data.leaderboardBackground,
|
||||
challengesBackground: data.challengesBackground,
|
||||
profileBackground: data.profileBackground,
|
||||
houseBackground: data.houseBackground,
|
||||
eventRegistrationPoints: data.eventRegistrationPoints,
|
||||
eventFeedbackPoints: data.eventFeedbackPoints,
|
||||
houseJoinPoints: data.houseJoinPoints,
|
||||
houseLeavePoints: data.houseLeavePoints,
|
||||
houseCreatePoints: data.houseCreatePoints,
|
||||
});
|
||||
|
||||
revalidatePath("/admin");
|
||||
@@ -40,6 +50,8 @@ export async function updateSitePreferences(data: {
|
||||
revalidatePath("/events");
|
||||
revalidatePath("/leaderboard");
|
||||
revalidatePath("/challenges");
|
||||
revalidatePath("/profile");
|
||||
revalidatePath("/houses");
|
||||
|
||||
return { success: true, data: preferences };
|
||||
} catch (error) {
|
||||
|
||||
@@ -127,3 +127,5 @@ export async function cancelChallenge(challengeId: string) {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
149
actions/houses/update.ts
Normal file
149
actions/houses/update.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
"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 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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeMember(houseId: string, memberId: string) {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return {
|
||||
success: false,
|
||||
error: "Vous devez être connecté",
|
||||
};
|
||||
}
|
||||
|
||||
await houseService.removeMember(houseId, memberId, session.user.id);
|
||||
|
||||
revalidatePath("/houses");
|
||||
revalidatePath("/profile");
|
||||
|
||||
return { success: true, message: "Membre retiré de la maison" };
|
||||
} catch (error) {
|
||||
console.error("Remove member error:", error);
|
||||
|
||||
if (
|
||||
error instanceof ForbiddenError ||
|
||||
error instanceof NotFoundError
|
||||
) {
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: "Une erreur est survenue lors du retrait du membre",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
26
app/admin/challenges/page.tsx
Normal file
26
app/admin/challenges/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import ChallengeManagement from "@/components/admin/ChallengeManagement";
|
||||
import { Card } from "@/components/ui";
|
||||
import { challengeService } from "@/services/challenges/challenge.service";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminChallengesPage() {
|
||||
const challenges = await challengeService.getAllChallenges();
|
||||
|
||||
// Sérialiser les dates pour le client
|
||||
const serializedChallenges = challenges.map((challenge) => ({
|
||||
...challenge,
|
||||
createdAt: challenge.createdAt.toISOString(),
|
||||
acceptedAt: challenge.acceptedAt?.toISOString() ?? null,
|
||||
completedAt: challenge.completedAt?.toISOString() ?? null,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card variant="dark" className="p-6">
|
||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||
Gestion des Défis
|
||||
</h2>
|
||||
<ChallengeManagement initialChallenges={serializedChallenges} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
34
app/admin/events/page.tsx
Normal file
34
app/admin/events/page.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import EventManagement from "@/components/admin/EventManagement";
|
||||
import { Card } from "@/components/ui";
|
||||
import { eventService } from "@/services/events/event.service";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminEventsPage() {
|
||||
const events = await eventService.getEventsWithStatus();
|
||||
|
||||
// Transformer les données pour la sérialisation
|
||||
const serializedEvents = events.map((event) => ({
|
||||
id: event.id,
|
||||
date: event.date.toISOString(),
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
type: event.type,
|
||||
status: event.status,
|
||||
room: event.room,
|
||||
time: event.time,
|
||||
maxPlaces: event.maxPlaces,
|
||||
createdAt: event.createdAt.toISOString(),
|
||||
updatedAt: event.updatedAt.toISOString(),
|
||||
registrationsCount: event.registrationsCount,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card variant="dark" className="p-6">
|
||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||
Gestion des Événements
|
||||
</h2>
|
||||
<EventManagement initialEvents={serializedEvents} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
77
app/admin/feedbacks/page.tsx
Normal file
77
app/admin/feedbacks/page.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import FeedbackManagement from "@/components/admin/FeedbackManagement";
|
||||
import { Card } from "@/components/ui";
|
||||
import { eventFeedbackService } from "@/services/events/event-feedback.service";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminFeedbacksPage() {
|
||||
const [feedbacksRaw, statistics] = await Promise.all([
|
||||
eventFeedbackService.getAllFeedbacks(),
|
||||
eventFeedbackService.getFeedbackStatistics(),
|
||||
]);
|
||||
|
||||
// Type assertion car getAllFeedbacks inclut event et user par défaut
|
||||
const feedbacks = feedbacksRaw as unknown as Array<{
|
||||
id: string;
|
||||
rating: number;
|
||||
comment: string | null;
|
||||
isRead: boolean;
|
||||
createdAt: Date;
|
||||
event: {
|
||||
id: string;
|
||||
name: string;
|
||||
date: Date;
|
||||
type: string;
|
||||
};
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
avatar: string | null;
|
||||
score: number;
|
||||
};
|
||||
}>;
|
||||
|
||||
// Sérialiser les dates pour le client
|
||||
const serializedFeedbacks = feedbacks.map((feedback) => ({
|
||||
id: feedback.id,
|
||||
rating: feedback.rating,
|
||||
comment: feedback.comment,
|
||||
isRead: feedback.isRead,
|
||||
createdAt: feedback.createdAt.toISOString(),
|
||||
event: {
|
||||
id: feedback.event.id,
|
||||
name: feedback.event.name,
|
||||
date: feedback.event.date.toISOString(),
|
||||
type: feedback.event.type,
|
||||
},
|
||||
user: {
|
||||
id: feedback.user.id,
|
||||
username: feedback.user.username,
|
||||
email: feedback.user.email,
|
||||
avatar: feedback.user.avatar,
|
||||
score: feedback.user.score,
|
||||
},
|
||||
}));
|
||||
|
||||
const serializedStatistics = statistics.map((stat) => ({
|
||||
eventId: stat.eventId,
|
||||
eventName: stat.eventName,
|
||||
eventDate: stat.eventDate?.toISOString() ?? null,
|
||||
eventType: stat.eventType,
|
||||
averageRating: stat.averageRating,
|
||||
feedbackCount: stat.feedbackCount,
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card variant="dark" className="p-6">
|
||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||
Gestion des Feedbacks
|
||||
</h2>
|
||||
<FeedbackManagement
|
||||
initialFeedbacks={serializedFeedbacks}
|
||||
initialStatistics={serializedStatistics}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
90
app/admin/houses/page.tsx
Normal file
90
app/admin/houses/page.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import HouseManagement from "@/components/admin/HouseManagement";
|
||||
import { Card } from "@/components/ui";
|
||||
import { houseService } from "@/services/houses/house.service";
|
||||
import { Prisma } from "@/prisma/generated/prisma/client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminHousesPage() {
|
||||
type HouseWithIncludes = Prisma.HouseGetPayload<{
|
||||
include: {
|
||||
creator: {
|
||||
select: {
|
||||
id: true;
|
||||
username: true;
|
||||
avatar: true;
|
||||
};
|
||||
};
|
||||
memberships: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true;
|
||||
username: true;
|
||||
avatar: true;
|
||||
score: true;
|
||||
level: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
const houses = (await houseService.getAllHouses({
|
||||
include: {
|
||||
creator: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
},
|
||||
},
|
||||
memberships: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
score: true,
|
||||
level: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ role: "asc" }, { joinedAt: "asc" }],
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
})) as unknown as HouseWithIncludes[];
|
||||
|
||||
// Transformer les données pour la sérialisation
|
||||
const serializedHouses = houses.map((house) => ({
|
||||
id: house.id,
|
||||
name: house.name,
|
||||
description: house.description,
|
||||
creatorId: house.creatorId,
|
||||
creator: house.creator,
|
||||
createdAt: house.createdAt.toISOString(),
|
||||
updatedAt: house.updatedAt.toISOString(),
|
||||
membersCount: house.memberships?.length || 0,
|
||||
memberships:
|
||||
house.memberships?.map((membership) => ({
|
||||
id: membership.id,
|
||||
role: membership.role,
|
||||
joinedAt: membership.joinedAt.toISOString(),
|
||||
user: membership.user,
|
||||
})) || [],
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card variant="dark" className="p-6">
|
||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||
Gestion des Maisons
|
||||
</h2>
|
||||
<HouseManagement initialHouses={serializedHouses} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
52
app/admin/layout.tsx
Normal file
52
app/admin/layout.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { Role } from "@/prisma/generated/prisma/client";
|
||||
import NavigationWrapper from "@/components/navigation/NavigationWrapper";
|
||||
import AdminNavigation from "@/components/admin/AdminNavigation";
|
||||
import { SectionTitle } from "@/components/ui";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
if (session.user.role !== Role.ADMIN) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black relative">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="fixed inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('/got-light.jpg')`,
|
||||
}}
|
||||
>
|
||||
{/* Dark overlay for readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||||
</div>
|
||||
<NavigationWrapper />
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16">
|
||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
|
||||
<SectionTitle variant="gradient" size="md" className="mb-16 text-center">
|
||||
ADMIN
|
||||
</SectionTitle>
|
||||
|
||||
<AdminNavigation />
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,41 +1,7 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
|
||||
import { Role } from "@/prisma/generated/prisma/client";
|
||||
import NavigationWrapper from "@/components/navigation/NavigationWrapper";
|
||||
import AdminPanel from "@/components/admin/AdminPanel";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminPage() {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
if (session.user.role !== Role.ADMIN) {
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
// Récupérer les préférences globales du site (ou créer si elles n'existent pas)
|
||||
const sitePreferences =
|
||||
await sitePreferencesService.getOrCreateSitePreferences();
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black relative">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('/got-light.jpg')`,
|
||||
}}
|
||||
>
|
||||
{/* Dark overlay for readability */}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-black/70 via-black/60 to-black/80"></div>
|
||||
</div>
|
||||
<NavigationWrapper />
|
||||
<AdminPanel initialPreferences={sitePreferences} />
|
||||
</main>
|
||||
);
|
||||
redirect("/admin/preferences");
|
||||
}
|
||||
|
||||
30
app/admin/preferences/page.tsx
Normal file
30
app/admin/preferences/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
|
||||
import BackgroundPreferences from "@/components/admin/BackgroundPreferences";
|
||||
import EventPointsPreferences from "@/components/admin/EventPointsPreferences";
|
||||
import EventFeedbackPointsPreferences from "@/components/admin/EventFeedbackPointsPreferences";
|
||||
import HousePointsPreferences from "@/components/admin/HousePointsPreferences";
|
||||
import { Card } from "@/components/ui";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminPreferencesPage() {
|
||||
const sitePreferences =
|
||||
await sitePreferencesService.getOrCreateSitePreferences();
|
||||
|
||||
return (
|
||||
<Card variant="dark" className="p-4 sm:p-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
||||
<h2 className="text-xl sm:text-2xl font-gaming font-bold text-pixel-gold break-words">
|
||||
Préférences UI Globales
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<BackgroundPreferences initialPreferences={sitePreferences} />
|
||||
<EventPointsPreferences initialPreferences={sitePreferences} />
|
||||
<EventFeedbackPointsPreferences initialPreferences={sitePreferences} />
|
||||
<HousePointsPreferences initialPreferences={sitePreferences} />
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
42
app/admin/users/page.tsx
Normal file
42
app/admin/users/page.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import UserManagement from "@/components/admin/UserManagement";
|
||||
import { Card } from "@/components/ui";
|
||||
import { userService } from "@/services/users/user.service";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function AdminUsersPage() {
|
||||
const users = await userService.getAllUsers({
|
||||
orderBy: {
|
||||
score: "desc",
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
role: true,
|
||||
score: true,
|
||||
level: true,
|
||||
hp: true,
|
||||
maxHp: true,
|
||||
xp: true,
|
||||
maxXp: true,
|
||||
avatar: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Sérialiser les dates pour le client
|
||||
const serializedUsers = users.map((user) => ({
|
||||
...user,
|
||||
createdAt: user.createdAt.toISOString(),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Card variant="dark" className="p-6">
|
||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||
Gestion des Utilisateurs
|
||||
</h2>
|
||||
<UserManagement initialUsers={serializedUsers} />
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
99
app/api/admin/houses/route.ts
Normal file
99
app/api/admin/houses/route.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { houseService } from "@/services/houses/house.service";
|
||||
import { Role, Prisma } from "@/prisma/generated/prisma/client";
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user || session.user.role !== Role.ADMIN) {
|
||||
return NextResponse.json({ error: "Accès refusé" }, { status: 403 });
|
||||
}
|
||||
|
||||
// Récupérer toutes les maisons avec leurs membres
|
||||
type HouseWithIncludes = Prisma.HouseGetPayload<{
|
||||
include: {
|
||||
creator: {
|
||||
select: {
|
||||
id: true;
|
||||
username: true;
|
||||
avatar: true;
|
||||
};
|
||||
};
|
||||
memberships: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true;
|
||||
username: true;
|
||||
avatar: true;
|
||||
score: true;
|
||||
level: true;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
const houses = (await houseService.getAllHouses({
|
||||
include: {
|
||||
creator: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
},
|
||||
},
|
||||
memberships: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
score: true,
|
||||
level: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [
|
||||
{ role: "asc" }, // OWNER, ADMIN, MEMBER
|
||||
{ joinedAt: "asc" },
|
||||
],
|
||||
},
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
})) as unknown as HouseWithIncludes[];
|
||||
|
||||
// Transformer les données pour la sérialisation
|
||||
const housesWithData = houses.map((house) => ({
|
||||
id: house.id,
|
||||
name: house.name,
|
||||
description: house.description,
|
||||
creatorId: house.creatorId,
|
||||
creator: house.creator,
|
||||
createdAt: house.createdAt.toISOString(),
|
||||
updatedAt: house.updatedAt.toISOString(),
|
||||
membersCount: house.memberships?.length || 0,
|
||||
memberships:
|
||||
house.memberships?.map((membership) => ({
|
||||
id: membership.id,
|
||||
role: membership.role,
|
||||
joinedAt: membership.joinedAt.toISOString(),
|
||||
user: membership.user,
|
||||
})) || [],
|
||||
}));
|
||||
|
||||
return NextResponse.json(housesWithData);
|
||||
} catch (error) {
|
||||
console.error("Error fetching houses:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Erreur lors de la récupération des maisons" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -27,3 +27,5 @@ export async function GET() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
23
app/api/invitations/pending-count/route.ts
Normal file
23
app/api/invitations/pending-count/route.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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({ count: 0 });
|
||||
}
|
||||
|
||||
// Compter les invitations ET les demandes d'adhésion en attente
|
||||
const count = await houseService.getPendingHouseActionsCount(
|
||||
session.user.id
|
||||
);
|
||||
|
||||
return NextResponse.json({ count });
|
||||
} catch (error) {
|
||||
console.error("Error fetching pending house actions count:", error);
|
||||
return NextResponse.json({ count: 0 });
|
||||
}
|
||||
}
|
||||
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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ export async function GET() {
|
||||
eventsBackground: null,
|
||||
leaderboardBackground: null,
|
||||
challengesBackground: null,
|
||||
profileBackground: null,
|
||||
houseBackground: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -21,6 +23,8 @@ export async function GET() {
|
||||
eventsBackground: sitePreferences.eventsBackground,
|
||||
leaderboardBackground: sitePreferences.leaderboardBackground,
|
||||
challengesBackground: sitePreferences.challengesBackground,
|
||||
profileBackground: sitePreferences.profileBackground,
|
||||
houseBackground: sitePreferences.houseBackground,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error fetching preferences:", error);
|
||||
@@ -30,6 +34,8 @@ export async function GET() {
|
||||
eventsBackground: null,
|
||||
leaderboardBackground: null,
|
||||
challengesBackground: null,
|
||||
profileBackground: null,
|
||||
houseBackground: null,
|
||||
},
|
||||
{ status: 200 }
|
||||
);
|
||||
|
||||
@@ -39,3 +39,5 @@ export async function GET() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
207
app/houses/page.tsx
Normal file
207
app/houses/page.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
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 { prisma } from "@/services/database";
|
||||
import type {
|
||||
House,
|
||||
HouseMembership,
|
||||
HouseInvitation,
|
||||
} from "@/prisma/generated/prisma/client";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
// Types pour les données sérialisées
|
||||
type HouseWithRelations = House & {
|
||||
creator?: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
} | null;
|
||||
creatorId?: string;
|
||||
memberships?: Array<
|
||||
HouseMembership & {
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
score: number | null;
|
||||
level: number | null;
|
||||
};
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
type InvitationWithRelations = HouseInvitation & {
|
||||
house: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
inviter: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
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 sans maison pour les invitations
|
||||
prisma.user.findMany({
|
||||
where: {
|
||||
houseMemberships: {
|
||||
none: {},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
avatar: true,
|
||||
},
|
||||
orderBy: {
|
||||
username: "asc",
|
||||
},
|
||||
}),
|
||||
getBackgroundImage("houses", "/got-2.jpg"),
|
||||
]);
|
||||
|
||||
// Sérialiser les données pour le client
|
||||
const houses = (housesData as HouseWithRelations[]).map(
|
||||
(house: HouseWithRelations) => ({
|
||||
id: house.id,
|
||||
name: house.name,
|
||||
description: house.description,
|
||||
creator: house.creator || {
|
||||
id: house.creatorId || "",
|
||||
username: "Unknown",
|
||||
avatar: null,
|
||||
},
|
||||
memberships: (house.memberships || []).map((m) => ({
|
||||
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 HouseWithRelations).creator || {
|
||||
id: (myHouseData as HouseWithRelations).creatorId || "",
|
||||
username: "Unknown",
|
||||
avatar: null,
|
||||
},
|
||||
memberships: (
|
||||
(myHouseData as HouseWithRelations).memberships || []
|
||||
).map((m) => ({
|
||||
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 as InvitationWithRelations[]).map(
|
||||
(inv: InvitationWithRelations) => ({
|
||||
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() {
|
||||
// Paralléliser les appels DB
|
||||
const [leaderboard, backgroundImage] = await Promise.all([
|
||||
const [leaderboard, houseLeaderboard, backgroundImage] = await Promise.all([
|
||||
userStatsService.getLeaderboard(10),
|
||||
userStatsService.getHouseLeaderboard(10),
|
||||
getBackgroundImage("leaderboard", "/leaderboard-bg.jpg"),
|
||||
]);
|
||||
|
||||
@@ -17,6 +18,7 @@ export default async function LeaderboardPage() {
|
||||
<NavigationWrapper />
|
||||
<LeaderboardSection
|
||||
leaderboard={leaderboard}
|
||||
houseLeaderboard={houseLeaderboard}
|
||||
backgroundImage={backgroundImage}
|
||||
/>
|
||||
</main>
|
||||
|
||||
@@ -20,7 +20,7 @@ export default async function Home() {
|
||||
}));
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-black relative">
|
||||
<main className="min-h-screen relative" style={{ backgroundColor: "var(--background)" }}>
|
||||
<NavigationWrapper />
|
||||
<HeroSection backgroundImage={backgroundImage} />
|
||||
<EventsSection events={serializedEvents} />
|
||||
|
||||
@@ -29,7 +29,7 @@ export default async function ProfilePage() {
|
||||
score: true,
|
||||
createdAt: true,
|
||||
}),
|
||||
getBackgroundImage("home", "/got-background.jpg"),
|
||||
getBackgroundImage("profile", "/got-background.jpg"),
|
||||
]);
|
||||
|
||||
if (!user) {
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function StyleGuidePage() {
|
||||
<Navigation />
|
||||
<BackgroundSection backgroundImage="/got-2.jpg" className="pt-24 pb-16">
|
||||
<div className="w-full max-w-6xl mx-auto px-8">
|
||||
<SectionTitle variant="gradient" size="xl" className="mb-12">
|
||||
<SectionTitle variant="gradient" size="xl" className="mb-16">
|
||||
STYLE GUIDE
|
||||
</SectionTitle>
|
||||
<p className="text-gray-400 text-center mb-12 max-w-3xl mx-auto">
|
||||
|
||||
41
components/admin/AdminNavigation.tsx
Normal file
41
components/admin/AdminNavigation.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Button } from "@/components/ui";
|
||||
|
||||
const adminSections = [
|
||||
{ id: "preferences", label: "Préférences UI", path: "/admin/preferences" },
|
||||
{ id: "users", label: "Utilisateurs", path: "/admin/users" },
|
||||
{ id: "events", label: "Événements", path: "/admin/events" },
|
||||
{ id: "feedbacks", label: "Feedbacks", path: "/admin/feedbacks" },
|
||||
{ id: "challenges", label: "Défis", path: "/admin/challenges" },
|
||||
{ id: "houses", label: "Maisons", path: "/admin/houses" },
|
||||
];
|
||||
|
||||
export default function AdminNavigation() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<div className="flex gap-4 mb-8 justify-center flex-wrap">
|
||||
{adminSections.map((section) => {
|
||||
const isActive = pathname === section.path ||
|
||||
(section.path === "/admin/preferences" && pathname === "/admin");
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={section.id}
|
||||
as={Link}
|
||||
href={section.path}
|
||||
variant={isActive ? "primary" : "secondary"}
|
||||
size="md"
|
||||
className={isActive ? "bg-pixel-gold/10" : ""}
|
||||
>
|
||||
{section.label}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import UserManagement from "@/components/admin/UserManagement";
|
||||
import EventManagement from "@/components/admin/EventManagement";
|
||||
import FeedbackManagement from "@/components/admin/FeedbackManagement";
|
||||
import ChallengeManagement from "@/components/admin/ChallengeManagement";
|
||||
import BackgroundPreferences from "@/components/admin/BackgroundPreferences";
|
||||
import EventPointsPreferences from "@/components/admin/EventPointsPreferences";
|
||||
import EventFeedbackPointsPreferences from "@/components/admin/EventFeedbackPointsPreferences";
|
||||
import { Button, Card, SectionTitle } from "@/components/ui";
|
||||
|
||||
interface SitePreferences {
|
||||
id: string;
|
||||
homeBackground: string | null;
|
||||
eventsBackground: string | null;
|
||||
leaderboardBackground: string | null;
|
||||
challengesBackground: string | null;
|
||||
eventRegistrationPoints: number;
|
||||
eventFeedbackPoints: number;
|
||||
}
|
||||
|
||||
interface AdminPanelProps {
|
||||
initialPreferences: SitePreferences;
|
||||
}
|
||||
|
||||
type AdminSection =
|
||||
| "preferences"
|
||||
| "users"
|
||||
| "events"
|
||||
| "feedbacks"
|
||||
| "challenges";
|
||||
|
||||
export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
|
||||
const [activeSection, setActiveSection] =
|
||||
useState<AdminSection>("preferences");
|
||||
|
||||
return (
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16">
|
||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-8 py-16">
|
||||
<SectionTitle variant="gradient" size="md" className="mb-8 text-center">
|
||||
ADMIN
|
||||
</SectionTitle>
|
||||
|
||||
{/* Navigation Tabs */}
|
||||
<div className="flex gap-4 mb-8 justify-center flex-wrap">
|
||||
<Button
|
||||
onClick={() => setActiveSection("preferences")}
|
||||
variant={activeSection === "preferences" ? "primary" : "secondary"}
|
||||
size="md"
|
||||
className={
|
||||
activeSection === "preferences" ? "bg-pixel-gold/10" : ""
|
||||
}
|
||||
>
|
||||
Préférences UI
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setActiveSection("users")}
|
||||
variant={activeSection === "users" ? "primary" : "secondary"}
|
||||
size="md"
|
||||
className={activeSection === "users" ? "bg-pixel-gold/10" : ""}
|
||||
>
|
||||
Utilisateurs
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setActiveSection("events")}
|
||||
variant={activeSection === "events" ? "primary" : "secondary"}
|
||||
size="md"
|
||||
className={activeSection === "events" ? "bg-pixel-gold/10" : ""}
|
||||
>
|
||||
Événements
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setActiveSection("feedbacks")}
|
||||
variant={activeSection === "feedbacks" ? "primary" : "secondary"}
|
||||
size="md"
|
||||
className={activeSection === "feedbacks" ? "bg-pixel-gold/10" : ""}
|
||||
>
|
||||
Feedbacks
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setActiveSection("challenges")}
|
||||
variant={activeSection === "challenges" ? "primary" : "secondary"}
|
||||
size="md"
|
||||
className={activeSection === "challenges" ? "bg-pixel-gold/10" : ""}
|
||||
>
|
||||
Défis
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{activeSection === "preferences" && (
|
||||
<Card variant="dark" className="p-4 sm:p-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
|
||||
<h2 className="text-xl sm:text-2xl font-gaming font-bold text-pixel-gold break-words">
|
||||
Préférences UI Globales
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<BackgroundPreferences initialPreferences={initialPreferences} />
|
||||
<EventPointsPreferences initialPreferences={initialPreferences} />
|
||||
<EventFeedbackPointsPreferences initialPreferences={initialPreferences} />
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeSection === "users" && (
|
||||
<Card variant="dark" className="p-6">
|
||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||
Gestion des Utilisateurs
|
||||
</h2>
|
||||
<UserManagement />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeSection === "events" && (
|
||||
<Card variant="dark" className="p-6">
|
||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||
Gestion des Événements
|
||||
</h2>
|
||||
<EventManagement />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeSection === "feedbacks" && (
|
||||
<Card variant="dark" className="p-6">
|
||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||
Gestion des Feedbacks
|
||||
</h2>
|
||||
<FeedbackManagement />
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{activeSection === "challenges" && (
|
||||
<Card variant="dark" className="p-6">
|
||||
<h2 className="text-2xl font-gaming font-bold mb-6 text-pixel-gold">
|
||||
Gestion des Défis
|
||||
</h2>
|
||||
<ChallengeManagement />
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,8 @@ interface SitePreferences {
|
||||
eventsBackground: string | null;
|
||||
leaderboardBackground: string | null;
|
||||
challengesBackground: string | null;
|
||||
profileBackground: string | null;
|
||||
houseBackground: string | null;
|
||||
eventRegistrationPoints?: number;
|
||||
}
|
||||
|
||||
@@ -23,6 +25,8 @@ const DEFAULT_IMAGES = {
|
||||
events: "/got-2.jpg",
|
||||
leaderboard: "/leaderboard-bg.jpg",
|
||||
challenges: "/got-2.jpg",
|
||||
profile: "/got-background.jpg",
|
||||
houses: "/got-2.jpg",
|
||||
};
|
||||
|
||||
export default function BackgroundPreferences({
|
||||
@@ -64,6 +68,14 @@ export default function BackgroundPreferences({
|
||||
initialPreferences.challengesBackground,
|
||||
DEFAULT_IMAGES.challenges
|
||||
),
|
||||
profileBackground: getFormValue(
|
||||
initialPreferences.profileBackground,
|
||||
DEFAULT_IMAGES.profile
|
||||
),
|
||||
houseBackground: getFormValue(
|
||||
initialPreferences.houseBackground,
|
||||
DEFAULT_IMAGES.houses
|
||||
),
|
||||
}),
|
||||
[initialPreferences]
|
||||
);
|
||||
@@ -101,6 +113,14 @@ export default function BackgroundPreferences({
|
||||
formData.challengesBackground,
|
||||
DEFAULT_IMAGES.challenges
|
||||
),
|
||||
profileBackground: getApiValue(
|
||||
formData.profileBackground,
|
||||
DEFAULT_IMAGES.profile
|
||||
),
|
||||
houseBackground: getApiValue(
|
||||
formData.houseBackground,
|
||||
DEFAULT_IMAGES.houses
|
||||
),
|
||||
};
|
||||
|
||||
const result = await updateSitePreferences(apiData);
|
||||
@@ -125,6 +145,14 @@ export default function BackgroundPreferences({
|
||||
result.data.challengesBackground,
|
||||
DEFAULT_IMAGES.challenges
|
||||
),
|
||||
profileBackground: getFormValue(
|
||||
result.data.profileBackground,
|
||||
DEFAULT_IMAGES.profile
|
||||
),
|
||||
houseBackground: getFormValue(
|
||||
result.data.houseBackground,
|
||||
DEFAULT_IMAGES.houses
|
||||
),
|
||||
});
|
||||
setIsEditing(false);
|
||||
} else {
|
||||
@@ -157,6 +185,14 @@ export default function BackgroundPreferences({
|
||||
preferences.challengesBackground,
|
||||
DEFAULT_IMAGES.challenges
|
||||
),
|
||||
profileBackground: getFormValue(
|
||||
preferences.profileBackground,
|
||||
DEFAULT_IMAGES.profile
|
||||
),
|
||||
houseBackground: getFormValue(
|
||||
preferences.houseBackground,
|
||||
DEFAULT_IMAGES.houses
|
||||
),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -226,6 +262,26 @@ export default function BackgroundPreferences({
|
||||
}
|
||||
label="Background Challenges"
|
||||
/>
|
||||
<ImageSelector
|
||||
value={formData.profileBackground}
|
||||
onChange={(url) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
profileBackground: url,
|
||||
})
|
||||
}
|
||||
label="Background Profile"
|
||||
/>
|
||||
<ImageSelector
|
||||
value={formData.houseBackground}
|
||||
onChange={(url) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
houseBackground: url,
|
||||
})
|
||||
}
|
||||
label="Background Houses"
|
||||
/>
|
||||
<div className="flex flex-col sm:flex-row gap-2 pt-4">
|
||||
<Button onClick={handleSave} variant="success" size="md">
|
||||
Enregistrer
|
||||
@@ -461,6 +517,118 @@ export default function BackgroundPreferences({
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
||||
<span className="text-pixel-gold font-bold text-sm sm:text-base min-w-0 sm:min-w-[120px] flex-shrink-0">
|
||||
Profile:
|
||||
</span>
|
||||
{(() => {
|
||||
const currentImage =
|
||||
preferences?.profileBackground &&
|
||||
preferences.profileBackground.trim() !== ""
|
||||
? preferences.profileBackground
|
||||
: DEFAULT_IMAGES.profile;
|
||||
const isDefault =
|
||||
!preferences?.profileBackground ||
|
||||
preferences.profileBackground.trim() === "";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
|
||||
<div className="relative w-16 h-10 sm:w-20 sm:h-12 rounded border border-pixel-gold/30 overflow-hidden bg-black/60 flex-shrink-0">
|
||||
<img
|
||||
src={currentImage}
|
||||
alt="Profile background"
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget;
|
||||
const currentSrc = target.src;
|
||||
const fallbackSrc = "/got-background.jpg";
|
||||
if (!currentSrc.includes(fallbackSrc)) {
|
||||
target.src = fallbackSrc;
|
||||
} else {
|
||||
target.style.display = "none";
|
||||
const fallbackDiv =
|
||||
target.nextElementSibling as HTMLElement;
|
||||
if (fallbackDiv) {
|
||||
fallbackDiv.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/60 text-gray-500 text-xs hidden">
|
||||
No image
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className="text-xs text-gray-400 truncate min-w-0">
|
||||
{isDefault ? "Par défaut: " : ""}
|
||||
{currentImage}
|
||||
</span>
|
||||
{isDefault && (
|
||||
<span className="text-[10px] text-gray-500 italic">
|
||||
(Image par défaut)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
||||
<span className="text-pixel-gold font-bold text-sm sm:text-base min-w-0 sm:min-w-[120px] flex-shrink-0">
|
||||
Houses:
|
||||
</span>
|
||||
{(() => {
|
||||
const currentImage =
|
||||
preferences?.houseBackground &&
|
||||
preferences.houseBackground.trim() !== ""
|
||||
? preferences.houseBackground
|
||||
: DEFAULT_IMAGES.houses;
|
||||
const isDefault =
|
||||
!preferences?.houseBackground ||
|
||||
preferences.houseBackground.trim() === "";
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
|
||||
<div className="relative w-16 h-10 sm:w-20 sm:h-12 rounded border border-pixel-gold/30 overflow-hidden bg-black/60 flex-shrink-0">
|
||||
<img
|
||||
src={currentImage}
|
||||
alt="Houses background"
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
const target = e.currentTarget;
|
||||
const currentSrc = target.src;
|
||||
const fallbackSrc = "/got-2.jpg";
|
||||
if (!currentSrc.includes(fallbackSrc)) {
|
||||
target.src = fallbackSrc;
|
||||
} else {
|
||||
target.style.display = "none";
|
||||
const fallbackDiv =
|
||||
target.nextElementSibling as HTMLElement;
|
||||
if (fallbackDiv) {
|
||||
fallbackDiv.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-black/60 text-gray-500 text-xs hidden">
|
||||
No image
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<span className="text-xs text-gray-400 truncate min-w-0">
|
||||
{isDefault ? "Par défaut: " : ""}
|
||||
{currentImage}
|
||||
</span>
|
||||
{isDefault && (
|
||||
<span className="text-[10px] text-gray-500 italic">
|
||||
(Image par défaut)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { useState, useTransition } from "react";
|
||||
import {
|
||||
validateChallenge,
|
||||
rejectChallenge,
|
||||
@@ -42,9 +42,12 @@ interface Challenge {
|
||||
acceptedAt: string | null;
|
||||
}
|
||||
|
||||
export default function ChallengeManagement() {
|
||||
const [challenges, setChallenges] = useState<Challenge[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
interface ChallengeManagementProps {
|
||||
initialChallenges: Challenge[];
|
||||
}
|
||||
|
||||
export default function ChallengeManagement({ initialChallenges }: ChallengeManagementProps) {
|
||||
const [challenges, setChallenges] = useState<Challenge[]>(initialChallenges);
|
||||
const [selectedChallenge, setSelectedChallenge] = useState<Challenge | null>(
|
||||
null
|
||||
);
|
||||
@@ -60,10 +63,6 @@ export default function ChallengeManagement() {
|
||||
const [successMessage, setSuccessMessage] = useState<string | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchChallenges();
|
||||
}, []);
|
||||
|
||||
const fetchChallenges = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/admin/challenges");
|
||||
@@ -73,8 +72,6 @@ export default function ChallengeManagement() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching challenges:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -262,12 +259,6 @@ export default function ChallengeManagement() {
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="text-center text-pixel-gold py-8">Chargement...</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (challenges.length === 0) {
|
||||
return <div className="text-center text-gray-400 py-8">Aucun défi</div>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useTransition } from "react";
|
||||
import { useState, useTransition } from "react";
|
||||
import { calculateEventStatus } from "@/lib/eventStatus";
|
||||
import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events";
|
||||
import {
|
||||
@@ -92,9 +92,12 @@ const getStatusLabel = (status: Event["status"]) => {
|
||||
}
|
||||
};
|
||||
|
||||
export default function EventManagement() {
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
interface EventManagementProps {
|
||||
initialEvents: Event[];
|
||||
}
|
||||
|
||||
export default function EventManagement({ initialEvents }: EventManagementProps) {
|
||||
const [events, setEvents] = useState<Event[]>(initialEvents);
|
||||
const [editingEvent, setEditingEvent] = useState<Event | null>(null);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
@@ -116,10 +119,6 @@ export default function EventManagement() {
|
||||
maxPlaces: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
fetchEvents();
|
||||
}, []);
|
||||
|
||||
const fetchEvents = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/admin/events");
|
||||
@@ -129,8 +128,6 @@ export default function EventManagement() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching events:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -151,8 +148,10 @@ export default function EventManagement() {
|
||||
const handleEdit = (event: Event) => {
|
||||
setEditingEvent(event);
|
||||
setIsCreating(false);
|
||||
// Convertir la date ISO en format YYYY-MM-DD pour l'input date
|
||||
const dateValue = event.date ? new Date(event.date).toISOString().split('T')[0] : "";
|
||||
setFormData({
|
||||
date: event.date,
|
||||
date: dateValue,
|
||||
name: event.name,
|
||||
description: event.description,
|
||||
type: event.type,
|
||||
@@ -304,10 +303,6 @@ export default function EventManagement() {
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-4">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
addFeedbackBonusPoints,
|
||||
markFeedbackAsRead,
|
||||
@@ -38,10 +38,17 @@ interface EventStatistics {
|
||||
feedbackCount: number;
|
||||
}
|
||||
|
||||
export default function FeedbackManagement() {
|
||||
const [feedbacks, setFeedbacks] = useState<Feedback[]>([]);
|
||||
const [statistics, setStatistics] = useState<EventStatistics[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
interface FeedbackManagementProps {
|
||||
initialFeedbacks: Feedback[];
|
||||
initialStatistics: EventStatistics[];
|
||||
}
|
||||
|
||||
export default function FeedbackManagement({
|
||||
initialFeedbacks,
|
||||
initialStatistics,
|
||||
}: FeedbackManagementProps) {
|
||||
const [feedbacks, setFeedbacks] = useState<Feedback[]>(initialFeedbacks);
|
||||
const [statistics, setStatistics] = useState<EventStatistics[]>(initialStatistics);
|
||||
const [error, setError] = useState("");
|
||||
const [selectedEvent, setSelectedEvent] = useState<string | null>(null);
|
||||
const [addingPoints, setAddingPoints] = useState<Record<string, boolean>>(
|
||||
@@ -49,10 +56,6 @@ export default function FeedbackManagement() {
|
||||
);
|
||||
const [markingRead, setMarkingRead] = useState<Record<string, boolean>>({});
|
||||
|
||||
useEffect(() => {
|
||||
fetchFeedbacks();
|
||||
}, []);
|
||||
|
||||
const fetchFeedbacks = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/admin/feedback");
|
||||
@@ -65,8 +68,6 @@ export default function FeedbackManagement() {
|
||||
setStatistics(data.statistics || []);
|
||||
} catch {
|
||||
setError("Erreur lors du chargement des feedbacks");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -159,14 +160,6 @@ export default function FeedbackManagement() {
|
||||
);
|
||||
});
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="bg-black/60 border border-pixel-gold/30 rounded-lg p-4 sm:p-8">
|
||||
<p className="text-gray-400 text-center text-sm">Chargement...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{/* Statistiques par événement */}
|
||||
|
||||
447
components/admin/HouseManagement.tsx
Normal file
447
components/admin/HouseManagement.tsx
Normal file
@@ -0,0 +1,447 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useTransition } from "react";
|
||||
import {
|
||||
Input,
|
||||
Textarea,
|
||||
Button,
|
||||
Card,
|
||||
Badge,
|
||||
Modal,
|
||||
CloseButton,
|
||||
Avatar,
|
||||
} from "@/components/ui";
|
||||
import { updateHouse, deleteHouse, removeMember } from "@/actions/admin/houses";
|
||||
|
||||
interface House {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
creatorId: string;
|
||||
creator: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
membersCount: number;
|
||||
memberships: Array<{
|
||||
id: string;
|
||||
role: string;
|
||||
joinedAt: string;
|
||||
user: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
score: number;
|
||||
level: number;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
interface HouseFormData {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const getRoleLabel = (role: string) => {
|
||||
switch (role) {
|
||||
case "OWNER":
|
||||
return "👑 Propriétaire";
|
||||
case "ADMIN":
|
||||
return "⚡ Admin";
|
||||
case "MEMBER":
|
||||
return "👤 Membre";
|
||||
default:
|
||||
return role;
|
||||
}
|
||||
};
|
||||
|
||||
const getRoleColor = (role: string) => {
|
||||
switch (role) {
|
||||
case "OWNER":
|
||||
return "var(--accent)";
|
||||
case "ADMIN":
|
||||
return "var(--primary)";
|
||||
case "MEMBER":
|
||||
return "var(--muted-foreground)";
|
||||
default:
|
||||
return "var(--gray)";
|
||||
}
|
||||
};
|
||||
|
||||
interface HouseManagementProps {
|
||||
initialHouses: House[];
|
||||
}
|
||||
|
||||
export default function HouseManagement({ initialHouses }: HouseManagementProps) {
|
||||
const [houses, setHouses] = useState<House[]>(initialHouses);
|
||||
const [editingHouse, setEditingHouse] = useState<House | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deletingHouseId, setDeletingHouseId] = useState<string | null>(null);
|
||||
const [viewingMembers, setViewingMembers] = useState<House | null>(null);
|
||||
const [removingMemberId, setRemovingMemberId] = useState<string | null>(null);
|
||||
const [formData, setFormData] = useState<HouseFormData>({
|
||||
name: "",
|
||||
description: "",
|
||||
});
|
||||
const [, startTransition] = useTransition();
|
||||
|
||||
const fetchHouses = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/admin/houses");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setHouses(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching houses:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (house: House) => {
|
||||
setEditingHouse(house);
|
||||
setFormData({
|
||||
name: house.name,
|
||||
description: house.description || "",
|
||||
});
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!editingHouse) return;
|
||||
|
||||
setSaving(true);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const result = await updateHouse(editingHouse.id, {
|
||||
name: formData.name,
|
||||
description: formData.description || null,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
await fetchHouses();
|
||||
setEditingHouse(null);
|
||||
setFormData({ name: "", description: "" });
|
||||
} else {
|
||||
alert(result.error || "Erreur lors de la mise à jour");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating house:", error);
|
||||
alert("Erreur lors de la mise à jour");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async (houseId: string) => {
|
||||
if (
|
||||
!confirm(
|
||||
"Êtes-vous sûr de vouloir supprimer cette maison ? Cette action est irréversible et supprimera tous les membres."
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingHouseId(houseId);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const result = await deleteHouse(houseId);
|
||||
|
||||
if (result.success) {
|
||||
await fetchHouses();
|
||||
} else {
|
||||
alert(result.error || "Erreur lors de la suppression");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error deleting house:", error);
|
||||
alert("Erreur lors de la suppression");
|
||||
} finally {
|
||||
setDeletingHouseId(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setEditingHouse(null);
|
||||
setFormData({ name: "", description: "" });
|
||||
};
|
||||
|
||||
const handleRemoveMember = async (houseId: string, memberId: string) => {
|
||||
if (
|
||||
!confirm(
|
||||
"Êtes-vous sûr de vouloir retirer ce membre de la maison ? Cette action lui retirera des points."
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRemovingMemberId(memberId);
|
||||
startTransition(async () => {
|
||||
try {
|
||||
const result = await removeMember(houseId, memberId);
|
||||
|
||||
if (result.success) {
|
||||
// Récupérer les maisons mises à jour
|
||||
const response = await fetch("/api/admin/houses");
|
||||
if (response.ok) {
|
||||
const updatedHouses = await response.json();
|
||||
setHouses(updatedHouses);
|
||||
// Mettre à jour la modal si elle est ouverte
|
||||
if (viewingMembers) {
|
||||
const updatedHouse = updatedHouses.find((h: House) => h.id === houseId);
|
||||
if (updatedHouse) {
|
||||
setViewingMembers(updatedHouse);
|
||||
} else {
|
||||
// Si la maison n'existe plus, fermer la modal
|
||||
setViewingMembers(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert(result.error || "Erreur lors du retrait du membre");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error removing member:", error);
|
||||
alert("Erreur lors du retrait du membre");
|
||||
} finally {
|
||||
setRemovingMemberId(null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const formatNumber = (num: number) => {
|
||||
return num.toLocaleString("en-US");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-4">
|
||||
<h3 className="text-lg sm:text-xl font-gaming font-bold text-pixel-gold break-words">
|
||||
Maisons ({houses.length})
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
{/* Modal d'édition */}
|
||||
{editingHouse && (
|
||||
<Modal isOpen={!!editingHouse} onClose={handleCancel} size="lg">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||
Modifier la maison
|
||||
</h4>
|
||||
<CloseButton onClick={handleCancel} size="lg" />
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Input
|
||||
type="text"
|
||||
label="Nom de la maison"
|
||||
value={formData.name}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, name: e.target.value })
|
||||
}
|
||||
placeholder="Nom de la maison"
|
||||
className="text-xs sm:text-sm px-3 py-2"
|
||||
/>
|
||||
<Textarea
|
||||
label="Description"
|
||||
value={formData.description}
|
||||
onChange={(e) =>
|
||||
setFormData({ ...formData, description: e.target.value })
|
||||
}
|
||||
placeholder="Description de la maison"
|
||||
rows={4}
|
||||
className="text-xs sm:text-sm px-3 py-2"
|
||||
/>
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="success"
|
||||
size="md"
|
||||
disabled={saving}
|
||||
>
|
||||
{saving ? "Enregistrement..." : "Enregistrer"}
|
||||
</Button>
|
||||
<Button onClick={handleCancel} variant="secondary" size="md">
|
||||
Annuler
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{/* Modal des membres */}
|
||||
{viewingMembers && (
|
||||
<Modal
|
||||
isOpen={!!viewingMembers}
|
||||
onClose={() => setViewingMembers(null)}
|
||||
size="lg"
|
||||
>
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||
Membres de "{viewingMembers.name}"
|
||||
</h4>
|
||||
<CloseButton onClick={() => setViewingMembers(null)} size="lg" />
|
||||
</div>
|
||||
|
||||
{viewingMembers.memberships.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-8">
|
||||
Aucun membre dans cette maison
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3 max-h-[60vh] overflow-y-auto">
|
||||
{viewingMembers.memberships.map((membership) => {
|
||||
const roleColor = getRoleColor(membership.role);
|
||||
return (
|
||||
<Card
|
||||
key={membership.id}
|
||||
variant="default"
|
||||
className="p-3 sm:p-4"
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<Avatar
|
||||
src={membership.user.avatar}
|
||||
username={membership.user.username}
|
||||
size="md"
|
||||
borderClassName="border-2"
|
||||
style={{
|
||||
borderColor: roleColor,
|
||||
}}
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h5 className="text-pixel-gold font-bold text-sm sm:text-base break-words">
|
||||
{membership.user.username}
|
||||
</h5>
|
||||
<p className="text-gray-400 text-xs sm:text-sm">
|
||||
Niveau {membership.user.level} • Score:{" "}
|
||||
{formatNumber(membership.user.score)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<Badge
|
||||
variant="default"
|
||||
size="sm"
|
||||
style={{
|
||||
color: roleColor,
|
||||
backgroundColor: `color-mix(in srgb, ${roleColor} 15%, transparent)`,
|
||||
borderColor: `color-mix(in srgb, ${roleColor} 30%, transparent)`,
|
||||
}}
|
||||
>
|
||||
{getRoleLabel(membership.role)}
|
||||
</Badge>
|
||||
{membership.role !== "OWNER" && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleRemoveMember(
|
||||
viewingMembers.id,
|
||||
membership.user.id
|
||||
)
|
||||
}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
disabled={removingMemberId === membership.user.id}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{removingMemberId === membership.user.id
|
||||
? "..."
|
||||
: "Retirer"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{houses.length === 0 ? (
|
||||
<div className="text-center text-gray-400 py-8">
|
||||
Aucune maison trouvée
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{houses.map((house) => {
|
||||
return (
|
||||
<Card key={house.id} variant="default" className="p-3 sm: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">
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3 mb-2">
|
||||
<h4 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||
{house.name}
|
||||
</h4>
|
||||
<Badge variant="info" size="sm">
|
||||
{house.membersCount} membre
|
||||
{house.membersCount !== 1 ? "s" : ""}
|
||||
</Badge>
|
||||
</div>
|
||||
{house.description && (
|
||||
<p className="text-gray-400 text-xs sm:text-sm mb-2 break-words">
|
||||
{house.description}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-2 sm:gap-4 mt-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar
|
||||
src={house.creator.avatar}
|
||||
username={house.creator.username}
|
||||
size="sm"
|
||||
borderClassName="border border-pixel-gold/50"
|
||||
/>
|
||||
<p className="text-gray-500 text-[10px] sm:text-xs">
|
||||
Créée par {house.creator.username}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-gray-500 text-[10px] sm:text-xs whitespace-nowrap">
|
||||
Créée le{" "}
|
||||
{new Date(house.createdAt).toLocaleDateString("fr-FR")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{!editingHouse && (
|
||||
<div className="flex gap-2 sm:ml-4 flex-shrink-0 flex-wrap">
|
||||
<Button
|
||||
onClick={() => setViewingMembers(house)}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
Membres ({house.membersCount})
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleEdit(house)}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
Modifier
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleDelete(house.id)}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
disabled={deletingHouseId === house.id}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{deletingHouseId === house.id ? "..." : "Supprimer"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
269
components/admin/HousePointsPreferences.tsx
Normal file
269
components/admin/HousePointsPreferences.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { updateSitePreferences } from "@/actions/admin/preferences";
|
||||
import { Button, Card, Input } from "@/components/ui";
|
||||
|
||||
interface SitePreferences {
|
||||
id: string;
|
||||
houseJoinPoints: number;
|
||||
houseLeavePoints: number;
|
||||
houseCreatePoints: number;
|
||||
}
|
||||
|
||||
interface HousePointsPreferencesProps {
|
||||
initialPreferences: SitePreferences;
|
||||
}
|
||||
|
||||
export default function HousePointsPreferences({
|
||||
initialPreferences,
|
||||
}: HousePointsPreferencesProps) {
|
||||
const [preferences, setPreferences] = useState<SitePreferences | null>(
|
||||
initialPreferences
|
||||
);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [formData, setFormData] = useState({
|
||||
houseJoinPoints: initialPreferences.houseJoinPoints.toString(),
|
||||
houseLeavePoints: initialPreferences.houseLeavePoints.toString(),
|
||||
houseCreatePoints: initialPreferences.houseCreatePoints.toString(),
|
||||
});
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
// Synchroniser les préférences quand initialPreferences change
|
||||
useEffect(() => {
|
||||
setPreferences(initialPreferences);
|
||||
setFormData({
|
||||
houseJoinPoints: initialPreferences.houseJoinPoints.toString(),
|
||||
houseLeavePoints: initialPreferences.houseLeavePoints.toString(),
|
||||
houseCreatePoints: initialPreferences.houseCreatePoints.toString(),
|
||||
});
|
||||
}, [initialPreferences]);
|
||||
|
||||
const handleEdit = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const joinPoints = parseInt(formData.houseJoinPoints, 10);
|
||||
const leavePoints = parseInt(formData.houseLeavePoints, 10);
|
||||
const createPoints = parseInt(formData.houseCreatePoints, 10);
|
||||
|
||||
if (isNaN(joinPoints) || joinPoints < 0) {
|
||||
alert("Le nombre de points pour rejoindre une maison doit être un nombre positif");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNaN(leavePoints) || leavePoints < 0) {
|
||||
alert("Le nombre de points pour quitter une maison doit être un nombre positif");
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNaN(createPoints) || createPoints < 0) {
|
||||
alert("Le nombre de points pour créer une maison doit être un nombre positif");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const result = await updateSitePreferences({
|
||||
houseJoinPoints: joinPoints,
|
||||
houseLeavePoints: leavePoints,
|
||||
houseCreatePoints: createPoints,
|
||||
});
|
||||
|
||||
if (result.success && result.data) {
|
||||
setPreferences(result.data);
|
||||
setFormData({
|
||||
houseJoinPoints: result.data.houseJoinPoints.toString(),
|
||||
houseLeavePoints: result.data.houseLeavePoints.toString(),
|
||||
houseCreatePoints: result.data.houseCreatePoints.toString(),
|
||||
});
|
||||
setIsEditing(false);
|
||||
} else {
|
||||
console.error("Error updating preferences:", result.error);
|
||||
alert(result.error || "Erreur lors de la mise à jour");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error updating preferences:", error);
|
||||
alert("Erreur lors de la mise à jour");
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsEditing(false);
|
||||
if (preferences) {
|
||||
setFormData({
|
||||
houseJoinPoints: preferences.houseJoinPoints.toString(),
|
||||
houseLeavePoints: preferences.houseLeavePoints.toString(),
|
||||
houseCreatePoints: preferences.houseCreatePoints.toString(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card variant="default" className="p-3 sm:p-4">
|
||||
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-3 mb-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-pixel-gold font-bold text-base sm:text-lg break-words">
|
||||
Points des Maisons
|
||||
</h3>
|
||||
<p className="text-gray-400 text-xs sm:text-sm">
|
||||
Nombre de points attribués ou retirés pour les actions liées aux maisons
|
||||
</p>
|
||||
</div>
|
||||
{!isEditing && (
|
||||
<Button
|
||||
onClick={handleEdit}
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="whitespace-nowrap flex-shrink-0"
|
||||
>
|
||||
Modifier
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{isEditing ? (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="houseJoinPoints"
|
||||
className="block text-sm font-medium text-pixel-gold mb-2"
|
||||
>
|
||||
Points pour rejoindre une maison
|
||||
</label>
|
||||
<Input
|
||||
id="houseJoinPoints"
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.houseJoinPoints}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
houseJoinPoints: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="100"
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Les utilisateurs gagneront ce nombre de points lorsqu'ils rejoignent une maison
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="houseLeavePoints"
|
||||
className="block text-sm font-medium text-pixel-gold mb-2"
|
||||
>
|
||||
Points retirés en quittant une maison
|
||||
</label>
|
||||
<Input
|
||||
id="houseLeavePoints"
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.houseLeavePoints}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
houseLeavePoints: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="100"
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Les utilisateurs perdront ce nombre de points lorsqu'ils quittent une maison
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="houseCreatePoints"
|
||||
className="block text-sm font-medium text-pixel-gold mb-2"
|
||||
>
|
||||
Points pour créer une maison
|
||||
</label>
|
||||
<Input
|
||||
id="houseCreatePoints"
|
||||
type="number"
|
||||
min="0"
|
||||
value={formData.houseCreatePoints}
|
||||
onChange={(e) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
houseCreatePoints: e.target.value,
|
||||
})
|
||||
}
|
||||
placeholder="100"
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
Les utilisateurs gagneront ce nombre de points lorsqu'ils créent une maison
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-2 pt-4">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="success"
|
||||
size="md"
|
||||
disabled={isSaving}
|
||||
>
|
||||
{isSaving ? "Enregistrement..." : "Enregistrer"}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCancel}
|
||||
variant="secondary"
|
||||
size="md"
|
||||
disabled={isSaving}
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
||||
<span className="text-pixel-gold font-bold text-sm sm:text-base min-w-0 sm:min-w-[200px] flex-shrink-0">
|
||||
Points pour rejoindre:
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg sm:text-xl font-bold text-white">
|
||||
{preferences?.houseJoinPoints ?? 100}
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm text-gray-400">points</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
||||
<span className="text-pixel-gold font-bold text-sm sm:text-base min-w-0 sm:min-w-[200px] flex-shrink-0">
|
||||
Points retirés en quittant:
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg sm:text-xl font-bold text-white">
|
||||
{preferences?.houseLeavePoints ?? 100}
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm text-gray-400">points</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-2 sm:gap-4">
|
||||
<span className="text-pixel-gold font-bold text-sm sm:text-base min-w-0 sm:min-w-[200px] flex-shrink-0">
|
||||
Points pour créer:
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-lg sm:text-xl font-bold text-white">
|
||||
{preferences?.houseCreatePoints ?? 100}
|
||||
</span>
|
||||
<span className="text-xs sm:text-sm text-gray-400">points</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useTransition } from "react";
|
||||
import { useState, useTransition } from "react";
|
||||
import {
|
||||
Avatar,
|
||||
Input,
|
||||
@@ -37,19 +37,18 @@ interface EditingUser {
|
||||
role: string | null;
|
||||
}
|
||||
|
||||
export default function UserManagement() {
|
||||
const [users, setUsers] = useState<User[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
interface UserManagementProps {
|
||||
initialUsers: User[];
|
||||
}
|
||||
|
||||
export default function UserManagement({ initialUsers }: UserManagementProps) {
|
||||
const [users, setUsers] = useState<User[]>(initialUsers);
|
||||
const [editingUser, setEditingUser] = useState<EditingUser | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deletingUserId, setDeletingUserId] = useState<string | null>(null);
|
||||
const [, startTransition] = useTransition();
|
||||
const [uploadingAvatar, setUploadingAvatar] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/admin/users");
|
||||
@@ -59,8 +58,6 @@ export default function UserManagement() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching users:", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -185,10 +182,6 @@ export default function UserManagement() {
|
||||
? Math.max(0, currentEditingUserData.xp + editingUser.xpDelta)
|
||||
: 0;
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{users.length === 0 ? (
|
||||
|
||||
@@ -140,7 +140,7 @@ export default function ChallengesSection({
|
||||
<section className="relative w-full min-h-screen flex flex-col items-center overflow-hidden pt-24 pb-16">
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
className="fixed inset-0 bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url('${backgroundImage}')`,
|
||||
}}
|
||||
@@ -159,9 +159,18 @@ export default function ChallengesSection({
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 w-full max-w-6xl mx-auto px-4 sm:px-8 py-16">
|
||||
<SectionTitle variant="gradient" size="md" className="mb-8 text-center">
|
||||
<SectionTitle
|
||||
variant="gradient"
|
||||
size="xl"
|
||||
subtitle="Défiez vos collègues et gagnez des points"
|
||||
className="mb-16"
|
||||
>
|
||||
DÉFIS ENTRE JOUEURS
|
||||
</SectionTitle>
|
||||
<p className="text-gray-400 text-sm max-w-2xl mx-auto text-center mb-16">
|
||||
Créez des défis personnalisés, acceptez ceux de vos collègues et
|
||||
remportez des récompenses en points pour monter dans le classement
|
||||
</p>
|
||||
|
||||
{successMessage && (
|
||||
<Alert variant="success" className="mb-4">
|
||||
|
||||
@@ -9,34 +9,61 @@ interface EventsSectionProps {
|
||||
}
|
||||
|
||||
export default function EventsSection({ events }: EventsSectionProps) {
|
||||
if (events.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<section className="w-full bg-gray-950 border-t border-pixel-gold/30 py-16">
|
||||
<section
|
||||
className="w-full py-16 border-t relative z-10"
|
||||
style={{
|
||||
backgroundColor: "var(--card-column)",
|
||||
borderColor: "color-mix(in srgb, var(--pixel-gold) 50%, transparent)",
|
||||
borderTopWidth: "2px",
|
||||
}}
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-8">
|
||||
<div className="flex flex-col md:flex-row items-center justify-around gap-8">
|
||||
{events.map((event, index) => (
|
||||
<div key={index} className="flex flex-col items-center">
|
||||
<div className="flex flex-col items-center mb-4">
|
||||
<span className="text-pixel-gold text-xs uppercase tracking-widest mb-2">
|
||||
Événement
|
||||
</span>
|
||||
<div className="w-16 h-px bg-pixel-gold"></div>
|
||||
{events.length === 0 ? (
|
||||
<div className="text-center">
|
||||
<p
|
||||
className="text-base"
|
||||
style={{ color: "var(--muted-foreground)" }}
|
||||
>
|
||||
Aucun événement à venir pour le moment
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col md:flex-row items-center justify-around gap-8">
|
||||
{events.map((event, index) => (
|
||||
<div key={index} className="flex flex-col items-center">
|
||||
<div className="flex flex-col items-center mb-4">
|
||||
<span
|
||||
className="text-xs uppercase tracking-widest mb-2"
|
||||
style={{ color: "var(--pixel-gold)" }}
|
||||
>
|
||||
Événement
|
||||
</span>
|
||||
<div
|
||||
className="w-16 h-px"
|
||||
style={{ backgroundColor: "var(--pixel-gold)" }}
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
className="text-lg font-bold mb-2 uppercase tracking-wide"
|
||||
style={{ color: "var(--foreground)" }}
|
||||
>
|
||||
{new Date(event.date).toLocaleDateString("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</div>
|
||||
<div
|
||||
className="text-base text-center"
|
||||
style={{ color: "var(--foreground)" }}
|
||||
>
|
||||
{event.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-white text-lg font-bold mb-2 uppercase tracking-wide">
|
||||
{new Date(event.date).toLocaleDateString("fr-FR", {
|
||||
day: "numeric",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
})}
|
||||
</div>
|
||||
<div className="text-white text-base text-center">
|
||||
{event.name}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
169
components/houses/HouseCard.tsx
Normal file
169
components/houses/HouseCard.tsx
Normal file
@@ -0,0 +1,169 @@
|
||||
"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) {
|
||||
// Rafraîchir le badge d'invitations/demandes dans le header
|
||||
window.dispatchEvent(new Event("refreshInvitations"));
|
||||
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-lg sm: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>
|
||||
);
|
||||
}
|
||||
|
||||
94
components/houses/HouseForm.tsx
Normal file
94
components/houses/HouseForm.tsx
Normal file
@@ -0,0 +1,94 @@
|
||||
"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) {
|
||||
// Rafraîchir le score dans le header si on crée une maison (pas si on met à jour)
|
||||
if (!house) {
|
||||
window.dispatchEvent(new Event("refreshUserScore"));
|
||||
}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
628
components/houses/HouseManagement.tsx
Normal file
628
components/houses/HouseManagement.tsx
Normal file
@@ -0,0 +1,628 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useTransition, useCallback } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Card from "@/components/ui/Card";
|
||||
import Button from "@/components/ui/Button";
|
||||
import HouseForm from "./HouseForm";
|
||||
import RequestList from "./RequestList";
|
||||
import Alert from "@/components/ui/Alert";
|
||||
import { deleteHouse, leaveHouse, removeMember } from "@/actions/houses/update";
|
||||
import { inviteUser, cancelInvitation } 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 HouseInvitation {
|
||||
id: string;
|
||||
invitee: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
};
|
||||
inviter: {
|
||||
id: string;
|
||||
username: string;
|
||||
avatar: string | null;
|
||||
};
|
||||
status: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
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 [invitations, setInvitations] = useState<HouseInvitation[]>([]);
|
||||
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, isAdmin]);
|
||||
|
||||
const fetchInvitations = useCallback(async () => {
|
||||
if (!house || !isAdmin) return;
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/houses/${house.id}/invitations?status=PENDING`
|
||||
);
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setInvitations(data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching invitations:", error);
|
||||
}
|
||||
}, [house, isAdmin]);
|
||||
|
||||
useEffect(() => {
|
||||
// Utiliser un timeout pour éviter l'appel synchrone de setState dans l'effect
|
||||
const timeout = setTimeout(() => {
|
||||
fetchInvitations();
|
||||
}, 0);
|
||||
return () => clearTimeout(timeout);
|
||||
}, [fetchInvitations]);
|
||||
|
||||
const handleUpdate = useCallback(() => {
|
||||
fetchInvitations();
|
||||
onUpdate?.();
|
||||
}, [fetchInvitations, onUpdate]);
|
||||
|
||||
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) {
|
||||
// Rafraîchir le score dans le header (le créateur perd des points)
|
||||
window.dispatchEvent(new Event("refreshUserScore"));
|
||||
handleUpdate();
|
||||
} 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) {
|
||||
window.dispatchEvent(new Event("refreshUserScore"));
|
||||
handleUpdate();
|
||||
} 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) {
|
||||
// Rafraîchir le badge d'invitations/demandes dans le header (pour l'invité)
|
||||
window.dispatchEvent(new Event("refreshInvitations"));
|
||||
setSuccess("Invitation envoyée");
|
||||
setShowInviteForm(false);
|
||||
setSelectedUserId("");
|
||||
// Rafraîchir la liste des invitations
|
||||
await fetchInvitations();
|
||||
handleUpdate();
|
||||
} 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">
|
||||
<h2
|
||||
className="text-lg sm:text-xl font-bold mb-4"
|
||||
style={{ color: "var(--foreground)" }}
|
||||
>
|
||||
Ma Maison
|
||||
</h2>
|
||||
<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"
|
||||
style={{
|
||||
borderColor: `color-mix(in srgb, var(--accent) 40%, var(--border))`,
|
||||
borderWidth: "2px",
|
||||
boxShadow: `0 0 20px color-mix(in srgb, var(--accent) 10%, transparent)`,
|
||||
}}
|
||||
>
|
||||
<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 sm:text-2xl font-bold mb-2 break-words"
|
||||
style={{
|
||||
color: "var(--accent)",
|
||||
textShadow: `0 0 10px color-mix(in srgb, var(--accent) 30%, transparent)`,
|
||||
}}
|
||||
>
|
||||
{house.name}
|
||||
</h3>
|
||||
{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);
|
||||
handleUpdate();
|
||||
}}
|
||||
onCancel={() => setIsEditing(false)}
|
||||
/>
|
||||
) : (
|
||||
<div>
|
||||
<h4
|
||||
className="text-sm font-semibold uppercase tracking-wider mb-3"
|
||||
style={{
|
||||
color: "var(--primary)",
|
||||
borderBottom: `2px solid color-mix(in srgb, var(--primary) 30%, transparent)`,
|
||||
paddingBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
Membres ({house.memberships?.length ?? 0})
|
||||
{isAdmin && invitations.length > 0 && (
|
||||
<span className="ml-2 text-xs normal-case" style={{ color: "var(--muted-foreground)" }}>
|
||||
• {invitations.length} invitation{invitations.length > 1 ? "s" : ""} en cours
|
||||
</span>
|
||||
)}
|
||||
</h4>
|
||||
<div className="space-y-2">
|
||||
{(house.memberships || []).map((membership) => {
|
||||
const isCurrentUser = membership.user.id === session?.user?.id;
|
||||
const roleColor =
|
||||
membership.role === "OWNER"
|
||||
? "var(--accent)"
|
||||
: membership.role === "ADMIN"
|
||||
? "var(--primary)"
|
||||
: "var(--muted-foreground)";
|
||||
|
||||
return (
|
||||
<div
|
||||
key={membership.id}
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-3 rounded"
|
||||
style={{
|
||||
backgroundColor: isCurrentUser
|
||||
? "color-mix(in srgb, var(--primary) 10%, var(--card-hover))"
|
||||
: "var(--card-hover)",
|
||||
borderLeft: `3px solid ${roleColor}`,
|
||||
borderColor: isCurrentUser
|
||||
? "var(--primary)"
|
||||
: "transparent",
|
||||
}}
|
||||
>
|
||||
<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 border-2"
|
||||
style={{ borderColor: roleColor }}
|
||||
/>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<span
|
||||
className="font-semibold block sm:inline"
|
||||
style={{
|
||||
color: isCurrentUser
|
||||
? "var(--primary)"
|
||||
: "var(--foreground)",
|
||||
}}
|
||||
>
|
||||
{membership.user.username}
|
||||
{isCurrentUser && " (Vous)"}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs block sm:inline sm:ml-2"
|
||||
style={{ color: "var(--muted-foreground)" }}
|
||||
>
|
||||
<span style={{ color: "var(--success)" }}>
|
||||
{membership.user.score} pts
|
||||
</span>
|
||||
{" • "}
|
||||
<span style={{ color: "var(--blue)" }}>
|
||||
Niveau {membership.user.level}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<span
|
||||
className="text-xs uppercase px-2 py-1 rounded font-bold"
|
||||
style={{
|
||||
color: roleColor,
|
||||
backgroundColor: `color-mix(in srgb, ${roleColor} 15%, transparent)`,
|
||||
border: `1px solid color-mix(in srgb, ${roleColor} 30%, transparent)`,
|
||||
}}
|
||||
>
|
||||
{membership.role === "OWNER" && "👑 "}
|
||||
{membership.role}
|
||||
</span>
|
||||
{isAdmin &&
|
||||
!isCurrentUser &&
|
||||
(isOwner || membership.role === "MEMBER") &&
|
||||
membership.role !== "OWNER" && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (
|
||||
confirm(
|
||||
`Êtes-vous sûr de vouloir retirer ${membership.user.username} de la maison ?`
|
||||
)
|
||||
) {
|
||||
startTransition(async () => {
|
||||
const result = await removeMember(
|
||||
house.id,
|
||||
membership.user.id
|
||||
);
|
||||
if (result.success) {
|
||||
// Rafraîchir le score dans le header (le membre retiré perd des points)
|
||||
window.dispatchEvent(
|
||||
new Event("refreshUserScore")
|
||||
);
|
||||
handleUpdate();
|
||||
} else {
|
||||
setError(
|
||||
result.error ||
|
||||
"Erreur lors du retrait du membre"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={isPending}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
>
|
||||
Retirer
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isAdmin && invitations.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h5
|
||||
className="text-xs font-semibold uppercase tracking-wider mb-2"
|
||||
style={{
|
||||
color: "var(--primary)",
|
||||
opacity: 0.7,
|
||||
}}
|
||||
>
|
||||
Invitations en cours
|
||||
</h5>
|
||||
<div className="space-y-2">
|
||||
{invitations
|
||||
.filter((inv) => inv.status === "PENDING")
|
||||
.map((invitation) => (
|
||||
<div
|
||||
key={invitation.id}
|
||||
className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 p-3 rounded"
|
||||
style={{
|
||||
backgroundColor: "var(--card-hover)",
|
||||
borderLeft: `3px solid var(--primary)`,
|
||||
opacity: 0.8,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0 flex-1">
|
||||
{invitation.invitee.avatar && (
|
||||
<img
|
||||
src={invitation.invitee.avatar}
|
||||
alt={invitation.invitee.username}
|
||||
className="w-8 h-8 rounded-full flex-shrink-0 border-2"
|
||||
style={{ borderColor: "var(--primary)" }}
|
||||
/>
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<span
|
||||
className="font-semibold block sm:inline"
|
||||
style={{ color: "var(--foreground)" }}
|
||||
>
|
||||
{invitation.invitee.username}
|
||||
</span>
|
||||
<span
|
||||
className="text-xs block sm:inline sm:ml-2"
|
||||
style={{ color: "var(--muted-foreground)" }}
|
||||
>
|
||||
Invité par {invitation.inviter.username}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<span
|
||||
className="text-xs uppercase px-2 py-1 rounded font-bold"
|
||||
style={{
|
||||
color: "var(--primary)",
|
||||
backgroundColor: `color-mix(in srgb, var(--primary) 15%, transparent)`,
|
||||
border: `1px solid color-mix(in srgb, var(--primary) 30%, transparent)`,
|
||||
}}
|
||||
>
|
||||
En attente
|
||||
</span>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (
|
||||
confirm(
|
||||
`Êtes-vous sûr de vouloir annuler l'invitation pour ${invitation.invitee.username} ?`
|
||||
)
|
||||
) {
|
||||
startTransition(async () => {
|
||||
const result = await cancelInvitation(
|
||||
invitation.id
|
||||
);
|
||||
if (result.success) {
|
||||
window.dispatchEvent(
|
||||
new Event("refreshInvitations")
|
||||
);
|
||||
handleUpdate();
|
||||
} else {
|
||||
setError(
|
||||
result.error ||
|
||||
"Erreur lors de l'annulation"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
disabled={isPending}
|
||||
variant="danger"
|
||||
size="sm"
|
||||
>
|
||||
Annuler
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</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">
|
||||
<h2
|
||||
className="text-lg sm:text-xl font-bold mb-4"
|
||||
style={{
|
||||
color: "var(--purple)",
|
||||
borderBottom: `2px solid color-mix(in srgb, var(--purple) 30%, transparent)`,
|
||||
paddingBottom: "0.5rem",
|
||||
}}
|
||||
>
|
||||
Demandes d'adhésion
|
||||
</h2>
|
||||
<RequestList requests={pendingRequests} onUpdate={handleUpdate} />
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
265
components/houses/HousesSection.tsx
Normal file
265
components/houses/HousesSection.tsx
Normal file
@@ -0,0 +1,265 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } 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 = useCallback(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);
|
||||
}
|
||||
}, [searchTerm]);
|
||||
|
||||
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 {
|
||||
// Utiliser un timeout pour éviter setState synchrone dans effect
|
||||
const timeout = setTimeout(() => {
|
||||
fetchHouses();
|
||||
}, 0);
|
||||
return () => clearTimeout(timeout);
|
||||
}
|
||||
}, [searchTerm, fetchHouses]);
|
||||
|
||||
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="xl"
|
||||
subtitle="Rejoignez une maison ou créez la vôtre"
|
||||
className="mb-16"
|
||||
>
|
||||
MAISONS
|
||||
</SectionTitle>
|
||||
<p className="text-gray-400 text-sm max-w-2xl mx-auto text-center mb-16">
|
||||
Formez des équipes, créez votre propre maison et rivalisez avec les
|
||||
autres maisons pour dominer le classement collectif
|
||||
</p>
|
||||
|
||||
<div className="space-y-4 sm:space-y-6">
|
||||
{session?.user && (
|
||||
<>
|
||||
{invitations.length > 0 && (
|
||||
<Card className="p-4 sm:p-6">
|
||||
<h2
|
||||
className="text-lg sm:text-xl font-bold mb-4"
|
||||
style={{ color: "var(--foreground)" }}
|
||||
>
|
||||
Mes Invitations
|
||||
</h2>
|
||||
<InvitationList
|
||||
invitations={invitations}
|
||||
onUpdate={handleUpdate}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card className="p-4 sm:p-6">
|
||||
<h2
|
||||
className="text-lg sm:text-xl font-bold mb-4"
|
||||
style={{ color: "var(--foreground)" }}
|
||||
>
|
||||
Ma Maison
|
||||
</h2>
|
||||
{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">
|
||||
<h2
|
||||
className="text-lg sm:text-xl font-bold mb-4"
|
||||
style={{ color: "var(--foreground)" }}
|
||||
>
|
||||
Toutes les Maisons
|
||||
</h2>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
129
components/houses/InvitationList.tsx
Normal file
129
components/houses/InvitationList.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
"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) {
|
||||
// Rafraîchir le score dans le header (l'utilisateur reçoit des points)
|
||||
window.dispatchEvent(new Event("refreshUserScore"));
|
||||
// Rafraîchir le badge d'invitations dans le header
|
||||
window.dispatchEvent(new Event("refreshInvitations"));
|
||||
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) {
|
||||
// Rafraîchir le badge d'invitations dans le header
|
||||
window.dispatchEvent(new Event("refreshInvitations"));
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
125
components/houses/RequestList.tsx
Normal file
125
components/houses/RequestList.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
"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) {
|
||||
// Rafraîchir le score dans le header (le requester reçoit des points)
|
||||
window.dispatchEvent(new Event("refreshUserScore"));
|
||||
// Rafraîchir le badge d'invitations/demandes dans le header (le requester n'a plus de demande en attente)
|
||||
window.dispatchEvent(new Event("refreshInvitations"));
|
||||
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) {
|
||||
// Rafraîchir le badge d'invitations/demandes dans le header (le requester n'a plus de demande en attente)
|
||||
window.dispatchEvent(new Event("refreshInvitations"));
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import Link from "next/link";
|
||||
export default function Footer() {
|
||||
return (
|
||||
<footer
|
||||
className="w-full py-6 px-4 sm:px-8 border-t"
|
||||
className="w-full py-6 px-4 sm:px-8 border-t relative z-10"
|
||||
style={{
|
||||
backgroundColor: "var(--background)",
|
||||
borderColor: "color-mix(in srgb, var(--gray-800) 30%, transparent)",
|
||||
|
||||
@@ -26,8 +26,29 @@ interface LeaderboardEntry {
|
||||
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 {
|
||||
leaderboard: LeaderboardEntry[];
|
||||
houseLeaderboard: HouseLeaderboardEntry[];
|
||||
backgroundImage: string;
|
||||
}
|
||||
|
||||
@@ -38,25 +59,34 @@ const formatScore = (score: number): string => {
|
||||
|
||||
export default function LeaderboardSection({
|
||||
leaderboard,
|
||||
houseLeaderboard,
|
||||
backgroundImage,
|
||||
}: LeaderboardSectionProps) {
|
||||
const [selectedEntry, setSelectedEntry] = useState<LeaderboardEntry | null>(
|
||||
null
|
||||
);
|
||||
const [selectedHouse, setSelectedHouse] = useState<HouseLeaderboardEntry | null>(
|
||||
null
|
||||
);
|
||||
|
||||
return (
|
||||
<BackgroundSection backgroundImage={backgroundImage}>
|
||||
{/* Title Section */}
|
||||
<SectionTitle
|
||||
variant="gradient"
|
||||
size="lg"
|
||||
size="xl"
|
||||
subtitle="Top Players"
|
||||
className="mb-12 overflow-hidden"
|
||||
className="mb-16"
|
||||
>
|
||||
LEADERBOARD
|
||||
</SectionTitle>
|
||||
<p className="text-gray-400 text-sm max-w-2xl mx-auto text-center mb-16">
|
||||
Consultez le classement des meilleurs joueurs et des maisons les plus
|
||||
performantes. Montez dans les rangs en participant aux événements et en
|
||||
relevant des défis
|
||||
</p>
|
||||
|
||||
{/* Leaderboard Table */}
|
||||
{/* Players Leaderboard Table */}
|
||||
<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">
|
||||
@@ -143,6 +173,90 @@ export default function LeaderboardSection({
|
||||
</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 */}
|
||||
<div className="mt-8 text-center">
|
||||
<p className="text-gray-500 text-sm">
|
||||
@@ -151,6 +265,112 @@ export default function LeaderboardSection({
|
||||
<p className="text-gray-600 text-xs mt-2">Rankings update every hour</p>
|
||||
</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 */}
|
||||
{selectedEntry && (
|
||||
<Modal
|
||||
|
||||
73
components/navigation/InvitationBadge.tsx
Normal file
73
components/navigation/InvitationBadge.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface InvitationBadgeProps {
|
||||
initialCount?: number;
|
||||
onNavigate?: () => void;
|
||||
}
|
||||
|
||||
export default function InvitationBadge({
|
||||
initialCount = 0,
|
||||
onNavigate,
|
||||
}: InvitationBadgeProps) {
|
||||
const [count, setCount] = useState(initialCount);
|
||||
|
||||
// Utiliser le count initial (déjà récupéré côté serveur)
|
||||
useEffect(() => {
|
||||
setCount(initialCount);
|
||||
}, [initialCount]);
|
||||
|
||||
// Écouter les événements de refresh des invitations (déclenché après acceptation/refus)
|
||||
useEffect(() => {
|
||||
const handleRefreshInvitations = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/invitations/pending-count");
|
||||
const data = await response.json();
|
||||
setCount(data.count || 0);
|
||||
} catch (error) {
|
||||
console.error("Error fetching pending invitations count:", error);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("refreshInvitations", handleRefreshInvitations);
|
||||
return () => {
|
||||
window.removeEventListener("refreshInvitations", handleRefreshInvitations);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Link
|
||||
href="/houses"
|
||||
onClick={onNavigate}
|
||||
className={`inline-flex items-center gap-1.5 transition text-xs font-normal uppercase tracking-widest ${
|
||||
onNavigate ? "py-2" : ""
|
||||
}`}
|
||||
style={{ color: "var(--foreground)" }}
|
||||
onMouseEnter={(e) =>
|
||||
(e.currentTarget.style.color = "var(--accent-color)")
|
||||
}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.color = "var(--foreground)")}
|
||||
title={
|
||||
count > 0
|
||||
? `${count} action${count > 1 ? "s" : ""} en attente (invitations et demandes)`
|
||||
: "Maisons"
|
||||
}
|
||||
>
|
||||
<span>MAISONS</span>
|
||||
{count > 0 && (
|
||||
<span
|
||||
className="flex h-5 w-5 min-w-[20px] items-center justify-center rounded-full text-[10px] font-bold leading-none"
|
||||
style={{
|
||||
backgroundColor: "var(--accent)",
|
||||
color: "var(--background)",
|
||||
}}
|
||||
>
|
||||
{count > 9 ? "9+" : count}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
|
||||
import PlayerStats from "@/components/profile/PlayerStats";
|
||||
import { Button, ThemeToggle } from "@/components/ui";
|
||||
import ChallengeBadge from "./ChallengeBadge";
|
||||
import InvitationBadge from "./InvitationBadge";
|
||||
|
||||
interface UserData {
|
||||
username: string;
|
||||
@@ -23,12 +24,14 @@ interface NavigationProps {
|
||||
initialUserData?: UserData | null;
|
||||
initialIsAdmin?: boolean;
|
||||
initialActiveChallengesCount?: number;
|
||||
initialPendingInvitationsCount?: number;
|
||||
}
|
||||
|
||||
export default function Navigation({
|
||||
initialUserData,
|
||||
initialIsAdmin,
|
||||
initialActiveChallengesCount = 0,
|
||||
initialPendingInvitationsCount = 0,
|
||||
}: NavigationProps) {
|
||||
const { data: session } = useSession();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
@@ -118,7 +121,10 @@ export default function Navigation({
|
||||
LEADERBOARD
|
||||
</Link>
|
||||
{isAuthenticated && (
|
||||
<ChallengeBadge initialCount={initialActiveChallengesCount} />
|
||||
<>
|
||||
<InvitationBadge initialCount={initialPendingInvitationsCount} />
|
||||
<ChallengeBadge initialCount={initialActiveChallengesCount} />
|
||||
</>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Link
|
||||
@@ -279,10 +285,16 @@ export default function Navigation({
|
||||
LEADERBOARD
|
||||
</Link>
|
||||
{isAuthenticated && (
|
||||
<ChallengeBadge
|
||||
initialCount={initialActiveChallengesCount}
|
||||
onNavigate={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
<>
|
||||
<InvitationBadge
|
||||
initialCount={initialPendingInvitationsCount}
|
||||
onNavigate={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
<ChallengeBadge
|
||||
initialCount={initialActiveChallengesCount}
|
||||
onNavigate={() => setIsMenuOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<Link
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import { userService } from "@/services/users/user.service";
|
||||
import { challengeService } from "@/services/challenges/challenge.service";
|
||||
import { houseService } from "@/services/houses/house.service";
|
||||
import Navigation from "./Navigation";
|
||||
|
||||
interface UserData {
|
||||
@@ -20,10 +21,11 @@ export default async function NavigationWrapper() {
|
||||
let userData: UserData | null = null;
|
||||
const isAdmin = session?.user?.role === "ADMIN";
|
||||
let activeChallengesCount = 0;
|
||||
let pendingHouseActionsCount = 0;
|
||||
|
||||
if (session?.user?.id) {
|
||||
// Paralléliser les appels DB
|
||||
const [user, count] = await Promise.all([
|
||||
const [user, challengesCount, houseActionsCount] = await Promise.all([
|
||||
userService.getUserById(session.user.id, {
|
||||
username: true,
|
||||
avatar: true,
|
||||
@@ -35,13 +37,15 @@ export default async function NavigationWrapper() {
|
||||
score: true,
|
||||
}),
|
||||
challengeService.getActiveChallengesCount(session.user.id),
|
||||
houseService.getPendingHouseActionsCount(session.user.id),
|
||||
]);
|
||||
|
||||
if (user) {
|
||||
userData = user;
|
||||
}
|
||||
|
||||
activeChallengesCount = count;
|
||||
activeChallengesCount = challengesCount;
|
||||
pendingHouseActionsCount = houseActionsCount;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -49,6 +53,7 @@ export default async function NavigationWrapper() {
|
||||
initialUserData={userData}
|
||||
initialIsAdmin={isAdmin}
|
||||
initialActiveChallengesCount={activeChallengesCount}
|
||||
initialPendingInvitationsCount={pendingHouseActionsCount}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -171,12 +171,17 @@ export default function ProfileForm({
|
||||
{/* Title Section */}
|
||||
<SectionTitle
|
||||
variant="gradient"
|
||||
size="lg"
|
||||
size="xl"
|
||||
subtitle="Gérez votre profil"
|
||||
className="mb-12"
|
||||
className="mb-16"
|
||||
>
|
||||
PROFIL
|
||||
</SectionTitle>
|
||||
<p className="text-gray-400 text-sm max-w-2xl mx-auto text-center mb-16">
|
||||
Personnalisez votre avatar, votre bio, votre classe de personnage et
|
||||
consultez vos statistiques. Gérez vos préférences et votre mot de
|
||||
passe
|
||||
</p>
|
||||
|
||||
{/* Profile Card */}
|
||||
<Card variant="default" className="overflow-hidden">
|
||||
|
||||
@@ -10,6 +10,7 @@ interface AvatarProps {
|
||||
className?: string;
|
||||
borderClassName?: string;
|
||||
fallbackText?: string;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
@@ -28,6 +29,7 @@ export default function Avatar({
|
||||
className = "",
|
||||
borderClassName = "",
|
||||
fallbackText,
|
||||
style,
|
||||
}: AvatarProps) {
|
||||
const [avatarError, setAvatarError] = useState(false);
|
||||
const prevSrcRef = useRef<string | null | undefined>(undefined);
|
||||
@@ -53,6 +55,7 @@ export default function Avatar({
|
||||
style={{
|
||||
backgroundColor: "var(--card)",
|
||||
borderColor: "var(--border)",
|
||||
...style,
|
||||
}}
|
||||
>
|
||||
{displaySrc ? (
|
||||
|
||||
@@ -22,7 +22,7 @@ export default function BackgroundSection({
|
||||
>
|
||||
{/* Background Image */}
|
||||
<div
|
||||
className="absolute inset-0 bg-cover bg-center bg-no-repeat"
|
||||
className="fixed inset-0 bg-cover bg-center bg-no-repeat z-0"
|
||||
style={{
|
||||
backgroundImage: `url('${backgroundImage}')`,
|
||||
}}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { ButtonHTMLAttributes, ReactNode, ElementType } from "react";
|
||||
import Link from "next/link";
|
||||
|
||||
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
variant?: "primary" | "secondary" | "success" | "danger" | "ghost";
|
||||
size?: "sm" | "md" | "lg";
|
||||
children: ReactNode;
|
||||
as?: ElementType;
|
||||
}
|
||||
} & (
|
||||
| { as?: Exclude<ElementType, typeof Link> }
|
||||
| { as: typeof Link; href: string }
|
||||
);
|
||||
|
||||
const variantClasses = {
|
||||
primary: "btn-primary border transition-colors",
|
||||
|
||||
@@ -35,13 +35,18 @@ services:
|
||||
- "3040:3000"
|
||||
environment:
|
||||
- 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
|
||||
- NEXTAUTH_URL=${NEXTAUTH_URL:-http://localhost:3000}
|
||||
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET:-change-this-secret-in-production}
|
||||
volumes:
|
||||
# Persist uploaded images (avatars and backgrounds)
|
||||
- ${UPLOADS_PATH:-./public/uploads}:/app/public/uploads
|
||||
- ./prisma/migrations:/app/prisma/migrations
|
||||
# Migrations: décommenter uniquement en développement local pour modifier les migrations sans rebuild
|
||||
# En production, les migrations sont incluses dans l'image Docker
|
||||
# - ./prisma/migrations:/app/prisma/migrations
|
||||
depends_on:
|
||||
got-postgres:
|
||||
condition: service_healthy
|
||||
|
||||
@@ -6,6 +6,8 @@ interface Preferences {
|
||||
eventsBackground: string | null;
|
||||
leaderboardBackground: string | null;
|
||||
challengesBackground: string | null;
|
||||
profileBackground: string | null;
|
||||
houseBackground: string | null;
|
||||
}
|
||||
|
||||
export function usePreferences() {
|
||||
@@ -23,6 +25,8 @@ export function usePreferences() {
|
||||
eventsBackground: null,
|
||||
leaderboardBackground: null,
|
||||
challengesBackground: null,
|
||||
profileBackground: null,
|
||||
houseBackground: null,
|
||||
}
|
||||
);
|
||||
setLoading(false);
|
||||
@@ -33,6 +37,8 @@ export function usePreferences() {
|
||||
eventsBackground: null,
|
||||
leaderboardBackground: null,
|
||||
challengesBackground: null,
|
||||
profileBackground: null,
|
||||
houseBackground: null,
|
||||
});
|
||||
setLoading(false);
|
||||
});
|
||||
@@ -42,7 +48,7 @@ export function usePreferences() {
|
||||
}
|
||||
|
||||
export function useBackgroundImage(
|
||||
page: "home" | "events" | "leaderboard" | "challenges",
|
||||
page: "home" | "events" | "leaderboard" | "challenges" | "profile" | "houses",
|
||||
defaultImage: string
|
||||
) {
|
||||
const { preferences } = usePreferences();
|
||||
@@ -51,7 +57,9 @@ export function useBackgroundImage(
|
||||
|
||||
useEffect(() => {
|
||||
if (preferences) {
|
||||
const imageKey = `${page}Background` as keyof Preferences;
|
||||
// Mapping spécial pour "houses" -> "house" (car la colonne est houseBackground)
|
||||
const dbPage = page === "houses" ? "house" : page;
|
||||
const imageKey = `${dbPage}Background` as keyof Preferences;
|
||||
const customImage = preferences[imageKey];
|
||||
const rawImage = customImage || defaultImage;
|
||||
// Normaliser l'URL pour utiliser l'API si nécessaire
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
|
||||
|
||||
export async function getBackgroundImage(
|
||||
page: "home" | "events" | "leaderboard" | "challenges",
|
||||
page: "home" | "events" | "leaderboard" | "challenges" | "profile" | "houses",
|
||||
defaultImage: string
|
||||
): Promise<string> {
|
||||
return sitePreferencesService.getBackgroundImage(page, defaultImage);
|
||||
|
||||
@@ -2,8 +2,27 @@ import { PrismaClient } from "@/prisma/generated/prisma/client";
|
||||
import { PrismaPg } from "@prisma/adapter-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");
|
||||
}
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
connectionString: databaseUrl,
|
||||
});
|
||||
|
||||
const adapter = new PrismaPg(pool);
|
||||
@@ -16,10 +35,7 @@ export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
adapter,
|
||||
log:
|
||||
process.env.NODE_ENV === "development"
|
||||
? ["query", "error", "warn"]
|
||||
: ["error"],
|
||||
log: ["error"],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;
|
||||
|
||||
@@ -52,3 +52,23 @@ export type SitePreferences = Prisma.SitePreferencesModel
|
||||
*
|
||||
*/
|
||||
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
|
||||
/**
|
||||
* 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>
|
||||
}
|
||||
|
||||
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> = {
|
||||
equals?: string | Prisma.StringFieldRefInput<$PrismaModel>
|
||||
in?: string[] | Prisma.ListStringFieldRefInput<$PrismaModel>
|
||||
@@ -539,4 +590,55 @@ export type NestedDateTimeNullableWithAggregatesFilter<$PrismaModel = never> = {
|
||||
_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
|
||||
|
||||
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',
|
||||
EventFeedback: 'EventFeedback',
|
||||
SitePreferences: 'SitePreferences',
|
||||
Challenge: 'Challenge'
|
||||
Challenge: 'Challenge',
|
||||
House: 'House',
|
||||
HouseMembership: 'HouseMembership',
|
||||
HouseInvitation: 'HouseInvitation',
|
||||
HouseRequest: 'HouseRequest'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
@@ -406,7 +410,7 @@ export type TypeMap<ExtArgs extends runtime.Types.Extensions.InternalArgs = runt
|
||||
omit: GlobalOmitOptions
|
||||
}
|
||||
meta: {
|
||||
modelProps: "user" | "userPreferences" | "event" | "eventRegistration" | "eventFeedback" | "sitePreferences" | "challenge"
|
||||
modelProps: "user" | "userPreferences" | "event" | "eventRegistration" | "eventFeedback" | "sitePreferences" | "challenge" | "house" | "houseMembership" | "houseInvitation" | "houseRequest"
|
||||
txIsolationLevel: TransactionIsolationLevel
|
||||
}
|
||||
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: {
|
||||
@@ -1049,8 +1349,13 @@ export const SitePreferencesScalarFieldEnum = {
|
||||
eventsBackground: 'eventsBackground',
|
||||
leaderboardBackground: 'leaderboardBackground',
|
||||
challengesBackground: 'challengesBackground',
|
||||
profileBackground: 'profileBackground',
|
||||
houseBackground: 'houseBackground',
|
||||
eventRegistrationPoints: 'eventRegistrationPoints',
|
||||
eventFeedbackPoints: 'eventFeedbackPoints',
|
||||
houseJoinPoints: 'houseJoinPoints',
|
||||
houseLeavePoints: 'houseLeavePoints',
|
||||
houseCreatePoints: 'houseCreatePoints',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
@@ -1078,6 +1383,54 @@ export const 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 = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
@@ -1213,6 +1566,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'
|
||||
*/
|
||||
@@ -1328,6 +1723,10 @@ export type GlobalOmitConfig = {
|
||||
eventFeedback?: Prisma.EventFeedbackOmit
|
||||
sitePreferences?: Prisma.SitePreferencesOmit
|
||||
challenge?: Prisma.ChallengeOmit
|
||||
house?: Prisma.HouseOmit
|
||||
houseMembership?: Prisma.HouseMembershipOmit
|
||||
houseInvitation?: Prisma.HouseInvitationOmit
|
||||
houseRequest?: Prisma.HouseRequestOmit
|
||||
}
|
||||
|
||||
/* Types for Logging */
|
||||
|
||||
@@ -57,7 +57,11 @@ export const ModelName = {
|
||||
EventRegistration: 'EventRegistration',
|
||||
EventFeedback: 'EventFeedback',
|
||||
SitePreferences: 'SitePreferences',
|
||||
Challenge: 'Challenge'
|
||||
Challenge: 'Challenge',
|
||||
House: 'House',
|
||||
HouseMembership: 'HouseMembership',
|
||||
HouseInvitation: 'HouseInvitation',
|
||||
HouseRequest: 'HouseRequest'
|
||||
} as const
|
||||
|
||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
||||
@@ -158,8 +162,13 @@ export const SitePreferencesScalarFieldEnum = {
|
||||
eventsBackground: 'eventsBackground',
|
||||
leaderboardBackground: 'leaderboardBackground',
|
||||
challengesBackground: 'challengesBackground',
|
||||
profileBackground: 'profileBackground',
|
||||
houseBackground: 'houseBackground',
|
||||
eventRegistrationPoints: 'eventRegistrationPoints',
|
||||
eventFeedbackPoints: 'eventFeedbackPoints',
|
||||
houseJoinPoints: 'houseJoinPoints',
|
||||
houseLeavePoints: 'houseLeavePoints',
|
||||
houseCreatePoints: 'houseCreatePoints',
|
||||
createdAt: 'createdAt',
|
||||
updatedAt: 'updatedAt'
|
||||
} as const
|
||||
@@ -187,6 +196,54 @@ export const 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 = {
|
||||
asc: 'asc',
|
||||
desc: 'desc'
|
||||
|
||||
@@ -15,4 +15,8 @@ export type * from './models/EventRegistration'
|
||||
export type * from './models/EventFeedback'
|
||||
export type * from './models/SitePreferences'
|
||||
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'
|
||||
@@ -29,11 +29,17 @@ export type AggregateSitePreferences = {
|
||||
export type SitePreferencesAvgAggregateOutputType = {
|
||||
eventRegistrationPoints: number | null
|
||||
eventFeedbackPoints: number | null
|
||||
houseJoinPoints: number | null
|
||||
houseLeavePoints: number | null
|
||||
houseCreatePoints: number | null
|
||||
}
|
||||
|
||||
export type SitePreferencesSumAggregateOutputType = {
|
||||
eventRegistrationPoints: number | null
|
||||
eventFeedbackPoints: number | null
|
||||
houseJoinPoints: number | null
|
||||
houseLeavePoints: number | null
|
||||
houseCreatePoints: number | null
|
||||
}
|
||||
|
||||
export type SitePreferencesMinAggregateOutputType = {
|
||||
@@ -42,8 +48,13 @@ export type SitePreferencesMinAggregateOutputType = {
|
||||
eventsBackground: string | null
|
||||
leaderboardBackground: string | null
|
||||
challengesBackground: string | null
|
||||
profileBackground: string | null
|
||||
houseBackground: string | null
|
||||
eventRegistrationPoints: number | null
|
||||
eventFeedbackPoints: number | null
|
||||
houseJoinPoints: number | null
|
||||
houseLeavePoints: number | null
|
||||
houseCreatePoints: number | null
|
||||
createdAt: Date | null
|
||||
updatedAt: Date | null
|
||||
}
|
||||
@@ -54,8 +65,13 @@ export type SitePreferencesMaxAggregateOutputType = {
|
||||
eventsBackground: string | null
|
||||
leaderboardBackground: string | null
|
||||
challengesBackground: string | null
|
||||
profileBackground: string | null
|
||||
houseBackground: string | null
|
||||
eventRegistrationPoints: number | null
|
||||
eventFeedbackPoints: number | null
|
||||
houseJoinPoints: number | null
|
||||
houseLeavePoints: number | null
|
||||
houseCreatePoints: number | null
|
||||
createdAt: Date | null
|
||||
updatedAt: Date | null
|
||||
}
|
||||
@@ -66,8 +82,13 @@ export type SitePreferencesCountAggregateOutputType = {
|
||||
eventsBackground: number
|
||||
leaderboardBackground: number
|
||||
challengesBackground: number
|
||||
profileBackground: number
|
||||
houseBackground: number
|
||||
eventRegistrationPoints: number
|
||||
eventFeedbackPoints: number
|
||||
houseJoinPoints: number
|
||||
houseLeavePoints: number
|
||||
houseCreatePoints: number
|
||||
createdAt: number
|
||||
updatedAt: number
|
||||
_all: number
|
||||
@@ -77,11 +98,17 @@ export type SitePreferencesCountAggregateOutputType = {
|
||||
export type SitePreferencesAvgAggregateInputType = {
|
||||
eventRegistrationPoints?: true
|
||||
eventFeedbackPoints?: true
|
||||
houseJoinPoints?: true
|
||||
houseLeavePoints?: true
|
||||
houseCreatePoints?: true
|
||||
}
|
||||
|
||||
export type SitePreferencesSumAggregateInputType = {
|
||||
eventRegistrationPoints?: true
|
||||
eventFeedbackPoints?: true
|
||||
houseJoinPoints?: true
|
||||
houseLeavePoints?: true
|
||||
houseCreatePoints?: true
|
||||
}
|
||||
|
||||
export type SitePreferencesMinAggregateInputType = {
|
||||
@@ -90,8 +117,13 @@ export type SitePreferencesMinAggregateInputType = {
|
||||
eventsBackground?: true
|
||||
leaderboardBackground?: true
|
||||
challengesBackground?: true
|
||||
profileBackground?: true
|
||||
houseBackground?: true
|
||||
eventRegistrationPoints?: true
|
||||
eventFeedbackPoints?: true
|
||||
houseJoinPoints?: true
|
||||
houseLeavePoints?: true
|
||||
houseCreatePoints?: true
|
||||
createdAt?: true
|
||||
updatedAt?: true
|
||||
}
|
||||
@@ -102,8 +134,13 @@ export type SitePreferencesMaxAggregateInputType = {
|
||||
eventsBackground?: true
|
||||
leaderboardBackground?: true
|
||||
challengesBackground?: true
|
||||
profileBackground?: true
|
||||
houseBackground?: true
|
||||
eventRegistrationPoints?: true
|
||||
eventFeedbackPoints?: true
|
||||
houseJoinPoints?: true
|
||||
houseLeavePoints?: true
|
||||
houseCreatePoints?: true
|
||||
createdAt?: true
|
||||
updatedAt?: true
|
||||
}
|
||||
@@ -114,8 +151,13 @@ export type SitePreferencesCountAggregateInputType = {
|
||||
eventsBackground?: true
|
||||
leaderboardBackground?: true
|
||||
challengesBackground?: true
|
||||
profileBackground?: true
|
||||
houseBackground?: true
|
||||
eventRegistrationPoints?: true
|
||||
eventFeedbackPoints?: true
|
||||
houseJoinPoints?: true
|
||||
houseLeavePoints?: true
|
||||
houseCreatePoints?: true
|
||||
createdAt?: true
|
||||
updatedAt?: true
|
||||
_all?: true
|
||||
@@ -213,8 +255,13 @@ export type SitePreferencesGroupByOutputType = {
|
||||
eventsBackground: string | null
|
||||
leaderboardBackground: string | null
|
||||
challengesBackground: string | null
|
||||
profileBackground: string | null
|
||||
houseBackground: string | null
|
||||
eventRegistrationPoints: number
|
||||
eventFeedbackPoints: number
|
||||
houseJoinPoints: number
|
||||
houseLeavePoints: number
|
||||
houseCreatePoints: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
_count: SitePreferencesCountAggregateOutputType | null
|
||||
@@ -248,8 +295,13 @@ export type SitePreferencesWhereInput = {
|
||||
eventsBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
|
||||
leaderboardBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
|
||||
challengesBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
|
||||
profileBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
|
||||
houseBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
|
||||
eventRegistrationPoints?: Prisma.IntFilter<"SitePreferences"> | number
|
||||
eventFeedbackPoints?: Prisma.IntFilter<"SitePreferences"> | number
|
||||
houseJoinPoints?: Prisma.IntFilter<"SitePreferences"> | number
|
||||
houseLeavePoints?: Prisma.IntFilter<"SitePreferences"> | number
|
||||
houseCreatePoints?: Prisma.IntFilter<"SitePreferences"> | number
|
||||
createdAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string
|
||||
}
|
||||
@@ -260,8 +312,13 @@ export type SitePreferencesOrderByWithRelationInput = {
|
||||
eventsBackground?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
leaderboardBackground?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
challengesBackground?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
profileBackground?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
houseBackground?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
eventRegistrationPoints?: Prisma.SortOrder
|
||||
eventFeedbackPoints?: Prisma.SortOrder
|
||||
houseJoinPoints?: Prisma.SortOrder
|
||||
houseLeavePoints?: Prisma.SortOrder
|
||||
houseCreatePoints?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
}
|
||||
@@ -275,8 +332,13 @@ export type SitePreferencesWhereUniqueInput = Prisma.AtLeast<{
|
||||
eventsBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
|
||||
leaderboardBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
|
||||
challengesBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
|
||||
profileBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
|
||||
houseBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
|
||||
eventRegistrationPoints?: Prisma.IntFilter<"SitePreferences"> | number
|
||||
eventFeedbackPoints?: Prisma.IntFilter<"SitePreferences"> | number
|
||||
houseJoinPoints?: Prisma.IntFilter<"SitePreferences"> | number
|
||||
houseLeavePoints?: Prisma.IntFilter<"SitePreferences"> | number
|
||||
houseCreatePoints?: Prisma.IntFilter<"SitePreferences"> | number
|
||||
createdAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string
|
||||
}, "id">
|
||||
@@ -287,8 +349,13 @@ export type SitePreferencesOrderByWithAggregationInput = {
|
||||
eventsBackground?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
leaderboardBackground?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
challengesBackground?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
profileBackground?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
houseBackground?: Prisma.SortOrderInput | Prisma.SortOrder
|
||||
eventRegistrationPoints?: Prisma.SortOrder
|
||||
eventFeedbackPoints?: Prisma.SortOrder
|
||||
houseJoinPoints?: Prisma.SortOrder
|
||||
houseLeavePoints?: Prisma.SortOrder
|
||||
houseCreatePoints?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
_count?: Prisma.SitePreferencesCountOrderByAggregateInput
|
||||
@@ -307,8 +374,13 @@ export type SitePreferencesScalarWhereWithAggregatesInput = {
|
||||
eventsBackground?: Prisma.StringNullableWithAggregatesFilter<"SitePreferences"> | string | null
|
||||
leaderboardBackground?: Prisma.StringNullableWithAggregatesFilter<"SitePreferences"> | string | null
|
||||
challengesBackground?: Prisma.StringNullableWithAggregatesFilter<"SitePreferences"> | string | null
|
||||
profileBackground?: Prisma.StringNullableWithAggregatesFilter<"SitePreferences"> | string | null
|
||||
houseBackground?: Prisma.StringNullableWithAggregatesFilter<"SitePreferences"> | string | null
|
||||
eventRegistrationPoints?: Prisma.IntWithAggregatesFilter<"SitePreferences"> | number
|
||||
eventFeedbackPoints?: Prisma.IntWithAggregatesFilter<"SitePreferences"> | number
|
||||
houseJoinPoints?: Prisma.IntWithAggregatesFilter<"SitePreferences"> | number
|
||||
houseLeavePoints?: Prisma.IntWithAggregatesFilter<"SitePreferences"> | number
|
||||
houseCreatePoints?: Prisma.IntWithAggregatesFilter<"SitePreferences"> | number
|
||||
createdAt?: Prisma.DateTimeWithAggregatesFilter<"SitePreferences"> | Date | string
|
||||
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"SitePreferences"> | Date | string
|
||||
}
|
||||
@@ -319,8 +391,13 @@ export type SitePreferencesCreateInput = {
|
||||
eventsBackground?: string | null
|
||||
leaderboardBackground?: string | null
|
||||
challengesBackground?: string | null
|
||||
profileBackground?: string | null
|
||||
houseBackground?: string | null
|
||||
eventRegistrationPoints?: number
|
||||
eventFeedbackPoints?: number
|
||||
houseJoinPoints?: number
|
||||
houseLeavePoints?: number
|
||||
houseCreatePoints?: number
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
}
|
||||
@@ -331,8 +408,13 @@ export type SitePreferencesUncheckedCreateInput = {
|
||||
eventsBackground?: string | null
|
||||
leaderboardBackground?: string | null
|
||||
challengesBackground?: string | null
|
||||
profileBackground?: string | null
|
||||
houseBackground?: string | null
|
||||
eventRegistrationPoints?: number
|
||||
eventFeedbackPoints?: number
|
||||
houseJoinPoints?: number
|
||||
houseLeavePoints?: number
|
||||
houseCreatePoints?: number
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
}
|
||||
@@ -343,8 +425,13 @@ export type SitePreferencesUpdateInput = {
|
||||
eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
profileBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
houseBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
houseJoinPoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
houseLeavePoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
houseCreatePoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
@@ -355,8 +442,13 @@ export type SitePreferencesUncheckedUpdateInput = {
|
||||
eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
profileBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
houseBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
houseJoinPoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
houseLeavePoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
houseCreatePoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
@@ -367,8 +459,13 @@ export type SitePreferencesCreateManyInput = {
|
||||
eventsBackground?: string | null
|
||||
leaderboardBackground?: string | null
|
||||
challengesBackground?: string | null
|
||||
profileBackground?: string | null
|
||||
houseBackground?: string | null
|
||||
eventRegistrationPoints?: number
|
||||
eventFeedbackPoints?: number
|
||||
houseJoinPoints?: number
|
||||
houseLeavePoints?: number
|
||||
houseCreatePoints?: number
|
||||
createdAt?: Date | string
|
||||
updatedAt?: Date | string
|
||||
}
|
||||
@@ -379,8 +476,13 @@ export type SitePreferencesUpdateManyMutationInput = {
|
||||
eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
profileBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
houseBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
houseJoinPoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
houseLeavePoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
houseCreatePoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
@@ -391,8 +493,13 @@ export type SitePreferencesUncheckedUpdateManyInput = {
|
||||
eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
profileBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
houseBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
|
||||
eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
houseJoinPoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
houseLeavePoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
houseCreatePoints?: Prisma.IntFieldUpdateOperationsInput | number
|
||||
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
|
||||
}
|
||||
@@ -403,8 +510,13 @@ export type SitePreferencesCountOrderByAggregateInput = {
|
||||
eventsBackground?: Prisma.SortOrder
|
||||
leaderboardBackground?: Prisma.SortOrder
|
||||
challengesBackground?: Prisma.SortOrder
|
||||
profileBackground?: Prisma.SortOrder
|
||||
houseBackground?: Prisma.SortOrder
|
||||
eventRegistrationPoints?: Prisma.SortOrder
|
||||
eventFeedbackPoints?: Prisma.SortOrder
|
||||
houseJoinPoints?: Prisma.SortOrder
|
||||
houseLeavePoints?: Prisma.SortOrder
|
||||
houseCreatePoints?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
}
|
||||
@@ -412,6 +524,9 @@ export type SitePreferencesCountOrderByAggregateInput = {
|
||||
export type SitePreferencesAvgOrderByAggregateInput = {
|
||||
eventRegistrationPoints?: Prisma.SortOrder
|
||||
eventFeedbackPoints?: Prisma.SortOrder
|
||||
houseJoinPoints?: Prisma.SortOrder
|
||||
houseLeavePoints?: Prisma.SortOrder
|
||||
houseCreatePoints?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
export type SitePreferencesMaxOrderByAggregateInput = {
|
||||
@@ -420,8 +535,13 @@ export type SitePreferencesMaxOrderByAggregateInput = {
|
||||
eventsBackground?: Prisma.SortOrder
|
||||
leaderboardBackground?: Prisma.SortOrder
|
||||
challengesBackground?: Prisma.SortOrder
|
||||
profileBackground?: Prisma.SortOrder
|
||||
houseBackground?: Prisma.SortOrder
|
||||
eventRegistrationPoints?: Prisma.SortOrder
|
||||
eventFeedbackPoints?: Prisma.SortOrder
|
||||
houseJoinPoints?: Prisma.SortOrder
|
||||
houseLeavePoints?: Prisma.SortOrder
|
||||
houseCreatePoints?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
}
|
||||
@@ -432,8 +552,13 @@ export type SitePreferencesMinOrderByAggregateInput = {
|
||||
eventsBackground?: Prisma.SortOrder
|
||||
leaderboardBackground?: Prisma.SortOrder
|
||||
challengesBackground?: Prisma.SortOrder
|
||||
profileBackground?: Prisma.SortOrder
|
||||
houseBackground?: Prisma.SortOrder
|
||||
eventRegistrationPoints?: Prisma.SortOrder
|
||||
eventFeedbackPoints?: Prisma.SortOrder
|
||||
houseJoinPoints?: Prisma.SortOrder
|
||||
houseLeavePoints?: Prisma.SortOrder
|
||||
houseCreatePoints?: Prisma.SortOrder
|
||||
createdAt?: Prisma.SortOrder
|
||||
updatedAt?: Prisma.SortOrder
|
||||
}
|
||||
@@ -441,6 +566,9 @@ export type SitePreferencesMinOrderByAggregateInput = {
|
||||
export type SitePreferencesSumOrderByAggregateInput = {
|
||||
eventRegistrationPoints?: Prisma.SortOrder
|
||||
eventFeedbackPoints?: Prisma.SortOrder
|
||||
houseJoinPoints?: Prisma.SortOrder
|
||||
houseLeavePoints?: Prisma.SortOrder
|
||||
houseCreatePoints?: Prisma.SortOrder
|
||||
}
|
||||
|
||||
|
||||
@@ -451,8 +579,13 @@ export type SitePreferencesSelect<ExtArgs extends runtime.Types.Extensions.Inter
|
||||
eventsBackground?: boolean
|
||||
leaderboardBackground?: boolean
|
||||
challengesBackground?: boolean
|
||||
profileBackground?: boolean
|
||||
houseBackground?: boolean
|
||||
eventRegistrationPoints?: boolean
|
||||
eventFeedbackPoints?: boolean
|
||||
houseJoinPoints?: boolean
|
||||
houseLeavePoints?: boolean
|
||||
houseCreatePoints?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
}, ExtArgs["result"]["sitePreferences"]>
|
||||
@@ -463,8 +596,13 @@ export type SitePreferencesSelectCreateManyAndReturn<ExtArgs extends runtime.Typ
|
||||
eventsBackground?: boolean
|
||||
leaderboardBackground?: boolean
|
||||
challengesBackground?: boolean
|
||||
profileBackground?: boolean
|
||||
houseBackground?: boolean
|
||||
eventRegistrationPoints?: boolean
|
||||
eventFeedbackPoints?: boolean
|
||||
houseJoinPoints?: boolean
|
||||
houseLeavePoints?: boolean
|
||||
houseCreatePoints?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
}, ExtArgs["result"]["sitePreferences"]>
|
||||
@@ -475,8 +613,13 @@ export type SitePreferencesSelectUpdateManyAndReturn<ExtArgs extends runtime.Typ
|
||||
eventsBackground?: boolean
|
||||
leaderboardBackground?: boolean
|
||||
challengesBackground?: boolean
|
||||
profileBackground?: boolean
|
||||
houseBackground?: boolean
|
||||
eventRegistrationPoints?: boolean
|
||||
eventFeedbackPoints?: boolean
|
||||
houseJoinPoints?: boolean
|
||||
houseLeavePoints?: boolean
|
||||
houseCreatePoints?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
}, ExtArgs["result"]["sitePreferences"]>
|
||||
@@ -487,13 +630,18 @@ export type SitePreferencesSelectScalar = {
|
||||
eventsBackground?: boolean
|
||||
leaderboardBackground?: boolean
|
||||
challengesBackground?: boolean
|
||||
profileBackground?: boolean
|
||||
houseBackground?: boolean
|
||||
eventRegistrationPoints?: boolean
|
||||
eventFeedbackPoints?: boolean
|
||||
houseJoinPoints?: boolean
|
||||
houseLeavePoints?: boolean
|
||||
houseCreatePoints?: boolean
|
||||
createdAt?: boolean
|
||||
updatedAt?: boolean
|
||||
}
|
||||
|
||||
export type SitePreferencesOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "homeBackground" | "eventsBackground" | "leaderboardBackground" | "challengesBackground" | "eventRegistrationPoints" | "eventFeedbackPoints" | "createdAt" | "updatedAt", ExtArgs["result"]["sitePreferences"]>
|
||||
export type SitePreferencesOmit<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = runtime.Types.Extensions.GetOmit<"id" | "homeBackground" | "eventsBackground" | "leaderboardBackground" | "challengesBackground" | "profileBackground" | "houseBackground" | "eventRegistrationPoints" | "eventFeedbackPoints" | "houseJoinPoints" | "houseLeavePoints" | "houseCreatePoints" | "createdAt" | "updatedAt", ExtArgs["result"]["sitePreferences"]>
|
||||
|
||||
export type $SitePreferencesPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
|
||||
name: "SitePreferences"
|
||||
@@ -504,8 +652,13 @@ export type $SitePreferencesPayload<ExtArgs extends runtime.Types.Extensions.Int
|
||||
eventsBackground: string | null
|
||||
leaderboardBackground: string | null
|
||||
challengesBackground: string | null
|
||||
profileBackground: string | null
|
||||
houseBackground: string | null
|
||||
eventRegistrationPoints: number
|
||||
eventFeedbackPoints: number
|
||||
houseJoinPoints: number
|
||||
houseLeavePoints: number
|
||||
houseCreatePoints: number
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}, ExtArgs["result"]["sitePreferences"]>
|
||||
@@ -936,8 +1089,13 @@ export interface SitePreferencesFieldRefs {
|
||||
readonly eventsBackground: Prisma.FieldRef<"SitePreferences", 'String'>
|
||||
readonly leaderboardBackground: Prisma.FieldRef<"SitePreferences", 'String'>
|
||||
readonly challengesBackground: Prisma.FieldRef<"SitePreferences", 'String'>
|
||||
readonly profileBackground: Prisma.FieldRef<"SitePreferences", 'String'>
|
||||
readonly houseBackground: Prisma.FieldRef<"SitePreferences", 'String'>
|
||||
readonly eventRegistrationPoints: Prisma.FieldRef<"SitePreferences", 'Int'>
|
||||
readonly eventFeedbackPoints: Prisma.FieldRef<"SitePreferences", 'Int'>
|
||||
readonly houseJoinPoints: Prisma.FieldRef<"SitePreferences", 'Int'>
|
||||
readonly houseLeavePoints: Prisma.FieldRef<"SitePreferences", 'Int'>
|
||||
readonly houseCreatePoints: Prisma.FieldRef<"SitePreferences", 'Int'>
|
||||
readonly createdAt: Prisma.FieldRef<"SitePreferences", 'DateTime'>
|
||||
readonly updatedAt: Prisma.FieldRef<"SitePreferences", 'DateTime'>
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,9 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SitePreferences" ADD COLUMN "houseJoinPoints" INTEGER NOT NULL DEFAULT 100;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "SitePreferences" ADD COLUMN "houseLeavePoints" INTEGER NOT NULL DEFAULT 100;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "SitePreferences" ADD COLUMN "houseCreatePoints" INTEGER NOT NULL DEFAULT 100;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SitePreferences" ADD COLUMN "profileBackground" TEXT;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "SitePreferences" ADD COLUMN "houseBackground" TEXT;
|
||||
|
||||
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")
|
||||
challengesAsAdmin Challenge[] @relation("AdminValidator")
|
||||
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([email])
|
||||
@@ -102,8 +107,13 @@ model SitePreferences {
|
||||
eventsBackground String?
|
||||
leaderboardBackground String?
|
||||
challengesBackground String?
|
||||
profileBackground String?
|
||||
houseBackground String?
|
||||
eventRegistrationPoints Int @default(100)
|
||||
eventFeedbackPoints Int @default(100)
|
||||
houseJoinPoints Int @default(100)
|
||||
houseLeavePoints Int @default(100)
|
||||
houseCreatePoints Int @default(100)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
@@ -166,3 +176,87 @@ model Challenge {
|
||||
@@index([status])
|
||||
@@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 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({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
connectionString: databaseUrl,
|
||||
});
|
||||
|
||||
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()
|
||||
|
||||
@@ -346,8 +346,13 @@ export class ChallengeService {
|
||||
where: { id: challengeId },
|
||||
data: updateData,
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error?.code === "P2025") {
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
"code" in error &&
|
||||
error.code === "P2025"
|
||||
) {
|
||||
// Record not found
|
||||
throw new NotFoundError("Défi");
|
||||
}
|
||||
@@ -431,8 +436,13 @@ export class ChallengeService {
|
||||
await prisma.challenge.delete({
|
||||
where: { id: challengeId },
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error?.code === "P2025") {
|
||||
} catch (error: unknown) {
|
||||
if (
|
||||
error &&
|
||||
typeof error === "object" &&
|
||||
"code" in error &&
|
||||
error.code === "P2025"
|
||||
) {
|
||||
// Record not found
|
||||
throw new NotFoundError("Défi");
|
||||
}
|
||||
|
||||
@@ -34,3 +34,10 @@ export class ConflictError extends BusinessError {
|
||||
this.name = "ConflictError";
|
||||
}
|
||||
}
|
||||
|
||||
export class ForbiddenError extends BusinessError {
|
||||
constructor(message: string) {
|
||||
super(message, "FORBIDDEN");
|
||||
this.name = "ForbiddenError";
|
||||
}
|
||||
}
|
||||
|
||||
1254
services/houses/house.service.ts
Normal file
1254
services/houses/house.service.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,13 +2,25 @@ import { prisma } from "../database";
|
||||
import { normalizeBackgroundUrl } from "@/lib/avatars";
|
||||
import type { SitePreferences } from "@/prisma/generated/prisma/client";
|
||||
|
||||
// Type étendu pour les préférences avec les nouveaux champs de points des maisons
|
||||
type SitePreferencesWithHousePoints = SitePreferences & {
|
||||
houseJoinPoints?: number;
|
||||
houseLeavePoints?: number;
|
||||
houseCreatePoints?: number;
|
||||
};
|
||||
|
||||
export interface UpdateSitePreferencesInput {
|
||||
homeBackground?: string | null;
|
||||
eventsBackground?: string | null;
|
||||
leaderboardBackground?: string | null;
|
||||
challengesBackground?: string | null;
|
||||
profileBackground?: string | null;
|
||||
houseBackground?: string | null;
|
||||
eventRegistrationPoints?: number;
|
||||
eventFeedbackPoints?: number;
|
||||
houseJoinPoints?: number;
|
||||
houseLeavePoints?: number;
|
||||
houseCreatePoints?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,12 +52,23 @@ export class SitePreferencesService {
|
||||
eventsBackground: null,
|
||||
leaderboardBackground: null,
|
||||
challengesBackground: null,
|
||||
profileBackground: null,
|
||||
houseBackground: null,
|
||||
eventRegistrationPoints: 100,
|
||||
eventFeedbackPoints: 100,
|
||||
houseJoinPoints: 100,
|
||||
houseLeavePoints: 100,
|
||||
houseCreatePoints: 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// S'assurer que les valeurs par défaut sont présentes même si les colonnes n'existent pas encore
|
||||
const prefs = sitePreferences as SitePreferencesWithHousePoints;
|
||||
if (prefs.houseJoinPoints == null) prefs.houseJoinPoints = 100;
|
||||
if (prefs.houseLeavePoints == null) prefs.houseLeavePoints = 100;
|
||||
if (prefs.houseCreatePoints == null) prefs.houseCreatePoints = 100;
|
||||
|
||||
return sitePreferences;
|
||||
}
|
||||
|
||||
@@ -74,6 +97,14 @@ export class SitePreferencesService {
|
||||
data.challengesBackground === ""
|
||||
? null
|
||||
: (data.challengesBackground ?? undefined),
|
||||
profileBackground:
|
||||
data.profileBackground === ""
|
||||
? null
|
||||
: (data.profileBackground ?? undefined),
|
||||
houseBackground:
|
||||
data.houseBackground === ""
|
||||
? null
|
||||
: (data.houseBackground ?? undefined),
|
||||
eventRegistrationPoints:
|
||||
data.eventRegistrationPoints !== undefined
|
||||
? data.eventRegistrationPoints
|
||||
@@ -82,6 +113,16 @@ export class SitePreferencesService {
|
||||
data.eventFeedbackPoints !== undefined
|
||||
? data.eventFeedbackPoints
|
||||
: undefined,
|
||||
houseJoinPoints:
|
||||
data.houseJoinPoints !== undefined ? data.houseJoinPoints : undefined,
|
||||
houseLeavePoints:
|
||||
data.houseLeavePoints !== undefined
|
||||
? data.houseLeavePoints
|
||||
: undefined,
|
||||
houseCreatePoints:
|
||||
data.houseCreatePoints !== undefined
|
||||
? data.houseCreatePoints
|
||||
: undefined,
|
||||
},
|
||||
create: {
|
||||
id: "global",
|
||||
@@ -97,8 +138,17 @@ export class SitePreferencesService {
|
||||
data.challengesBackground === ""
|
||||
? null
|
||||
: (data.challengesBackground ?? null),
|
||||
profileBackground:
|
||||
data.profileBackground === ""
|
||||
? null
|
||||
: (data.profileBackground ?? null),
|
||||
houseBackground:
|
||||
data.houseBackground === "" ? null : (data.houseBackground ?? null),
|
||||
eventRegistrationPoints: data.eventRegistrationPoints ?? 100,
|
||||
eventFeedbackPoints: data.eventFeedbackPoints ?? 100,
|
||||
houseJoinPoints: data.houseJoinPoints ?? 100,
|
||||
houseLeavePoints: data.houseLeavePoints ?? 100,
|
||||
houseCreatePoints: data.houseCreatePoints ?? 100,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -107,7 +157,13 @@ export class SitePreferencesService {
|
||||
* Récupère l'image de fond pour une page donnée
|
||||
*/
|
||||
async getBackgroundImage(
|
||||
page: "home" | "events" | "leaderboard" | "challenges",
|
||||
page:
|
||||
| "home"
|
||||
| "events"
|
||||
| "leaderboard"
|
||||
| "challenges"
|
||||
| "profile"
|
||||
| "houses",
|
||||
defaultImage: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
@@ -119,7 +175,9 @@ export class SitePreferencesService {
|
||||
return defaultImage;
|
||||
}
|
||||
|
||||
const imageKey = `${page}Background` as keyof typeof sitePreferences;
|
||||
// Mapping spécial pour "houses" -> "house" (car la colonne est houseBackground)
|
||||
const dbPage = page === "houses" ? "house" : page;
|
||||
const imageKey = `${dbPage}Background` as keyof typeof sitePreferences;
|
||||
const customImage = sitePreferences[imageKey];
|
||||
|
||||
const imageUrl = (customImage as string | null) || defaultImage;
|
||||
|
||||
@@ -27,6 +27,26 @@ export interface LeaderboardEntry {
|
||||
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
|
||||
*/
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user