Compare commits

..

12 Commits

Author SHA1 Message Date
Julien Froidefond
0c47bf916c Refactor management components to remove loading state: Eliminate unused loading state and related conditional rendering from ChallengeManagement, EventManagement, FeedbackManagement, HouseManagement, and UserManagement components, simplifying the code and improving readability.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m43s
2025-12-22 08:56:37 +01:00
Julien Froidefond
9bcafe54d3 Add profile and house background preferences to SitePreferences: Extend SitePreferences model and related services to include profileBackground and houseBackground fields. Update API and UI components to support new background settings, enhancing user customization options. 2025-12-22 08:54:51 +01:00
Julien Froidefond
14c767cfc0 Refactor AdminPage and remove AdminPanel component: Simplify admin navigation by redirecting to preferences page and eliminating the AdminPanel component, streamlining the admin interface.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 6m21s
2025-12-19 14:02:06 +01:00
Julien Froidefond
82069c74bc Add HouseManagement integration to AdminPanel and implement removeMemberAsAdmin feature in HouseService: Enhance admin capabilities with new section for house management and functionality to remove members from houses by admins, including points deduction logic. 2025-12-19 13:58:04 +01:00
Julien Froidefond
a062f5573b Refactor Dockerfile to improve DATABASE_URL handling and enhance entrypoint script: Introduce ARG for DATABASE_URL during build, streamline migration commands, and add error handling for migration failures in the entrypoint script.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 5m40s
2025-12-19 09:04:30 +01:00
Julien Froidefond
6e7c5d3eaf Update Dockerfile to include prisma.config.ts and streamline entrypoint script: Add copying of prisma.config.ts for Prisma 7 compatibility and remove unnecessary migration checks from the entrypoint script for improved clarity.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m28s
2025-12-19 09:00:37 +01:00
Julien Froidefond
5dc178543e Update entrypoint script in Dockerfile and remove DATABASE_URL reference from schema.prisma: Modify entrypoint to check for prisma.config.ts instead of schema.prisma, and streamline migration command. Remove DATABASE_URL environment variable usage from schema.prisma for improved clarity.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 1m9s
2025-12-19 08:58:48 +01:00
Julien Froidefond
881b8149e5 Refactor Dockerfile and schema.prisma for improved migration handling: Remove migration checks during build, enhance entrypoint script to validate DATABASE_URL and schema.prisma presence, and update schema.prisma to use environment variable for database URL.
Some checks failed
Deploy with Docker Compose / deploy (push) Failing after 14s
2025-12-19 08:47:06 +01:00
Julien Froidefond
d6a1e21e9f Update Docker configuration for Prisma migrations: Comment out migrations volume in docker-compose.yml for production use, and add checks in Dockerfile to verify the presence of migrations during build and entrypoint execution.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 14s
2025-12-19 08:40:18 +01:00
Julien Froidefond
0b56d625ec Enhance HouseManagement and HousesPage components: Introduce invitation management features, including fetching and displaying pending invitations. Refactor data handling and UI updates for improved user experience and maintainability. Optimize state management with useCallback and useEffect for better performance.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 2m43s
2025-12-18 09:16:13 +01:00
Julien Froidefond
f5dab3cb95 Refactor HousesPage and HouseManagement components: Introduce TypeScript types for house and invitation data structures to enhance type safety. Update data serialization logic for improved clarity and maintainability. Refactor UI components for better readability and consistency, including adjustments to conditional rendering and styling in HouseManagement. Optimize fetch logic in HousesSection with useCallback for performance improvements.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 4m25s
2025-12-18 08:50:14 +01:00
Julien Froidefond
1b82bd9ee6 Implement house points system: Add houseJoinPoints, houseLeavePoints, and houseCreatePoints to SitePreferences model and update related services. Enhance house management features to award and deduct points for house creation, membership removal, and leaving a house. Update environment configuration for PostgreSQL and adjust UI components to reflect new functionalities.
Some checks failed
Deploy with Docker Compose / deploy (push) Has been cancelled
2025-12-18 08:48:31 +01:00
55 changed files with 2921 additions and 495 deletions

2
.env
View File

@@ -25,7 +25,7 @@ POSTGRES_PORT=5433
# Database URL (construite automatiquement si non définie) # Database URL (construite automatiquement si non définie)
# Si vous définissez cette variable, elle sera utilisée telle quelle # Si vous définissez cette variable, elle sera utilisée telle quelle
# Sinon, elle sera construite à partir des variables POSTGRES_* ci-dessus # Sinon, elle sera construite à partir des variables POSTGRES_* ci-dessus
# DATABASE_URL=postgresql://gotgaming:change-this-in-production@got-postgres:5432/gotgaming?schema=public DATABASE_URL=postgresql://gotgaming:change-this-in-production@localhost:5433/gotgaming?schema=public
# Docker Volumes (optionnel) # Docker Volumes (optionnel)
POSTGRES_DATA_PATH=./data/postgres POSTGRES_DATA_PATH=./data/postgres

View File

@@ -19,7 +19,9 @@ RUN corepack enable && corepack prepare pnpm@latest --activate
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . 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 RUN pnpm prisma generate
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
@@ -45,17 +47,20 @@ COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder /app/package.json ./package.json COPY --from=builder /app/package.json ./package.json
COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml COPY --from=builder /app/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=builder /app/next.config.js ./next.config.js COPY --from=builder /app/next.config.js ./next.config.js
RUN mkdir -p /app/prisma/migrations # Copier le répertoire prisma complet (schema + migrations)
COPY --from=builder --chown=nextjs:nodejs /app/prisma/schema.prisma ./prisma/schema.prisma COPY --from=builder --chown=nextjs:nodejs /app/prisma ./prisma
COPY --from=builder --chown=nextjs:nodejs /app/prisma/migrations ./prisma/migrations # Copier prisma.config.ts (nécessaire pour Prisma 7)
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts COPY --from=builder --chown=nextjs:nodejs /app/prisma.config.ts ./prisma.config.ts
ENV DATABASE_URL="postgresql://user:pass@localhost:5432/db"
# Installer seulement les dépendances de production puis générer Prisma Client # 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 \ RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
pnpm install --frozen-lockfile --prod && \ pnpm install --frozen-lockfile --prod && \
pnpm dlx prisma generate 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 # Create uploads directories
RUN mkdir -p /app/public/uploads /app/public/uploads/backgrounds && \ RUN mkdir -p /app/public/uploads /app/public/uploads/backgrounds && \
@@ -65,7 +70,18 @@ RUN echo '#!/bin/sh' > /app/entrypoint.sh && \
echo 'set -e' >> /app/entrypoint.sh && \ echo 'set -e' >> /app/entrypoint.sh && \
echo 'mkdir -p /app/public/uploads' >> /app/entrypoint.sh && \ echo 'mkdir -p /app/public/uploads' >> /app/entrypoint.sh && \
echo 'mkdir -p /app/public/uploads/backgrounds' >> /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 && \ echo 'exec pnpm start' >> /app/entrypoint.sh && \
chmod +x /app/entrypoint.sh && \ chmod +x /app/entrypoint.sh && \
chown nextjs:nodejs /app/entrypoint.sh chown nextjs:nodejs /app/entrypoint.sh

139
actions/admin/houses.ts Normal file
View 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",
};
}
}

View File

@@ -20,8 +20,13 @@ export async function updateSitePreferences(data: {
eventsBackground?: string | null; eventsBackground?: string | null;
leaderboardBackground?: string | null; leaderboardBackground?: string | null;
challengesBackground?: string | null; challengesBackground?: string | null;
profileBackground?: string | null;
houseBackground?: string | null;
eventRegistrationPoints?: number; eventRegistrationPoints?: number;
eventFeedbackPoints?: number; eventFeedbackPoints?: number;
houseJoinPoints?: number;
houseLeavePoints?: number;
houseCreatePoints?: number;
}) { }) {
try { try {
await checkAdminAccess()(); await checkAdminAccess()();
@@ -31,8 +36,13 @@ export async function updateSitePreferences(data: {
eventsBackground: data.eventsBackground, eventsBackground: data.eventsBackground,
leaderboardBackground: data.leaderboardBackground, leaderboardBackground: data.leaderboardBackground,
challengesBackground: data.challengesBackground, challengesBackground: data.challengesBackground,
profileBackground: data.profileBackground,
houseBackground: data.houseBackground,
eventRegistrationPoints: data.eventRegistrationPoints, eventRegistrationPoints: data.eventRegistrationPoints,
eventFeedbackPoints: data.eventFeedbackPoints, eventFeedbackPoints: data.eventFeedbackPoints,
houseJoinPoints: data.houseJoinPoints,
houseLeavePoints: data.houseLeavePoints,
houseCreatePoints: data.houseCreatePoints,
}); });
revalidatePath("/admin"); revalidatePath("/admin");
@@ -40,6 +50,8 @@ export async function updateSitePreferences(data: {
revalidatePath("/events"); revalidatePath("/events");
revalidatePath("/leaderboard"); revalidatePath("/leaderboard");
revalidatePath("/challenges"); revalidatePath("/challenges");
revalidatePath("/profile");
revalidatePath("/houses");
return { success: true, data: preferences }; return { success: true, data: preferences };
} catch (error) { } catch (error) {

View File

@@ -127,3 +127,5 @@ export async function cancelChallenge(challengeId: string) {
}; };
} }
} }

View File

@@ -7,6 +7,7 @@ import {
ValidationError, ValidationError,
ConflictError, ConflictError,
ForbiddenError, ForbiddenError,
NotFoundError,
} from "@/services/errors"; } from "@/services/errors";
export async function updateHouse( export async function updateHouse(
@@ -112,3 +113,37 @@ export async function leaveHouse(houseId: string) {
} }
} }
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",
};
}
}

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

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

View File

@@ -1,41 +1,7 @@
import { redirect } from "next/navigation"; 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 const dynamic = "force-dynamic";
export default async function AdminPage() { export default async function AdminPage() {
const session = await auth(); redirect("/admin/preferences");
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="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 />
<AdminPanel initialPreferences={sitePreferences} />
</main>
);
} }

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

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

View File

@@ -27,3 +27,5 @@ export async function GET() {
); );
} }
} }

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

View File

@@ -13,6 +13,8 @@ export async function GET() {
eventsBackground: null, eventsBackground: null,
leaderboardBackground: null, leaderboardBackground: null,
challengesBackground: null, challengesBackground: null,
profileBackground: null,
houseBackground: null,
}); });
} }
@@ -21,6 +23,8 @@ export async function GET() {
eventsBackground: sitePreferences.eventsBackground, eventsBackground: sitePreferences.eventsBackground,
leaderboardBackground: sitePreferences.leaderboardBackground, leaderboardBackground: sitePreferences.leaderboardBackground,
challengesBackground: sitePreferences.challengesBackground, challengesBackground: sitePreferences.challengesBackground,
profileBackground: sitePreferences.profileBackground,
houseBackground: sitePreferences.houseBackground,
}); });
} catch (error) { } catch (error) {
console.error("Error fetching preferences:", error); console.error("Error fetching preferences:", error);
@@ -30,6 +34,8 @@ export async function GET() {
eventsBackground: null, eventsBackground: null,
leaderboardBackground: null, leaderboardBackground: null,
challengesBackground: null, challengesBackground: null,
profileBackground: null,
houseBackground: null,
}, },
{ status: 200 } { status: 200 }
); );

View File

@@ -39,3 +39,5 @@ export async function GET() {
); );
} }
} }

View File

@@ -4,10 +4,48 @@ import { getBackgroundImage } from "@/lib/preferences";
import NavigationWrapper from "@/components/navigation/NavigationWrapper"; import NavigationWrapper from "@/components/navigation/NavigationWrapper";
import HousesSection from "@/components/houses/HousesSection"; import HousesSection from "@/components/houses/HousesSection";
import { houseService } from "@/services/houses/house.service"; import { houseService } from "@/services/houses/house.service";
import { userService } from "@/services/users/user.service"; import { prisma } from "@/services/database";
import type {
House,
HouseMembership,
HouseInvitation,
} from "@/prisma/generated/prisma/client";
export const dynamic = "force-dynamic"; 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() { export default async function HousesPage() {
const session = await auth(); const session = await auth();
@@ -15,10 +53,39 @@ export default async function HousesPage() {
redirect("/login"); redirect("/login");
} }
const [housesData, myHouseData, invitationsData, users, backgroundImage] = await Promise.all([ const [housesData, myHouseData, invitationsData, users, backgroundImage] =
// Récupérer les maisons await Promise.all([
houseService.getAllHouses({ // Récupérer les maisons
include: { 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: { memberships: {
include: { include: {
user: { user: {
@@ -31,10 +98,6 @@ export default async function HousesPage() {
}, },
}, },
}, },
orderBy: [
{ role: "asc" }, // OWNER, ADMIN, MEMBER
{ user: { score: "desc" } }, // Puis par score décroissant
],
}, },
creator: { creator: {
select: { select: {
@@ -43,70 +106,66 @@ export default async function HousesPage() {
avatar: true, avatar: true,
}, },
}, },
}, }),
}), // Récupérer les invitations de l'utilisateur
// Récupérer la maison de l'utilisateur houseService.getUserInvitations(session.user.id, "PENDING"),
houseService.getUserHouse(session.user.id, { // Récupérer tous les utilisateurs sans maison pour les invitations
memberships: { prisma.user.findMany({
include: { where: {
user: { houseMemberships: {
select: { none: {},
id: true,
username: true,
avatar: true,
score: true,
level: true,
},
}, },
}, },
},
creator: {
select: { select: {
id: true, id: true,
username: true, username: true,
avatar: true, avatar: true,
}, },
}, orderBy: {
}), username: "asc",
// Récupérer les invitations de l'utilisateur },
houseService.getUserInvitations(session.user.id, "PENDING"), }),
// Récupérer tous les utilisateurs pour les invitations getBackgroundImage("houses", "/got-2.jpg"),
userService.getAllUsers({ ]);
select: {
id: true,
username: true,
avatar: true,
},
}),
getBackgroundImage("challenges", "/got-2.jpg"),
]);
// Sérialiser les données pour le client // Sérialiser les données pour le client
const houses = (housesData as any[]).map((house: any) => ({ const houses = (housesData as HouseWithRelations[]).map(
id: house.id, (house: HouseWithRelations) => ({
name: house.name, id: house.id,
description: house.description, name: house.name,
creator: house.creator || { id: house.creatorId, username: "Unknown", avatar: null }, description: house.description,
memberships: (house.memberships || []).map((m: any) => ({ creator: house.creator || {
id: m.id, id: house.creatorId || "",
role: m.role, username: "Unknown",
user: { avatar: null,
id: m.user.id,
username: m.user.username,
avatar: m.user.avatar,
score: m.user.score ?? 0,
level: m.user.level ?? 1,
}, },
})), 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 const myHouse = myHouseData
? { ? {
id: myHouseData.id, id: myHouseData.id,
name: myHouseData.name, name: myHouseData.name,
description: myHouseData.description, description: myHouseData.description,
creator: (myHouseData as any).creator || { id: (myHouseData as any).creatorId, username: "Unknown", avatar: null }, creator: (myHouseData as HouseWithRelations).creator || {
memberships: ((myHouseData as any).memberships || []).map((m: any) => ({ id: (myHouseData as HouseWithRelations).creatorId || "",
username: "Unknown",
avatar: null,
},
memberships: (
(myHouseData as HouseWithRelations).memberships || []
).map((m) => ({
id: m.id, id: m.id,
role: m.role, role: m.role,
user: { user: {
@@ -120,16 +179,18 @@ export default async function HousesPage() {
} }
: null; : null;
const invitations = invitationsData.map((inv: any) => ({ const invitations = (invitationsData as InvitationWithRelations[]).map(
id: inv.id, (inv: InvitationWithRelations) => ({
house: { id: inv.id,
id: inv.house.id, house: {
name: inv.house.name, id: inv.house.id,
}, name: inv.house.name,
inviter: inv.inviter, },
status: inv.status, inviter: inv.inviter,
createdAt: inv.createdAt.toISOString(), status: inv.status,
})); createdAt: inv.createdAt.toISOString(),
})
);
return ( return (
<main className="min-h-screen bg-black relative"> <main className="min-h-screen bg-black relative">

View File

@@ -29,7 +29,7 @@ export default async function ProfilePage() {
score: true, score: true,
createdAt: true, createdAt: true,
}), }),
getBackgroundImage("home", "/got-background.jpg"), getBackgroundImage("profile", "/got-background.jpg"),
]); ]);
if (!user) { if (!user) {

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

View File

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

View File

@@ -11,6 +11,8 @@ interface SitePreferences {
eventsBackground: string | null; eventsBackground: string | null;
leaderboardBackground: string | null; leaderboardBackground: string | null;
challengesBackground: string | null; challengesBackground: string | null;
profileBackground: string | null;
houseBackground: string | null;
eventRegistrationPoints?: number; eventRegistrationPoints?: number;
} }
@@ -23,6 +25,8 @@ const DEFAULT_IMAGES = {
events: "/got-2.jpg", events: "/got-2.jpg",
leaderboard: "/leaderboard-bg.jpg", leaderboard: "/leaderboard-bg.jpg",
challenges: "/got-2.jpg", challenges: "/got-2.jpg",
profile: "/got-background.jpg",
houses: "/got-2.jpg",
}; };
export default function BackgroundPreferences({ export default function BackgroundPreferences({
@@ -64,6 +68,14 @@ export default function BackgroundPreferences({
initialPreferences.challengesBackground, initialPreferences.challengesBackground,
DEFAULT_IMAGES.challenges DEFAULT_IMAGES.challenges
), ),
profileBackground: getFormValue(
initialPreferences.profileBackground,
DEFAULT_IMAGES.profile
),
houseBackground: getFormValue(
initialPreferences.houseBackground,
DEFAULT_IMAGES.houses
),
}), }),
[initialPreferences] [initialPreferences]
); );
@@ -101,6 +113,14 @@ export default function BackgroundPreferences({
formData.challengesBackground, formData.challengesBackground,
DEFAULT_IMAGES.challenges DEFAULT_IMAGES.challenges
), ),
profileBackground: getApiValue(
formData.profileBackground,
DEFAULT_IMAGES.profile
),
houseBackground: getApiValue(
formData.houseBackground,
DEFAULT_IMAGES.houses
),
}; };
const result = await updateSitePreferences(apiData); const result = await updateSitePreferences(apiData);
@@ -125,6 +145,14 @@ export default function BackgroundPreferences({
result.data.challengesBackground, result.data.challengesBackground,
DEFAULT_IMAGES.challenges DEFAULT_IMAGES.challenges
), ),
profileBackground: getFormValue(
result.data.profileBackground,
DEFAULT_IMAGES.profile
),
houseBackground: getFormValue(
result.data.houseBackground,
DEFAULT_IMAGES.houses
),
}); });
setIsEditing(false); setIsEditing(false);
} else { } else {
@@ -157,6 +185,14 @@ export default function BackgroundPreferences({
preferences.challengesBackground, preferences.challengesBackground,
DEFAULT_IMAGES.challenges 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" 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"> <div className="flex flex-col sm:flex-row gap-2 pt-4">
<Button onClick={handleSave} variant="success" size="md"> <Button onClick={handleSave} variant="success" size="md">
Enregistrer Enregistrer
@@ -461,6 +517,118 @@ export default function BackgroundPreferences({
); );
})()} })()}
</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">
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> </div>
)} )}
</Card> </Card>

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useEffect, useState, useTransition } from "react"; import { useState, useTransition } from "react";
import { import {
validateChallenge, validateChallenge,
rejectChallenge, rejectChallenge,
@@ -42,9 +42,12 @@ interface Challenge {
acceptedAt: string | null; acceptedAt: string | null;
} }
export default function ChallengeManagement() { interface ChallengeManagementProps {
const [challenges, setChallenges] = useState<Challenge[]>([]); initialChallenges: Challenge[];
const [loading, setLoading] = useState(true); }
export default function ChallengeManagement({ initialChallenges }: ChallengeManagementProps) {
const [challenges, setChallenges] = useState<Challenge[]>(initialChallenges);
const [selectedChallenge, setSelectedChallenge] = useState<Challenge | null>( const [selectedChallenge, setSelectedChallenge] = useState<Challenge | null>(
null null
); );
@@ -60,10 +63,6 @@ export default function ChallengeManagement() {
const [successMessage, setSuccessMessage] = useState<string | null>(null); const [successMessage, setSuccessMessage] = useState<string | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null); const [errorMessage, setErrorMessage] = useState<string | null>(null);
useEffect(() => {
fetchChallenges();
}, []);
const fetchChallenges = async () => { const fetchChallenges = async () => {
try { try {
const response = await fetch("/api/admin/challenges"); const response = await fetch("/api/admin/challenges");
@@ -73,8 +72,6 @@ export default function ChallengeManagement() {
} }
} catch (error) { } catch (error) {
console.error("Error fetching challenges:", 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) { if (challenges.length === 0) {
return <div className="text-center text-gray-400 py-8">Aucun défi</div>; return <div className="text-center text-gray-400 py-8">Aucun défi</div>;
} }

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect, useTransition } from "react"; import { useState, useTransition } from "react";
import { calculateEventStatus } from "@/lib/eventStatus"; import { calculateEventStatus } from "@/lib/eventStatus";
import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events"; import { createEvent, updateEvent, deleteEvent } from "@/actions/admin/events";
import { import {
@@ -92,9 +92,12 @@ const getStatusLabel = (status: Event["status"]) => {
} }
}; };
export default function EventManagement() { interface EventManagementProps {
const [events, setEvents] = useState<Event[]>([]); initialEvents: Event[];
const [loading, setLoading] = useState(true); }
export default function EventManagement({ initialEvents }: EventManagementProps) {
const [events, setEvents] = useState<Event[]>(initialEvents);
const [editingEvent, setEditingEvent] = useState<Event | null>(null); const [editingEvent, setEditingEvent] = useState<Event | null>(null);
const [isCreating, setIsCreating] = useState(false); const [isCreating, setIsCreating] = useState(false);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@@ -116,10 +119,6 @@ export default function EventManagement() {
maxPlaces: undefined, maxPlaces: undefined,
}); });
useEffect(() => {
fetchEvents();
}, []);
const fetchEvents = async () => { const fetchEvents = async () => {
try { try {
const response = await fetch("/api/admin/events"); const response = await fetch("/api/admin/events");
@@ -129,8 +128,6 @@ export default function EventManagement() {
} }
} catch (error) { } catch (error) {
console.error("Error fetching events:", error); console.error("Error fetching events:", error);
} finally {
setLoading(false);
} }
}; };
@@ -306,10 +303,6 @@ export default function EventManagement() {
}); });
}; };
if (loading) {
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-4"> <div className="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-3 mb-4">

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState } from "react";
import { import {
addFeedbackBonusPoints, addFeedbackBonusPoints,
markFeedbackAsRead, markFeedbackAsRead,
@@ -38,10 +38,17 @@ interface EventStatistics {
feedbackCount: number; feedbackCount: number;
} }
export default function FeedbackManagement() { interface FeedbackManagementProps {
const [feedbacks, setFeedbacks] = useState<Feedback[]>([]); initialFeedbacks: Feedback[];
const [statistics, setStatistics] = useState<EventStatistics[]>([]); initialStatistics: EventStatistics[];
const [loading, setLoading] = useState(true); }
export default function FeedbackManagement({
initialFeedbacks,
initialStatistics,
}: FeedbackManagementProps) {
const [feedbacks, setFeedbacks] = useState<Feedback[]>(initialFeedbacks);
const [statistics, setStatistics] = useState<EventStatistics[]>(initialStatistics);
const [error, setError] = useState(""); const [error, setError] = useState("");
const [selectedEvent, setSelectedEvent] = useState<string | null>(null); const [selectedEvent, setSelectedEvent] = useState<string | null>(null);
const [addingPoints, setAddingPoints] = useState<Record<string, boolean>>( const [addingPoints, setAddingPoints] = useState<Record<string, boolean>>(
@@ -49,10 +56,6 @@ export default function FeedbackManagement() {
); );
const [markingRead, setMarkingRead] = useState<Record<string, boolean>>({}); const [markingRead, setMarkingRead] = useState<Record<string, boolean>>({});
useEffect(() => {
fetchFeedbacks();
}, []);
const fetchFeedbacks = async () => { const fetchFeedbacks = async () => {
try { try {
const response = await fetch("/api/admin/feedback"); const response = await fetch("/api/admin/feedback");
@@ -65,8 +68,6 @@ export default function FeedbackManagement() {
setStatistics(data.statistics || []); setStatistics(data.statistics || []);
} catch { } catch {
setError("Erreur lors du chargement des feedbacks"); 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 ( return (
<div className="space-y-4 sm:space-y-6"> <div className="space-y-4 sm:space-y-6">
{/* Statistiques par événement */} {/* Statistiques par événement */}

View 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 &quot;{viewingMembers.name}&quot;
</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>
);
}

View 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&apos;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&apos;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&apos;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>
);
}

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect, useTransition } from "react"; import { useState, useTransition } from "react";
import { import {
Avatar, Avatar,
Input, Input,
@@ -37,19 +37,18 @@ interface EditingUser {
role: string | null; role: string | null;
} }
export default function UserManagement() { interface UserManagementProps {
const [users, setUsers] = useState<User[]>([]); initialUsers: User[];
const [loading, setLoading] = useState(true); }
export default function UserManagement({ initialUsers }: UserManagementProps) {
const [users, setUsers] = useState<User[]>(initialUsers);
const [editingUser, setEditingUser] = useState<EditingUser | null>(null); const [editingUser, setEditingUser] = useState<EditingUser | null>(null);
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
const [deletingUserId, setDeletingUserId] = useState<string | null>(null); const [deletingUserId, setDeletingUserId] = useState<string | null>(null);
const [, startTransition] = useTransition(); const [, startTransition] = useTransition();
const [uploadingAvatar, setUploadingAvatar] = useState<string | null>(null); const [uploadingAvatar, setUploadingAvatar] = useState<string | null>(null);
useEffect(() => {
fetchUsers();
}, []);
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
const response = await fetch("/api/admin/users"); const response = await fetch("/api/admin/users");
@@ -59,8 +58,6 @@ export default function UserManagement() {
} }
} catch (error) { } catch (error) {
console.error("Error fetching users:", 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) ? Math.max(0, currentEditingUserData.xp + editingUser.xpDelta)
: 0; : 0;
if (loading) {
return <div className="text-center text-gray-400 py-8">Chargement...</div>;
}
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{users.length === 0 ? ( {users.length === 0 ? (

View File

@@ -60,6 +60,8 @@ export default function HouseCard({ house, onRequestSent }: HouseCardProps) {
const result = await requestToJoin(house.id); const result = await requestToJoin(house.id);
if (result.success) { 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"); setSuccess("Demande envoyée avec succès");
onRequestSent?.(); onRequestSent?.();
} else { } else {

View File

@@ -38,6 +38,10 @@ export default function HouseForm({
: await createHouse({ name, description: description || null }); : await createHouse({ name, description: description || null });
if (result.success) { 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?.(); onSuccess?.();
} else { } else {
setError(result.error || "Une erreur est survenue"); setError(result.error || "Une erreur est survenue");

View File

@@ -1,14 +1,14 @@
"use client"; "use client";
import { useState, useEffect, useTransition } from "react"; import { useState, useEffect, useTransition, useCallback } from "react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import Card from "@/components/ui/Card"; import Card from "@/components/ui/Card";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
import HouseForm from "./HouseForm"; import HouseForm from "./HouseForm";
import RequestList from "./RequestList"; import RequestList from "./RequestList";
import Alert from "@/components/ui/Alert"; import Alert from "@/components/ui/Alert";
import { deleteHouse, leaveHouse } from "@/actions/houses/update"; import { deleteHouse, leaveHouse, removeMember } from "@/actions/houses/update";
import { inviteUser } from "@/actions/houses/invitations"; import { inviteUser, cancelInvitation } from "@/actions/houses/invitations";
interface House { interface House {
id: string; id: string;
@@ -38,6 +38,22 @@ interface User {
avatar: string | null; 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 { interface HouseManagementProps {
house: House | null; house: House | null;
users?: User[]; users?: User[];
@@ -76,6 +92,7 @@ export default function HouseManagement({
const [showInviteForm, setShowInviteForm] = useState(false); const [showInviteForm, setShowInviteForm] = useState(false);
const [selectedUserId, setSelectedUserId] = useState(""); const [selectedUserId, setSelectedUserId] = useState("");
const [requests, setRequests] = useState<Request[]>(initialRequests); const [requests, setRequests] = useState<Request[]>(initialRequests);
const [invitations, setInvitations] = useState<HouseInvitation[]>([]);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState<string | null>(null); const [success, setSuccess] = useState<string | null>(null);
@@ -91,7 +108,9 @@ export default function HouseManagement({
const fetchRequests = async () => { const fetchRequests = async () => {
if (!house || !isAdmin) return; if (!house || !isAdmin) return;
try { try {
const response = await fetch(`/api/houses/${house.id}/requests?status=PENDING`); const response = await fetch(
`/api/houses/${house.id}/requests?status=PENDING`
);
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
setRequests(data); setRequests(data);
@@ -101,10 +120,41 @@ export default function HouseManagement({
} }
}; };
fetchRequests(); fetchRequests();
}, [house?.id, isAdmin]); }, [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 = () => { const handleDelete = () => {
if (!house || !confirm("Êtes-vous sûr de vouloir supprimer cette maison ?")) { if (
!house ||
!confirm("Êtes-vous sûr de vouloir supprimer cette maison ?")
) {
return; return;
} }
@@ -112,7 +162,9 @@ export default function HouseManagement({
startTransition(async () => { startTransition(async () => {
const result = await deleteHouse(house.id); const result = await deleteHouse(house.id);
if (result.success) { if (result.success) {
onUpdate?.(); // Rafraîchir le score dans le header (le créateur perd des points)
window.dispatchEvent(new Event("refreshUserScore"));
handleUpdate();
} else { } else {
setError(result.error || "Erreur lors de la suppression"); setError(result.error || "Erreur lors de la suppression");
} }
@@ -128,7 +180,8 @@ export default function HouseManagement({
startTransition(async () => { startTransition(async () => {
const result = await leaveHouse(house.id); const result = await leaveHouse(house.id);
if (result.success) { if (result.success) {
onUpdate?.(); window.dispatchEvent(new Event("refreshUserScore"));
handleUpdate();
} else { } else {
setError(result.error || "Erreur lors de la sortie"); setError(result.error || "Erreur lors de la sortie");
} }
@@ -144,10 +197,14 @@ export default function HouseManagement({
startTransition(async () => { startTransition(async () => {
const result = await inviteUser(house.id, selectedUserId); const result = await inviteUser(house.id, selectedUserId);
if (result.success) { 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"); setSuccess("Invitation envoyée");
setShowInviteForm(false); setShowInviteForm(false);
setSelectedUserId(""); setSelectedUserId("");
onUpdate?.(); // Rafraîchir la liste des invitations
await fetchInvitations();
handleUpdate();
} else { } else {
setError(result.error || "Erreur lors de l'envoi de l'invitation"); setError(result.error || "Erreur lors de l'envoi de l'invitation");
} }
@@ -163,11 +220,17 @@ export default function HouseManagement({
if (!house) { if (!house) {
return ( return (
<Card className="p-6"> <Card className="p-6">
<h2 className="text-lg sm:text-xl font-bold mb-4" style={{ color: "var(--foreground)" }}> <h2
className="text-lg sm:text-xl font-bold mb-4"
style={{ color: "var(--foreground)" }}
>
Ma Maison Ma Maison
</h2> </h2>
<p className="text-sm mb-4" style={{ color: "var(--muted-foreground)" }}> <p
Vous n'êtes membre d'aucune maison pour le moment. className="text-sm mb-4"
style={{ color: "var(--muted-foreground)" }}
>
Vous n&apos;êtes membre d&apos;aucune maison pour le moment.
</p> </p>
</Card> </Card>
); );
@@ -180,7 +243,7 @@ export default function HouseManagement({
style={{ style={{
borderColor: `color-mix(in srgb, var(--accent) 40%, var(--border))`, borderColor: `color-mix(in srgb, var(--accent) 40%, var(--border))`,
borderWidth: "2px", borderWidth: "2px",
boxShadow: `0 0 20px color-mix(in srgb, var(--accent) 10%, transparent)` 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 flex-col sm:flex-row sm:justify-between sm:items-start gap-4 mb-4">
@@ -189,13 +252,16 @@ export default function HouseManagement({
className="text-xl sm:text-2xl font-bold mb-2 break-words" className="text-xl sm:text-2xl font-bold mb-2 break-words"
style={{ style={{
color: "var(--accent)", color: "var(--accent)",
textShadow: `0 0 10px color-mix(in srgb, var(--accent) 30%, transparent)` textShadow: `0 0 10px color-mix(in srgb, var(--accent) 30%, transparent)`,
}} }}
> >
{house.name} {house.name}
</h3> </h3>
{house.description && ( {house.description && (
<p className="text-sm mt-2 break-words" style={{ color: "var(--muted-foreground)" }}> <p
className="text-sm mt-2 break-words"
style={{ color: "var(--muted-foreground)" }}
>
{house.description} {house.description}
</p> </p>
)} )}
@@ -212,29 +278,47 @@ export default function HouseManagement({
{isEditing ? "Annuler" : "Modifier"} {isEditing ? "Annuler" : "Modifier"}
</Button> </Button>
{isOwner && ( {isOwner && (
<Button onClick={handleDelete} variant="danger" size="sm" className="flex-1 sm:flex-none"> <Button
onClick={handleDelete}
variant="danger"
size="sm"
className="flex-1 sm:flex-none"
>
Supprimer Supprimer
</Button> </Button>
)} )}
</> </>
)} )}
{!isOwner && ( {!isOwner && (
<Button onClick={handleLeave} variant="danger" size="sm" className="flex-1 sm:flex-none"> <Button
onClick={handleLeave}
variant="danger"
size="sm"
className="flex-1 sm:flex-none"
>
Quitter Quitter
</Button> </Button>
)} )}
</div> </div>
</div> </div>
{error && <Alert variant="error" className="mb-4">{error}</Alert>} {error && (
{success && <Alert variant="success" className="mb-4">{success}</Alert>} <Alert variant="error" className="mb-4">
{error}
</Alert>
)}
{success && (
<Alert variant="success" className="mb-4">
{success}
</Alert>
)}
{isEditing ? ( {isEditing ? (
<HouseForm <HouseForm
house={house} house={house}
onSuccess={() => { onSuccess={() => {
setIsEditing(false); setIsEditing(false);
onUpdate?.(); handleUpdate();
}} }}
onCancel={() => setIsEditing(false)} onCancel={() => setIsEditing(false)}
/> />
@@ -245,18 +329,25 @@ export default function HouseManagement({
style={{ style={{
color: "var(--primary)", color: "var(--primary)",
borderBottom: `2px solid color-mix(in srgb, var(--primary) 30%, transparent)`, borderBottom: `2px solid color-mix(in srgb, var(--primary) 30%, transparent)`,
paddingBottom: "0.5rem" paddingBottom: "0.5rem",
}} }}
> >
Membres ({house.memberships?.length ?? 0}) 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> </h4>
<div className="space-y-2"> <div className="space-y-2">
{(house.memberships || []).map((membership) => { {(house.memberships || []).map((membership) => {
const isCurrentUser = membership.user.id === session?.user?.id; const isCurrentUser = membership.user.id === session?.user?.id;
const roleColor = const roleColor =
membership.role === "OWNER" ? "var(--accent)" : membership.role === "OWNER"
membership.role === "ADMIN" ? "var(--primary)" : ? "var(--accent)"
"var(--muted-foreground)"; : membership.role === "ADMIN"
? "var(--primary)"
: "var(--muted-foreground)";
return ( return (
<div <div
@@ -267,7 +358,9 @@ export default function HouseManagement({
? "color-mix(in srgb, var(--primary) 10%, var(--card-hover))" ? "color-mix(in srgb, var(--primary) 10%, var(--card-hover))"
: "var(--card-hover)", : "var(--card-hover)",
borderLeft: `3px solid ${roleColor}`, borderLeft: `3px solid ${roleColor}`,
borderColor: isCurrentUser ? "var(--primary)" : "transparent" borderColor: isCurrentUser
? "var(--primary)"
: "transparent",
}} }}
> >
<div className="flex items-center gap-2 min-w-0 flex-1"> <div className="flex items-center gap-2 min-w-0 flex-1">
@@ -283,7 +376,9 @@ export default function HouseManagement({
<span <span
className="font-semibold block sm:inline" className="font-semibold block sm:inline"
style={{ style={{
color: isCurrentUser ? "var(--primary)" : "var(--foreground)" color: isCurrentUser
? "var(--primary)"
: "var(--foreground)",
}} }}
> >
{membership.user.username} {membership.user.username}
@@ -303,22 +398,159 @@ export default function HouseManagement({
</span> </span>
</div> </div>
</div> </div>
<span <div className="flex items-center gap-2 flex-shrink-0">
className="text-xs uppercase flex-shrink-0 px-2 py-1 rounded font-bold" <span
style={{ className="text-xs uppercase px-2 py-1 rounded font-bold"
color: roleColor, style={{
backgroundColor: `color-mix(in srgb, ${roleColor} 15%, transparent)`, color: roleColor,
border: `1px solid color-mix(in srgb, ${roleColor} 30%, transparent)` backgroundColor: `color-mix(in srgb, ${roleColor} 15%, transparent)`,
}} border: `1px solid color-mix(in srgb, ${roleColor} 30%, transparent)`,
> }}
{membership.role === "OWNER" && "👑 "} >
{membership.role} {membership.role === "OWNER" && "👑 "}
</span> {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>
); );
})} })}
</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 && ( {isAdmin && (
<div className="mt-4"> <div className="mt-4">
{showInviteForm ? ( {showInviteForm ? (
@@ -383,15 +615,14 @@ export default function HouseManagement({
style={{ style={{
color: "var(--purple)", color: "var(--purple)",
borderBottom: `2px solid color-mix(in srgb, var(--purple) 30%, transparent)`, borderBottom: `2px solid color-mix(in srgb, var(--purple) 30%, transparent)`,
paddingBottom: "0.5rem" paddingBottom: "0.5rem",
}} }}
> >
Demandes d'adhésion Demandes d&apos;adhésion
</h2> </h2>
<RequestList requests={pendingRequests} onUpdate={onUpdate} /> <RequestList requests={pendingRequests} onUpdate={handleUpdate} />
</Card> </Card>
)} )}
</div> </div>
); );
} }

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import { useState, useEffect, useCallback } from "react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import Card from "@/components/ui/Card"; import Card from "@/components/ui/Card";
import Button from "@/components/ui/Button"; import Button from "@/components/ui/Button";
@@ -78,7 +78,7 @@ export default function HousesSection({
const [showCreateForm, setShowCreateForm] = useState(false); const [showCreateForm, setShowCreateForm] = useState(false);
const [searchTerm, setSearchTerm] = useState(""); const [searchTerm, setSearchTerm] = useState("");
const fetchHouses = async () => { const fetchHouses = useCallback(async () => {
try { try {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (searchTerm) { if (searchTerm) {
@@ -94,7 +94,7 @@ export default function HousesSection({
} catch (error) { } catch (error) {
console.error("Error fetching houses:", error); console.error("Error fetching houses:", error);
} }
}; }, [searchTerm]);
const fetchMyHouse = async () => { const fetchMyHouse = async () => {
try { try {
@@ -129,9 +129,13 @@ export default function HousesSection({
}, 300); }, 300);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
} else { } else {
fetchHouses(); // Utiliser un timeout pour éviter setState synchrone dans effect
const timeout = setTimeout(() => {
fetchHouses();
}, 0);
return () => clearTimeout(timeout);
} }
}, [searchTerm]); }, [searchTerm, fetchHouses]);
const handleUpdate = () => { const handleUpdate = () => {
fetchMyHouse(); fetchMyHouse();
@@ -165,15 +169,24 @@ export default function HousesSection({
<> <>
{invitations.length > 0 && ( {invitations.length > 0 && (
<Card className="p-4 sm:p-6"> <Card className="p-4 sm:p-6">
<h2 className="text-lg sm:text-xl font-bold mb-4" style={{ color: "var(--foreground)" }}> <h2
className="text-lg sm:text-xl font-bold mb-4"
style={{ color: "var(--foreground)" }}
>
Mes Invitations Mes Invitations
</h2> </h2>
<InvitationList invitations={invitations} onUpdate={handleUpdate} /> <InvitationList
invitations={invitations}
onUpdate={handleUpdate}
/>
</Card> </Card>
)} )}
<Card className="p-4 sm:p-6"> <Card className="p-4 sm:p-6">
<h2 className="text-lg sm:text-xl font-bold mb-4" style={{ color: "var(--foreground)" }}> <h2
className="text-lg sm:text-xl font-bold mb-4"
style={{ color: "var(--foreground)" }}
>
Ma Maison Ma Maison
</h2> </h2>
{myHouse ? ( {myHouse ? (
@@ -194,8 +207,12 @@ export default function HousesSection({
/> />
) : ( ) : (
<div> <div>
<p className="text-sm mb-4 break-words" style={{ color: "var(--muted-foreground)" }}> <p
Vous n'êtes membre d'aucune maison. Créez-en une ou demandez à rejoindre une maison existante. className="text-sm mb-4 break-words"
style={{ color: "var(--muted-foreground)" }}
>
Vous n&apos;êtes membre d&apos;aucune maison. Créez-en
une ou demandez à rejoindre une maison existante.
</p> </p>
<Button <Button
onClick={() => setShowCreateForm(true)} onClick={() => setShowCreateForm(true)}
@@ -213,7 +230,10 @@ export default function HousesSection({
)} )}
<Card className="p-4 sm:p-6"> <Card className="p-4 sm:p-6">
<h2 className="text-lg sm:text-xl font-bold mb-4" style={{ color: "var(--foreground)" }}> <h2
className="text-lg sm:text-xl font-bold mb-4"
style={{ color: "var(--foreground)" }}
>
Toutes les Maisons Toutes les Maisons
</h2> </h2>
<div className="mb-4"> <div className="mb-4">
@@ -243,4 +263,3 @@ export default function HousesSection({
</BackgroundSection> </BackgroundSection>
); );
} }

View File

@@ -41,6 +41,10 @@ export default function InvitationList({
startTransition(async () => { startTransition(async () => {
const result = await acceptInvitation(invitationId); const result = await acceptInvitation(invitationId);
if (result.success) { 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?.(); onUpdate?.();
} else { } else {
setError(result.error || "Erreur lors de l'acceptation"); setError(result.error || "Erreur lors de l'acceptation");
@@ -53,6 +57,8 @@ export default function InvitationList({
startTransition(async () => { startTransition(async () => {
const result = await rejectInvitation(invitationId); const result = await rejectInvitation(invitationId);
if (result.success) { if (result.success) {
// Rafraîchir le badge d'invitations dans le header
window.dispatchEvent(new Event("refreshInvitations"));
onUpdate?.(); onUpdate?.();
} else { } else {
setError(result.error || "Erreur lors du refus"); setError(result.error || "Erreur lors du refus");

View File

@@ -37,6 +37,10 @@ export default function RequestList({
startTransition(async () => { startTransition(async () => {
const result = await acceptRequest(requestId); const result = await acceptRequest(requestId);
if (result.success) { 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?.(); onUpdate?.();
} else { } else {
setError(result.error || "Erreur lors de l'acceptation"); setError(result.error || "Erreur lors de l'acceptation");
@@ -49,6 +53,8 @@ export default function RequestList({
startTransition(async () => { startTransition(async () => {
const result = await rejectRequest(requestId); const result = await rejectRequest(requestId);
if (result.success) { 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?.(); onUpdate?.();
} else { } else {
setError(result.error || "Erreur lors du refus"); setError(result.error || "Erreur lors du refus");

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

View File

@@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
import PlayerStats from "@/components/profile/PlayerStats"; import PlayerStats from "@/components/profile/PlayerStats";
import { Button, ThemeToggle } from "@/components/ui"; import { Button, ThemeToggle } from "@/components/ui";
import ChallengeBadge from "./ChallengeBadge"; import ChallengeBadge from "./ChallengeBadge";
import InvitationBadge from "./InvitationBadge";
interface UserData { interface UserData {
username: string; username: string;
@@ -23,12 +24,14 @@ interface NavigationProps {
initialUserData?: UserData | null; initialUserData?: UserData | null;
initialIsAdmin?: boolean; initialIsAdmin?: boolean;
initialActiveChallengesCount?: number; initialActiveChallengesCount?: number;
initialPendingInvitationsCount?: number;
} }
export default function Navigation({ export default function Navigation({
initialUserData, initialUserData,
initialIsAdmin, initialIsAdmin,
initialActiveChallengesCount = 0, initialActiveChallengesCount = 0,
initialPendingInvitationsCount = 0,
}: NavigationProps) { }: NavigationProps) {
const { data: session } = useSession(); const { data: session } = useSession();
const [isMenuOpen, setIsMenuOpen] = useState(false); const [isMenuOpen, setIsMenuOpen] = useState(false);
@@ -119,19 +122,7 @@ export default function Navigation({
</Link> </Link>
{isAuthenticated && ( {isAuthenticated && (
<> <>
<Link <InvitationBadge initialCount={initialPendingInvitationsCount} />
href="/houses"
className="transition text-xs font-normal uppercase tracking-widest"
style={{ color: "var(--foreground)" }}
onMouseEnter={(e) =>
(e.currentTarget.style.color = "var(--accent-color)")
}
onMouseLeave={(e) =>
(e.currentTarget.style.color = "var(--foreground)")
}
>
MAISONS
</Link>
<ChallengeBadge initialCount={initialActiveChallengesCount} /> <ChallengeBadge initialCount={initialActiveChallengesCount} />
</> </>
)} )}
@@ -295,20 +286,10 @@ export default function Navigation({
</Link> </Link>
{isAuthenticated && ( {isAuthenticated && (
<> <>
<Link <InvitationBadge
href="/houses" initialCount={initialPendingInvitationsCount}
onClick={() => setIsMenuOpen(false)} onNavigate={() => setIsMenuOpen(false)}
className="transition text-xs font-normal uppercase tracking-widest py-2" />
style={{ color: "var(--foreground)" }}
onMouseEnter={(e) =>
(e.currentTarget.style.color = "var(--accent-color)")
}
onMouseLeave={(e) =>
(e.currentTarget.style.color = "var(--foreground)")
}
>
MAISONS
</Link>
<ChallengeBadge <ChallengeBadge
initialCount={initialActiveChallengesCount} initialCount={initialActiveChallengesCount}
onNavigate={() => setIsMenuOpen(false)} onNavigate={() => setIsMenuOpen(false)}

View File

@@ -1,6 +1,7 @@
import { auth } from "@/lib/auth"; import { auth } from "@/lib/auth";
import { userService } from "@/services/users/user.service"; import { userService } from "@/services/users/user.service";
import { challengeService } from "@/services/challenges/challenge.service"; import { challengeService } from "@/services/challenges/challenge.service";
import { houseService } from "@/services/houses/house.service";
import Navigation from "./Navigation"; import Navigation from "./Navigation";
interface UserData { interface UserData {
@@ -20,10 +21,11 @@ export default async function NavigationWrapper() {
let userData: UserData | null = null; let userData: UserData | null = null;
const isAdmin = session?.user?.role === "ADMIN"; const isAdmin = session?.user?.role === "ADMIN";
let activeChallengesCount = 0; let activeChallengesCount = 0;
let pendingHouseActionsCount = 0;
if (session?.user?.id) { if (session?.user?.id) {
// Paralléliser les appels DB // Paralléliser les appels DB
const [user, count] = await Promise.all([ const [user, challengesCount, houseActionsCount] = await Promise.all([
userService.getUserById(session.user.id, { userService.getUserById(session.user.id, {
username: true, username: true,
avatar: true, avatar: true,
@@ -35,13 +37,15 @@ export default async function NavigationWrapper() {
score: true, score: true,
}), }),
challengeService.getActiveChallengesCount(session.user.id), challengeService.getActiveChallengesCount(session.user.id),
houseService.getPendingHouseActionsCount(session.user.id),
]); ]);
if (user) { if (user) {
userData = user; userData = user;
} }
activeChallengesCount = count; activeChallengesCount = challengesCount;
pendingHouseActionsCount = houseActionsCount;
} }
return ( return (
@@ -49,6 +53,7 @@ export default async function NavigationWrapper() {
initialUserData={userData} initialUserData={userData}
initialIsAdmin={isAdmin} initialIsAdmin={isAdmin}
initialActiveChallengesCount={activeChallengesCount} initialActiveChallengesCount={activeChallengesCount}
initialPendingInvitationsCount={pendingHouseActionsCount}
/> />
); );
} }

View File

@@ -10,6 +10,7 @@ interface AvatarProps {
className?: string; className?: string;
borderClassName?: string; borderClassName?: string;
fallbackText?: string; fallbackText?: string;
style?: React.CSSProperties;
} }
const sizeClasses = { const sizeClasses = {
@@ -28,6 +29,7 @@ export default function Avatar({
className = "", className = "",
borderClassName = "", borderClassName = "",
fallbackText, fallbackText,
style,
}: AvatarProps) { }: AvatarProps) {
const [avatarError, setAvatarError] = useState(false); const [avatarError, setAvatarError] = useState(false);
const prevSrcRef = useRef<string | null | undefined>(undefined); const prevSrcRef = useRef<string | null | undefined>(undefined);
@@ -53,6 +55,7 @@ export default function Avatar({
style={{ style={{
backgroundColor: "var(--card)", backgroundColor: "var(--card)",
borderColor: "var(--border)", borderColor: "var(--border)",
...style,
}} }}
> >
{displaySrc ? ( {displaySrc ? (

View File

@@ -1,13 +1,17 @@
"use client"; "use client";
import { ButtonHTMLAttributes, ReactNode, ElementType } from "react"; 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"; variant?: "primary" | "secondary" | "success" | "danger" | "ghost";
size?: "sm" | "md" | "lg"; size?: "sm" | "md" | "lg";
children: ReactNode; children: ReactNode;
as?: ElementType; as?: ElementType;
} } & (
| { as?: Exclude<ElementType, typeof Link> }
| { as: typeof Link; href: string }
);
const variantClasses = { const variantClasses = {
primary: "btn-primary border transition-colors", primary: "btn-primary border transition-colors",

View File

@@ -44,7 +44,9 @@ services:
volumes: volumes:
# Persist uploaded images (avatars and backgrounds) # Persist uploaded images (avatars and backgrounds)
- ${UPLOADS_PATH:-./public/uploads}:/app/public/uploads - ${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: depends_on:
got-postgres: got-postgres:
condition: service_healthy condition: service_healthy

View File

@@ -6,6 +6,8 @@ interface Preferences {
eventsBackground: string | null; eventsBackground: string | null;
leaderboardBackground: string | null; leaderboardBackground: string | null;
challengesBackground: string | null; challengesBackground: string | null;
profileBackground: string | null;
houseBackground: string | null;
} }
export function usePreferences() { export function usePreferences() {
@@ -23,6 +25,8 @@ export function usePreferences() {
eventsBackground: null, eventsBackground: null,
leaderboardBackground: null, leaderboardBackground: null,
challengesBackground: null, challengesBackground: null,
profileBackground: null,
houseBackground: null,
} }
); );
setLoading(false); setLoading(false);
@@ -33,6 +37,8 @@ export function usePreferences() {
eventsBackground: null, eventsBackground: null,
leaderboardBackground: null, leaderboardBackground: null,
challengesBackground: null, challengesBackground: null,
profileBackground: null,
houseBackground: null,
}); });
setLoading(false); setLoading(false);
}); });
@@ -42,7 +48,7 @@ export function usePreferences() {
} }
export function useBackgroundImage( export function useBackgroundImage(
page: "home" | "events" | "leaderboard" | "challenges", page: "home" | "events" | "leaderboard" | "challenges" | "profile" | "houses",
defaultImage: string defaultImage: string
) { ) {
const { preferences } = usePreferences(); const { preferences } = usePreferences();
@@ -51,7 +57,9 @@ export function useBackgroundImage(
useEffect(() => { useEffect(() => {
if (preferences) { 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 customImage = preferences[imageKey];
const rawImage = customImage || defaultImage; const rawImage = customImage || defaultImage;
// Normaliser l'URL pour utiliser l'API si nécessaire // Normaliser l'URL pour utiliser l'API si nécessaire

View File

@@ -1,7 +1,7 @@
import { sitePreferencesService } from "@/services/preferences/site-preferences.service"; import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
export async function getBackgroundImage( export async function getBackgroundImage(
page: "home" | "events" | "leaderboard" | "challenges", page: "home" | "events" | "leaderboard" | "challenges" | "profile" | "houses",
defaultImage: string defaultImage: string
): Promise<string> { ): Promise<string> {
return sitePreferencesService.getBackgroundImage(page, defaultImage); return sitePreferencesService.getBackgroundImage(page, defaultImage);

File diff suppressed because one or more lines are too long

View File

@@ -1349,8 +1349,13 @@ export const SitePreferencesScalarFieldEnum = {
eventsBackground: 'eventsBackground', eventsBackground: 'eventsBackground',
leaderboardBackground: 'leaderboardBackground', leaderboardBackground: 'leaderboardBackground',
challengesBackground: 'challengesBackground', challengesBackground: 'challengesBackground',
profileBackground: 'profileBackground',
houseBackground: 'houseBackground',
eventRegistrationPoints: 'eventRegistrationPoints', eventRegistrationPoints: 'eventRegistrationPoints',
eventFeedbackPoints: 'eventFeedbackPoints', eventFeedbackPoints: 'eventFeedbackPoints',
houseJoinPoints: 'houseJoinPoints',
houseLeavePoints: 'houseLeavePoints',
houseCreatePoints: 'houseCreatePoints',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
} as const } as const

View File

@@ -162,8 +162,13 @@ export const SitePreferencesScalarFieldEnum = {
eventsBackground: 'eventsBackground', eventsBackground: 'eventsBackground',
leaderboardBackground: 'leaderboardBackground', leaderboardBackground: 'leaderboardBackground',
challengesBackground: 'challengesBackground', challengesBackground: 'challengesBackground',
profileBackground: 'profileBackground',
houseBackground: 'houseBackground',
eventRegistrationPoints: 'eventRegistrationPoints', eventRegistrationPoints: 'eventRegistrationPoints',
eventFeedbackPoints: 'eventFeedbackPoints', eventFeedbackPoints: 'eventFeedbackPoints',
houseJoinPoints: 'houseJoinPoints',
houseLeavePoints: 'houseLeavePoints',
houseCreatePoints: 'houseCreatePoints',
createdAt: 'createdAt', createdAt: 'createdAt',
updatedAt: 'updatedAt' updatedAt: 'updatedAt'
} as const } as const

View File

@@ -29,11 +29,17 @@ export type AggregateSitePreferences = {
export type SitePreferencesAvgAggregateOutputType = { export type SitePreferencesAvgAggregateOutputType = {
eventRegistrationPoints: number | null eventRegistrationPoints: number | null
eventFeedbackPoints: number | null eventFeedbackPoints: number | null
houseJoinPoints: number | null
houseLeavePoints: number | null
houseCreatePoints: number | null
} }
export type SitePreferencesSumAggregateOutputType = { export type SitePreferencesSumAggregateOutputType = {
eventRegistrationPoints: number | null eventRegistrationPoints: number | null
eventFeedbackPoints: number | null eventFeedbackPoints: number | null
houseJoinPoints: number | null
houseLeavePoints: number | null
houseCreatePoints: number | null
} }
export type SitePreferencesMinAggregateOutputType = { export type SitePreferencesMinAggregateOutputType = {
@@ -42,8 +48,13 @@ export type SitePreferencesMinAggregateOutputType = {
eventsBackground: string | null eventsBackground: string | null
leaderboardBackground: string | null leaderboardBackground: string | null
challengesBackground: string | null challengesBackground: string | null
profileBackground: string | null
houseBackground: string | null
eventRegistrationPoints: number | null eventRegistrationPoints: number | null
eventFeedbackPoints: number | null eventFeedbackPoints: number | null
houseJoinPoints: number | null
houseLeavePoints: number | null
houseCreatePoints: number | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null updatedAt: Date | null
} }
@@ -54,8 +65,13 @@ export type SitePreferencesMaxAggregateOutputType = {
eventsBackground: string | null eventsBackground: string | null
leaderboardBackground: string | null leaderboardBackground: string | null
challengesBackground: string | null challengesBackground: string | null
profileBackground: string | null
houseBackground: string | null
eventRegistrationPoints: number | null eventRegistrationPoints: number | null
eventFeedbackPoints: number | null eventFeedbackPoints: number | null
houseJoinPoints: number | null
houseLeavePoints: number | null
houseCreatePoints: number | null
createdAt: Date | null createdAt: Date | null
updatedAt: Date | null updatedAt: Date | null
} }
@@ -66,8 +82,13 @@ export type SitePreferencesCountAggregateOutputType = {
eventsBackground: number eventsBackground: number
leaderboardBackground: number leaderboardBackground: number
challengesBackground: number challengesBackground: number
profileBackground: number
houseBackground: number
eventRegistrationPoints: number eventRegistrationPoints: number
eventFeedbackPoints: number eventFeedbackPoints: number
houseJoinPoints: number
houseLeavePoints: number
houseCreatePoints: number
createdAt: number createdAt: number
updatedAt: number updatedAt: number
_all: number _all: number
@@ -77,11 +98,17 @@ export type SitePreferencesCountAggregateOutputType = {
export type SitePreferencesAvgAggregateInputType = { export type SitePreferencesAvgAggregateInputType = {
eventRegistrationPoints?: true eventRegistrationPoints?: true
eventFeedbackPoints?: true eventFeedbackPoints?: true
houseJoinPoints?: true
houseLeavePoints?: true
houseCreatePoints?: true
} }
export type SitePreferencesSumAggregateInputType = { export type SitePreferencesSumAggregateInputType = {
eventRegistrationPoints?: true eventRegistrationPoints?: true
eventFeedbackPoints?: true eventFeedbackPoints?: true
houseJoinPoints?: true
houseLeavePoints?: true
houseCreatePoints?: true
} }
export type SitePreferencesMinAggregateInputType = { export type SitePreferencesMinAggregateInputType = {
@@ -90,8 +117,13 @@ export type SitePreferencesMinAggregateInputType = {
eventsBackground?: true eventsBackground?: true
leaderboardBackground?: true leaderboardBackground?: true
challengesBackground?: true challengesBackground?: true
profileBackground?: true
houseBackground?: true
eventRegistrationPoints?: true eventRegistrationPoints?: true
eventFeedbackPoints?: true eventFeedbackPoints?: true
houseJoinPoints?: true
houseLeavePoints?: true
houseCreatePoints?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
} }
@@ -102,8 +134,13 @@ export type SitePreferencesMaxAggregateInputType = {
eventsBackground?: true eventsBackground?: true
leaderboardBackground?: true leaderboardBackground?: true
challengesBackground?: true challengesBackground?: true
profileBackground?: true
houseBackground?: true
eventRegistrationPoints?: true eventRegistrationPoints?: true
eventFeedbackPoints?: true eventFeedbackPoints?: true
houseJoinPoints?: true
houseLeavePoints?: true
houseCreatePoints?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
} }
@@ -114,8 +151,13 @@ export type SitePreferencesCountAggregateInputType = {
eventsBackground?: true eventsBackground?: true
leaderboardBackground?: true leaderboardBackground?: true
challengesBackground?: true challengesBackground?: true
profileBackground?: true
houseBackground?: true
eventRegistrationPoints?: true eventRegistrationPoints?: true
eventFeedbackPoints?: true eventFeedbackPoints?: true
houseJoinPoints?: true
houseLeavePoints?: true
houseCreatePoints?: true
createdAt?: true createdAt?: true
updatedAt?: true updatedAt?: true
_all?: true _all?: true
@@ -213,8 +255,13 @@ export type SitePreferencesGroupByOutputType = {
eventsBackground: string | null eventsBackground: string | null
leaderboardBackground: string | null leaderboardBackground: string | null
challengesBackground: string | null challengesBackground: string | null
profileBackground: string | null
houseBackground: string | null
eventRegistrationPoints: number eventRegistrationPoints: number
eventFeedbackPoints: number eventFeedbackPoints: number
houseJoinPoints: number
houseLeavePoints: number
houseCreatePoints: number
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
_count: SitePreferencesCountAggregateOutputType | null _count: SitePreferencesCountAggregateOutputType | null
@@ -248,8 +295,13 @@ export type SitePreferencesWhereInput = {
eventsBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null eventsBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
leaderboardBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null leaderboardBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
challengesBackground?: 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 eventRegistrationPoints?: Prisma.IntFilter<"SitePreferences"> | number
eventFeedbackPoints?: 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 createdAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string updatedAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string
} }
@@ -260,8 +312,13 @@ export type SitePreferencesOrderByWithRelationInput = {
eventsBackground?: Prisma.SortOrderInput | Prisma.SortOrder eventsBackground?: Prisma.SortOrderInput | Prisma.SortOrder
leaderboardBackground?: Prisma.SortOrderInput | Prisma.SortOrder leaderboardBackground?: Prisma.SortOrderInput | Prisma.SortOrder
challengesBackground?: Prisma.SortOrderInput | Prisma.SortOrder challengesBackground?: Prisma.SortOrderInput | Prisma.SortOrder
profileBackground?: Prisma.SortOrderInput | Prisma.SortOrder
houseBackground?: Prisma.SortOrderInput | Prisma.SortOrder
eventRegistrationPoints?: Prisma.SortOrder eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder eventFeedbackPoints?: Prisma.SortOrder
houseJoinPoints?: Prisma.SortOrder
houseLeavePoints?: Prisma.SortOrder
houseCreatePoints?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
} }
@@ -275,8 +332,13 @@ export type SitePreferencesWhereUniqueInput = Prisma.AtLeast<{
eventsBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null eventsBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
leaderboardBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null leaderboardBackground?: Prisma.StringNullableFilter<"SitePreferences"> | string | null
challengesBackground?: 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 eventRegistrationPoints?: Prisma.IntFilter<"SitePreferences"> | number
eventFeedbackPoints?: 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 createdAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string
updatedAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string updatedAt?: Prisma.DateTimeFilter<"SitePreferences"> | Date | string
}, "id"> }, "id">
@@ -287,8 +349,13 @@ export type SitePreferencesOrderByWithAggregationInput = {
eventsBackground?: Prisma.SortOrderInput | Prisma.SortOrder eventsBackground?: Prisma.SortOrderInput | Prisma.SortOrder
leaderboardBackground?: Prisma.SortOrderInput | Prisma.SortOrder leaderboardBackground?: Prisma.SortOrderInput | Prisma.SortOrder
challengesBackground?: Prisma.SortOrderInput | Prisma.SortOrder challengesBackground?: Prisma.SortOrderInput | Prisma.SortOrder
profileBackground?: Prisma.SortOrderInput | Prisma.SortOrder
houseBackground?: Prisma.SortOrderInput | Prisma.SortOrder
eventRegistrationPoints?: Prisma.SortOrder eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder eventFeedbackPoints?: Prisma.SortOrder
houseJoinPoints?: Prisma.SortOrder
houseLeavePoints?: Prisma.SortOrder
houseCreatePoints?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
_count?: Prisma.SitePreferencesCountOrderByAggregateInput _count?: Prisma.SitePreferencesCountOrderByAggregateInput
@@ -307,8 +374,13 @@ export type SitePreferencesScalarWhereWithAggregatesInput = {
eventsBackground?: Prisma.StringNullableWithAggregatesFilter<"SitePreferences"> | string | null eventsBackground?: Prisma.StringNullableWithAggregatesFilter<"SitePreferences"> | string | null
leaderboardBackground?: Prisma.StringNullableWithAggregatesFilter<"SitePreferences"> | string | null leaderboardBackground?: Prisma.StringNullableWithAggregatesFilter<"SitePreferences"> | string | null
challengesBackground?: 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 eventRegistrationPoints?: Prisma.IntWithAggregatesFilter<"SitePreferences"> | number
eventFeedbackPoints?: 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 createdAt?: Prisma.DateTimeWithAggregatesFilter<"SitePreferences"> | Date | string
updatedAt?: Prisma.DateTimeWithAggregatesFilter<"SitePreferences"> | Date | string updatedAt?: Prisma.DateTimeWithAggregatesFilter<"SitePreferences"> | Date | string
} }
@@ -319,8 +391,13 @@ export type SitePreferencesCreateInput = {
eventsBackground?: string | null eventsBackground?: string | null
leaderboardBackground?: string | null leaderboardBackground?: string | null
challengesBackground?: string | null challengesBackground?: string | null
profileBackground?: string | null
houseBackground?: string | null
eventRegistrationPoints?: number eventRegistrationPoints?: number
eventFeedbackPoints?: number eventFeedbackPoints?: number
houseJoinPoints?: number
houseLeavePoints?: number
houseCreatePoints?: number
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
} }
@@ -331,8 +408,13 @@ export type SitePreferencesUncheckedCreateInput = {
eventsBackground?: string | null eventsBackground?: string | null
leaderboardBackground?: string | null leaderboardBackground?: string | null
challengesBackground?: string | null challengesBackground?: string | null
profileBackground?: string | null
houseBackground?: string | null
eventRegistrationPoints?: number eventRegistrationPoints?: number
eventFeedbackPoints?: number eventFeedbackPoints?: number
houseJoinPoints?: number
houseLeavePoints?: number
houseCreatePoints?: number
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
} }
@@ -343,8 +425,13 @@ export type SitePreferencesUpdateInput = {
eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
profileBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
houseBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number
eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number
houseJoinPoints?: Prisma.IntFieldUpdateOperationsInput | number
houseLeavePoints?: Prisma.IntFieldUpdateOperationsInput | number
houseCreatePoints?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
@@ -355,8 +442,13 @@ export type SitePreferencesUncheckedUpdateInput = {
eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
profileBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
houseBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number
eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number
houseJoinPoints?: Prisma.IntFieldUpdateOperationsInput | number
houseLeavePoints?: Prisma.IntFieldUpdateOperationsInput | number
houseCreatePoints?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
@@ -367,8 +459,13 @@ export type SitePreferencesCreateManyInput = {
eventsBackground?: string | null eventsBackground?: string | null
leaderboardBackground?: string | null leaderboardBackground?: string | null
challengesBackground?: string | null challengesBackground?: string | null
profileBackground?: string | null
houseBackground?: string | null
eventRegistrationPoints?: number eventRegistrationPoints?: number
eventFeedbackPoints?: number eventFeedbackPoints?: number
houseJoinPoints?: number
houseLeavePoints?: number
houseCreatePoints?: number
createdAt?: Date | string createdAt?: Date | string
updatedAt?: Date | string updatedAt?: Date | string
} }
@@ -379,8 +476,13 @@ export type SitePreferencesUpdateManyMutationInput = {
eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
profileBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
houseBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number
eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number
houseJoinPoints?: Prisma.IntFieldUpdateOperationsInput | number
houseLeavePoints?: Prisma.IntFieldUpdateOperationsInput | number
houseCreatePoints?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
@@ -391,8 +493,13 @@ export type SitePreferencesUncheckedUpdateManyInput = {
eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null eventsBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null leaderboardBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null challengesBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
profileBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
houseBackground?: Prisma.NullableStringFieldUpdateOperationsInput | string | null
eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number eventRegistrationPoints?: Prisma.IntFieldUpdateOperationsInput | number
eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number eventFeedbackPoints?: Prisma.IntFieldUpdateOperationsInput | number
houseJoinPoints?: Prisma.IntFieldUpdateOperationsInput | number
houseLeavePoints?: Prisma.IntFieldUpdateOperationsInput | number
houseCreatePoints?: Prisma.IntFieldUpdateOperationsInput | number
createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string createdAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string updatedAt?: Prisma.DateTimeFieldUpdateOperationsInput | Date | string
} }
@@ -403,8 +510,13 @@ export type SitePreferencesCountOrderByAggregateInput = {
eventsBackground?: Prisma.SortOrder eventsBackground?: Prisma.SortOrder
leaderboardBackground?: Prisma.SortOrder leaderboardBackground?: Prisma.SortOrder
challengesBackground?: Prisma.SortOrder challengesBackground?: Prisma.SortOrder
profileBackground?: Prisma.SortOrder
houseBackground?: Prisma.SortOrder
eventRegistrationPoints?: Prisma.SortOrder eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder eventFeedbackPoints?: Prisma.SortOrder
houseJoinPoints?: Prisma.SortOrder
houseLeavePoints?: Prisma.SortOrder
houseCreatePoints?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
} }
@@ -412,6 +524,9 @@ export type SitePreferencesCountOrderByAggregateInput = {
export type SitePreferencesAvgOrderByAggregateInput = { export type SitePreferencesAvgOrderByAggregateInput = {
eventRegistrationPoints?: Prisma.SortOrder eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder eventFeedbackPoints?: Prisma.SortOrder
houseJoinPoints?: Prisma.SortOrder
houseLeavePoints?: Prisma.SortOrder
houseCreatePoints?: Prisma.SortOrder
} }
export type SitePreferencesMaxOrderByAggregateInput = { export type SitePreferencesMaxOrderByAggregateInput = {
@@ -420,8 +535,13 @@ export type SitePreferencesMaxOrderByAggregateInput = {
eventsBackground?: Prisma.SortOrder eventsBackground?: Prisma.SortOrder
leaderboardBackground?: Prisma.SortOrder leaderboardBackground?: Prisma.SortOrder
challengesBackground?: Prisma.SortOrder challengesBackground?: Prisma.SortOrder
profileBackground?: Prisma.SortOrder
houseBackground?: Prisma.SortOrder
eventRegistrationPoints?: Prisma.SortOrder eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder eventFeedbackPoints?: Prisma.SortOrder
houseJoinPoints?: Prisma.SortOrder
houseLeavePoints?: Prisma.SortOrder
houseCreatePoints?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
} }
@@ -432,8 +552,13 @@ export type SitePreferencesMinOrderByAggregateInput = {
eventsBackground?: Prisma.SortOrder eventsBackground?: Prisma.SortOrder
leaderboardBackground?: Prisma.SortOrder leaderboardBackground?: Prisma.SortOrder
challengesBackground?: Prisma.SortOrder challengesBackground?: Prisma.SortOrder
profileBackground?: Prisma.SortOrder
houseBackground?: Prisma.SortOrder
eventRegistrationPoints?: Prisma.SortOrder eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: Prisma.SortOrder eventFeedbackPoints?: Prisma.SortOrder
houseJoinPoints?: Prisma.SortOrder
houseLeavePoints?: Prisma.SortOrder
houseCreatePoints?: Prisma.SortOrder
createdAt?: Prisma.SortOrder createdAt?: Prisma.SortOrder
updatedAt?: Prisma.SortOrder updatedAt?: Prisma.SortOrder
} }
@@ -441,6 +566,9 @@ export type SitePreferencesMinOrderByAggregateInput = {
export type SitePreferencesSumOrderByAggregateInput = { export type SitePreferencesSumOrderByAggregateInput = {
eventRegistrationPoints?: Prisma.SortOrder eventRegistrationPoints?: Prisma.SortOrder
eventFeedbackPoints?: 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 eventsBackground?: boolean
leaderboardBackground?: boolean leaderboardBackground?: boolean
challengesBackground?: boolean challengesBackground?: boolean
profileBackground?: boolean
houseBackground?: boolean
eventRegistrationPoints?: boolean eventRegistrationPoints?: boolean
eventFeedbackPoints?: boolean eventFeedbackPoints?: boolean
houseJoinPoints?: boolean
houseLeavePoints?: boolean
houseCreatePoints?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
}, ExtArgs["result"]["sitePreferences"]> }, ExtArgs["result"]["sitePreferences"]>
@@ -463,8 +596,13 @@ export type SitePreferencesSelectCreateManyAndReturn<ExtArgs extends runtime.Typ
eventsBackground?: boolean eventsBackground?: boolean
leaderboardBackground?: boolean leaderboardBackground?: boolean
challengesBackground?: boolean challengesBackground?: boolean
profileBackground?: boolean
houseBackground?: boolean
eventRegistrationPoints?: boolean eventRegistrationPoints?: boolean
eventFeedbackPoints?: boolean eventFeedbackPoints?: boolean
houseJoinPoints?: boolean
houseLeavePoints?: boolean
houseCreatePoints?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
}, ExtArgs["result"]["sitePreferences"]> }, ExtArgs["result"]["sitePreferences"]>
@@ -475,8 +613,13 @@ export type SitePreferencesSelectUpdateManyAndReturn<ExtArgs extends runtime.Typ
eventsBackground?: boolean eventsBackground?: boolean
leaderboardBackground?: boolean leaderboardBackground?: boolean
challengesBackground?: boolean challengesBackground?: boolean
profileBackground?: boolean
houseBackground?: boolean
eventRegistrationPoints?: boolean eventRegistrationPoints?: boolean
eventFeedbackPoints?: boolean eventFeedbackPoints?: boolean
houseJoinPoints?: boolean
houseLeavePoints?: boolean
houseCreatePoints?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: boolean updatedAt?: boolean
}, ExtArgs["result"]["sitePreferences"]> }, ExtArgs["result"]["sitePreferences"]>
@@ -487,13 +630,18 @@ export type SitePreferencesSelectScalar = {
eventsBackground?: boolean eventsBackground?: boolean
leaderboardBackground?: boolean leaderboardBackground?: boolean
challengesBackground?: boolean challengesBackground?: boolean
profileBackground?: boolean
houseBackground?: boolean
eventRegistrationPoints?: boolean eventRegistrationPoints?: boolean
eventFeedbackPoints?: boolean eventFeedbackPoints?: boolean
houseJoinPoints?: boolean
houseLeavePoints?: boolean
houseCreatePoints?: boolean
createdAt?: boolean createdAt?: boolean
updatedAt?: 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> = { export type $SitePreferencesPayload<ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = {
name: "SitePreferences" name: "SitePreferences"
@@ -504,8 +652,13 @@ export type $SitePreferencesPayload<ExtArgs extends runtime.Types.Extensions.Int
eventsBackground: string | null eventsBackground: string | null
leaderboardBackground: string | null leaderboardBackground: string | null
challengesBackground: string | null challengesBackground: string | null
profileBackground: string | null
houseBackground: string | null
eventRegistrationPoints: number eventRegistrationPoints: number
eventFeedbackPoints: number eventFeedbackPoints: number
houseJoinPoints: number
houseLeavePoints: number
houseCreatePoints: number
createdAt: Date createdAt: Date
updatedAt: Date updatedAt: Date
}, ExtArgs["result"]["sitePreferences"]> }, ExtArgs["result"]["sitePreferences"]>
@@ -936,8 +1089,13 @@ export interface SitePreferencesFieldRefs {
readonly eventsBackground: Prisma.FieldRef<"SitePreferences", 'String'> readonly eventsBackground: Prisma.FieldRef<"SitePreferences", 'String'>
readonly leaderboardBackground: Prisma.FieldRef<"SitePreferences", 'String'> readonly leaderboardBackground: Prisma.FieldRef<"SitePreferences", 'String'>
readonly challengesBackground: 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 eventRegistrationPoints: Prisma.FieldRef<"SitePreferences", 'Int'>
readonly eventFeedbackPoints: 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 createdAt: Prisma.FieldRef<"SitePreferences", 'DateTime'>
readonly updatedAt: Prisma.FieldRef<"SitePreferences", 'DateTime'> readonly updatedAt: Prisma.FieldRef<"SitePreferences", 'DateTime'>
} }

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "SitePreferences" ADD COLUMN "profileBackground" TEXT;

View File

@@ -0,0 +1,3 @@
-- AlterTable
ALTER TABLE "SitePreferences" ADD COLUMN "houseBackground" TEXT;

View File

@@ -107,8 +107,13 @@ model SitePreferences {
eventsBackground String? eventsBackground String?
leaderboardBackground String? leaderboardBackground String?
challengesBackground String? challengesBackground String?
profileBackground String?
houseBackground String?
eventRegistrationPoints Int @default(100) eventRegistrationPoints Int @default(100)
eventFeedbackPoints Int @default(100) eventFeedbackPoints Int @default(100)
houseJoinPoints Int @default(100)
houseLeavePoints Int @default(100)
houseCreatePoints Int @default(100)
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
} }

View File

@@ -346,8 +346,13 @@ export class ChallengeService {
where: { id: challengeId }, where: { id: challengeId },
data: updateData, data: updateData,
}); });
} catch (error: any) { } catch (error: unknown) {
if (error?.code === "P2025") { if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === "P2025"
) {
// Record not found // Record not found
throw new NotFoundError("Défi"); throw new NotFoundError("Défi");
} }
@@ -431,8 +436,13 @@ export class ChallengeService {
await prisma.challenge.delete({ await prisma.challenge.delete({
where: { id: challengeId }, where: { id: challengeId },
}); });
} catch (error: any) { } catch (error: unknown) {
if (error?.code === "P2025") { if (
error &&
typeof error === "object" &&
"code" in error &&
error.code === "P2025"
) {
// Record not found // Record not found
throw new NotFoundError("Défi"); throw new NotFoundError("Défi");
} }

View File

@@ -8,6 +8,7 @@ import type {
InvitationStatus, InvitationStatus,
RequestStatus, RequestStatus,
Prisma, Prisma,
SitePreferences,
} from "@/prisma/generated/prisma/client"; } from "@/prisma/generated/prisma/client";
import { import {
ValidationError, ValidationError,
@@ -15,6 +16,14 @@ import {
ConflictError, ConflictError,
ForbiddenError, ForbiddenError,
} from "../errors"; } from "../errors";
import { sitePreferencesService } from "../preferences/site-preferences.service";
// Type étendu pour les préférences avec les nouveaux champs de points des maisons
type SitePreferencesWithHousePoints = SitePreferences & {
houseJoinPoints?: number;
houseLeavePoints?: number;
houseCreatePoints?: number;
};
const HOUSE_NAME_MIN_LENGTH = 3; const HOUSE_NAME_MIN_LENGTH = 3;
const HOUSE_NAME_MAX_LENGTH = 50; const HOUSE_NAME_MAX_LENGTH = 50;
@@ -143,10 +152,7 @@ export class HouseService {
/** /**
* Vérifie si un utilisateur est membre d'une maison * Vérifie si un utilisateur est membre d'une maison
*/ */
async isUserMemberOfHouse( async isUserMemberOfHouse(userId: string, houseId: string): Promise<boolean> {
userId: string,
houseId: string
): Promise<boolean> {
const membership = await prisma.houseMembership.findUnique({ const membership = await prisma.houseMembership.findUnique({
where: { where: {
houseId_userId: { houseId_userId: {
@@ -161,10 +167,7 @@ export class HouseService {
/** /**
* Vérifie si un utilisateur est propriétaire ou admin d'une maison * Vérifie si un utilisateur est propriétaire ou admin d'une maison
*/ */
async isUserOwnerOrAdmin( async isUserOwnerOrAdmin(userId: string, houseId: string): Promise<boolean> {
userId: string,
houseId: string
): Promise<boolean> {
const membership = await prisma.houseMembership.findUnique({ const membership = await prisma.houseMembership.findUnique({
where: { where: {
houseId_userId: { houseId_userId: {
@@ -248,7 +251,10 @@ export class HouseService {
); );
} }
if (data.description && data.description.length > HOUSE_DESCRIPTION_MAX_LENGTH) { if (
data.description &&
data.description.length > HOUSE_DESCRIPTION_MAX_LENGTH
) {
throw new ValidationError( throw new ValidationError(
`La description ne peut pas dépasser ${HOUSE_DESCRIPTION_MAX_LENGTH} caractères`, `La description ne peut pas dépasser ${HOUSE_DESCRIPTION_MAX_LENGTH} caractères`,
"description" "description"
@@ -280,19 +286,46 @@ export class HouseService {
throw new ConflictError("Ce nom de maison est déjà utilisé"); throw new ConflictError("Ce nom de maison est déjà utilisé");
} }
// Créer la maison et ajouter le créateur comme OWNER // Récupérer les points à attribuer depuis les préférences du site
return prisma.house.create({ const sitePreferences =
data: { await sitePreferencesService.getOrCreateSitePreferences();
name: data.name.trim(), const pointsToAward =
description: data.description?.trim() || null, (sitePreferences as SitePreferencesWithHousePoints).houseCreatePoints ??
creatorId: data.creatorId, 100;
memberships: { console.log(
create: { "[HouseService] Creating house - points to award:",
userId: data.creatorId, pointsToAward,
role: "OWNER", "preferences:",
sitePreferences
);
// Créer la maison et ajouter le créateur comme OWNER, puis attribuer les points
return prisma.$transaction(async (tx) => {
const house = await tx.house.create({
data: {
name: data.name.trim(),
description: data.description?.trim() || null,
creatorId: data.creatorId,
memberships: {
create: {
userId: data.creatorId,
role: "OWNER",
},
}, },
}, },
}, });
// Attribuer les points au créateur
await tx.user.update({
where: { id: data.creatorId },
data: {
score: {
increment: pointsToAward,
},
},
});
return house;
}); });
} }
@@ -348,7 +381,10 @@ export class HouseService {
} }
if (data.description !== undefined) { if (data.description !== undefined) {
if (data.description && data.description.length > HOUSE_DESCRIPTION_MAX_LENGTH) { if (
data.description &&
data.description.length > HOUSE_DESCRIPTION_MAX_LENGTH
) {
throw new ValidationError( throw new ValidationError(
`La description ne peut pas dépasser ${HOUSE_DESCRIPTION_MAX_LENGTH} caractères`, `La description ne peut pas dépasser ${HOUSE_DESCRIPTION_MAX_LENGTH} caractères`,
"description" "description"
@@ -370,13 +406,48 @@ export class HouseService {
// Vérifier que l'utilisateur est propriétaire // Vérifier que l'utilisateur est propriétaire
const isOwner = await this.isUserOwner(userId, houseId); const isOwner = await this.isUserOwner(userId, houseId);
if (!isOwner) { if (!isOwner) {
throw new ForbiddenError( throw new ForbiddenError("Seul le propriétaire peut supprimer la maison");
"Seul le propriétaire peut supprimer la maison"
);
} }
await prisma.house.delete({ // Récupérer la maison pour obtenir le créateur
const house = await prisma.house.findUnique({
where: { id: houseId }, where: { id: houseId },
select: { creatorId: true },
});
if (!house) {
throw new NotFoundError("Maison");
}
// Récupérer les points à enlever depuis les préférences du site
const sitePreferences =
await sitePreferencesService.getOrCreateSitePreferences();
const pointsToDeduct =
(sitePreferences as SitePreferencesWithHousePoints).houseCreatePoints ??
100;
console.log(
"[HouseService] Deleting house - points to deduct:",
pointsToDeduct,
"creatorId:",
house.creatorId
);
// Supprimer la maison et enlever les points au créateur
await prisma.$transaction(async (tx) => {
// Enlever les points au créateur
await tx.user.update({
where: { id: house.creatorId },
data: {
score: {
decrement: pointsToDeduct,
},
},
});
// Supprimer la maison (cela supprimera automatiquement les membreships, invitations, etc. grâce aux CASCADE)
await tx.house.delete({
where: { id: houseId },
});
}); });
} }
@@ -404,22 +475,6 @@ export class HouseService {
throw new ConflictError("Cet utilisateur est déjà membre de la maison"); throw new ConflictError("Cet utilisateur est déjà membre de la maison");
} }
// Vérifier qu'il n'y a pas déjà une invitation en attente
const existingInvitation = await prisma.houseInvitation.findUnique({
where: {
houseId_inviteeId: {
houseId: data.houseId,
inviteeId: data.inviteeId,
},
},
});
if (existingInvitation && existingInvitation.status === "PENDING") {
throw new ConflictError(
"Une invitation est déjà en attente pour cet utilisateur"
);
}
// Vérifier que l'invité n'est pas déjà dans une autre maison // Vérifier que l'invité n'est pas déjà dans une autre maison
const existingMembership = await prisma.houseMembership.findFirst({ const existingMembership = await prisma.houseMembership.findFirst({
where: { userId: data.inviteeId }, where: { userId: data.inviteeId },
@@ -431,6 +486,37 @@ export class HouseService {
); );
} }
// Vérifier s'il existe déjà une invitation (peu importe le statut)
const existingInvitation = await prisma.houseInvitation.findUnique({
where: {
houseId_inviteeId: {
houseId: data.houseId,
inviteeId: data.inviteeId,
},
},
});
if (existingInvitation) {
if (existingInvitation.status === "PENDING") {
throw new ConflictError(
"Une invitation est déjà en attente pour cet utilisateur"
);
}
// Si l'invitation existe avec un autre statut, on la réinitialise
return prisma.houseInvitation.update({
where: {
houseId_inviteeId: {
houseId: data.houseId,
inviteeId: data.inviteeId,
},
},
data: {
inviterId: data.inviterId,
status: "PENDING",
},
});
}
// Créer l'invitation // Créer l'invitation
return prisma.houseInvitation.create({ return prisma.houseInvitation.create({
data: { data: {
@@ -476,6 +562,19 @@ export class HouseService {
); );
} }
// Récupérer les points à attribuer depuis les préférences du site
const sitePreferences =
await sitePreferencesService.getOrCreateSitePreferences();
const pointsToAward =
(sitePreferences as SitePreferencesWithHousePoints).houseJoinPoints ??
100;
console.log(
"[HouseService] Accepting invitation - points to award:",
pointsToAward,
"userId:",
userId
);
// Créer le membership et mettre à jour l'invitation // Créer le membership et mettre à jour l'invitation
return prisma.$transaction(async (tx) => { return prisma.$transaction(async (tx) => {
const membership = await tx.houseMembership.create({ const membership = await tx.houseMembership.create({
@@ -510,6 +609,16 @@ export class HouseService {
data: { status: "CANCELLED" }, data: { status: "CANCELLED" },
}); });
// Attribuer les points à l'utilisateur qui rejoint
await tx.user.update({
where: { id: userId },
data: {
score: {
increment: pointsToAward,
},
},
});
return membership; return membership;
}); });
} }
@@ -592,7 +701,7 @@ export class HouseService {
); );
} }
// Vérifier qu'il n'y a pas déjà une demande en attente // Vérifier s'il existe déjà une demande
const existingRequest = await prisma.houseRequest.findUnique({ const existingRequest = await prisma.houseRequest.findUnique({
where: { where: {
houseId_requesterId: { houseId_requesterId: {
@@ -602,13 +711,27 @@ export class HouseService {
}, },
}); });
if (existingRequest && existingRequest.status === "PENDING") { if (existingRequest) {
throw new ConflictError( if (existingRequest.status === "PENDING") {
"Une demande est déjà en attente pour cette maison" throw new ConflictError(
); "Une demande est déjà en attente pour cette maison"
);
}
// Si la demande existe mais n'est pas PENDING (REJECTED, CANCELLED), on la réactive
return prisma.houseRequest.update({
where: {
houseId_requesterId: {
houseId: data.houseId,
requesterId: data.requesterId,
},
},
data: {
status: "PENDING",
},
});
} }
// Créer la demande // Créer une nouvelle demande
return prisma.houseRequest.create({ return prisma.houseRequest.create({
data: { data: {
houseId: data.houseId, houseId: data.houseId,
@@ -635,10 +758,7 @@ export class HouseService {
} }
// Vérifier que l'utilisateur est propriétaire ou admin de la maison // Vérifier que l'utilisateur est propriétaire ou admin de la maison
const isAuthorized = await this.isUserOwnerOrAdmin( const isAuthorized = await this.isUserOwnerOrAdmin(userId, request.houseId);
userId,
request.houseId
);
if (!isAuthorized) { if (!isAuthorized) {
throw new ForbiddenError( throw new ForbiddenError(
"Vous n'avez pas les permissions pour accepter cette demande" "Vous n'avez pas les permissions pour accepter cette demande"
@@ -660,6 +780,19 @@ export class HouseService {
); );
} }
// Récupérer les points à attribuer depuis les préférences du site
const sitePreferences =
await sitePreferencesService.getOrCreateSitePreferences();
const pointsToAward =
(sitePreferences as SitePreferencesWithHousePoints).houseJoinPoints ??
100;
console.log(
"[HouseService] Accepting request - points to award:",
pointsToAward,
"requesterId:",
request.requesterId
);
// Créer le membership et mettre à jour la demande // Créer le membership et mettre à jour la demande
return prisma.$transaction(async (tx) => { return prisma.$transaction(async (tx) => {
const membership = await tx.houseMembership.create({ const membership = await tx.houseMembership.create({
@@ -694,6 +827,16 @@ export class HouseService {
data: { status: "CANCELLED" }, data: { status: "CANCELLED" },
}); });
// Attribuer les points à l'utilisateur qui rejoint
await tx.user.update({
where: { id: request.requesterId },
data: {
score: {
increment: pointsToAward,
},
},
});
return membership; return membership;
}); });
} }
@@ -711,10 +854,7 @@ export class HouseService {
} }
// Vérifier que l'utilisateur est propriétaire ou admin de la maison // Vérifier que l'utilisateur est propriétaire ou admin de la maison
const isAuthorized = await this.isUserOwnerOrAdmin( const isAuthorized = await this.isUserOwnerOrAdmin(userId, request.houseId);
userId,
request.houseId
);
if (!isAuthorized) { if (!isAuthorized) {
throw new ForbiddenError( throw new ForbiddenError(
"Vous n'avez pas les permissions pour refuser cette demande" "Vous n'avez pas les permissions pour refuser cette demande"
@@ -759,6 +899,144 @@ export class HouseService {
}); });
} }
/**
* Retire un membre d'une maison (par un OWNER ou ADMIN)
*/
async removeMember(
houseId: string,
memberIdToRemove: string,
removerId: string
): Promise<void> {
// Vérifier que celui qui retire est OWNER ou ADMIN
const isAuthorized = await this.isUserOwnerOrAdmin(removerId, houseId);
if (!isAuthorized) {
throw new ForbiddenError(
"Vous n'avez pas les permissions pour retirer un membre"
);
}
// Récupérer les membreships
const removerMembership = await prisma.houseMembership.findUnique({
where: {
houseId_userId: {
houseId,
userId: removerId,
},
},
});
const memberToRemoveMembership = await prisma.houseMembership.findUnique({
where: {
houseId_userId: {
houseId,
userId: memberIdToRemove,
},
},
});
if (!memberToRemoveMembership) {
throw new NotFoundError("Membre");
}
// Un OWNER ne peut pas être retiré
if (memberToRemoveMembership.role === "OWNER") {
throw new ForbiddenError("Le propriétaire ne peut pas être retiré");
}
// Un ADMIN ne peut retirer que des MEMBER (pas d'autres ADMIN)
if (
removerMembership?.role === "ADMIN" &&
memberToRemoveMembership.role === "ADMIN"
) {
throw new ForbiddenError("Un admin ne peut pas retirer un autre admin");
}
// Récupérer les points à enlever depuis les préférences du site
const sitePreferences =
await sitePreferencesService.getOrCreateSitePreferences();
const pointsToDeduct =
(sitePreferences as SitePreferencesWithHousePoints).houseLeavePoints ??
100;
// Supprimer le membership et enlever les points
await prisma.$transaction(async (tx) => {
await tx.houseMembership.delete({
where: {
houseId_userId: {
houseId,
userId: memberIdToRemove,
},
},
});
// Enlever les points à l'utilisateur retiré
await tx.user.update({
where: { id: memberIdToRemove },
data: {
score: {
decrement: pointsToDeduct,
},
},
});
});
}
/**
* Retire un membre d'une maison (par un admin du site)
* Bypass les vérifications normales de permissions
*/
async removeMemberAsAdmin(
houseId: string,
memberIdToRemove: string
): Promise<void> {
const memberToRemoveMembership = await prisma.houseMembership.findUnique({
where: {
houseId_userId: {
houseId,
userId: memberIdToRemove,
},
},
});
if (!memberToRemoveMembership) {
throw new NotFoundError("Membre");
}
// Un OWNER ne peut pas être retiré même par un admin
if (memberToRemoveMembership.role === "OWNER") {
throw new ForbiddenError("Le propriétaire ne peut pas être retiré");
}
// Récupérer les points à enlever depuis les préférences du site
const sitePreferences =
await sitePreferencesService.getOrCreateSitePreferences();
const pointsToDeduct =
(sitePreferences as SitePreferencesWithHousePoints).houseLeavePoints ??
100;
// Supprimer le membership et enlever les points
await prisma.$transaction(async (tx) => {
await tx.houseMembership.delete({
where: {
houseId_userId: {
houseId,
userId: memberIdToRemove,
},
},
});
// Enlever les points à l'utilisateur retiré
await tx.user.update({
where: { id: memberIdToRemove },
data: {
score: {
decrement: pointsToDeduct,
},
},
});
});
}
/** /**
* Quitte une maison * Quitte une maison
*/ */
@@ -783,13 +1061,39 @@ export class HouseService {
); );
} }
await prisma.houseMembership.delete({ // Récupérer les points à enlever depuis les préférences du site
where: { const sitePreferences =
houseId_userId: { await sitePreferencesService.getOrCreateSitePreferences();
houseId, const pointsToDeduct =
userId, (sitePreferences as SitePreferencesWithHousePoints).houseLeavePoints ??
100;
console.log(
"[HouseService] Leaving house - points to deduct:",
pointsToDeduct,
"userId:",
userId
);
// Supprimer le membership et enlever les points
await prisma.$transaction(async (tx) => {
await tx.houseMembership.delete({
where: {
houseId_userId: {
houseId,
userId,
},
}, },
}, });
// Enlever les points à l'utilisateur qui quitte
await tx.user.update({
where: { id: userId },
data: {
score: {
decrement: pointsToDeduct,
},
},
});
}); });
} }
@@ -819,6 +1123,66 @@ export class HouseService {
}); });
} }
/**
* Compte les invitations en attente pour un utilisateur
*/
async getPendingInvitationsCount(userId: string): Promise<number> {
return prisma.houseInvitation.count({
where: {
inviteeId: userId,
status: "PENDING",
},
});
}
/**
* Compte les demandes d'adhésion en attente pour un utilisateur
* (demandes reçues pour les maisons dont l'utilisateur est propriétaire ou admin)
*/
async getPendingRequestsCount(userId: string): Promise<number> {
// Trouver toutes les maisons où l'utilisateur est OWNER ou ADMIN
const userHouses = await prisma.houseMembership.findMany({
where: {
userId,
role: {
in: ["OWNER", "ADMIN"],
},
},
select: {
houseId: true,
},
});
const houseIds = userHouses.map((m) => m.houseId);
if (houseIds.length === 0) {
return 0;
}
// Compter les demandes PENDING pour ces maisons
return prisma.houseRequest.count({
where: {
houseId: {
in: houseIds,
},
status: "PENDING",
},
});
}
/**
* Compte le total des invitations et demandes en attente pour un utilisateur
* - Invitations : invitations reçues par l'utilisateur (inviteeId)
* - Demandes : demandes reçues pour les maisons dont l'utilisateur est OWNER ou ADMIN
*/
async getPendingHouseActionsCount(userId: string): Promise<number> {
const [invitationsCount, requestsCount] = await Promise.all([
this.getPendingInvitationsCount(userId),
this.getPendingRequestsCount(userId),
]);
return invitationsCount + requestsCount;
}
/** /**
* Récupère les demandes d'une maison * Récupère les demandes d'une maison
*/ */
@@ -879,9 +1243,7 @@ export class HouseService {
/** /**
* Récupère une invitation par son ID (avec seulement houseId) * Récupère une invitation par son ID (avec seulement houseId)
*/ */
async getInvitationById( async getInvitationById(id: string): Promise<{ houseId: string } | null> {
id: string
): Promise<{ houseId: string } | null> {
return prisma.houseInvitation.findUnique({ return prisma.houseInvitation.findUnique({
where: { id }, where: { id },
select: { houseId: true }, select: { houseId: true },
@@ -890,4 +1252,3 @@ export class HouseService {
} }
export const houseService = new HouseService(); export const houseService = new HouseService();

View File

@@ -2,13 +2,25 @@ import { prisma } from "../database";
import { normalizeBackgroundUrl } from "@/lib/avatars"; import { normalizeBackgroundUrl } from "@/lib/avatars";
import type { SitePreferences } from "@/prisma/generated/prisma/client"; 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 { export interface UpdateSitePreferencesInput {
homeBackground?: string | null; homeBackground?: string | null;
eventsBackground?: string | null; eventsBackground?: string | null;
leaderboardBackground?: string | null; leaderboardBackground?: string | null;
challengesBackground?: string | null; challengesBackground?: string | null;
profileBackground?: string | null;
houseBackground?: string | null;
eventRegistrationPoints?: number; eventRegistrationPoints?: number;
eventFeedbackPoints?: number; eventFeedbackPoints?: number;
houseJoinPoints?: number;
houseLeavePoints?: number;
houseCreatePoints?: number;
} }
/** /**
@@ -40,12 +52,23 @@ export class SitePreferencesService {
eventsBackground: null, eventsBackground: null,
leaderboardBackground: null, leaderboardBackground: null,
challengesBackground: null, challengesBackground: null,
profileBackground: null,
houseBackground: null,
eventRegistrationPoints: 100, eventRegistrationPoints: 100,
eventFeedbackPoints: 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; return sitePreferences;
} }
@@ -74,6 +97,14 @@ export class SitePreferencesService {
data.challengesBackground === "" data.challengesBackground === ""
? null ? null
: (data.challengesBackground ?? undefined), : (data.challengesBackground ?? undefined),
profileBackground:
data.profileBackground === ""
? null
: (data.profileBackground ?? undefined),
houseBackground:
data.houseBackground === ""
? null
: (data.houseBackground ?? undefined),
eventRegistrationPoints: eventRegistrationPoints:
data.eventRegistrationPoints !== undefined data.eventRegistrationPoints !== undefined
? data.eventRegistrationPoints ? data.eventRegistrationPoints
@@ -82,6 +113,16 @@ export class SitePreferencesService {
data.eventFeedbackPoints !== undefined data.eventFeedbackPoints !== undefined
? data.eventFeedbackPoints ? data.eventFeedbackPoints
: undefined, : undefined,
houseJoinPoints:
data.houseJoinPoints !== undefined ? data.houseJoinPoints : undefined,
houseLeavePoints:
data.houseLeavePoints !== undefined
? data.houseLeavePoints
: undefined,
houseCreatePoints:
data.houseCreatePoints !== undefined
? data.houseCreatePoints
: undefined,
}, },
create: { create: {
id: "global", id: "global",
@@ -97,8 +138,17 @@ export class SitePreferencesService {
data.challengesBackground === "" data.challengesBackground === ""
? null ? null
: (data.challengesBackground ?? null), : (data.challengesBackground ?? null),
profileBackground:
data.profileBackground === ""
? null
: (data.profileBackground ?? null),
houseBackground:
data.houseBackground === "" ? null : (data.houseBackground ?? null),
eventRegistrationPoints: data.eventRegistrationPoints ?? 100, eventRegistrationPoints: data.eventRegistrationPoints ?? 100,
eventFeedbackPoints: data.eventFeedbackPoints ?? 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 * Récupère l'image de fond pour une page donnée
*/ */
async getBackgroundImage( async getBackgroundImage(
page: "home" | "events" | "leaderboard" | "challenges", page:
| "home"
| "events"
| "leaderboard"
| "challenges"
| "profile"
| "houses",
defaultImage: string defaultImage: string
): Promise<string> { ): Promise<string> {
try { try {
@@ -119,7 +175,9 @@ export class SitePreferencesService {
return defaultImage; 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 customImage = sitePreferences[imageKey];
const imageUrl = (customImage as string | null) || defaultImage; const imageUrl = (customImage as string | null) || defaultImage;