feat: Implement pricing page, AI generation/transformation APIs, user/project management, and database schema.
This commit is contained in:
@@ -5,11 +5,7 @@ import { auth } from '@/lib/auth';
|
||||
import getDB from '@/lib/prisma';
|
||||
import { generateStoryContent } from '@/lib/gemini';
|
||||
|
||||
const PLAN_AI_LIMITS: Record<string, number> = {
|
||||
free: 100,
|
||||
pro: 5000,
|
||||
master: 999999,
|
||||
};
|
||||
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -23,17 +19,19 @@ export async function POST(request: NextRequest) {
|
||||
// Check AI usage limit from DB
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: { plan: true, aiActionsUsed: true },
|
||||
});
|
||||
include: { subscriptionPlan: true },
|
||||
}) as any; // Bypass Prisma client types for this relation
|
||||
|
||||
if (!dbUser) {
|
||||
return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
const limit = PLAN_AI_LIMITS[dbUser.plan] || PLAN_AI_LIMITS.free;
|
||||
if (dbUser.aiActionsUsed >= limit) {
|
||||
const limit = dbUser.subscriptionPlan?.maxAiActions ?? 100;
|
||||
const planName = dbUser.subscriptionPlan?.displayName || 'Gratuit';
|
||||
|
||||
if (limit !== -1 && dbUser.aiActionsUsed >= limit) {
|
||||
return NextResponse.json(
|
||||
{ error: `Limite de ${limit} actions IA atteinte pour le plan ${dbUser.plan}. Passez au plan supérieur !` },
|
||||
{ error: `Limite de ${limit} actions IA atteinte pour le plan ${planName}. Passez au plan supérieur !` },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,7 @@ import { auth } from '@/lib/auth';
|
||||
import getDB from '@/lib/prisma';
|
||||
import { transformTextServer } from '@/lib/gemini';
|
||||
|
||||
const PLAN_AI_LIMITS: Record<string, number> = {
|
||||
free: 100,
|
||||
pro: 5000,
|
||||
master: 999999,
|
||||
};
|
||||
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
@@ -23,17 +19,19 @@ export async function POST(request: NextRequest) {
|
||||
// Check AI usage limit from DB
|
||||
const dbUser = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
select: { plan: true, aiActionsUsed: true },
|
||||
});
|
||||
include: { subscriptionPlan: true },
|
||||
}) as any; // Bypass Prisma type cache
|
||||
|
||||
if (!dbUser) {
|
||||
return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
const limit = PLAN_AI_LIMITS[dbUser.plan] || PLAN_AI_LIMITS.free;
|
||||
if (dbUser.aiActionsUsed >= limit) {
|
||||
const limit = dbUser.subscriptionPlan?.maxAiActions ?? 100;
|
||||
const planName = dbUser.subscriptionPlan?.displayName || 'Gratuit';
|
||||
|
||||
if (limit !== -1 && dbUser.aiActionsUsed >= limit) {
|
||||
return NextResponse.json(
|
||||
{ error: `Limite de ${limit} actions IA atteinte pour le plan ${dbUser.plan}. Passez au plan supérieur !` },
|
||||
{ error: `Limite de ${limit} actions IA atteinte pour le plan ${planName}. Passez au plan supérieur !` },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
19
src/app/api/plans/route.ts
Normal file
19
src/app/api/plans/route.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import getDB from '@/lib/prisma';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const prisma = getDB();
|
||||
const plans = await prisma.plan.findMany({
|
||||
orderBy: { price: 'asc' }
|
||||
});
|
||||
const response = NextResponse.json(plans);
|
||||
response.headers.set('Cache-Control', 'no-store, max-age=0');
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch plans', error);
|
||||
return NextResponse.json({ error: 'Failed to fetch plans' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,7 @@ export async function GET() {
|
||||
return NextResponse.json(projects);
|
||||
}
|
||||
|
||||
// Plan limits for project creation
|
||||
const PLAN_PROJECT_LIMITS: Record<string, number> = {
|
||||
free: 3,
|
||||
pro: 20,
|
||||
master: 999,
|
||||
};
|
||||
|
||||
|
||||
// POST /api/projects — Create a new project
|
||||
export async function POST(request: NextRequest) {
|
||||
@@ -36,17 +31,20 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
// Check plan limits
|
||||
const prisma = getDB();
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
include: { subscriptionPlan: true }
|
||||
}) as any; // Cast to any to bypass Prisma type cache issues
|
||||
|
||||
// Check plan limit
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { plan: true } });
|
||||
const plan = user?.plan || 'free';
|
||||
const limit = PLAN_PROJECT_LIMITS[plan] || PLAN_PROJECT_LIMITS.free;
|
||||
const limit = user?.subscriptionPlan?.maxProjects ?? 3;
|
||||
const planName = user?.subscriptionPlan?.displayName || 'Gratuit';
|
||||
const currentCount = await prisma.project.count({ where: { userId: session.user.id } });
|
||||
|
||||
if (currentCount >= limit) {
|
||||
if (limit !== -1 && currentCount >= limit) {
|
||||
return NextResponse.json(
|
||||
{ error: `Limite de ${limit} projets atteinte pour le plan ${plan}. Passez au plan supérieur !` },
|
||||
{ error: `Limite de ${limit} projets atteinte pour le plan ${planName}. Passez au plan supérieur !` },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ export async function GET() {
|
||||
const prisma = getDB();
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: session.user.id },
|
||||
});
|
||||
include: { subscriptionPlan: true }
|
||||
} as any) as any; // Bypass Prisma type cache
|
||||
|
||||
if (!user) {
|
||||
return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 });
|
||||
@@ -31,13 +32,24 @@ export async function GET() {
|
||||
return total + (text ? text.split(/\s+/).length : 0);
|
||||
}, 0);
|
||||
|
||||
return NextResponse.json({
|
||||
const response = NextResponse.json({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
avatar: user.avatar,
|
||||
bio: user.bio,
|
||||
plan: user.plan,
|
||||
plan: user.planId || user.plan || 'free',
|
||||
planDetails: user.subscriptionPlan ? {
|
||||
id: user.subscriptionPlan.id,
|
||||
name: user.subscriptionPlan.name,
|
||||
displayName: user.subscriptionPlan.displayName,
|
||||
price: user.subscriptionPlan.price,
|
||||
description: user.subscriptionPlan.description,
|
||||
features: user.subscriptionPlan.features,
|
||||
maxProjects: user.subscriptionPlan.maxProjects,
|
||||
maxAiActions: user.subscriptionPlan.maxAiActions,
|
||||
isPopular: user.subscriptionPlan.isPopular
|
||||
} : undefined,
|
||||
aiActionsUsed: user.aiActionsUsed,
|
||||
dailyWordGoal: user.dailyWordGoal,
|
||||
writingStreak: user.writingStreak,
|
||||
@@ -45,6 +57,8 @@ export async function GET() {
|
||||
createdAt: user.createdAt,
|
||||
totalWords,
|
||||
});
|
||||
response.headers.set('Cache-Control', 'no-store, max-age=0');
|
||||
return response;
|
||||
}
|
||||
|
||||
// PUT /api/user/profile — Update user profile
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import Pricing from '@/components/Pricing';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthContext } from '@/providers/AuthProvider';
|
||||
@@ -8,8 +9,26 @@ export default function PricingPage() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuthContext();
|
||||
|
||||
const [plans, setPlans] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/api/plans', { cache: 'no-store' })
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
setPlans(data);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error(err);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Pricing
|
||||
plans={plans}
|
||||
isLoading={isLoading}
|
||||
currentPlan={user?.subscription.plan || 'free'}
|
||||
onBack={() => router.push(user ? '/dashboard' : '/')}
|
||||
onSelectPlan={() => router.push(user ? '/checkout' : '/login')}
|
||||
|
||||
Reference in New Issue
Block a user