feat: implement core platform features including business directory, admin dashboard, authentication, and API infrastructure
Some checks failed
Build and Push App / build (push) Failing after 16m2s

This commit is contained in:
2026-04-18 22:10:19 +02:00
parent 6ec1a3ae84
commit 3e2063e4fa
89 changed files with 4405 additions and 967 deletions

View File

@@ -0,0 +1,115 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function POST(
req: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const { id: paramId } = await context.params;
const userId = req.headers.get('x-user-id');
if (!userId) {
return NextResponse.json({ error: 'Utilisateur non identifié' }, { status: 401 });
}
try {
const { value, comment } = await req.json();
if (!value || value < 1 || value > 5) {
return NextResponse.json({ error: 'Valeur de note invalide (1-5)' }, { status: 400 });
}
// 1. Resolve actual Business UUID if paramId is a slug
const business = await prisma.business.findFirst({
where: {
OR: [
{ id: paramId },
{ slug: paramId }
]
},
select: { id: true }
});
if (!business) {
return NextResponse.json({ error: "Les données de démonstration ne peuvent pas recevoir d'avis. Veuillez créer votre propre entreprise pour tester." }, { status: 403 });
}
const businessId = business.id;
const ownerId = (business as any).ownerId;
// 2. Security: Don't allow owner to rate themselves
if (userId === ownerId) {
return NextResponse.json({ error: "Vous ne pouvez pas noter votre propre entreprise." }, { status: 403 });
}
// 3. Fetch existing rating to check rules
const existingRating = await prisma.rating.findUnique({
where: {
userId_businessId: {
userId,
businessId
}
}
});
if (existingRating) {
// Rule: Score can only be re-evaluated UPWARDS
if (value < existingRating.value) {
return NextResponse.json({
error: `La note ne peut être réévaluée qu'à la hausse. Votre note actuelle est de ${existingRating.value} étoiles.`
}, { status: 400 });
}
// Rule: Comment is IMMUTABLE once set
// We only update the value. If the comment was empty, we can set it once.
await prisma.rating.update({
where: { id: existingRating.id },
data: {
value,
comment: existingRating.comment ? undefined : comment, // Only set if it was null
status: 'PENDING' // Reset to pending for re-moderation
}
});
} else {
// 4. Create new rating
await prisma.rating.create({
data: {
value,
comment,
userId,
businessId,
status: 'PENDING'
}
});
}
// 3. Aggregate all ratings for this business
const stats = await prisma.rating.aggregate({
where: { businessId },
_avg: { value: true },
_count: { value: true }
});
const newRating = stats._avg.value || 0;
const newRatingCount = stats._count.value || 0;
// 4. Update the business
const updatedBusiness = await prisma.business.update({
where: { id: businessId },
data: {
rating: newRating,
ratingCount: newRatingCount
}
});
return NextResponse.json({
rating: updatedBusiness.rating,
ratingCount: updatedBusiness.ratingCount,
message: 'Vote enregistré avec succès'
});
} catch (error: any) {
console.error('Error rating business:', error);
return NextResponse.json({ error: 'Erreur serveur lors du vote' }, { status: 500 });
}
}

View File

@@ -0,0 +1,49 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET(
req: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const { id: paramId } = await context.params;
const userId = req.headers.get('x-user-id');
if (!userId) {
return NextResponse.json({ rating: null });
}
try {
// Resolve actual Business UUID if paramId is a slug
const business = await prisma.business.findFirst({
where: {
OR: [
{ id: paramId },
{ slug: paramId }
]
},
select: { id: true }
});
if (!business) {
return NextResponse.json({ rating: null });
}
const rating = await prisma.rating.findUnique({
where: {
userId_businessId: {
userId,
businessId: business.id
}
}
});
return NextResponse.json({
rating: rating ? rating.value : null,
status: rating ? rating.status : null,
comment: rating ? rating.comment : null
});
} catch (error: any) {
console.error('Error fetching user rating:', error);
return NextResponse.json({ error: 'Erreur lors de la récupération de votre note' }, { status: 500 });
}
}

View File

@@ -0,0 +1,55 @@
import { NextRequest, NextResponse } from 'next/server';
import prisma from '@/lib/prisma';
export async function GET(
req: NextRequest,
context: { params: Promise<{ id: string }> }
) {
const { id: paramId } = await context.params;
try {
// 1. Resolve actual Business UUID if paramId is a slug
const business = await prisma.business.findFirst({
where: {
OR: [
{ id: paramId },
{ slug: paramId }
]
},
select: { id: true }
});
if (!business) {
// Return empty array for mock/missing businesses instead of error
return NextResponse.json([]);
}
const businessId = business.id;
// 2. Fetch all approved ratings with user info
const ratings = await prisma.rating.findMany({
where: {
businessId,
status: 'APPROVED'
},
include: {
user: {
select: {
id: true,
name: true,
avatar: true
}
}
},
orderBy: {
createdAt: 'desc'
}
});
return NextResponse.json(ratings);
} catch (error: any) {
console.error('Error fetching ratings:', error);
return NextResponse.json({ error: 'Erreur serveur lors de la récupération des avis' }, { status: 500 });
}
}

View File

@@ -1,5 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'
import prisma from '../../../../lib/prisma'
import prisma from '@/lib/prisma'
type Params = { params: Promise<{ id: string }> }
@@ -49,12 +49,48 @@ export async function PATCH(
// DELETE /api/businesses/[id]
export async function DELETE(
_request: NextRequest,
request: NextRequest,
context: { params: Promise<{ id: string }> }
): Promise<Response> {
try {
const { id } = await context.params
const headerUserId = request.headers.get('x-user-id')
// 1. Find business to check owner
const business = await prisma.business.findUnique({
where: { id },
select: { ownerId: true }
})
if (!business) {
return NextResponse.json({ error: 'Boutique non trouvée' }, { status: 404 })
}
// 2. Auth check (Owner or Admin)
const user = headerUserId ? await prisma.user.findUnique({ where: { id: headerUserId } }) : null
const isAdmin = user?.role === 'ADMIN'
const isOwner = business.ownerId === headerUserId
if (!isOwner && !isAdmin) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 403 })
}
// 3. Delete business
await prisma.business.delete({ where: { id } })
// 4. Update user role if they don't have other businesses
if (headerUserId && !isAdmin) {
const otherBusinesses = await prisma.business.findFirst({
where: { ownerId: headerUserId }
})
if (!otherBusinesses) {
await prisma.user.update({
where: { id: headerUserId },
data: { role: 'VISITOR' }
})
}
}
return NextResponse.json({ success: true })
} catch (error) {
console.error('DELETE /api/businesses/[id] error:', error)

View File

@@ -1,6 +1,6 @@
// Force refresh for Next.js 16 params fix
import { NextRequest, NextResponse } from 'next/server';
import prisma from '../../../../../lib/prisma';
import prisma from '@/lib/prisma';
export async function POST(
request: NextRequest,
@@ -13,10 +13,27 @@ export async function POST(
return NextResponse.json({ error: 'ID requis' }, { status: 400 });
}
// Attempt to increment in DB
// Note: We don't care about mock businesses here as they are static
// 1. Resolve actual Business UUID if id is a slug
const businessFound = await prisma.business.findFirst({
where: {
OR: [
{ id },
{ slug: id }
]
},
select: { id: true }
});
if (!businessFound) {
// If business not found in DB (might be mock), just return success but no update
return NextResponse.json({ success: true, message: 'Mock business, skipping view count' });
}
const businessId = businessFound.id;
// 2. Attempt to increment in DB
const business = await prisma.business.update({
where: { id },
where: { id: businessId },
data: {
viewCount: {
increment: 1,
@@ -26,8 +43,7 @@ export async function POST(
return NextResponse.json({ success: true, viewCount: business.viewCount });
} catch (error) {
// If business not found in DB (might be mock), just return success but no update
console.error('Error incrementing view count:', error);
return NextResponse.json({ success: false, error: 'Business not found or error' }, { status: 404 });
return NextResponse.json({ success: false, error: 'Internal server error' }, { status: 500 });
}
}