connection base prisma + postgres + login ok
This commit is contained in:
28
src/app/api/ai/generate/route.ts
Normal file
28
src/app/api/ai/generate/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { generateStoryContent } from '@/lib/gemini';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { project, chapterId, prompt, user } = body;
|
||||
|
||||
if (!project || !prompt || !user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: project, prompt, user' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const result = await generateStoryContent(project, chapterId || '', prompt, user);
|
||||
|
||||
return NextResponse.json(result);
|
||||
} catch (error) {
|
||||
console.error('AI generate error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'AI generation failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
28
src/app/api/ai/transform/route.ts
Normal file
28
src/app/api/ai/transform/route.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { transformTextServer } from '@/lib/gemini';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { text, mode, context, user } = body;
|
||||
|
||||
if (!text || !mode || !user) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Missing required fields: text, mode, user' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const result = await transformTextServer(text, mode, context || '', user);
|
||||
|
||||
return NextResponse.json({ text: result });
|
||||
} catch (error) {
|
||||
console.error('AI transform error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'AI transformation failed' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
12
src/app/api/auth/[...nextauth]/route.ts
Normal file
12
src/app/api/auth/[...nextauth]/route.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { handlers } from '@/lib/auth';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
return handlers.GET(request);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
return handlers.POST(request);
|
||||
}
|
||||
53
src/app/api/auth/register/route.ts
Normal file
53
src/app/api/auth/register/route.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import getDB from '@/lib/prisma';
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const { email, password, name } = await request.json();
|
||||
|
||||
if (!email || !password) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Email et mot de passe requis' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const prisma = getDB();
|
||||
|
||||
// Check if user exists
|
||||
const existing = await prisma.user.findUnique({ where: { email } });
|
||||
if (existing) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Un compte existe déjà avec cet email' },
|
||||
{ status: 409 }
|
||||
);
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
// Create user
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
name: name || 'User',
|
||||
hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Register error:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Erreur lors de la création du compte', details: error?.message || String(error) },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
65
src/app/api/chapters/[id]/route.ts
Normal file
65
src/app/api/chapters/[id]/route.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import getDB from '@/lib/prisma';
|
||||
|
||||
// PUT /api/chapters/[id] — Update a chapter
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
// Verify ownership via project
|
||||
const chapter = await getDB().chapter.findUnique({
|
||||
where: { id },
|
||||
include: { project: { select: { userId: true } } },
|
||||
});
|
||||
if (!chapter || chapter.project.userId !== session.user.id) {
|
||||
return NextResponse.json({ error: 'Non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updated = await getDB().chapter.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.title !== undefined && { title: body.title }),
|
||||
...(body.content !== undefined && { content: body.content }),
|
||||
...(body.summary !== undefined && { summary: body.summary }),
|
||||
...(body.orderIndex !== undefined && { orderIndex: body.orderIndex }),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
// DELETE /api/chapters/[id]
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const chapter = await getDB().chapter.findUnique({
|
||||
where: { id },
|
||||
include: { project: { select: { userId: true } } },
|
||||
});
|
||||
if (!chapter || chapter.project.userId !== session.user.id) {
|
||||
return NextResponse.json({ error: 'Non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
await getDB().chapter.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
35
src/app/api/chapters/route.ts
Normal file
35
src/app/api/chapters/route.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import getDB from '@/lib/prisma';
|
||||
|
||||
// POST /api/chapters — Create a chapter
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
// Verify project ownership
|
||||
const project = await getDB().project.findFirst({
|
||||
where: { id: body.projectId, userId: session.user.id },
|
||||
});
|
||||
if (!project) {
|
||||
return NextResponse.json({ error: 'Projet non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
const chapter = await getDB().chapter.create({
|
||||
data: {
|
||||
title: body.title || 'Nouveau Chapitre',
|
||||
content: body.content || '',
|
||||
summary: body.summary || null,
|
||||
orderIndex: body.orderIndex || 0,
|
||||
projectId: body.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(chapter, { status: 201 });
|
||||
}
|
||||
67
src/app/api/entities/[id]/route.ts
Normal file
67
src/app/api/entities/[id]/route.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import getDB from '@/lib/prisma';
|
||||
|
||||
// PUT /api/entities/[id]
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
const entity = await getDB().entity.findUnique({
|
||||
where: { id },
|
||||
include: { project: { select: { userId: true } } },
|
||||
});
|
||||
if (!entity || entity.project.userId !== session.user.id) {
|
||||
return NextResponse.json({ error: 'Non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updated = await getDB().entity.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.name !== undefined && { name: body.name }),
|
||||
...(body.type !== undefined && { type: body.type }),
|
||||
...(body.description !== undefined && { description: body.description }),
|
||||
...(body.details !== undefined && { details: body.details }),
|
||||
...(body.storyContext !== undefined && { storyContext: body.storyContext }),
|
||||
...(body.attributes !== undefined && { attributes: body.attributes }),
|
||||
...(body.customValues !== undefined && { customValues: body.customValues }),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
// DELETE /api/entities/[id]
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const entity = await getDB().entity.findUnique({
|
||||
where: { id },
|
||||
include: { project: { select: { userId: true } } },
|
||||
});
|
||||
if (!entity || entity.project.userId !== session.user.id) {
|
||||
return NextResponse.json({ error: 'Non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
await getDB().entity.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
37
src/app/api/entities/route.ts
Normal file
37
src/app/api/entities/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import getDB from '@/lib/prisma';
|
||||
|
||||
// POST /api/entities — Create an entity
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const project = await getDB().project.findFirst({
|
||||
where: { id: body.projectId, userId: session.user.id },
|
||||
});
|
||||
if (!project) {
|
||||
return NextResponse.json({ error: 'Projet non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
const entity = await getDB().entity.create({
|
||||
data: {
|
||||
type: body.type,
|
||||
name: body.name || 'Nouvelle entité',
|
||||
description: body.description || '',
|
||||
details: body.details || '',
|
||||
storyContext: body.storyContext || null,
|
||||
attributes: body.attributes || null,
|
||||
customValues: body.customValues || null,
|
||||
projectId: body.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(entity, { status: 201 });
|
||||
}
|
||||
64
src/app/api/ideas/[id]/route.ts
Normal file
64
src/app/api/ideas/[id]/route.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import getDB from '@/lib/prisma';
|
||||
|
||||
// PUT /api/ideas/[id]
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
const idea = await getDB().idea.findUnique({
|
||||
where: { id },
|
||||
include: { project: { select: { userId: true } } },
|
||||
});
|
||||
if (!idea || idea.project.userId !== session.user.id) {
|
||||
return NextResponse.json({ error: 'Non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
const updated = await getDB().idea.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.title !== undefined && { title: body.title }),
|
||||
...(body.description !== undefined && { description: body.description }),
|
||||
...(body.status !== undefined && { status: body.status }),
|
||||
...(body.category !== undefined && { category: body.category }),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(updated);
|
||||
}
|
||||
|
||||
// DELETE /api/ideas/[id]
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const idea = await getDB().idea.findUnique({
|
||||
where: { id },
|
||||
include: { project: { select: { userId: true } } },
|
||||
});
|
||||
if (!idea || idea.project.userId !== session.user.id) {
|
||||
return NextResponse.json({ error: 'Non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
await getDB().idea.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
34
src/app/api/ideas/route.ts
Normal file
34
src/app/api/ideas/route.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import getDB from '@/lib/prisma';
|
||||
|
||||
// POST /api/ideas
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const project = await getDB().project.findFirst({
|
||||
where: { id: body.projectId, userId: session.user.id },
|
||||
});
|
||||
if (!project) {
|
||||
return NextResponse.json({ error: 'Projet non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
const idea = await getDB().idea.create({
|
||||
data: {
|
||||
title: body.title || 'Nouvelle idée',
|
||||
description: body.description || '',
|
||||
status: body.status || 'todo',
|
||||
category: body.category || 'plot',
|
||||
projectId: body.projectId,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(idea, { status: 201 });
|
||||
}
|
||||
94
src/app/api/projects/[id]/route.ts
Normal file
94
src/app/api/projects/[id]/route.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import getDB from '@/lib/prisma';
|
||||
|
||||
// GET /api/projects/[id] — Get project with all related data
|
||||
export async function GET(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
const project = await getDB().project.findFirst({
|
||||
where: { id, userId: session.user.id },
|
||||
include: {
|
||||
chapters: { orderBy: { orderIndex: 'asc' } },
|
||||
entities: true,
|
||||
ideas: { orderBy: { createdAt: 'desc' } },
|
||||
plotNodes: true,
|
||||
plotConnections: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return NextResponse.json({ error: 'Projet non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json(project);
|
||||
}
|
||||
|
||||
// PUT /api/projects/[id] — Update a project
|
||||
export async function PUT(
|
||||
request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
|
||||
// Verify ownership
|
||||
const existing = await getDB().project.findFirst({
|
||||
where: { id, userId: session.user.id },
|
||||
});
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Projet non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
const project = await getDB().project.update({
|
||||
where: { id },
|
||||
data: {
|
||||
...(body.title !== undefined && { title: body.title }),
|
||||
...(body.author !== undefined && { author: body.author }),
|
||||
...(body.settings !== undefined && { settings: body.settings }),
|
||||
...(body.styleGuide !== undefined && { styleGuide: body.styleGuide }),
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(project);
|
||||
}
|
||||
|
||||
// DELETE /api/projects/[id] — Delete project (cascades to all children)
|
||||
export async function DELETE(
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
|
||||
// Verify ownership
|
||||
const existing = await getDB().project.findFirst({
|
||||
where: { id, userId: session.user.id },
|
||||
});
|
||||
if (!existing) {
|
||||
return NextResponse.json({ error: 'Projet non trouvé' }, { status: 404 });
|
||||
}
|
||||
|
||||
await getDB().project.delete({ where: { id } });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
44
src/app/api/projects/route.ts
Normal file
44
src/app/api/projects/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
export const dynamic = 'force-dynamic';
|
||||
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { auth } from '@/lib/auth';
|
||||
import getDB from '@/lib/prisma';
|
||||
|
||||
// GET /api/projects — List all user's projects
|
||||
export async function GET() {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const projects = await getDB().project.findMany({
|
||||
where: { userId: session.user.id },
|
||||
orderBy: { updatedAt: 'desc' },
|
||||
include: {
|
||||
_count: { select: { chapters: true, entities: true } },
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(projects);
|
||||
}
|
||||
|
||||
// POST /api/projects — Create a new project
|
||||
export async function POST(request: NextRequest) {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
const project = await getDB().project.create({
|
||||
data: {
|
||||
title: body.title || 'Nouveau Roman',
|
||||
author: body.author || session.user.name || 'Auteur',
|
||||
settings: body.settings || null,
|
||||
userId: session.user.id,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json(project, { status: 201 });
|
||||
}
|
||||
15
src/app/checkout/page.tsx
Normal file
15
src/app/checkout/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import Checkout from '@/components/Checkout';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<Checkout
|
||||
onComplete={() => router.push('/dashboard')}
|
||||
onCancel={() => router.push('/pricing')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
56
src/app/dashboard/page.tsx
Normal file
56
src/app/dashboard/page.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthContext } from '@/providers/AuthProvider';
|
||||
import { useProjects } from '@/hooks/useProjects';
|
||||
import Dashboard from '@/components/Dashboard';
|
||||
import { Loader2, BookOpen } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const router = useRouter();
|
||||
const { user, logout, loading } = useAuthContext();
|
||||
const { projects, setCurrentProjectId, createProject } = useProjects(user);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) {
|
||||
router.replace('/login');
|
||||
}
|
||||
}, [user, loading, router]);
|
||||
|
||||
if (loading || !user) {
|
||||
return (
|
||||
<div className="h-screen w-full flex flex-col items-center justify-center bg-slate-900 text-white">
|
||||
<Loader2 className="animate-spin text-blue-500 mb-4" size={48} />
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen className="text-blue-500" size={20} />
|
||||
<span className="text-lg font-bold">PlumeIA</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dashboard
|
||||
user={user}
|
||||
projects={projects}
|
||||
onSelect={(id) => {
|
||||
setCurrentProjectId(id);
|
||||
router.push(`/project/${id}`);
|
||||
}}
|
||||
onCreate={async () => {
|
||||
const id = await createProject();
|
||||
if (id) {
|
||||
setCurrentProjectId(id);
|
||||
router.push(`/project/${id}`);
|
||||
}
|
||||
}}
|
||||
onLogout={() => {
|
||||
logout();
|
||||
router.push('/');
|
||||
}}
|
||||
onPricing={() => router.push('/pricing')}
|
||||
onProfile={() => router.push('/profile')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
16
src/app/features/page.tsx
Normal file
16
src/app/features/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import FeaturesPage from '@/components/FeaturesPage';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
export default function Features() {
|
||||
const router = useRouter();
|
||||
const { data: session } = useSession();
|
||||
|
||||
return (
|
||||
<FeaturesPage
|
||||
onBack={() => router.push(session ? '/dashboard' : '/')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
75
src/app/globals.css
Normal file
75
src/app/globals.css
Normal file
@@ -0,0 +1,75 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
/* Custom Theme */
|
||||
@theme {
|
||||
--font-sans: 'Inter', sans-serif;
|
||||
--font-serif: 'Merriweather', serif;
|
||||
--color-paper: #fcfbf7;
|
||||
}
|
||||
|
||||
/* Editor placeholder */
|
||||
.editor-content:empty:before {
|
||||
content: attr(placeholder);
|
||||
color: #9ca3af;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Custom scrollbar for webkit */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
/* Print styles */
|
||||
@media print {
|
||||
@page {
|
||||
margin: 2cm;
|
||||
size: auto;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
#__next {
|
||||
height: auto !important;
|
||||
overflow: visible !important;
|
||||
display: block !important;
|
||||
position: relative !important;
|
||||
}
|
||||
|
||||
.no-print { display: none !important; }
|
||||
.print-only { display: block !important; }
|
||||
|
||||
.break-before-page { page-break-before: always; break-before: page; }
|
||||
.break-after-page { page-break-after: always; break-after: page; }
|
||||
|
||||
p {
|
||||
text-align: justify;
|
||||
widows: 3;
|
||||
orphans: 3;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4 {
|
||||
color: black !important;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
a { text-decoration: none; color: black !important; }
|
||||
}
|
||||
36
src/app/layout.tsx
Normal file
36
src/app/layout.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Inter, Merriweather } from "next/font/google";
|
||||
import { AuthProvider } from "@/providers/AuthProvider";
|
||||
import "./globals.css";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
const merriweather = Merriweather({
|
||||
subsets: ["latin"],
|
||||
weight: ["300", "400", "700", "900"],
|
||||
variable: "--font-serif",
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "PlumeIA - Éditeur Intelligent",
|
||||
description: "Votre assistant éditorial intelligent propulsé par l'IA pour écrire votre prochain roman.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="fr">
|
||||
<body className={`${inter.variable} ${merriweather.variable} font-sans bg-gray-100 text-slate-800 h-screen overflow-hidden antialiased`}>
|
||||
<AuthProvider>
|
||||
{children}
|
||||
</AuthProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
15
src/app/login/page.tsx
Normal file
15
src/app/login/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import LoginPage from '@/components/LoginPage';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function Login() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<LoginPage
|
||||
onSuccess={() => router.push('/dashboard')}
|
||||
onRegister={() => router.push('/signup')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
16
src/app/page.tsx
Normal file
16
src/app/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import LandingPage from '@/components/LandingPage';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function Home() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<LandingPage
|
||||
onLogin={() => router.push('/login')}
|
||||
onFeatures={() => router.push('/features')}
|
||||
onPricing={() => router.push('/pricing')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
18
src/app/pricing/page.tsx
Normal file
18
src/app/pricing/page.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import Pricing from '@/components/Pricing';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthContext } from '@/providers/AuthProvider';
|
||||
|
||||
export default function PricingPage() {
|
||||
const router = useRouter();
|
||||
const { user } = useAuthContext();
|
||||
|
||||
return (
|
||||
<Pricing
|
||||
currentPlan={user?.subscription.plan || 'free'}
|
||||
onBack={() => router.push(user ? '/dashboard' : '/')}
|
||||
onSelectPlan={() => router.push(user ? '/checkout' : '/login')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
27
src/app/profile/page.tsx
Normal file
27
src/app/profile/page.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
'use client';
|
||||
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useAuthContext } from '@/providers/AuthProvider';
|
||||
import UserProfileSettings from '@/components/UserProfileSettings';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function ProfilePage() {
|
||||
const router = useRouter();
|
||||
const { user, loading } = useAuthContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (!loading && !user) {
|
||||
router.replace('/login');
|
||||
}
|
||||
}, [user, loading, router]);
|
||||
|
||||
if (loading || !user) return null;
|
||||
|
||||
return (
|
||||
<UserProfileSettings
|
||||
user={user}
|
||||
onUpdate={(updates) => console.log('Profile update:', updates)}
|
||||
onBack={() => router.push('/dashboard')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
15
src/app/project/[id]/ideas/page.tsx
Normal file
15
src/app/project/[id]/ideas/page.tsx
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import IdeaBoard from '@/components/IdeaBoard';
|
||||
import { useProjectContext } from '@/providers/ProjectProvider';
|
||||
|
||||
export default function IdeasPage() {
|
||||
const { project, updateProject } = useProjectContext();
|
||||
|
||||
return (
|
||||
<IdeaBoard
|
||||
ideas={project.ideas || []}
|
||||
onUpdate={(ideas) => updateProject({ ideas })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
141
src/app/project/[id]/layout.tsx
Normal file
141
src/app/project/[id]/layout.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useParams, useRouter, usePathname } from 'next/navigation';
|
||||
import { useAuthContext } from '@/providers/AuthProvider';
|
||||
import { ProjectProvider } from '@/providers/ProjectProvider';
|
||||
import { useProjects } from '@/hooks/useProjects';
|
||||
import { useChat } from '@/hooks/useChat';
|
||||
import { ViewMode } from '@/lib/types';
|
||||
import EditorShell from '@/components/layout/EditorShell';
|
||||
import ExportModal from '@/components/ExportModal';
|
||||
import HelpModal from '@/components/HelpModal';
|
||||
import { Loader2, BookOpen } from 'lucide-react';
|
||||
|
||||
function getViewModeFromPath(pathname: string): ViewMode {
|
||||
if (pathname.endsWith('/world')) return 'world_building';
|
||||
if (pathname.endsWith('/ideas')) return 'ideas';
|
||||
if (pathname.endsWith('/workflow')) return 'workflow';
|
||||
if (pathname.endsWith('/settings')) return 'settings';
|
||||
return 'write';
|
||||
}
|
||||
|
||||
export default function ProjectLayout({ children }: { children: React.ReactNode }) {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const projectId = params.id as string;
|
||||
|
||||
const { user, logout, incrementUsage, loading: authLoading } = useAuthContext();
|
||||
const {
|
||||
projects, setCurrentProjectId,
|
||||
updateProject, updateChapter, addChapter,
|
||||
} = useProjects(user);
|
||||
const { chatHistory, isGenerating, sendMessage } = useChat();
|
||||
|
||||
const [currentChapterId, setCurrentChapterId] = useState('');
|
||||
const [isExportModalOpen, setIsExportModalOpen] = useState(false);
|
||||
const [isHelpModalOpen, setIsHelpModalOpen] = useState(false);
|
||||
|
||||
const viewMode = getViewModeFromPath(pathname);
|
||||
|
||||
useEffect(() => {
|
||||
if (projectId) setCurrentProjectId(projectId);
|
||||
}, [projectId, setCurrentProjectId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!authLoading && !user) router.replace('/login');
|
||||
}, [user, authLoading, router]);
|
||||
|
||||
const project = projects.find(p => p.id === projectId);
|
||||
|
||||
useEffect(() => {
|
||||
if (project && (!currentChapterId || !project.chapters.some(c => c.id === currentChapterId))) {
|
||||
setCurrentChapterId(project.chapters[0]?.id || '');
|
||||
}
|
||||
}, [project, currentChapterId]);
|
||||
|
||||
if (authLoading || !user) {
|
||||
return (
|
||||
<div className="h-screen w-full flex flex-col items-center justify-center bg-slate-900 text-white">
|
||||
<Loader2 className="animate-spin text-blue-500 mb-4" size={48} />
|
||||
<div className="flex items-center gap-2">
|
||||
<BookOpen className="text-blue-500" size={20} />
|
||||
<span className="text-lg font-bold">PlumeIA</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!project) {
|
||||
return (
|
||||
<div className="h-screen w-full flex flex-col items-center justify-center bg-slate-900 text-white">
|
||||
<Loader2 className="animate-spin text-blue-500 mb-4" size={48} />
|
||||
<p className="text-slate-400">Chargement du projet...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleViewModeChange = (mode: ViewMode) => {
|
||||
const base = `/project/${projectId}`;
|
||||
switch (mode) {
|
||||
case 'write': router.push(base); break;
|
||||
case 'world_building': router.push(`${base}/world`); break;
|
||||
case 'ideas': router.push(`${base}/ideas`); break;
|
||||
case 'workflow': router.push(`${base}/workflow`); break;
|
||||
case 'settings': router.push(`${base}/settings`); break;
|
||||
case 'dashboard': router.push('/dashboard'); break;
|
||||
default: router.push(base);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ProjectProvider value={{
|
||||
project,
|
||||
user,
|
||||
projectId,
|
||||
currentChapterId,
|
||||
setCurrentChapterId,
|
||||
updateProject: (updates) => updateProject(projectId, updates),
|
||||
updateChapter: (chapterId, data) => updateChapter(projectId, chapterId, data),
|
||||
incrementUsage,
|
||||
}}>
|
||||
<EditorShell
|
||||
project={project}
|
||||
user={user}
|
||||
viewMode={viewMode}
|
||||
currentChapterId={currentChapterId}
|
||||
chatHistory={chatHistory}
|
||||
isGenerating={isGenerating}
|
||||
onViewModeChange={handleViewModeChange}
|
||||
onChapterSelect={(id) => { setCurrentChapterId(id); router.push(`/project/${projectId}`); }}
|
||||
onUpdateProject={(updates) => updateProject(projectId, updates)}
|
||||
onAddChapter={async () => {
|
||||
const id = await addChapter(projectId, {});
|
||||
if (id) {
|
||||
setCurrentChapterId(id);
|
||||
router.push(`/project/${projectId}`);
|
||||
}
|
||||
}}
|
||||
onDeleteChapter={(id) => {
|
||||
if (project.chapters.length > 1) {
|
||||
const newChapters = project.chapters.filter(c => c.id !== id);
|
||||
updateProject(projectId, { chapters: newChapters });
|
||||
if (currentChapterId === id) setCurrentChapterId(newChapters[0].id);
|
||||
}
|
||||
}}
|
||||
onLogout={() => { logout(); router.push('/'); }}
|
||||
onSendMessage={(msg) => {
|
||||
if (project && user) sendMessage(project, 'global', msg, user, incrementUsage);
|
||||
}}
|
||||
onInsertText={() => { }}
|
||||
onOpenExport={() => setIsExportModalOpen(true)}
|
||||
onOpenHelp={() => setIsHelpModalOpen(true)}
|
||||
>
|
||||
<ExportModal isOpen={isExportModalOpen} onClose={() => setIsExportModalOpen(false)} project={project} onPrint={() => { }} />
|
||||
<HelpModal isOpen={isHelpModalOpen} onClose={() => setIsHelpModalOpen(false)} viewMode={viewMode} />
|
||||
{children}
|
||||
</EditorShell>
|
||||
</ProjectProvider>
|
||||
);
|
||||
}
|
||||
28
src/app/project/[id]/page.tsx
Normal file
28
src/app/project/[id]/page.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import React, { useRef } from 'react';
|
||||
import RichTextEditor, { RichTextEditorHandle } from '@/components/RichTextEditor';
|
||||
import { useProjectContext } from '@/providers/ProjectProvider';
|
||||
import api from '@/lib/api';
|
||||
|
||||
export default function WritePage() {
|
||||
const editorRef = useRef<RichTextEditorHandle>(null);
|
||||
const { project, user, currentChapterId, updateChapter, incrementUsage } = useProjectContext();
|
||||
|
||||
if (!currentChapterId) return null;
|
||||
|
||||
const currentChapter = project.chapters?.find(c => c.id === currentChapterId);
|
||||
|
||||
return (
|
||||
<RichTextEditor
|
||||
ref={editorRef}
|
||||
initialContent={currentChapter?.content || ''}
|
||||
onSave={(html) => updateChapter(currentChapterId, { content: html })}
|
||||
onAiTransform={async (text, mode) => {
|
||||
const result = await api.ai.transform(text, mode, currentChapter?.content || '', user);
|
||||
incrementUsage();
|
||||
return result;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
25
src/app/project/[id]/settings/page.tsx
Normal file
25
src/app/project/[id]/settings/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
'use client';
|
||||
|
||||
import BookSettingsComponent from '@/components/BookSettings';
|
||||
import { useProjectContext } from '@/providers/ProjectProvider';
|
||||
import { useAuthContext } from '@/providers/AuthProvider';
|
||||
import { useProjects } from '@/hooks/useProjects';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { project, projectId, updateProject } = useProjectContext();
|
||||
const { user } = useAuthContext();
|
||||
const { deleteProject } = useProjects(user);
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<BookSettingsComponent
|
||||
project={project}
|
||||
onUpdate={(updates) => updateProject(updates)}
|
||||
onDeleteProject={async () => {
|
||||
await deleteProject(projectId);
|
||||
router.push('/dashboard');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
19
src/app/project/[id]/workflow/page.tsx
Normal file
19
src/app/project/[id]/workflow/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
'use client';
|
||||
|
||||
import StoryWorkflow from '@/components/StoryWorkflow';
|
||||
import { useProjectContext } from '@/providers/ProjectProvider';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function WorkflowPage() {
|
||||
const { project, projectId, updateProject } = useProjectContext();
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<StoryWorkflow
|
||||
data={project.workflow || { nodes: [], connections: [] }}
|
||||
onUpdate={(workflow) => updateProject({ workflow })}
|
||||
entities={project.entities || []}
|
||||
onNavigateToEntity={() => router.push(`/project/${projectId}/world`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
26
src/app/project/[id]/world/page.tsx
Normal file
26
src/app/project/[id]/world/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import WorldBuilder from '@/components/WorldBuilder';
|
||||
import { useProjectContext } from '@/providers/ProjectProvider';
|
||||
import { useProjects } from '@/hooks/useProjects';
|
||||
import { useAuthContext } from '@/providers/AuthProvider';
|
||||
|
||||
export default function WorldPage() {
|
||||
const { project, projectId, updateProject } = useProjectContext();
|
||||
const { user } = useAuthContext();
|
||||
const { createEntity, updateEntity, deleteEntity } = useProjects(user);
|
||||
|
||||
return (
|
||||
<WorldBuilder
|
||||
entities={project.entities || []}
|
||||
onCreate={async (entityData) => {
|
||||
return await createEntity(projectId, entityData.type, entityData);
|
||||
}}
|
||||
onUpdate={(entityId, updates) => updateEntity(projectId, entityId, updates)}
|
||||
onDelete={(entityId) => deleteEntity(projectId, entityId)}
|
||||
templates={project.templates || []}
|
||||
onUpdateTemplates={(t) => updateProject({ templates: t })}
|
||||
initialSelectedId={null}
|
||||
/>
|
||||
);
|
||||
}
|
||||
16
src/app/signup/page.tsx
Normal file
16
src/app/signup/page.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import AuthPage from '@/components/AuthPage';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
export default function Signup() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<AuthPage
|
||||
onBack={() => router.push('/')}
|
||||
onSuccess={() => router.push('/dashboard')}
|
||||
initialMode="signup"
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user