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:
@@ -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]
|
||||
|
||||
@@ -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 });
|
||||
|
||||
76
src/app/api/checkout/route.ts
Normal file
76
src/app/api/checkout/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
70
src/app/api/webhooks/stripe/route.ts
Normal file
70
src/app/api/webhooks/stripe/route.ts
Normal 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 });
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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, {});
|
||||
|
||||
Reference in New Issue
Block a user