Compare commits
3 Commits
9b9cc3885a
...
b790ee21f2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b790ee21f2 | ||
|
|
321da3176e | ||
|
|
e4b0907801 |
25
Dockerfile
25
Dockerfile
@@ -19,8 +19,10 @@ 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="file:/tmp/build.db"
|
ENV DATABASE_URL="file:/app/data/dev.db"
|
||||||
RUN pnpm prisma generate
|
RUN pnpm prisma generate && \
|
||||||
|
pnpm prisma migrate deploy && \
|
||||||
|
pnpm prisma db push
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED=1
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
RUN pnpm build
|
RUN pnpm build
|
||||||
@@ -47,29 +49,22 @@ 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
|
||||||
COPY --from=builder /app/prisma ./prisma
|
COPY --from=builder /app/prisma ./prisma
|
||||||
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
|
COPY --from=builder /app/prisma.config.ts ./prisma.config.ts
|
||||||
COPY --from=builder /app/scripts ./scripts
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
|
|
||||||
ENV DATABASE_URL="file:/tmp/build.db"
|
|
||||||
|
|
||||||
# Installer seulement les dépendances de production puis générer Prisma Client
|
|
||||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
|
|
||||||
pnpm install --frozen-lockfile --prod && \
|
|
||||||
pnpm dlx prisma generate
|
|
||||||
|
|
||||||
ENV DATABASE_URL="file:/app/data/dev.db"
|
ENV DATABASE_URL="file:/app/data/dev.db"
|
||||||
|
|
||||||
|
# Nettoyer les dépendances de développement
|
||||||
|
RUN pnpm prune --prod
|
||||||
|
|
||||||
|
|
||||||
# Create data directory for SQLite database and uploads directories
|
# Create data directory for SQLite database and uploads directories
|
||||||
RUN mkdir -p /app/data /app/public/uploads /app/public/uploads/backgrounds && \
|
RUN mkdir -p /app/data /app/public/uploads /app/public/uploads/backgrounds && \
|
||||||
chown -R nextjs:nodejs /app/data /app/public/uploads
|
chown -R nextjs:nodejs /app/data /app/public/uploads
|
||||||
|
|
||||||
COPY --chown=nextjs:nodejs scripts/docker-entrypoint.sh /app/entrypoint.sh
|
|
||||||
RUN chmod +x /app/entrypoint.sh
|
|
||||||
|
|
||||||
USER nextjs
|
USER nextjs
|
||||||
|
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
ENV HOSTNAME="0.0.0.0"
|
ENV HOSTNAME="0.0.0.0"
|
||||||
ENV DATABASE_URL="file:/app/data/dev.db"
|
|
||||||
|
|
||||||
ENTRYPOINT ["./entrypoint.sh"]
|
ENTRYPOINT ["pnpm", "start"]
|
||||||
|
|||||||
@@ -1,20 +1,17 @@
|
|||||||
'use server'
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from "next/cache";
|
||||||
import { auth } from '@/lib/auth'
|
import { auth } from "@/lib/auth";
|
||||||
import { challengeService } from '@/services/challenges/challenge.service'
|
import { challengeService } from "@/services/challenges/challenge.service";
|
||||||
import { Role } from '@/prisma/generated/prisma/client'
|
import { Role } from "@/prisma/generated/prisma/client";
|
||||||
import {
|
import { ValidationError, NotFoundError } from "@/services/errors";
|
||||||
ValidationError,
|
|
||||||
NotFoundError,
|
|
||||||
} from '@/services/errors'
|
|
||||||
|
|
||||||
async function checkAdminAccess() {
|
async function checkAdminAccess() {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
if (!session?.user || session.user.role !== Role.ADMIN) {
|
if (!session?.user || session.user.role !== Role.ADMIN) {
|
||||||
throw new Error('Accès refusé - Admin uniquement')
|
throw new Error("Accès refusé - Admin uniquement");
|
||||||
}
|
}
|
||||||
return session
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateChallenge(
|
export async function validateChallenge(
|
||||||
@@ -23,34 +20,41 @@ export async function validateChallenge(
|
|||||||
adminComment?: string
|
adminComment?: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const session = await checkAdminAccess()
|
const session = await checkAdminAccess();
|
||||||
|
|
||||||
const challenge = await challengeService.validateChallenge(
|
const challenge = await challengeService.validateChallenge(
|
||||||
challengeId,
|
challengeId,
|
||||||
session.user.id,
|
session.user.id,
|
||||||
winnerId,
|
winnerId,
|
||||||
adminComment
|
adminComment
|
||||||
)
|
);
|
||||||
|
|
||||||
revalidatePath('/admin')
|
revalidatePath("/admin");
|
||||||
revalidatePath('/challenges')
|
revalidatePath("/challenges");
|
||||||
revalidatePath('/leaderboard')
|
revalidatePath("/leaderboard");
|
||||||
|
|
||||||
return { success: true, message: 'Défi validé avec succès', data: challenge }
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Défi validé avec succès",
|
||||||
|
data: challenge,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Validate challenge error:', error)
|
console.error("Validate challenge error:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError) {
|
if (error instanceof ValidationError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof Error && error.message.includes('Accès refusé')) {
|
if (error instanceof Error && error.message.includes("Accès refusé")) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Une erreur est survenue lors de la validation du défi' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Une erreur est survenue lors de la validation du défi",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,94 +63,106 @@ export async function rejectChallenge(
|
|||||||
adminComment?: string
|
adminComment?: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const session = await checkAdminAccess()
|
const session = await checkAdminAccess();
|
||||||
|
|
||||||
const challenge = await challengeService.rejectChallenge(
|
const challenge = await challengeService.rejectChallenge(
|
||||||
challengeId,
|
challengeId,
|
||||||
session.user.id,
|
session.user.id,
|
||||||
adminComment
|
adminComment
|
||||||
)
|
);
|
||||||
|
|
||||||
revalidatePath('/admin')
|
revalidatePath("/admin");
|
||||||
revalidatePath('/challenges')
|
revalidatePath("/challenges");
|
||||||
|
|
||||||
return { success: true, message: 'Défi rejeté', data: challenge }
|
return { success: true, message: "Défi rejeté", data: challenge };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Reject challenge error:', error)
|
console.error("Reject challenge error:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError) {
|
if (error instanceof ValidationError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof Error && error.message.includes('Accès refusé')) {
|
if (error instanceof Error && error.message.includes("Accès refusé")) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Une erreur est survenue lors du rejet du défi' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Une erreur est survenue lors du rejet du défi",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateChallenge(
|
export async function updateChallenge(
|
||||||
challengeId: string,
|
challengeId: string,
|
||||||
data: {
|
data: {
|
||||||
title?: string
|
title?: string;
|
||||||
description?: string
|
description?: string;
|
||||||
pointsReward?: number
|
pointsReward?: number;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
await checkAdminAccess()
|
await checkAdminAccess();
|
||||||
|
|
||||||
const challenge = await challengeService.updateChallenge(challengeId, {
|
const challenge = await challengeService.updateChallenge(challengeId, {
|
||||||
title: data.title,
|
title: data.title,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
pointsReward: data.pointsReward,
|
pointsReward: data.pointsReward,
|
||||||
})
|
});
|
||||||
|
|
||||||
revalidatePath('/admin')
|
revalidatePath("/admin");
|
||||||
revalidatePath('/challenges')
|
revalidatePath("/challenges");
|
||||||
|
|
||||||
return { success: true, message: 'Défi mis à jour avec succès', data: challenge }
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Défi mis à jour avec succès",
|
||||||
|
data: challenge,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Update challenge error:', error)
|
console.error("Update challenge error:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError) {
|
if (error instanceof ValidationError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof Error && error.message.includes('Accès refusé')) {
|
if (error instanceof Error && error.message.includes("Accès refusé")) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Une erreur est survenue lors de la mise à jour du défi' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Une erreur est survenue lors de la mise à jour du défi",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteChallenge(challengeId: string) {
|
export async function deleteChallenge(challengeId: string) {
|
||||||
try {
|
try {
|
||||||
await checkAdminAccess()
|
await checkAdminAccess();
|
||||||
|
|
||||||
await challengeService.deleteChallenge(challengeId)
|
await challengeService.deleteChallenge(challengeId);
|
||||||
|
|
||||||
revalidatePath('/admin')
|
revalidatePath("/admin");
|
||||||
revalidatePath('/challenges')
|
revalidatePath("/challenges");
|
||||||
|
|
||||||
return { success: true, message: 'Défi supprimé avec succès' }
|
return { success: true, message: "Défi supprimé avec succès" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Delete challenge error:', error)
|
console.error("Delete challenge error:", error);
|
||||||
|
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof Error && error.message.includes('Accès refusé')) {
|
if (error instanceof Error && error.message.includes("Accès refusé")) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Une erreur est survenue lors de la suppression du défi' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Une erreur est survenue lors de la suppression du défi",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,73 +1,79 @@
|
|||||||
'use server'
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from "next/cache";
|
||||||
import { auth } from '@/lib/auth'
|
import { auth } from "@/lib/auth";
|
||||||
import { eventService } from '@/services/events/event.service'
|
import { eventService } from "@/services/events/event.service";
|
||||||
import { Role, EventType } from '@/prisma/generated/prisma/client'
|
import { Role, EventType } from "@/prisma/generated/prisma/client";
|
||||||
import { ValidationError, NotFoundError } from '@/services/errors'
|
import { ValidationError, NotFoundError } from "@/services/errors";
|
||||||
|
|
||||||
function checkAdminAccess() {
|
function checkAdminAccess() {
|
||||||
return async () => {
|
return async () => {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
if (!session?.user || session.user.role !== Role.ADMIN) {
|
if (!session?.user || session.user.role !== Role.ADMIN) {
|
||||||
throw new Error('Accès refusé')
|
throw new Error("Accès refusé");
|
||||||
}
|
|
||||||
return session
|
|
||||||
}
|
}
|
||||||
|
return session;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createEvent(data: {
|
export async function createEvent(data: {
|
||||||
date: string
|
date: string;
|
||||||
name: string
|
name: string;
|
||||||
description?: string | null
|
description?: string | null;
|
||||||
type: string
|
type: string;
|
||||||
room?: string | null
|
room?: string | null;
|
||||||
time?: string | null
|
time?: string | null;
|
||||||
maxPlaces?: number | null
|
maxPlaces?: number | null;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
await checkAdminAccess()()
|
await checkAdminAccess()();
|
||||||
|
|
||||||
const event = await eventService.validateAndCreateEvent({
|
const event = await eventService.validateAndCreateEvent({
|
||||||
date: data.date,
|
date: data.date,
|
||||||
name: data.name,
|
name: data.name,
|
||||||
description: data.description ?? '',
|
description: data.description ?? "",
|
||||||
type: data.type as EventType,
|
type: data.type as EventType,
|
||||||
room: data.room ?? undefined,
|
room: data.room ?? undefined,
|
||||||
time: data.time ?? undefined,
|
time: data.time ?? undefined,
|
||||||
maxPlaces: data.maxPlaces ?? undefined,
|
maxPlaces: data.maxPlaces ?? undefined,
|
||||||
})
|
});
|
||||||
|
|
||||||
revalidatePath('/admin')
|
revalidatePath("/admin");
|
||||||
revalidatePath('/events')
|
revalidatePath("/events");
|
||||||
revalidatePath('/')
|
revalidatePath("/");
|
||||||
|
|
||||||
return { success: true, data: event }
|
return { success: true, data: event };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating event:', error)
|
console.error("Error creating event:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError) {
|
if (error instanceof ValidationError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof Error && error.message === 'Accès refusé') {
|
if (error instanceof Error && error.message === "Accès refusé") {
|
||||||
return { success: false, error: 'Accès refusé' }
|
return { success: false, error: "Accès refusé" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Erreur lors de la création de l\'événement' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Erreur lors de la création de l'événement",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateEvent(eventId: string, data: {
|
export async function updateEvent(
|
||||||
date?: string
|
eventId: string,
|
||||||
name?: string
|
data: {
|
||||||
description?: string | null
|
date?: string;
|
||||||
type?: string
|
name?: string;
|
||||||
room?: string | null
|
description?: string | null;
|
||||||
time?: string | null
|
type?: string;
|
||||||
maxPlaces?: number | null
|
room?: string | null;
|
||||||
}) {
|
time?: string | null;
|
||||||
|
maxPlaces?: number | null;
|
||||||
|
}
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
await checkAdminAccess()()
|
await checkAdminAccess()();
|
||||||
|
|
||||||
const event = await eventService.validateAndUpdateEvent(eventId, {
|
const event = await eventService.validateAndUpdateEvent(eventId, {
|
||||||
date: data.date,
|
date: data.date,
|
||||||
@@ -77,55 +83,60 @@ export async function updateEvent(eventId: string, data: {
|
|||||||
room: data.room ?? undefined,
|
room: data.room ?? undefined,
|
||||||
time: data.time ?? undefined,
|
time: data.time ?? undefined,
|
||||||
maxPlaces: data.maxPlaces ?? undefined,
|
maxPlaces: data.maxPlaces ?? undefined,
|
||||||
})
|
});
|
||||||
|
|
||||||
revalidatePath('/admin')
|
revalidatePath("/admin");
|
||||||
revalidatePath('/events')
|
revalidatePath("/events");
|
||||||
revalidatePath('/')
|
revalidatePath("/");
|
||||||
|
|
||||||
return { success: true, data: event }
|
return { success: true, data: event };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating event:', error)
|
console.error("Error updating event:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError) {
|
if (error instanceof ValidationError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof Error && error.message === 'Accès refusé') {
|
if (error instanceof Error && error.message === "Accès refusé") {
|
||||||
return { success: false, error: 'Accès refusé' }
|
return { success: false, error: "Accès refusé" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Erreur lors de la mise à jour de l\'événement' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Erreur lors de la mise à jour de l'événement",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEvent(eventId: string) {
|
export async function deleteEvent(eventId: string) {
|
||||||
try {
|
try {
|
||||||
await checkAdminAccess()()
|
await checkAdminAccess()();
|
||||||
|
|
||||||
const existingEvent = await eventService.getEventById(eventId)
|
const existingEvent = await eventService.getEventById(eventId);
|
||||||
|
|
||||||
if (!existingEvent) {
|
if (!existingEvent) {
|
||||||
return { success: false, error: 'Événement non trouvé' }
|
return { success: false, error: "Événement non trouvé" };
|
||||||
}
|
}
|
||||||
|
|
||||||
await eventService.deleteEvent(eventId)
|
await eventService.deleteEvent(eventId);
|
||||||
|
|
||||||
revalidatePath('/admin')
|
revalidatePath("/admin");
|
||||||
revalidatePath('/events')
|
revalidatePath("/events");
|
||||||
revalidatePath('/')
|
revalidatePath("/");
|
||||||
|
|
||||||
return { success: true }
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting event:', error)
|
console.error("Error deleting event:", error);
|
||||||
|
|
||||||
if (error instanceof Error && error.message === 'Accès refusé') {
|
if (error instanceof Error && error.message === "Accès refusé") {
|
||||||
return { success: false, error: 'Accès refusé' }
|
return { success: false, error: "Accès refusé" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Erreur lors de la suppression de l\'événement' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Erreur lors de la suppression de l'événement",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,48 +1,50 @@
|
|||||||
'use server'
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from "next/cache";
|
||||||
import { auth } from '@/lib/auth'
|
import { auth } from "@/lib/auth";
|
||||||
import { sitePreferencesService } from '@/services/preferences/site-preferences.service'
|
import { sitePreferencesService } from "@/services/preferences/site-preferences.service";
|
||||||
import { Role } from '@/prisma/generated/prisma/client'
|
import { Role } from "@/prisma/generated/prisma/client";
|
||||||
|
|
||||||
function checkAdminAccess() {
|
function checkAdminAccess() {
|
||||||
return async () => {
|
return async () => {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
if (!session?.user || session.user.role !== Role.ADMIN) {
|
if (!session?.user || session.user.role !== Role.ADMIN) {
|
||||||
throw new Error('Accès refusé')
|
throw new Error("Accès refusé");
|
||||||
}
|
|
||||||
return session
|
|
||||||
}
|
}
|
||||||
|
return session;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateSitePreferences(data: {
|
export async function updateSitePreferences(data: {
|
||||||
homeBackground?: string | null
|
homeBackground?: string | null;
|
||||||
eventsBackground?: string | null
|
eventsBackground?: string | null;
|
||||||
leaderboardBackground?: string | null
|
leaderboardBackground?: string | null;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
await checkAdminAccess()()
|
await checkAdminAccess()();
|
||||||
|
|
||||||
const preferences = await sitePreferencesService.updateSitePreferences({
|
const preferences = await sitePreferencesService.updateSitePreferences({
|
||||||
homeBackground: data.homeBackground,
|
homeBackground: data.homeBackground,
|
||||||
eventsBackground: data.eventsBackground,
|
eventsBackground: data.eventsBackground,
|
||||||
leaderboardBackground: data.leaderboardBackground,
|
leaderboardBackground: data.leaderboardBackground,
|
||||||
})
|
});
|
||||||
|
|
||||||
revalidatePath('/admin')
|
revalidatePath("/admin");
|
||||||
revalidatePath('/')
|
revalidatePath("/");
|
||||||
revalidatePath('/events')
|
revalidatePath("/events");
|
||||||
revalidatePath('/leaderboard')
|
revalidatePath("/leaderboard");
|
||||||
|
|
||||||
return { success: true, data: preferences }
|
return { success: true, data: preferences };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating admin preferences:', error)
|
console.error("Error updating admin preferences:", error);
|
||||||
|
|
||||||
if (error instanceof Error && error.message === 'Accès refusé') {
|
if (error instanceof Error && error.message === "Accès refusé") {
|
||||||
return { success: false, error: 'Accès refusé' }
|
return { success: false, error: "Accès refusé" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Erreur lors de la mise à jour des préférences' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Erreur lors de la mise à jour des préférences",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,55 @@
|
|||||||
'use server'
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from "next/cache";
|
||||||
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 { userStatsService } from '@/services/users/user-stats.service'
|
import { userStatsService } from "@/services/users/user-stats.service";
|
||||||
import { Role } from '@/prisma/generated/prisma/client'
|
import { Role } from "@/prisma/generated/prisma/client";
|
||||||
import {
|
import {
|
||||||
ValidationError,
|
ValidationError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
ConflictError,
|
ConflictError,
|
||||||
} from '@/services/errors'
|
} from "@/services/errors";
|
||||||
|
|
||||||
function checkAdminAccess() {
|
function checkAdminAccess() {
|
||||||
return async () => {
|
return async () => {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
if (!session?.user || session.user.role !== Role.ADMIN) {
|
if (!session?.user || session.user.role !== Role.ADMIN) {
|
||||||
throw new Error('Accès refusé')
|
throw new Error("Accès refusé");
|
||||||
}
|
|
||||||
return session
|
|
||||||
}
|
}
|
||||||
|
return session;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateUser(userId: string, data: {
|
export async function updateUser(
|
||||||
username?: string
|
userId: string,
|
||||||
avatar?: string | null
|
data: {
|
||||||
hpDelta?: number
|
username?: string;
|
||||||
xpDelta?: number
|
avatar?: string | null;
|
||||||
score?: number
|
hpDelta?: number;
|
||||||
level?: number
|
xpDelta?: number;
|
||||||
role?: string
|
score?: number;
|
||||||
}) {
|
level?: number;
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
await checkAdminAccess()()
|
await checkAdminAccess()();
|
||||||
|
|
||||||
// Valider username si fourni
|
// Valider username si fourni
|
||||||
if (data.username !== undefined) {
|
if (data.username !== undefined) {
|
||||||
try {
|
try {
|
||||||
await userService.validateAndUpdateUserProfile(userId, { username: data.username })
|
await userService.validateAndUpdateUserProfile(userId, {
|
||||||
|
username: data.username,
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof ValidationError || error instanceof ConflictError) {
|
if (
|
||||||
return { success: false, error: error.message }
|
error instanceof ValidationError ||
|
||||||
|
error instanceof ConflictError
|
||||||
|
) {
|
||||||
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
throw error
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,47 +78,52 @@ export async function updateUser(userId: string, data: {
|
|||||||
maxXp: true,
|
maxXp: true,
|
||||||
avatar: true,
|
avatar: true,
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
revalidatePath('/admin')
|
revalidatePath("/admin");
|
||||||
revalidatePath('/leaderboard')
|
revalidatePath("/leaderboard");
|
||||||
|
|
||||||
return { success: true, data: updatedUser }
|
return { success: true, data: updatedUser };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating user:', error)
|
console.error("Error updating user:", error);
|
||||||
|
|
||||||
if (error instanceof Error && error.message === 'Accès refusé') {
|
if (error instanceof Error && error.message === "Accès refusé") {
|
||||||
return { success: false, error: 'Accès refusé' }
|
return { success: false, error: "Accès refusé" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Erreur lors de la mise à jour de l\'utilisateur' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Erreur lors de la mise à jour de l'utilisateur",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteUser(userId: string) {
|
export async function deleteUser(userId: string) {
|
||||||
try {
|
try {
|
||||||
const session = await checkAdminAccess()()
|
const session = await checkAdminAccess()();
|
||||||
|
|
||||||
await userService.validateAndDeleteUser(userId, session.user.id)
|
await userService.validateAndDeleteUser(userId, session.user.id);
|
||||||
|
|
||||||
revalidatePath('/admin')
|
revalidatePath("/admin");
|
||||||
revalidatePath('/leaderboard')
|
revalidatePath("/leaderboard");
|
||||||
|
|
||||||
return { success: true }
|
return { success: true };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting user:', error)
|
console.error("Error deleting user:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError) {
|
if (error instanceof ValidationError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof Error && error.message === 'Accès refusé') {
|
if (error instanceof Error && error.message === "Accès refusé") {
|
||||||
return { success: false, error: 'Accès refusé' }
|
return { success: false, error: "Accès refusé" };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Erreur lors de la suppression de l\'utilisateur' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Erreur lors de la suppression de l'utilisateur",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,28 @@
|
|||||||
'use server'
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from "next/cache";
|
||||||
import { auth } from '@/lib/auth'
|
import { auth } from "@/lib/auth";
|
||||||
import { challengeService } from '@/services/challenges/challenge.service'
|
import { challengeService } from "@/services/challenges/challenge.service";
|
||||||
import {
|
import {
|
||||||
ValidationError,
|
ValidationError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
ConflictError,
|
ConflictError,
|
||||||
} from '@/services/errors'
|
} from "@/services/errors";
|
||||||
|
|
||||||
export async function createChallenge(data: {
|
export async function createChallenge(data: {
|
||||||
challengedId: string
|
challengedId: string;
|
||||||
title: string
|
title: string;
|
||||||
description: string
|
description: string;
|
||||||
pointsReward?: number
|
pointsReward?: number;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Vous devez être connecté pour créer un défi' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Vous devez être connecté pour créer un défi",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const challenge = await challengeService.createChallenge({
|
const challenge = await challengeService.createChallenge({
|
||||||
@@ -28,85 +31,99 @@ export async function createChallenge(data: {
|
|||||||
title: data.title,
|
title: data.title,
|
||||||
description: data.description,
|
description: data.description,
|
||||||
pointsReward: data.pointsReward || 100,
|
pointsReward: data.pointsReward || 100,
|
||||||
})
|
});
|
||||||
|
|
||||||
revalidatePath('/challenges')
|
revalidatePath("/challenges");
|
||||||
revalidatePath('/profile')
|
revalidatePath("/profile");
|
||||||
|
|
||||||
return { success: true, message: 'Défi créé avec succès', data: challenge }
|
return { success: true, message: "Défi créé avec succès", data: challenge };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Create challenge error:', error)
|
console.error("Create challenge error:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError || error instanceof ConflictError) {
|
if (error instanceof ValidationError || error instanceof ConflictError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Une erreur est survenue lors de la création du défi' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Une erreur est survenue lors de la création du défi",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function acceptChallenge(challengeId: string) {
|
export async function acceptChallenge(challengeId: string) {
|
||||||
try {
|
try {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Vous devez être connecté pour accepter un défi' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Vous devez être connecté pour accepter un défi",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const challenge = await challengeService.acceptChallenge(
|
const challenge = await challengeService.acceptChallenge(
|
||||||
challengeId,
|
challengeId,
|
||||||
session.user.id
|
session.user.id
|
||||||
)
|
);
|
||||||
|
|
||||||
revalidatePath('/challenges')
|
revalidatePath("/challenges");
|
||||||
revalidatePath('/profile')
|
revalidatePath("/profile");
|
||||||
|
|
||||||
return { success: true, message: 'Défi accepté', data: challenge }
|
return { success: true, message: "Défi accepté", data: challenge };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Accept challenge error:', error)
|
console.error("Accept challenge error:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError) {
|
if (error instanceof ValidationError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Une erreur est survenue lors de l\'acceptation du défi' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Une erreur est survenue lors de l'acceptation du défi",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function cancelChallenge(challengeId: string) {
|
export async function cancelChallenge(challengeId: string) {
|
||||||
try {
|
try {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Vous devez être connecté pour annuler un défi' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Vous devez être connecté pour annuler un défi",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const challenge = await challengeService.cancelChallenge(
|
const challenge = await challengeService.cancelChallenge(
|
||||||
challengeId,
|
challengeId,
|
||||||
session.user.id
|
session.user.id
|
||||||
)
|
);
|
||||||
|
|
||||||
revalidatePath('/challenges')
|
revalidatePath("/challenges");
|
||||||
revalidatePath('/profile')
|
revalidatePath("/profile");
|
||||||
|
|
||||||
return { success: true, message: 'Défi annulé', data: challenge }
|
return { success: true, message: "Défi annulé", data: challenge };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Cancel challenge error:', error)
|
console.error("Cancel challenge error:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError) {
|
if (error instanceof ValidationError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Une erreur est survenue lors de l\'annulation du défi' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Une erreur est survenue lors de l'annulation du défi",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +1,47 @@
|
|||||||
'use server'
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from "next/cache";
|
||||||
import { auth } from '@/lib/auth'
|
import { auth } from "@/lib/auth";
|
||||||
import { eventFeedbackService } from '@/services/events/event-feedback.service'
|
import { eventFeedbackService } from "@/services/events/event-feedback.service";
|
||||||
import {
|
import { ValidationError, NotFoundError } from "@/services/errors";
|
||||||
ValidationError,
|
|
||||||
NotFoundError,
|
|
||||||
} from '@/services/errors'
|
|
||||||
|
|
||||||
export async function createFeedback(eventId: string, data: {
|
export async function createFeedback(
|
||||||
rating: number
|
eventId: string,
|
||||||
comment?: string | null
|
data: {
|
||||||
}) {
|
rating: number;
|
||||||
|
comment?: string | null;
|
||||||
|
}
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Non authentifié' }
|
return { success: false, error: "Non authentifié" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const feedback = await eventFeedbackService.validateAndCreateFeedback(
|
const feedback = await eventFeedbackService.validateAndCreateFeedback(
|
||||||
session.user.id,
|
session.user.id,
|
||||||
eventId,
|
eventId,
|
||||||
{ rating: data.rating, comment: data.comment }
|
{ rating: data.rating, comment: data.comment }
|
||||||
)
|
);
|
||||||
|
|
||||||
revalidatePath(`/feedback/${eventId}`)
|
revalidatePath(`/feedback/${eventId}`);
|
||||||
revalidatePath('/events')
|
revalidatePath("/events");
|
||||||
|
|
||||||
return { success: true, data: feedback }
|
return { success: true, data: feedback };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving feedback:', error)
|
console.error("Error saving feedback:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError) {
|
if (error instanceof ValidationError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Erreur lors de l\'enregistrement du feedback' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Erreur lors de l'enregistrement du feedback",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +1,77 @@
|
|||||||
'use server'
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from "next/cache";
|
||||||
import { auth } from '@/lib/auth'
|
import { auth } from "@/lib/auth";
|
||||||
import { eventRegistrationService } from '@/services/events/event-registration.service'
|
import { eventRegistrationService } from "@/services/events/event-registration.service";
|
||||||
import {
|
import {
|
||||||
ValidationError,
|
ValidationError,
|
||||||
NotFoundError,
|
NotFoundError,
|
||||||
ConflictError,
|
ConflictError,
|
||||||
} from '@/services/errors'
|
} from "@/services/errors";
|
||||||
|
|
||||||
export async function registerForEvent(eventId: string) {
|
export async function registerForEvent(eventId: string) {
|
||||||
try {
|
try {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Vous devez être connecté pour vous inscrire' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Vous devez être connecté pour vous inscrire",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const registration = await eventRegistrationService.validateAndRegisterUser(
|
const registration = await eventRegistrationService.validateAndRegisterUser(
|
||||||
session.user.id,
|
session.user.id,
|
||||||
eventId
|
eventId
|
||||||
)
|
);
|
||||||
|
|
||||||
revalidatePath('/events')
|
revalidatePath("/events");
|
||||||
revalidatePath('/')
|
revalidatePath("/");
|
||||||
|
|
||||||
return { success: true, message: 'Inscription réussie', data: registration }
|
return {
|
||||||
|
success: true,
|
||||||
|
message: "Inscription réussie",
|
||||||
|
data: registration,
|
||||||
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Registration error:', error)
|
console.error("Registration error:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError || error instanceof ConflictError) {
|
if (error instanceof ValidationError || error instanceof ConflictError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Une erreur est survenue lors de l\'inscription' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Une erreur est survenue lors de l'inscription",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function unregisterFromEvent(eventId: string) {
|
export async function unregisterFromEvent(eventId: string) {
|
||||||
try {
|
try {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return { success: false, error: 'Vous devez être connecté' }
|
return { success: false, error: "Vous devez être connecté" };
|
||||||
}
|
}
|
||||||
|
|
||||||
await eventRegistrationService.unregisterUserFromEvent(
|
await eventRegistrationService.unregisterUserFromEvent(
|
||||||
session.user.id,
|
session.user.id,
|
||||||
eventId
|
eventId
|
||||||
)
|
);
|
||||||
|
|
||||||
revalidatePath('/events')
|
revalidatePath("/events");
|
||||||
revalidatePath('/')
|
revalidatePath("/");
|
||||||
|
|
||||||
return { success: true, message: 'Inscription annulée' }
|
return { success: true, message: "Inscription annulée" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Unregistration error:', error)
|
console.error("Unregistration error:", error);
|
||||||
return { success: false, error: 'Une erreur est survenue lors de l\'annulation' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Une erreur est survenue lors de l'annulation",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
'use server'
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from "next/cache";
|
||||||
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 {
|
import { ValidationError, NotFoundError } from "@/services/errors";
|
||||||
ValidationError,
|
|
||||||
NotFoundError,
|
|
||||||
} from '@/services/errors'
|
|
||||||
|
|
||||||
export async function updatePassword(data: {
|
export async function updatePassword(data: {
|
||||||
currentPassword: string
|
currentPassword: string;
|
||||||
newPassword: string
|
newPassword: string;
|
||||||
confirmPassword: string
|
confirmPassword: string;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
return { success: false, error: 'Non authentifié' }
|
return { success: false, error: "Non authentifié" };
|
||||||
}
|
}
|
||||||
|
|
||||||
await userService.validateAndUpdatePassword(
|
await userService.validateAndUpdatePassword(
|
||||||
@@ -25,22 +22,24 @@ export async function updatePassword(data: {
|
|||||||
data.currentPassword,
|
data.currentPassword,
|
||||||
data.newPassword,
|
data.newPassword,
|
||||||
data.confirmPassword
|
data.confirmPassword
|
||||||
)
|
);
|
||||||
|
|
||||||
revalidatePath('/profile')
|
revalidatePath("/profile");
|
||||||
|
|
||||||
return { success: true, message: 'Mot de passe modifié avec succès' }
|
return { success: true, message: "Mot de passe modifié avec succès" };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating password:', error)
|
console.error("Error updating password:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError) {
|
if (error instanceof ValidationError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Erreur lors de la modification du mot de passe' }
|
return {
|
||||||
|
success: false,
|
||||||
|
error: "Erreur lors de la modification du mot de passe",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
'use server'
|
"use server";
|
||||||
|
|
||||||
import { revalidatePath } from 'next/cache'
|
import { revalidatePath } from "next/cache";
|
||||||
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 { CharacterClass } from '@/prisma/generated/prisma/client'
|
import { CharacterClass } from "@/prisma/generated/prisma/client";
|
||||||
import {
|
import { ValidationError, ConflictError } from "@/services/errors";
|
||||||
ValidationError,
|
|
||||||
ConflictError,
|
|
||||||
} from '@/services/errors'
|
|
||||||
|
|
||||||
export async function updateProfile(data: {
|
export async function updateProfile(data: {
|
||||||
username?: string
|
username?: string;
|
||||||
avatar?: string | null
|
avatar?: string | null;
|
||||||
bio?: string | null
|
bio?: string | null;
|
||||||
characterClass?: string | null
|
characterClass?: string | null;
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const session = await auth()
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user) {
|
if (!session?.user) {
|
||||||
return { success: false, error: 'Non authentifié' }
|
return { success: false, error: "Non authentifié" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedUser = await userService.validateAndUpdateUserProfile(
|
const updatedUser = await userService.validateAndUpdateUserProfile(
|
||||||
@@ -28,7 +25,9 @@ export async function updateProfile(data: {
|
|||||||
username: data.username,
|
username: data.username,
|
||||||
avatar: data.avatar,
|
avatar: data.avatar,
|
||||||
bio: data.bio,
|
bio: data.bio,
|
||||||
characterClass: data.characterClass ? (data.characterClass as CharacterClass) : null,
|
characterClass: data.characterClass
|
||||||
|
? (data.characterClass as CharacterClass)
|
||||||
|
: null,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: true,
|
id: true,
|
||||||
@@ -44,20 +43,19 @@ export async function updateProfile(data: {
|
|||||||
level: true,
|
level: true,
|
||||||
score: true,
|
score: true,
|
||||||
}
|
}
|
||||||
)
|
);
|
||||||
|
|
||||||
revalidatePath('/profile')
|
revalidatePath("/profile");
|
||||||
revalidatePath('/')
|
revalidatePath("/");
|
||||||
|
|
||||||
return { success: true, data: updatedUser }
|
return { success: true, data: updatedUser };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating profile:', error)
|
console.error("Error updating profile:", error);
|
||||||
|
|
||||||
if (error instanceof ValidationError || error instanceof ConflictError) {
|
if (error instanceof ValidationError || error instanceof ConflictError) {
|
||||||
return { success: false, error: error.message }
|
return { success: false, error: error.message };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: false, error: 'Erreur lors de la mise à jour du profil' }
|
return { success: false, error: "Erreur lors de la mise à jour du profil" };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,4 +27,3 @@ export async function GET() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,4 +38,3 @@ export async function GET() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export async function GET() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer les préférences globales du site (ou créer si elles n'existent pas)
|
// Récupérer les préférences globales du site (ou créer si elles n'existent pas)
|
||||||
const sitePreferences = await sitePreferencesService.getOrCreateSitePreferences();
|
const sitePreferences =
|
||||||
|
await sitePreferencesService.getOrCreateSitePreferences();
|
||||||
|
|
||||||
return NextResponse.json(sitePreferences);
|
return NextResponse.json(sitePreferences);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -7,11 +7,16 @@ export async function GET() {
|
|||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return NextResponse.json({ error: "Vous devez être connecté" }, { status: 401 });
|
return NextResponse.json(
|
||||||
|
{ error: "Vous devez être connecté" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer tous les défis de l'utilisateur
|
// Récupérer tous les défis de l'utilisateur
|
||||||
const challenges = await challengeService.getUserChallenges(session.user.id);
|
const challenges = await challengeService.getUserChallenges(
|
||||||
|
session.user.id
|
||||||
|
);
|
||||||
|
|
||||||
return NextResponse.json(challenges);
|
return NextResponse.json(challenges);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -22,4 +27,3 @@ export async function GET() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { NextResponse } from "next/server";
|
|||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { eventRegistrationService } from "@/services/events/event-registration.service";
|
import { eventRegistrationService } from "@/services/events/event-registration.service";
|
||||||
|
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: Promise<{ id: string }> }
|
{ params }: { params: Promise<{ id: string }> }
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { NextResponse } from "next/server";
|
|||||||
import { auth } from "@/lib/auth";
|
import { auth } from "@/lib/auth";
|
||||||
import { eventFeedbackService } from "@/services/events/event-feedback.service";
|
import { eventFeedbackService } from "@/services/events/event-feedback.service";
|
||||||
|
|
||||||
|
|
||||||
export async function GET(
|
export async function GET(
|
||||||
request: Request,
|
request: Request,
|
||||||
{ params }: { params: Promise<{ eventId: string }> }
|
{ params }: { params: Promise<{ eventId: string }> }
|
||||||
|
|||||||
@@ -42,4 +42,3 @@ export async function GET() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,10 +30,7 @@ export async function POST(request: Request) {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error completing registration:", error);
|
console.error("Error completing registration:", error);
|
||||||
|
|
||||||
if (
|
if (error instanceof ValidationError || error instanceof ConflictError) {
|
||||||
error instanceof ValidationError ||
|
|
||||||
error instanceof ConflictError
|
|
||||||
) {
|
|
||||||
return NextResponse.json({ error: error.message }, { status: 400 });
|
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||||
}
|
}
|
||||||
if (error instanceof NotFoundError) {
|
if (error instanceof NotFoundError) {
|
||||||
|
|||||||
@@ -7,7 +7,10 @@ export async function GET() {
|
|||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
|
||||||
if (!session?.user?.id) {
|
if (!session?.user?.id) {
|
||||||
return NextResponse.json({ error: "Vous devez être connecté" }, { status: 401 });
|
return NextResponse.json(
|
||||||
|
{ error: "Vous devez être connecté" },
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Récupérer tous les utilisateurs (pour sélectionner qui défier)
|
// Récupérer tous les utilisateurs (pour sélectionner qui défier)
|
||||||
@@ -36,4 +39,3 @@ export async function GET() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,4 +25,3 @@ export default async function ChallengesPage() {
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,9 +39,7 @@ export default function StyleGuidePage() {
|
|||||||
|
|
||||||
{/* Buttons */}
|
{/* Buttons */}
|
||||||
<Card variant="dark" className="p-6 mb-8">
|
<Card variant="dark" className="p-6 mb-8">
|
||||||
<h2 className="text-2xl font-bold text-pixel-gold mb-6">
|
<h2 className="text-2xl font-bold text-pixel-gold mb-6">Buttons</h2>
|
||||||
Buttons
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg text-gray-300 mb-3">Variantes</h3>
|
<h3 className="text-lg text-gray-300 mb-3">Variantes</h3>
|
||||||
@@ -103,15 +101,8 @@ export default function StyleGuidePage() {
|
|||||||
type="password"
|
type="password"
|
||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input label="Number Input" type="number" placeholder="123" />
|
||||||
label="Number Input"
|
<Input label="Date Input" type="date" />
|
||||||
type="number"
|
|
||||||
placeholder="123"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
label="Date Input"
|
|
||||||
type="date"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -320,11 +311,7 @@ export default function StyleGuidePage() {
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg text-gray-300 mb-3">Interactif</h3>
|
<h3 className="text-lg text-gray-300 mb-3">Interactif</h3>
|
||||||
<StarRating
|
<StarRating value={rating} onChange={setRating} showValue />
|
||||||
value={rating}
|
|
||||||
onChange={setRating}
|
|
||||||
showValue
|
|
||||||
/>
|
|
||||||
<p className="text-gray-400 text-sm mt-2">
|
<p className="text-gray-400 text-sm mt-2">
|
||||||
Note sélectionnée : {rating}/5
|
Note sélectionnée : {rating}/5
|
||||||
</p>
|
</p>
|
||||||
@@ -356,21 +343,9 @@ export default function StyleGuidePage() {
|
|||||||
<div>
|
<div>
|
||||||
<h3 className="text-lg text-gray-300 mb-3">Tailles</h3>
|
<h3 className="text-lg text-gray-300 mb-3">Tailles</h3>
|
||||||
<div className="flex items-center gap-6">
|
<div className="flex items-center gap-6">
|
||||||
<Avatar
|
<Avatar src="/avatar-1.jpg" username="User" size="sm" />
|
||||||
src="/avatar-1.jpg"
|
<Avatar src="/avatar-2.jpg" username="User" size="md" />
|
||||||
username="User"
|
<Avatar src="/avatar-3.jpg" username="User" size="lg" />
|
||||||
size="sm"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/avatar-2.jpg"
|
|
||||||
username="User"
|
|
||||||
size="md"
|
|
||||||
/>
|
|
||||||
<Avatar
|
|
||||||
src="/avatar-3.jpg"
|
|
||||||
username="User"
|
|
||||||
size="lg"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -492,4 +467,3 @@ export default function StyleGuidePage() {
|
|||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,12 @@ interface AdminPanelProps {
|
|||||||
initialPreferences: SitePreferences;
|
initialPreferences: SitePreferences;
|
||||||
}
|
}
|
||||||
|
|
||||||
type AdminSection = "preferences" | "users" | "events" | "feedbacks" | "challenges";
|
type AdminSection =
|
||||||
|
| "preferences"
|
||||||
|
| "users"
|
||||||
|
| "events"
|
||||||
|
| "feedbacks"
|
||||||
|
| "challenges";
|
||||||
|
|
||||||
export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
|
export default function AdminPanel({ initialPreferences }: AdminPanelProps) {
|
||||||
const [activeSection, setActiveSection] =
|
const [activeSection, setActiveSection] =
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState, useTransition } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
import { validateChallenge, rejectChallenge, updateChallenge, deleteChallenge } from "@/actions/admin/challenges";
|
import {
|
||||||
|
validateChallenge,
|
||||||
|
rejectChallenge,
|
||||||
|
updateChallenge,
|
||||||
|
deleteChallenge,
|
||||||
|
} from "@/actions/admin/challenges";
|
||||||
import { Button, Card, Input, Textarea, Alert } from "@/components/ui";
|
import { Button, Card, Input, Textarea, Alert } from "@/components/ui";
|
||||||
import { Avatar } from "@/components/ui";
|
import { Avatar } from "@/components/ui";
|
||||||
|
|
||||||
@@ -29,8 +34,12 @@ interface Challenge {
|
|||||||
export default function ChallengeManagement() {
|
export default function ChallengeManagement() {
|
||||||
const [challenges, setChallenges] = useState<Challenge[]>([]);
|
const [challenges, setChallenges] = useState<Challenge[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [selectedChallenge, setSelectedChallenge] = useState<Challenge | null>(null);
|
const [selectedChallenge, setSelectedChallenge] = useState<Challenge | null>(
|
||||||
const [editingChallenge, setEditingChallenge] = useState<Challenge | null>(null);
|
null
|
||||||
|
);
|
||||||
|
const [editingChallenge, setEditingChallenge] = useState<Challenge | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const [winnerId, setWinnerId] = useState<string>("");
|
const [winnerId, setWinnerId] = useState<string>("");
|
||||||
const [adminComment, setAdminComment] = useState("");
|
const [adminComment, setAdminComment] = useState("");
|
||||||
const [editTitle, setEditTitle] = useState("");
|
const [editTitle, setEditTitle] = useState("");
|
||||||
@@ -73,7 +82,9 @@ export default function ChallengeManagement() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
setSuccessMessage("Défi validé avec succès ! Les points ont été attribués.");
|
setSuccessMessage(
|
||||||
|
"Défi validé avec succès ! Les points ont été attribués."
|
||||||
|
);
|
||||||
setSelectedChallenge(null);
|
setSelectedChallenge(null);
|
||||||
setWinnerId("");
|
setWinnerId("");
|
||||||
setAdminComment("");
|
setAdminComment("");
|
||||||
@@ -145,7 +156,11 @@ export default function ChallengeManagement() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async (challengeId: string) => {
|
const handleDelete = async (challengeId: string) => {
|
||||||
if (!confirm("Êtes-vous sûr de vouloir supprimer ce défi ? Cette action est irréversible.")) {
|
if (
|
||||||
|
!confirm(
|
||||||
|
"Êtes-vous sûr de vouloir supprimer ce défi ? Cette action est irréversible."
|
||||||
|
)
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,10 +208,12 @@ export default function ChallengeManagement() {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
<div className="text-sm text-gray-400 mb-4">
|
<div className="text-sm text-gray-400 mb-4">
|
||||||
{acceptedChallenges.length} défi{acceptedChallenges.length > 1 ? "s" : ""} en attente de validation
|
{acceptedChallenges.length} défi
|
||||||
|
{acceptedChallenges.length > 1 ? "s" : ""} en attente de validation
|
||||||
{pendingChallenges.length > 0 && (
|
{pendingChallenges.length > 0 && (
|
||||||
<span className="ml-2">
|
<span className="ml-2">
|
||||||
• {pendingChallenges.length} défi{pendingChallenges.length > 1 ? "s" : ""} en attente d'acceptation
|
• {pendingChallenges.length} défi
|
||||||
|
{pendingChallenges.length > 1 ? "s" : ""} en attente d'acceptation
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -233,20 +250,28 @@ export default function ChallengeManagement() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-sm text-gray-400">
|
<div className="text-sm text-gray-400">
|
||||||
Récompense: <span className="text-pixel-gold font-bold">{challenge.pointsReward} points</span>
|
Récompense:{" "}
|
||||||
|
<span className="text-pixel-gold font-bold">
|
||||||
|
{challenge.pointsReward} points
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs mt-2">
|
<div className="text-xs mt-2">
|
||||||
<span className={`px-2 py-1 rounded ${
|
<span
|
||||||
|
className={`px-2 py-1 rounded ${
|
||||||
challenge.status === "ACCEPTED"
|
challenge.status === "ACCEPTED"
|
||||||
? "bg-green-500/20 text-green-400"
|
? "bg-green-500/20 text-green-400"
|
||||||
: "bg-yellow-500/20 text-yellow-400"
|
: "bg-yellow-500/20 text-yellow-400"
|
||||||
}`}>
|
}`}
|
||||||
{challenge.status === "ACCEPTED" ? "Accepté" : "En attente d'acceptation"}
|
>
|
||||||
|
{challenge.status === "ACCEPTED"
|
||||||
|
? "Accepté"
|
||||||
|
: "En attente d'acceptation"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{challenge.acceptedAt && (
|
{challenge.acceptedAt && (
|
||||||
<div className="text-xs text-gray-500 mt-2">
|
<div className="text-xs text-gray-500 mt-2">
|
||||||
Accepté le: {new Date(challenge.acceptedAt).toLocaleDateString("fr-FR")}
|
Accepté le:{" "}
|
||||||
|
{new Date(challenge.acceptedAt).toLocaleDateString("fr-FR")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -466,7 +491,9 @@ export default function ChallengeManagement() {
|
|||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
value={editPointsReward}
|
value={editPointsReward}
|
||||||
onChange={(e) => setEditPointsReward(parseInt(e.target.value) || 0)}
|
onChange={(e) =>
|
||||||
|
setEditPointsReward(parseInt(e.target.value) || 0)
|
||||||
|
}
|
||||||
required
|
required
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
/>
|
/>
|
||||||
@@ -475,7 +502,12 @@ export default function ChallengeManagement() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleUpdate}
|
onClick={handleUpdate}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={isPending || !editTitle || !editDescription || editPointsReward <= 0}
|
disabled={
|
||||||
|
isPending ||
|
||||||
|
!editTitle ||
|
||||||
|
!editDescription ||
|
||||||
|
editPointsReward <= 0
|
||||||
|
}
|
||||||
className="flex-1"
|
className="flex-1"
|
||||||
>
|
>
|
||||||
{isPending ? "Mise à jour..." : "Enregistrer"}
|
{isPending ? "Mise à jour..." : "Enregistrer"}
|
||||||
@@ -501,4 +533,3 @@ export default function ChallengeManagement() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,19 @@
|
|||||||
|
|
||||||
import { useEffect, useState, useTransition } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { createChallenge, acceptChallenge, cancelChallenge } from "@/actions/challenges/create";
|
import {
|
||||||
import { Button, Card, SectionTitle, Input, Textarea, Alert } from "@/components/ui";
|
createChallenge,
|
||||||
|
acceptChallenge,
|
||||||
|
cancelChallenge,
|
||||||
|
} from "@/actions/challenges/create";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
SectionTitle,
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
Alert,
|
||||||
|
} from "@/components/ui";
|
||||||
import { Avatar } from "@/components/ui";
|
import { Avatar } from "@/components/ui";
|
||||||
|
|
||||||
interface User {
|
interface User {
|
||||||
@@ -44,7 +55,9 @@ interface ChallengesSectionProps {
|
|||||||
backgroundImage: string;
|
backgroundImage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ChallengesSection({ backgroundImage }: ChallengesSectionProps) {
|
export default function ChallengesSection({
|
||||||
|
backgroundImage,
|
||||||
|
}: ChallengesSectionProps) {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
const [challenges, setChallenges] = useState<Challenge[]>([]);
|
const [challenges, setChallenges] = useState<Challenge[]>([]);
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
@@ -159,7 +172,7 @@ export default function ChallengesSection({ backgroundImage }: ChallengesSection
|
|||||||
const getStatusLabel = (status: string) => {
|
const getStatusLabel = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "PENDING":
|
case "PENDING":
|
||||||
return "En attente d'acceptation";
|
return "En attente d'acceptation";
|
||||||
case "ACCEPTED":
|
case "ACCEPTED":
|
||||||
return "Accepté - En attente de validation admin";
|
return "Accepté - En attente de validation admin";
|
||||||
case "COMPLETED":
|
case "COMPLETED":
|
||||||
@@ -284,7 +297,9 @@ export default function ChallengesSection({ backgroundImage }: ChallengesSection
|
|||||||
<Input
|
<Input
|
||||||
type="number"
|
type="number"
|
||||||
value={pointsReward}
|
value={pointsReward}
|
||||||
onChange={(e) => setPointsReward(parseInt(e.target.value) || 100)}
|
onChange={(e) =>
|
||||||
|
setPointsReward(parseInt(e.target.value) || 100)
|
||||||
|
}
|
||||||
min={1}
|
min={1}
|
||||||
max={1000}
|
max={1000}
|
||||||
/>
|
/>
|
||||||
@@ -319,7 +334,8 @@ export default function ChallengesSection({ backgroundImage }: ChallengesSection
|
|||||||
const isChallenged = challenge.challenged.id === currentUserId;
|
const isChallenged = challenge.challenged.id === currentUserId;
|
||||||
const canAccept = challenge.status === "PENDING" && isChallenged;
|
const canAccept = challenge.status === "PENDING" && isChallenged;
|
||||||
const canCancel =
|
const canCancel =
|
||||||
(challenge.status === "PENDING" || challenge.status === "ACCEPTED") &&
|
(challenge.status === "PENDING" ||
|
||||||
|
challenge.status === "ACCEPTED") &&
|
||||||
(isChallenger || isChallenged);
|
(isChallenger || isChallenged);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -339,7 +355,9 @@ export default function ChallengesSection({ backgroundImage }: ChallengesSection
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p className="text-gray-300 mb-4">{challenge.description}</p>
|
<p className="text-gray-300 mb-4">
|
||||||
|
{challenge.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 mb-2">
|
<div className="flex items-center gap-4 mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -385,7 +403,10 @@ export default function ChallengesSection({ backgroundImage }: ChallengesSection
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="text-xs text-gray-500 mt-2">
|
<div className="text-xs text-gray-500 mt-2">
|
||||||
Créé le: {new Date(challenge.createdAt).toLocaleDateString("fr-FR")}
|
Créé le:{" "}
|
||||||
|
{new Date(challenge.createdAt).toLocaleDateString(
|
||||||
|
"fr-FR"
|
||||||
|
)}
|
||||||
{challenge.acceptedAt &&
|
{challenge.acceptedAt &&
|
||||||
` • Accepté le: ${new Date(challenge.acceptedAt).toLocaleDateString("fr-FR")}`}
|
` • Accepté le: ${new Date(challenge.acceptedAt).toLocaleDateString("fr-FR")}`}
|
||||||
{challenge.completedAt &&
|
{challenge.completedAt &&
|
||||||
@@ -425,4 +446,3 @@ export default function ChallengesSection({ backgroundImage }: ChallengesSection
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,4 +37,3 @@ export default function Footer() {
|
|||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,11 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Avatar } from "@/components/ui";
|
import { Avatar } from "@/components/ui";
|
||||||
import { getCharacterClassIcon, getCharacterClassName, type CharacterClass } from "@/lib/character-classes";
|
import {
|
||||||
|
getCharacterClassIcon,
|
||||||
|
getCharacterClassName,
|
||||||
|
type CharacterClass,
|
||||||
|
} from "@/lib/character-classes";
|
||||||
|
|
||||||
interface LeaderboardEntry {
|
interface LeaderboardEntry {
|
||||||
rank: number;
|
rank: number;
|
||||||
@@ -99,7 +103,8 @@ export default function Leaderboard() {
|
|||||||
</span>
|
</span>
|
||||||
{entry.characterClass && (
|
{entry.characterClass && (
|
||||||
<span className="text-xs text-gray-400 uppercase tracking-wider">
|
<span className="text-xs text-gray-400 uppercase tracking-wider">
|
||||||
[{getCharacterClassIcon(entry.characterClass)} {getCharacterClassName(entry.characterClass)}]
|
[{getCharacterClassIcon(entry.characterClass)}{" "}
|
||||||
|
{getCharacterClassName(entry.characterClass)}]
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -49,4 +49,3 @@ export default function BackgroundSection({
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ export default function Badge({
|
|||||||
}: BadgeProps) {
|
}: BadgeProps) {
|
||||||
const variantStyles = {
|
const variantStyles = {
|
||||||
default: {
|
default: {
|
||||||
backgroundColor: "color-mix(in srgb, var(--accent-color) 20%, transparent)",
|
backgroundColor:
|
||||||
|
"color-mix(in srgb, var(--accent-color) 20%, transparent)",
|
||||||
borderColor: "color-mix(in srgb, var(--accent-color) 50%, transparent)",
|
borderColor: "color-mix(in srgb, var(--accent-color) 50%, transparent)",
|
||||||
color: "var(--accent-color)",
|
color: "var(--accent-color)",
|
||||||
},
|
},
|
||||||
@@ -45,7 +46,8 @@ export default function Badge({
|
|||||||
color: "var(--yellow)",
|
color: "var(--yellow)",
|
||||||
},
|
},
|
||||||
danger: {
|
danger: {
|
||||||
backgroundColor: "color-mix(in srgb, var(--destructive) 20%, transparent)",
|
backgroundColor:
|
||||||
|
"color-mix(in srgb, var(--destructive) 20%, transparent)",
|
||||||
borderColor: "color-mix(in srgb, var(--destructive) 50%, transparent)",
|
borderColor: "color-mix(in srgb, var(--destructive) 50%, transparent)",
|
||||||
color: "var(--destructive)",
|
color: "var(--destructive)",
|
||||||
},
|
},
|
||||||
@@ -66,4 +68,3 @@ export default function Badge({
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,4 +39,3 @@ export default function Card({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,4 +26,3 @@ export default function CloseButton({
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,9 +35,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
{error && (
|
{error && <p className="text-red-400 text-xs mt-1">{error}</p>}
|
||||||
<p className="text-red-400 text-xs mt-1">{error}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -46,4 +44,3 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
Input.displayName = "Input";
|
Input.displayName = "Input";
|
||||||
|
|
||||||
export default Input;
|
export default Input;
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ export default function Modal({
|
|||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-[200] flex items-center justify-center p-4 backdrop-blur-sm"
|
className="fixed inset-0 z-[200] flex items-center justify-center p-4 backdrop-blur-sm"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "color-mix(in srgb, var(--background) 80%, transparent)",
|
backgroundColor:
|
||||||
|
"color-mix(in srgb, var(--background) 80%, transparent)",
|
||||||
}}
|
}}
|
||||||
onClick={closeOnOverlayClick ? onClose : undefined}
|
onClick={closeOnOverlayClick ? onClose : undefined}
|
||||||
>
|
>
|
||||||
@@ -49,7 +50,8 @@ export default function Modal({
|
|||||||
className={`border-2 rounded-lg w-full ${sizeClasses[size]} max-h-[90vh] overflow-y-auto shadow-2xl`}
|
className={`border-2 rounded-lg w-full ${sizeClasses[size]} max-h-[90vh] overflow-y-auto shadow-2xl`}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: "var(--card-hover)",
|
backgroundColor: "var(--card-hover)",
|
||||||
borderColor: "color-mix(in srgb, var(--accent-color) 70%, transparent)",
|
borderColor:
|
||||||
|
"color-mix(in srgb, var(--accent-color) 70%, transparent)",
|
||||||
}}
|
}}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
@@ -58,4 +60,3 @@ export default function Modal({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ interface ProgressBarProps extends HTMLAttributes<HTMLDivElement> {
|
|||||||
label?: string;
|
label?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGradientStyle = (variant: "hp" | "xp" | "default", percentage: number) => {
|
const getGradientStyle = (
|
||||||
|
variant: "hp" | "xp" | "default",
|
||||||
|
percentage: number
|
||||||
|
) => {
|
||||||
if (variant === "hp") {
|
if (variant === "hp") {
|
||||||
if (percentage > 60) {
|
if (percentage > 60) {
|
||||||
return {
|
return {
|
||||||
@@ -50,7 +53,10 @@ export default function ProgressBar({
|
|||||||
return (
|
return (
|
||||||
<div className={className} {...props}>
|
<div className={className} {...props}>
|
||||||
{showLabel && (
|
{showLabel && (
|
||||||
<div className="flex justify-between text-xs mb-1" style={{ color: "var(--gray-400)" }}>
|
<div
|
||||||
|
className="flex justify-between text-xs mb-1"
|
||||||
|
style={{ color: "var(--gray-400)" }}
|
||||||
|
>
|
||||||
<span>{label || variant.toUpperCase()}</span>
|
<span>{label || variant.toUpperCase()}</span>
|
||||||
<span>
|
<span>
|
||||||
{value} / {max}
|
{value} / {max}
|
||||||
@@ -74,7 +80,8 @@ export default function ProgressBar({
|
|||||||
<div
|
<div
|
||||||
className="absolute inset-0"
|
className="absolute inset-0"
|
||||||
style={{
|
style={{
|
||||||
background: "linear-gradient(to right, transparent, color-mix(in srgb, var(--foreground) 10%, transparent), transparent)",
|
background:
|
||||||
|
"linear-gradient(to right, transparent, color-mix(in srgb, var(--foreground) 10%, transparent), transparent)",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -88,4 +95,3 @@ export default function ProgressBar({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,10 @@ export default function StarRating({
|
|||||||
}}
|
}}
|
||||||
className={`transition-transform hover:scale-110 disabled:hover:scale-100 disabled:cursor-not-allowed ${sizeClasses[size]}`}
|
className={`transition-transform hover:scale-110 disabled:hover:scale-100 disabled:cursor-not-allowed ${sizeClasses[size]}`}
|
||||||
style={{
|
style={{
|
||||||
color: star <= displayValue ? "var(--accent-color)" : "var(--gray-500)",
|
color:
|
||||||
|
star <= displayValue
|
||||||
|
? "var(--accent-color)"
|
||||||
|
: "var(--gray-500)",
|
||||||
}}
|
}}
|
||||||
aria-label={`Noter ${star} étoile${star > 1 ? "s" : ""}`}
|
aria-label={`Noter ${star} étoile${star > 1 ? "s" : ""}`}
|
||||||
>
|
>
|
||||||
@@ -69,11 +72,8 @@ export default function StarRating({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{showValue && value > 0 && (
|
{showValue && value > 0 && (
|
||||||
<p className="text-gray-500 text-xs text-center">
|
<p className="text-gray-500 text-xs text-center">{value}/5</p>
|
||||||
{value}/5
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ interface TextareaProps extends TextareaHTMLAttributes<HTMLTextAreaElement> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||||
({ label, error, showCharCount, maxLength, className = "", value, ...props }, ref) => {
|
(
|
||||||
|
{ label, error, showCharCount, maxLength, className = "", value, ...props },
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
const charCount = typeof value === "string" ? value.length : 0;
|
const charCount = typeof value === "string" ? value.length : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -46,9 +49,7 @@ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
{charCount}/{maxLength} caractères
|
{charCount}/{maxLength} caractères
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{error && (
|
{error && <p className="text-red-400 text-xs mt-1">{error}</p>}
|
||||||
<p className="text-red-400 text-xs mt-1">{error}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -57,4 +58,3 @@ const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|||||||
Textarea.displayName = "Textarea";
|
Textarea.displayName = "Textarea";
|
||||||
|
|
||||||
export default Textarea;
|
export default Textarea;
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ export default function ThemeToggle() {
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
e.currentTarget.style.borderColor = "var(--accent-color)";
|
e.currentTarget.style.borderColor = "var(--accent-color)";
|
||||||
e.currentTarget.style.backgroundColor = "color-mix(in srgb, var(--accent-color) 10%, transparent)";
|
e.currentTarget.style.backgroundColor =
|
||||||
|
"color-mix(in srgb, var(--accent-color) 10%, transparent)";
|
||||||
}}
|
}}
|
||||||
onMouseLeave={(e) => {
|
onMouseLeave={(e) => {
|
||||||
e.currentTarget.style.borderColor = "var(--border)";
|
e.currentTarget.style.borderColor = "var(--border)";
|
||||||
@@ -28,4 +29,3 @@ export default function ThemeToggle() {
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,4 +12,3 @@ export { default as BackgroundSection } from "./BackgroundSection";
|
|||||||
export { default as Alert } from "./Alert";
|
export { default as Alert } from "./Alert";
|
||||||
export { default as CloseButton } from "./CloseButton";
|
export { default as CloseButton } from "./CloseButton";
|
||||||
export { default as ThemeToggle } from "./ThemeToggle";
|
export { default as ThemeToggle } from "./ThemeToggle";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -13,42 +12,42 @@
|
|||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as Prisma from './internal/prismaNamespaceBrowser'
|
import * as Prisma from "./internal/prismaNamespaceBrowser";
|
||||||
export { Prisma }
|
export { Prisma };
|
||||||
export * as $Enums from './enums'
|
export * as $Enums from "./enums";
|
||||||
export * from './enums';
|
export * from "./enums";
|
||||||
/**
|
/**
|
||||||
* Model User
|
* Model User
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type User = Prisma.UserModel
|
export type User = Prisma.UserModel;
|
||||||
/**
|
/**
|
||||||
* Model UserPreferences
|
* Model UserPreferences
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type UserPreferences = Prisma.UserPreferencesModel
|
export type UserPreferences = Prisma.UserPreferencesModel;
|
||||||
/**
|
/**
|
||||||
* Model Event
|
* Model Event
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Event = Prisma.EventModel
|
export type Event = Prisma.EventModel;
|
||||||
/**
|
/**
|
||||||
* Model EventRegistration
|
* Model EventRegistration
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type EventRegistration = Prisma.EventRegistrationModel
|
export type EventRegistration = Prisma.EventRegistrationModel;
|
||||||
/**
|
/**
|
||||||
* Model EventFeedback
|
* Model EventFeedback
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type EventFeedback = Prisma.EventFeedbackModel
|
export type EventFeedback = Prisma.EventFeedbackModel;
|
||||||
/**
|
/**
|
||||||
* Model SitePreferences
|
* Model SitePreferences
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type SitePreferences = Prisma.SitePreferencesModel
|
export type SitePreferences = Prisma.SitePreferencesModel;
|
||||||
/**
|
/**
|
||||||
* Model Challenge
|
* Model Challenge
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Challenge = Prisma.ChallengeModel
|
export type Challenge = Prisma.ChallengeModel;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -10,18 +9,18 @@
|
|||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as process from 'node:process'
|
import * as process from "node:process";
|
||||||
import * as path from 'node:path'
|
import * as path from "node:path";
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from "node:url";
|
||||||
globalThis['__dirname'] = path.dirname(fileURLToPath(import.meta.url))
|
globalThis["__dirname"] = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
import * as runtime from "@prisma/client/runtime/client"
|
import * as runtime from "@prisma/client/runtime/client";
|
||||||
import * as $Enums from "./enums"
|
import * as $Enums from "./enums";
|
||||||
import * as $Class from "./internal/class"
|
import * as $Class from "./internal/class";
|
||||||
import * as Prisma from "./internal/prismaNamespace"
|
import * as Prisma from "./internal/prismaNamespace";
|
||||||
|
|
||||||
export * as $Enums from './enums'
|
export * as $Enums from "./enums";
|
||||||
export * from "./enums"
|
export * from "./enums";
|
||||||
/**
|
/**
|
||||||
* ## Prisma Client
|
* ## Prisma Client
|
||||||
*
|
*
|
||||||
@@ -35,42 +34,48 @@ export * from "./enums"
|
|||||||
*
|
*
|
||||||
* Read more in our [docs](https://pris.ly/d/client).
|
* Read more in our [docs](https://pris.ly/d/client).
|
||||||
*/
|
*/
|
||||||
export const PrismaClient = $Class.getPrismaClientClass()
|
export const PrismaClient = $Class.getPrismaClientClass();
|
||||||
export type PrismaClient<LogOpts extends Prisma.LogLevel = never, OmitOpts extends Prisma.PrismaClientOptions["omit"] = Prisma.PrismaClientOptions["omit"], ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>
|
export type PrismaClient<
|
||||||
export { Prisma }
|
LogOpts extends Prisma.LogLevel = never,
|
||||||
|
OmitOpts extends Prisma.PrismaClientOptions["omit"] =
|
||||||
|
Prisma.PrismaClientOptions["omit"],
|
||||||
|
ExtArgs extends runtime.Types.Extensions.InternalArgs =
|
||||||
|
runtime.Types.Extensions.DefaultArgs,
|
||||||
|
> = $Class.PrismaClient<LogOpts, OmitOpts, ExtArgs>;
|
||||||
|
export { Prisma };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Model User
|
* Model User
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type User = Prisma.UserModel
|
export type User = Prisma.UserModel;
|
||||||
/**
|
/**
|
||||||
* Model UserPreferences
|
* Model UserPreferences
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type UserPreferences = Prisma.UserPreferencesModel
|
export type UserPreferences = Prisma.UserPreferencesModel;
|
||||||
/**
|
/**
|
||||||
* Model Event
|
* Model Event
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Event = Prisma.EventModel
|
export type Event = Prisma.EventModel;
|
||||||
/**
|
/**
|
||||||
* Model EventRegistration
|
* Model EventRegistration
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type EventRegistration = Prisma.EventRegistrationModel
|
export type EventRegistration = Prisma.EventRegistrationModel;
|
||||||
/**
|
/**
|
||||||
* Model EventFeedback
|
* Model EventFeedback
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type EventFeedback = Prisma.EventFeedbackModel
|
export type EventFeedback = Prisma.EventFeedbackModel;
|
||||||
/**
|
/**
|
||||||
* Model SitePreferences
|
* Model SitePreferences
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type SitePreferences = Prisma.SitePreferencesModel
|
export type SitePreferences = Prisma.SitePreferencesModel;
|
||||||
/**
|
/**
|
||||||
* Model Challenge
|
* Model Challenge
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
export type Challenge = Prisma.ChallengeModel
|
export type Challenge = Prisma.ChallengeModel;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,54 +1,52 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
// @ts-nocheck
|
// @ts-nocheck
|
||||||
/*
|
/*
|
||||||
* This file exports all enum related types from the schema.
|
* This file exports all enum related types from the schema.
|
||||||
*
|
*
|
||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const Role = {
|
export const Role = {
|
||||||
USER: 'USER',
|
USER: "USER",
|
||||||
ADMIN: 'ADMIN'
|
ADMIN: "ADMIN",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type Role = (typeof Role)[keyof typeof Role]
|
|
||||||
|
|
||||||
|
export type Role = (typeof Role)[keyof typeof Role];
|
||||||
|
|
||||||
export const EventType = {
|
export const EventType = {
|
||||||
ATELIER: 'ATELIER',
|
ATELIER: "ATELIER",
|
||||||
KATA: 'KATA',
|
KATA: "KATA",
|
||||||
PRESENTATION: 'PRESENTATION',
|
PRESENTATION: "PRESENTATION",
|
||||||
LEARNING_HOUR: 'LEARNING_HOUR'
|
LEARNING_HOUR: "LEARNING_HOUR",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type EventType = (typeof EventType)[keyof typeof EventType]
|
|
||||||
|
|
||||||
|
export type EventType = (typeof EventType)[keyof typeof EventType];
|
||||||
|
|
||||||
export const CharacterClass = {
|
export const CharacterClass = {
|
||||||
WARRIOR: 'WARRIOR',
|
WARRIOR: "WARRIOR",
|
||||||
MAGE: 'MAGE',
|
MAGE: "MAGE",
|
||||||
ROGUE: 'ROGUE',
|
ROGUE: "ROGUE",
|
||||||
RANGER: 'RANGER',
|
RANGER: "RANGER",
|
||||||
PALADIN: 'PALADIN',
|
PALADIN: "PALADIN",
|
||||||
ENGINEER: 'ENGINEER',
|
ENGINEER: "ENGINEER",
|
||||||
MERCHANT: 'MERCHANT',
|
MERCHANT: "MERCHANT",
|
||||||
SCHOLAR: 'SCHOLAR',
|
SCHOLAR: "SCHOLAR",
|
||||||
BERSERKER: 'BERSERKER',
|
BERSERKER: "BERSERKER",
|
||||||
NECROMANCER: 'NECROMANCER'
|
NECROMANCER: "NECROMANCER",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type CharacterClass = (typeof CharacterClass)[keyof typeof CharacterClass]
|
|
||||||
|
|
||||||
|
export type CharacterClass =
|
||||||
|
(typeof CharacterClass)[keyof typeof CharacterClass];
|
||||||
|
|
||||||
export const ChallengeStatus = {
|
export const ChallengeStatus = {
|
||||||
PENDING: 'PENDING',
|
PENDING: "PENDING",
|
||||||
ACCEPTED: 'ACCEPTED',
|
ACCEPTED: "ACCEPTED",
|
||||||
COMPLETED: 'COMPLETED',
|
COMPLETED: "COMPLETED",
|
||||||
REJECTED: 'REJECTED',
|
REJECTED: "REJECTED",
|
||||||
CANCELLED: 'CANCELLED'
|
CANCELLED: "CANCELLED",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type ChallengeStatus = (typeof ChallengeStatus)[keyof typeof ChallengeStatus]
|
export type ChallengeStatus =
|
||||||
|
(typeof ChallengeStatus)[keyof typeof ChallengeStatus];
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -15,183 +14,185 @@
|
|||||||
* model files in the `model` directory!
|
* model files in the `model` directory!
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as runtime from "@prisma/client/runtime/index-browser"
|
import * as runtime from "@prisma/client/runtime/index-browser";
|
||||||
|
|
||||||
export type * from '../models'
|
export type * from "../models";
|
||||||
export type * from './prismaNamespace'
|
export type * from "./prismaNamespace";
|
||||||
|
|
||||||
export const Decimal = runtime.Decimal
|
|
||||||
|
|
||||||
|
export const Decimal = runtime.Decimal;
|
||||||
|
|
||||||
export const NullTypes = {
|
export const NullTypes = {
|
||||||
DbNull: runtime.NullTypes.DbNull as (new (secret: never) => typeof runtime.DbNull),
|
DbNull: runtime.NullTypes.DbNull as new (
|
||||||
JsonNull: runtime.NullTypes.JsonNull as (new (secret: never) => typeof runtime.JsonNull),
|
secret: never
|
||||||
AnyNull: runtime.NullTypes.AnyNull as (new (secret: never) => typeof runtime.AnyNull),
|
) => typeof runtime.DbNull,
|
||||||
}
|
JsonNull: runtime.NullTypes.JsonNull as new (
|
||||||
|
secret: never
|
||||||
|
) => typeof runtime.JsonNull,
|
||||||
|
AnyNull: runtime.NullTypes.AnyNull as new (
|
||||||
|
secret: never
|
||||||
|
) => typeof runtime.AnyNull,
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
* Helper for filtering JSON entries that have `null` on the database (empty on the db)
|
||||||
*
|
*
|
||||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
*/
|
*/
|
||||||
export const DbNull = runtime.DbNull
|
export const DbNull = runtime.DbNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
* Helper for filtering JSON entries that have JSON `null` values (not empty on the db)
|
||||||
*
|
*
|
||||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
*/
|
*/
|
||||||
export const JsonNull = runtime.JsonNull
|
export const JsonNull = runtime.JsonNull;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
* Helper for filtering JSON entries that are `Prisma.DbNull` or `Prisma.JsonNull`
|
||||||
*
|
*
|
||||||
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
* @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-on-a-json-field
|
||||||
*/
|
*/
|
||||||
export const AnyNull = runtime.AnyNull
|
export const AnyNull = runtime.AnyNull;
|
||||||
|
|
||||||
|
|
||||||
export const ModelName = {
|
export const ModelName = {
|
||||||
User: 'User',
|
User: "User",
|
||||||
UserPreferences: 'UserPreferences',
|
UserPreferences: "UserPreferences",
|
||||||
Event: 'Event',
|
Event: "Event",
|
||||||
EventRegistration: 'EventRegistration',
|
EventRegistration: "EventRegistration",
|
||||||
EventFeedback: 'EventFeedback',
|
EventFeedback: "EventFeedback",
|
||||||
SitePreferences: 'SitePreferences',
|
SitePreferences: "SitePreferences",
|
||||||
Challenge: 'Challenge'
|
Challenge: "Challenge",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type ModelName = (typeof ModelName)[keyof typeof ModelName]
|
export type ModelName = (typeof ModelName)[keyof typeof ModelName];
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Enums
|
* Enums
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const TransactionIsolationLevel = {
|
export const TransactionIsolationLevel = {
|
||||||
Serializable: 'Serializable'
|
Serializable: "Serializable",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel]
|
|
||||||
|
|
||||||
|
export type TransactionIsolationLevel =
|
||||||
|
(typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel];
|
||||||
|
|
||||||
export const UserScalarFieldEnum = {
|
export const UserScalarFieldEnum = {
|
||||||
id: 'id',
|
id: "id",
|
||||||
email: 'email',
|
email: "email",
|
||||||
password: 'password',
|
password: "password",
|
||||||
username: 'username',
|
username: "username",
|
||||||
role: 'role',
|
role: "role",
|
||||||
score: 'score',
|
score: "score",
|
||||||
level: 'level',
|
level: "level",
|
||||||
hp: 'hp',
|
hp: "hp",
|
||||||
maxHp: 'maxHp',
|
maxHp: "maxHp",
|
||||||
xp: 'xp',
|
xp: "xp",
|
||||||
maxXp: 'maxXp',
|
maxXp: "maxXp",
|
||||||
avatar: 'avatar',
|
avatar: "avatar",
|
||||||
createdAt: 'createdAt',
|
createdAt: "createdAt",
|
||||||
updatedAt: 'updatedAt',
|
updatedAt: "updatedAt",
|
||||||
bio: 'bio',
|
bio: "bio",
|
||||||
characterClass: 'characterClass'
|
characterClass: "characterClass",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type UserScalarFieldEnum = (typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type UserScalarFieldEnum =
|
||||||
|
(typeof UserScalarFieldEnum)[keyof typeof UserScalarFieldEnum];
|
||||||
|
|
||||||
export const UserPreferencesScalarFieldEnum = {
|
export const UserPreferencesScalarFieldEnum = {
|
||||||
id: 'id',
|
id: "id",
|
||||||
userId: 'userId',
|
userId: "userId",
|
||||||
homeBackground: 'homeBackground',
|
homeBackground: "homeBackground",
|
||||||
eventsBackground: 'eventsBackground',
|
eventsBackground: "eventsBackground",
|
||||||
leaderboardBackground: 'leaderboardBackground',
|
leaderboardBackground: "leaderboardBackground",
|
||||||
theme: 'theme',
|
theme: "theme",
|
||||||
createdAt: 'createdAt',
|
createdAt: "createdAt",
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: "updatedAt",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type UserPreferencesScalarFieldEnum = (typeof UserPreferencesScalarFieldEnum)[keyof typeof UserPreferencesScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type UserPreferencesScalarFieldEnum =
|
||||||
|
(typeof UserPreferencesScalarFieldEnum)[keyof typeof UserPreferencesScalarFieldEnum];
|
||||||
|
|
||||||
export const EventScalarFieldEnum = {
|
export const EventScalarFieldEnum = {
|
||||||
id: 'id',
|
id: "id",
|
||||||
date: 'date',
|
date: "date",
|
||||||
name: 'name',
|
name: "name",
|
||||||
description: 'description',
|
description: "description",
|
||||||
type: 'type',
|
type: "type",
|
||||||
room: 'room',
|
room: "room",
|
||||||
time: 'time',
|
time: "time",
|
||||||
maxPlaces: 'maxPlaces',
|
maxPlaces: "maxPlaces",
|
||||||
createdAt: 'createdAt',
|
createdAt: "createdAt",
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: "updatedAt",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type EventScalarFieldEnum = (typeof EventScalarFieldEnum)[keyof typeof EventScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type EventScalarFieldEnum =
|
||||||
|
(typeof EventScalarFieldEnum)[keyof typeof EventScalarFieldEnum];
|
||||||
|
|
||||||
export const EventRegistrationScalarFieldEnum = {
|
export const EventRegistrationScalarFieldEnum = {
|
||||||
id: 'id',
|
id: "id",
|
||||||
userId: 'userId',
|
userId: "userId",
|
||||||
eventId: 'eventId',
|
eventId: "eventId",
|
||||||
createdAt: 'createdAt'
|
createdAt: "createdAt",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type EventRegistrationScalarFieldEnum = (typeof EventRegistrationScalarFieldEnum)[keyof typeof EventRegistrationScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type EventRegistrationScalarFieldEnum =
|
||||||
|
(typeof EventRegistrationScalarFieldEnum)[keyof typeof EventRegistrationScalarFieldEnum];
|
||||||
|
|
||||||
export const EventFeedbackScalarFieldEnum = {
|
export const EventFeedbackScalarFieldEnum = {
|
||||||
id: 'id',
|
id: "id",
|
||||||
userId: 'userId',
|
userId: "userId",
|
||||||
eventId: 'eventId',
|
eventId: "eventId",
|
||||||
rating: 'rating',
|
rating: "rating",
|
||||||
comment: 'comment',
|
comment: "comment",
|
||||||
createdAt: 'createdAt',
|
createdAt: "createdAt",
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: "updatedAt",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type EventFeedbackScalarFieldEnum = (typeof EventFeedbackScalarFieldEnum)[keyof typeof EventFeedbackScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type EventFeedbackScalarFieldEnum =
|
||||||
|
(typeof EventFeedbackScalarFieldEnum)[keyof typeof EventFeedbackScalarFieldEnum];
|
||||||
|
|
||||||
export const SitePreferencesScalarFieldEnum = {
|
export const SitePreferencesScalarFieldEnum = {
|
||||||
id: 'id',
|
id: "id",
|
||||||
homeBackground: 'homeBackground',
|
homeBackground: "homeBackground",
|
||||||
eventsBackground: 'eventsBackground',
|
eventsBackground: "eventsBackground",
|
||||||
leaderboardBackground: 'leaderboardBackground',
|
leaderboardBackground: "leaderboardBackground",
|
||||||
createdAt: 'createdAt',
|
createdAt: "createdAt",
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: "updatedAt",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type SitePreferencesScalarFieldEnum = (typeof SitePreferencesScalarFieldEnum)[keyof typeof SitePreferencesScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type SitePreferencesScalarFieldEnum =
|
||||||
|
(typeof SitePreferencesScalarFieldEnum)[keyof typeof SitePreferencesScalarFieldEnum];
|
||||||
|
|
||||||
export const ChallengeScalarFieldEnum = {
|
export const ChallengeScalarFieldEnum = {
|
||||||
id: 'id',
|
id: "id",
|
||||||
challengerId: 'challengerId',
|
challengerId: "challengerId",
|
||||||
challengedId: 'challengedId',
|
challengedId: "challengedId",
|
||||||
title: 'title',
|
title: "title",
|
||||||
description: 'description',
|
description: "description",
|
||||||
pointsReward: 'pointsReward',
|
pointsReward: "pointsReward",
|
||||||
status: 'status',
|
status: "status",
|
||||||
adminId: 'adminId',
|
adminId: "adminId",
|
||||||
adminComment: 'adminComment',
|
adminComment: "adminComment",
|
||||||
winnerId: 'winnerId',
|
winnerId: "winnerId",
|
||||||
createdAt: 'createdAt',
|
createdAt: "createdAt",
|
||||||
acceptedAt: 'acceptedAt',
|
acceptedAt: "acceptedAt",
|
||||||
completedAt: 'completedAt',
|
completedAt: "completedAt",
|
||||||
updatedAt: 'updatedAt'
|
updatedAt: "updatedAt",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type ChallengeScalarFieldEnum = (typeof ChallengeScalarFieldEnum)[keyof typeof ChallengeScalarFieldEnum]
|
|
||||||
|
|
||||||
|
export type ChallengeScalarFieldEnum =
|
||||||
|
(typeof ChallengeScalarFieldEnum)[keyof typeof ChallengeScalarFieldEnum];
|
||||||
|
|
||||||
export const SortOrder = {
|
export const SortOrder = {
|
||||||
asc: 'asc',
|
asc: "asc",
|
||||||
desc: 'desc'
|
desc: "desc",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder]
|
|
||||||
|
|
||||||
|
export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder];
|
||||||
|
|
||||||
export const NullsOrder = {
|
export const NullsOrder = {
|
||||||
first: 'first',
|
first: "first",
|
||||||
last: 'last'
|
last: "last",
|
||||||
} as const
|
} as const;
|
||||||
|
|
||||||
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder]
|
|
||||||
|
|
||||||
|
export type NullsOrder = (typeof NullsOrder)[keyof typeof NullsOrder];
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
/* !!! This is code generated by Prisma. Do not edit directly. !!! */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
// biome-ignore-all lint: generated file
|
// biome-ignore-all lint: generated file
|
||||||
@@ -8,11 +7,11 @@
|
|||||||
*
|
*
|
||||||
* 🟢 You can import this file directly.
|
* 🟢 You can import this file directly.
|
||||||
*/
|
*/
|
||||||
export type * from './models/User'
|
export type * from "./models/User";
|
||||||
export type * from './models/UserPreferences'
|
export type * from "./models/UserPreferences";
|
||||||
export type * from './models/Event'
|
export type * from "./models/Event";
|
||||||
export type * from './models/EventRegistration'
|
export type * from "./models/EventRegistration";
|
||||||
export type * from './models/EventFeedback'
|
export type * from "./models/EventFeedback";
|
||||||
export type * from './models/SitePreferences'
|
export type * from "./models/SitePreferences";
|
||||||
export type * from './models/Challenge'
|
export type * from "./models/Challenge";
|
||||||
export type * from './commonInputTypes'
|
export type * from "./commonInputTypes";
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,115 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# Create necessary directories
|
|
||||||
mkdir -p /app/data
|
|
||||||
mkdir -p /app/public/uploads
|
|
||||||
mkdir -p /app/public/uploads/backgrounds
|
|
||||||
|
|
||||||
# Function to deploy migrations with automatic failure resolution
|
|
||||||
deploy_migrations() {
|
|
||||||
echo "Deploying migrations..."
|
|
||||||
|
|
||||||
# Try to deploy migrations and capture both output and exit code
|
|
||||||
# Use a temporary file to capture exit code since tee masks it
|
|
||||||
pnpm dlx prisma migrate deploy > /tmp/migrate.log 2>&1
|
|
||||||
MIGRATE_EXIT=$?
|
|
||||||
cat /tmp/migrate.log # Display output
|
|
||||||
|
|
||||||
if [ $MIGRATE_EXIT -eq 0 ]; then
|
|
||||||
echo "Migrations deployed successfully"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Check if error is about failed migrations (P3009)
|
|
||||||
if ! grep -q "P3009" /tmp/migrate.log && ! grep -q "failed migrations" /tmp/migrate.log; then
|
|
||||||
echo "Migration error is not about failed migrations"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Found failed migrations, attempting to resolve..."
|
|
||||||
|
|
||||||
# Extract migration name from error - look for text between backticks
|
|
||||||
# Pattern: The `20251210120000_add_event_feedback` migration
|
|
||||||
MIGRATION_NAME=$(grep -oE "\`[0-9_]+[a-zA-Z_]+\`" /tmp/migrate.log | tr -d '\`' | head -1)
|
|
||||||
|
|
||||||
# If still not found, try without backticks (fallback)
|
|
||||||
if [ -z "$MIGRATION_NAME" ]; then
|
|
||||||
MIGRATION_NAME=$(grep "failed" /tmp/migrate.log | grep -oE "[0-9]{14}_[a-zA-Z_]+" | head -1)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$MIGRATION_NAME" ]; then
|
|
||||||
echo "Could not extract migration name from error. Log content:"
|
|
||||||
cat /tmp/migrate.log
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Resolving failed migration: $MIGRATION_NAME"
|
|
||||||
|
|
||||||
# First, try the standard Prisma resolve command
|
|
||||||
RESOLVE_OUTPUT=$(pnpm dlx prisma migrate resolve --applied "$MIGRATION_NAME" 2>&1)
|
|
||||||
RESOLVE_EXIT=$?
|
|
||||||
|
|
||||||
echo "$RESOLVE_OUTPUT"
|
|
||||||
|
|
||||||
# If Prisma resolve fails (e.g., migration files not found), resolve directly in database
|
|
||||||
if [ $RESOLVE_EXIT -ne 0 ]; then
|
|
||||||
if echo "$RESOLVE_OUTPUT" | grep -q "P3017\|could not be found"; then
|
|
||||||
echo "Migration files not found, resolving directly in database..."
|
|
||||||
|
|
||||||
# Use sqlite3 to mark migration as applied directly in _prisma_migrations table
|
|
||||||
# Check if sqlite3 is available
|
|
||||||
if command -v sqlite3 >/dev/null 2>&1; then
|
|
||||||
# Extract database path from DATABASE_URL (format: file:/app/data/dev.db)
|
|
||||||
DB_PATH=$(echo "$DATABASE_URL" | sed 's|file:||')
|
|
||||||
if [ -z "$DB_PATH" ] || [ "$DB_PATH" = "$DATABASE_URL" ]; then
|
|
||||||
DB_PATH="/app/data/dev.db"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Ensure database file exists
|
|
||||||
if [ ! -f "$DB_PATH" ]; then
|
|
||||||
echo "Database file not found at $DB_PATH"
|
|
||||||
else
|
|
||||||
echo "Updating migration status in database: $DB_PATH"
|
|
||||||
# Get current timestamp in milliseconds
|
|
||||||
TIMESTAMP=$(date +%s)000
|
|
||||||
# Update the migration record
|
|
||||||
sqlite3 "$DB_PATH" "UPDATE _prisma_migrations SET finished_at = $TIMESTAMP, rolled_back_at = NULL WHERE migration_name = '$MIGRATION_NAME' AND finished_at IS NULL;" 2>&1
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo "Migration marked as applied in database"
|
|
||||||
# Verify the update
|
|
||||||
RESULT=$(sqlite3 "$DB_PATH" "SELECT migration_name, finished_at FROM _prisma_migrations WHERE migration_name = '$MIGRATION_NAME';" 2>&1)
|
|
||||||
echo "Migration status: $RESULT"
|
|
||||||
else
|
|
||||||
echo "Failed to update database directly"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "sqlite3 not available, cannot resolve directly in database"
|
|
||||||
echo "Install sqlite3 in Dockerfile: RUN apk add --no-cache sqlite"
|
|
||||||
fi
|
|
||||||
elif echo "$RESOLVE_OUTPUT" | grep -q "already recorded as applied"; then
|
|
||||||
echo "Migration is already marked as applied"
|
|
||||||
else
|
|
||||||
echo "Failed to resolve migration: $RESOLVE_OUTPUT"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Retry migration deploy after resolution attempt
|
|
||||||
echo "Retrying migration deploy..."
|
|
||||||
if pnpm dlx prisma migrate deploy 2>&1; then
|
|
||||||
echo "Migrations deployed successfully after resolution"
|
|
||||||
return 0
|
|
||||||
else
|
|
||||||
echo "Migration deploy still failed after resolution, but continuing..."
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# Deploy migrations (don't fail if it errors - app might still work)
|
|
||||||
deploy_migrations || {
|
|
||||||
echo "Migration deployment had issues, but continuing to start app..."
|
|
||||||
}
|
|
||||||
|
|
||||||
# Start the application
|
|
||||||
exec pnpm start
|
|
||||||
@@ -52,9 +52,7 @@ export class ChallengeService {
|
|||||||
/**
|
/**
|
||||||
* Crée un nouveau défi
|
* Crée un nouveau défi
|
||||||
*/
|
*/
|
||||||
async createChallenge(
|
async createChallenge(data: CreateChallengeInput): Promise<Challenge> {
|
||||||
data: CreateChallengeInput
|
|
||||||
): Promise<Challenge> {
|
|
||||||
// Vérifier que les deux joueurs existent
|
// Vérifier que les deux joueurs existent
|
||||||
const [challenger, challenged] = await Promise.all([
|
const [challenger, challenged] = await Promise.all([
|
||||||
prisma.user.findUnique({ where: { id: data.challengerId } }),
|
prisma.user.findUnique({ where: { id: data.challengerId } }),
|
||||||
@@ -103,9 +101,7 @@ export class ChallengeService {
|
|||||||
|
|
||||||
// Vérifier que l'utilisateur est bien celui qui reçoit le défi
|
// Vérifier que l'utilisateur est bien celui qui reçoit le défi
|
||||||
if (challenge.challengedId !== userId) {
|
if (challenge.challengedId !== userId) {
|
||||||
throw new ValidationError(
|
throw new ValidationError("Vous n'êtes pas autorisé à accepter ce défi");
|
||||||
"Vous n'êtes pas autorisé à accepter ce défi"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier que le défi est en attente
|
// Vérifier que le défi est en attente
|
||||||
@@ -144,9 +140,7 @@ export class ChallengeService {
|
|||||||
challenge.challengerId !== userId &&
|
challenge.challengerId !== userId &&
|
||||||
challenge.challengedId !== userId
|
challenge.challengedId !== userId
|
||||||
) {
|
) {
|
||||||
throw new ValidationError(
|
throw new ValidationError("Vous n'êtes pas autorisé à annuler ce défi");
|
||||||
"Vous n'êtes pas autorisé à annuler ce défi"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vérifier que le défi peut être annulé
|
// Vérifier que le défi peut être annulé
|
||||||
@@ -293,13 +287,17 @@ export class ChallengeService {
|
|||||||
updateData.status = data.status;
|
updateData.status = data.status;
|
||||||
}
|
}
|
||||||
if (data.adminId !== undefined) {
|
if (data.adminId !== undefined) {
|
||||||
updateData.admin = data.adminId ? { connect: { id: data.adminId } } : { disconnect: true };
|
updateData.admin = data.adminId
|
||||||
|
? { connect: { id: data.adminId } }
|
||||||
|
: { disconnect: true };
|
||||||
}
|
}
|
||||||
if (data.adminComment !== undefined) {
|
if (data.adminComment !== undefined) {
|
||||||
updateData.adminComment = data.adminComment;
|
updateData.adminComment = data.adminComment;
|
||||||
}
|
}
|
||||||
if (data.winnerId !== undefined) {
|
if (data.winnerId !== undefined) {
|
||||||
updateData.winner = data.winnerId ? { connect: { id: data.winnerId } } : { disconnect: true };
|
updateData.winner = data.winnerId
|
||||||
|
? { connect: { id: data.winnerId } }
|
||||||
|
: { disconnect: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
return prisma.challenge.update({
|
return prisma.challenge.update({
|
||||||
@@ -370,10 +368,7 @@ export class ChallengeService {
|
|||||||
async getUserChallenges(userId: string): Promise<ChallengeWithUsers[]> {
|
async getUserChallenges(userId: string): Promise<ChallengeWithUsers[]> {
|
||||||
return prisma.challenge.findMany({
|
return prisma.challenge.findMany({
|
||||||
where: {
|
where: {
|
||||||
OR: [
|
OR: [{ challengerId: userId }, { challengedId: userId }],
|
||||||
{ challengerId: userId },
|
|
||||||
{ challengedId: userId },
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
challenger: {
|
challenger: {
|
||||||
@@ -497,4 +492,3 @@ export class ChallengeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const challengeService = new ChallengeService();
|
export const challengeService = new ChallengeService();
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,3 @@
|
|||||||
* Tous les services doivent importer depuis ici, pas directement depuis lib/prisma.ts
|
* Tous les services doivent importer depuis ici, pas directement depuis lib/prisma.ts
|
||||||
*/
|
*/
|
||||||
export { prisma } from "@/lib/prisma";
|
export { prisma } from "@/lib/prisma";
|
||||||
|
|
||||||
|
|||||||
@@ -2,14 +2,20 @@
|
|||||||
* Erreurs métier personnalisées
|
* Erreurs métier personnalisées
|
||||||
*/
|
*/
|
||||||
export class BusinessError extends Error {
|
export class BusinessError extends Error {
|
||||||
constructor(message: string, public code?: string) {
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public code?: string
|
||||||
|
) {
|
||||||
super(message);
|
super(message);
|
||||||
this.name = "BusinessError";
|
this.name = "BusinessError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ValidationError extends BusinessError {
|
export class ValidationError extends BusinessError {
|
||||||
constructor(message: string, public field?: string) {
|
constructor(
|
||||||
|
message: string,
|
||||||
|
public field?: string
|
||||||
|
) {
|
||||||
super(message, "VALIDATION_ERROR");
|
super(message, "VALIDATION_ERROR");
|
||||||
this.name = "ValidationError";
|
this.name = "ValidationError";
|
||||||
}
|
}
|
||||||
@@ -28,4 +34,3 @@ export class ConflictError extends BusinessError {
|
|||||||
this.name = "ConflictError";
|
this.name = "ConflictError";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import { prisma } from "../database";
|
import { prisma } from "../database";
|
||||||
import type {
|
import type { Event, Prisma } from "@/prisma/generated/prisma/client";
|
||||||
Event,
|
|
||||||
Prisma,
|
|
||||||
} from "@/prisma/generated/prisma/client";
|
|
||||||
import { EventType } from "@/prisma/generated/prisma/client";
|
import { EventType } from "@/prisma/generated/prisma/client";
|
||||||
import { ValidationError, NotFoundError } from "../errors";
|
import { ValidationError, NotFoundError } from "../errors";
|
||||||
import { calculateEventStatus } from "@/lib/eventStatus";
|
import { calculateEventStatus } from "@/lib/eventStatus";
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { prisma } from "../database";
|
import { prisma } from "../database";
|
||||||
import type { User, Role, Prisma, CharacterClass } from "@/prisma/generated/prisma/client";
|
import type {
|
||||||
|
User,
|
||||||
|
Role,
|
||||||
|
Prisma,
|
||||||
|
CharacterClass,
|
||||||
|
} from "@/prisma/generated/prisma/client";
|
||||||
import { NotFoundError } from "../errors";
|
import { NotFoundError } from "../errors";
|
||||||
import { userService } from "./user.service";
|
import { userService } from "./user.service";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user