mie a jour de la des mots du jour,

optimisation de la latence des pages
ajout d'option  clique droit plus paragraphe
This commit is contained in:
2026-03-14 20:45:59 +01:00
parent 1f77255a6b
commit 12de3a8328
28 changed files with 492 additions and 178 deletions

View File

@@ -37,13 +37,20 @@ export async function PUT(
});
// Update writing streak if content was explicitly updated
let userStats = null;
if (body.content !== undefined) {
import('@/lib/streak').then(({ updateWritingStreak }) => {
updateWritingStreak(session.user.id).catch(console.error);
});
const oldWords = (chapter.content || '').replace(/<[^>]*>/g, ' ').trim().split(/\s+/).filter(Boolean).length;
const newWords = (body.content || '').replace(/<[^>]*>/g, ' ').trim().split(/\s+/).filter(Boolean).length;
const wordDelta = Math.max(0, newWords - oldWords);
const { updateWritingStreak } = await import('@/lib/streak');
userStats = await updateWritingStreak(session.user.id, wordDelta);
}
return NextResponse.json(updated);
return NextResponse.json({
...updated,
_userStats: userStats
});
}
// DELETE /api/chapters/[id]

View File

@@ -35,8 +35,9 @@ export async function POST(request: NextRequest) {
});
// Update writing streak
const wordCount = (body.content || '').replace(/<[^>]*>/g, ' ').trim().split(/\s+/).filter(Boolean).length;
import('@/lib/streak').then(({ updateWritingStreak }) => {
updateWritingStreak(session.user.id).catch(console.error);
updateWritingStreak(session.user.id, wordCount).catch(console.error);
});
return NextResponse.json(chapter, { status: 201 });

View File

@@ -0,0 +1,76 @@
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { prisma } from '@/lib/prisma';
import { auth } from '@/lib/auth';
export async function POST(req: Request) {
try {
const session = await auth();
if (!session?.user?.email) {
return new NextResponse('Unauthorized', { status: 401 });
}
const { planId } = await req.json();
if (!planId) {
return new NextResponse('Plan ID is required', { status: 400 });
}
const plan = await prisma.plan.findUnique({
where: { id: planId },
});
if (!plan) {
return new NextResponse('Plan not found', { status: 404 });
}
// Free plan doesn't need checkout
if (plan.id === 'free') {
return new NextResponse('Free plan does not require payment', { status: 400 });
}
const user = await prisma.user.findUnique({
where: { email: session.user.email },
});
if (!user) {
return new NextResponse('User not found', { status: 404 });
}
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
const checkoutSession = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
customer_email: session.user.email,
line_items: [
{
price_data: {
currency: 'eur',
product_data: {
name: plan.displayName,
description: plan.description,
},
unit_amount: Math.round(plan.price * 100),
recurring: {
interval: 'month',
},
},
quantity: 1,
},
],
metadata: {
userId: user.id,
planId: plan.id,
},
success_url: `${baseUrl}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/pricing`,
});
return NextResponse.json({ url: checkoutSession.url });
} catch (error) {
console.error('[STRIPE_CHECKOUT_ERROR]', error);
return new NextResponse('Internal Error', { status: 500 });
}
}

View File

@@ -54,6 +54,7 @@ export async function GET() {
dailyWordGoal: user.dailyWordGoal,
writingStreak: user.writingStreak,
lastWriteDate: user.lastWriteDate,
dailyWordCount: user.dailyWordCount || 0,
customApiProvider: user.customApiProvider,
customApiKey: user.customApiKey,
createdAt: user.createdAt,
@@ -78,6 +79,7 @@ export async function PUT(request: NextRequest) {
if (body.avatar !== undefined) data.avatar = body.avatar;
if (body.bio !== undefined) data.bio = body.bio;
if (body.dailyWordGoal !== undefined) data.dailyWordGoal = body.dailyWordGoal;
if (body.dailyWordCount !== undefined) data.dailyWordCount = body.dailyWordCount;
if (body.writingStreak !== undefined) data.writingStreak = body.writingStreak;
if (body.lastWriteDate !== undefined) data.lastWriteDate = body.lastWriteDate ? new Date(body.lastWriteDate) : null;
if (body.customApiProvider !== undefined) data.customApiProvider = body.customApiProvider;

View File

@@ -0,0 +1,70 @@
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { prisma } from '@/lib/prisma';
import Stripe from 'stripe';
export async function POST(req: Request) {
const body = await req.text();
const headersList = await headers();
const signature = headersList.get('Stripe-Signature') as string;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (error: any) {
return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 });
}
const session = event.data.object as Stripe.Checkout.Session;
if (event.type === 'checkout.session.completed') {
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
) as any;
if (!session?.metadata?.userId) {
return new NextResponse('User id is required', { status: 400 });
}
await prisma.user.update({
where: {
id: session.metadata.userId,
},
data: {
stripeSubscriptionId: subscription.id,
stripeCustomerId: subscription.customer as string,
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(
subscription.current_period_end * 1000
),
planId: session.metadata.planId,
},
});
}
if (event.type === 'invoice.payment_succeeded') {
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
) as any;
await prisma.user.update({
where: {
stripeSubscriptionId: subscription.id,
},
data: {
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(
subscription.current_period_end * 1000
),
},
});
}
return new NextResponse(null, { status: 200 });
}

View File

@@ -1,67 +0,0 @@
'use client';
import Checkout from '@/components/Checkout';
import { useRouter, useSearchParams } from 'next/navigation';
import { useEffect, useState, Suspense } from 'react';
function CheckoutContent() {
const router = useRouter();
const searchParams = useSearchParams();
const planId = searchParams.get('plan') || 'pro';
const [plan, setPlan] = useState<{ id: string, displayName: string, price: number } | null>(null);
useEffect(() => {
fetch('/api/plans', { cache: 'no-store' })
.then(res => res.json())
.then(data => {
const found = data.find((p: any) => p.id === planId);
if (found) {
setPlan(found);
} else {
setPlan(data.find((p: any) => p.id === 'pro') || { id: 'pro', displayName: 'Auteur Pro', price: 12.00 });
}
})
.catch(err => {
console.error(err);
setPlan({ id: 'pro', displayName: 'Auteur Pro', price: 12.00 });
});
}, [planId]);
const handleComplete = async () => {
if (plan) {
try {
await fetch('/api/user/profile', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ planId: plan.id })
});
// Force a full reload to update user context in providers
window.location.href = '/dashboard';
} catch (err) {
console.error('Failed to update plan', err);
router.push('/dashboard');
}
} else {
router.push('/dashboard');
}
};
if (!plan) return <div className="min-h-screen bg-[#eef2ff] flex items-center justify-center p-8">Chargement...</div>;
return (
<Checkout
plan={plan}
onComplete={handleComplete}
onCancel={() => router.push('/pricing')}
/>
);
}
export default function CheckoutPage() {
return (
<Suspense fallback={<div className="min-h-screen bg-[#eef2ff] flex items-center justify-center p-8">Chargement...</div>}>
<CheckoutContent />
</Suspense>
);
}

View File

@@ -29,9 +29,29 @@ export default function PricingPage() {
<Pricing
plans={plans}
isLoading={isLoading}
currentPlan={user?.subscription.plan || 'free'}
currentPlan={user?.planId || 'free'}
onBack={() => router.push(user ? '/dashboard' : '/')}
onSelectPlan={(id) => router.push(user ? `/checkout?plan=${id}` : '/login')}
onSelectPlan={async (id) => {
if (!user) {
router.push('/login');
return;
}
try {
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ planId: id })
});
const data = await response.json();
if (data.url) {
window.location.href = data.url;
}
} catch (err) {
console.error('Checkout error:', err);
}
}}
/>
);
}

View File

@@ -26,7 +26,7 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
const pathname = usePathname();
const projectId = params.id as string;
const { user, logout, incrementUsage, loading: authLoading } = useAuthContext();
const { user, logout, incrementUsage, refreshProfile, loading: authLoading } = useAuthContext();
const hasEverLoaded = useRef(false);
const {
projects, setCurrentProjectId,
@@ -115,11 +115,12 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
createEntity: (type, data) => createEntity(projectId, type, data),
updateEntity: (entityId, data) => updateEntity(projectId, entityId, data),
deleteEntity: (entityId) => deleteEntity(projectId, entityId),
createIdea: (projectId, data) => createIdea(projectId, data),
updateIdea: (projectId, ideaId, data) => updateIdea(projectId, ideaId, data),
deleteIdea: (projectId, ideaId) => deleteIdea(projectId, ideaId),
createIdea: (id, data) => createIdea(id, data),
updateIdea: (id, ideaId, data) => updateIdea(id, ideaId, data),
deleteIdea: (id, ideaId) => deleteIdea(id, ideaId),
deleteProject: () => deleteProject(projectId),
incrementUsage,
refreshProfile,
}}>
<EditorShell
project={project}
@@ -131,7 +132,13 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
onViewModeChange={handleViewModeChange}
onChapterSelect={(id) => { setCurrentChapterId(id); router.push(`/project/${projectId}`); }}
onUpdateProject={(updates) => updateProject(projectId, updates)}
onUpdateChapter={(chapterId, data) => updateChapter(projectId, chapterId, data)}
onUpdateChapter={async (chapterId, data) => {
await updateChapter(projectId, chapterId, data);
// If content was updated, refresh profile to sync "mots du jour"
if (data.content !== undefined) {
refreshProfile();
}
}}
onReorderChapters={(chapters) => reorderChapters(projectId, chapters)}
onAddChapter={async () => {
const id = await addChapter(projectId, {});