connection base prisma + postgres + login ok

This commit is contained in:
2026-02-26 21:58:16 +01:00
parent 78dc8813f1
commit 56b5615abf
1462 changed files with 429582 additions and 2546 deletions

View 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 }
);
}
}

View 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 }
);
}
}

View 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);
}

View 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 }
);
}
}

View 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 });
}

View 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 });
}

View 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 });
}

View 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 });
}

View 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 });
}

View 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 });
}

View 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 });
}

View 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
View 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')}
/>
);
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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')}
/>
);
}

View 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 })}
/>
);
}

View 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>
);
}

View 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;
}}
/>
);
}

View 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');
}}
/>
);
}

View 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`)}
/>
);
}

View 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
View 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"
/>
);
}