Compare commits

...

2 Commits

49 changed files with 2788 additions and 1000 deletions

Binary file not shown.

View File

@@ -2048,3 +2048,75 @@ FAM | META SEQ | SST SEQ | RANGE
0 | 00001833 | 00001832 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh) 0 | 00001833 | 00001832 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00001834 | 00001831 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh) 1 | 00001834 | 00001831 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
2 | 00001835 | 00001830 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh) 2 | 00001835 | 00001830 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
Time 2026-03-04T12:36:39.8644102Z
Commit 00003275 4 keys in 16ms 18µs 900ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00003273 | 00003272 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00003274 | 00003270 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
2 | 00003275 | 00003271 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
Time 2026-03-04T12:37:11.6441233Z
Commit 00003281 4 keys in 16ms 671µs 200ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00003279 | 00003278 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00003280 | 00003276 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
2 | 00003281 | 00003277 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
Time 2026-03-04T12:37:39.2387776Z
Commit 00003287 167 keys in 7ms 121µs 300ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00003285 | 00003284 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00003286 | 00003283 SST | [=================================================================================================] | 028455b5e2dde135-fc457064ad31e0f5 (0 MiB, fresh)
2 | 00003287 | 00003282 SST | [===========================================================================================] | 0c409babb15ba5ad-f817792a9634ebf6 (0 MiB, fresh)
Time 2026-03-04T12:37:42.9528653Z
Commit 00003293 182 keys in 7ms 92µs 200ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00003291 | 00003290 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00003292 | 00003288 SST | [==================================================================================================] | 00eac999f8125084-fdfe83409d6b0c99 (0 MiB, fresh)
2 | 00003293 | 00003289 SST | [==================================================================================================] | 00eac999f8125084-fdfe83409d6b0c99 (0 MiB, fresh)
Time 2026-03-04T12:38:33.8981535Z
Commit 00003299 4 keys in 7ms 812µs 800ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00003297 | 00003296 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00003298 | 00003294 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
2 | 00003299 | 00003295 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
Time 2026-03-04T12:39:53.9007763Z
Commit 00003305 4 keys in 7ms 315µs 200ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00003303 | 00003302 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
2 | 00003304 | 00003301 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
1 | 00003305 | 00003300 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
Time 2026-03-04T12:40:58.7301258Z
Commit 00003311 4 keys in 7ms 785µs 100ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00003309 | 00003308 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00003310 | 00003306 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
2 | 00003311 | 00003307 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
Time 2026-03-04T12:43:28.8680306Z
Commit 00003317 4 keys in 15ms 125µs 100ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00003315 | 00003314 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00003316 | 00003312 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
2 | 00003317 | 00003313 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
Time 2026-03-04T12:48:29.2871737Z
Commit 00003323 4 keys in 8ms 64µs 700ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00003321 | 00003320 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00003322 | 00003318 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
2 | 00003323 | 00003319 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
Time 2026-03-04T12:48:42.1761162Z
Commit 00003329 4 keys in 16ms 153µs 100ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00003327 | 00003326 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00003328 | 00003324 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
2 | 00003329 | 00003325 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
Time 2026-03-04T14:02:11.6694894Z
Commit 00011431 4 keys in 8ms 335µs 200ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00011429 | 00011428 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00011430 | 00011426 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
2 | 00011431 | 00011427 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
Time 2026-03-04T14:02:14.7644028Z
Commit 00011437 4 keys in 16ms 501µs 800ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00011435 | 00011434 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00011436 | 00011432 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
2 | 00011437 | 00011433 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)

View File

@@ -1,10 +1,7 @@
{ {
"/api/auth/[...nextauth]/route": "app/api/auth/[...nextauth]/route.js", "/api/auth/[...nextauth]/route": "app/api/auth/[...nextauth]/route.js",
"/api/auth/register/route": "app/api/auth/register/route.js", "/api/plans/route": "app/api/plans/route.js",
"/api/projects/route": "app/api/projects/route.js", "/features/page": "app/features/page.js",
"/api/user/profile/route": "app/api/user/profile/route.js",
"/dashboard/page": "app/dashboard/page.js",
"/login/page": "app/login/page.js",
"/page": "app/page.js", "/page": "app/page.js",
"/signup/page": "app/signup/page.js" "/pricing/page": "app/pricing/page.js"
} }

View File

@@ -1,5 +1,5 @@
var R=require("../../../chunks/[turbopack]_runtime.js")("server/app/api/plans/route.js") var R=require("../../../chunks/[turbopack]_runtime.js")("server/app/api/plans/route.js")
R.c("server/chunks/[root-of-the-server]__596609d2._.js") R.c("server/chunks/[root-of-the-server]__f07a6d6f._.js")
R.c("server/chunks/[root-of-the-server]__174f1a89._.js") R.c("server/chunks/[root-of-the-server]__174f1a89._.js")
R.c("server/chunks/80b94_00 - projet_plumeia__next-internal_server_app_api_plans_route_actions_6db30635.js") R.c("server/chunks/80b94_00 - projet_plumeia__next-internal_server_app_api_plans_route_actions_6db30635.js")
R.m("[project]/Documents/00 - projet/plumeia/node_modules/next/dist/esm/build/templates/app-route.js { INNER_APP_ROUTE => \"[project]/Documents/00 - projet/plumeia/src/app/api/plans/route.ts [app-route] (ecmascript)\" } [app-route] (ecmascript)") R.m("[project]/Documents/00 - projet/plumeia/node_modules/next/dist/esm/build/templates/app-route.js { INNER_APP_ROUTE => \"[project]/Documents/00 - projet/plumeia/src/app/api/plans/route.ts [app-route] (ecmascript)\" } [app-route] (ecmascript)")

View File

@@ -4,7 +4,7 @@ R.c("server/chunks/ssr/[root-of-the-server]__8a903a6f._.js")
R.c("server/chunks/ssr/549ce_next_dist_a9a2f161._.js") R.c("server/chunks/ssr/549ce_next_dist_a9a2f161._.js")
R.c("server/chunks/ssr/[externals]__7f148858._.js") R.c("server/chunks/ssr/[externals]__7f148858._.js")
R.c("server/chunks/ssr/549ce_next_dist_client_components_builtin_global-error_316a03e7.js") R.c("server/chunks/ssr/549ce_next_dist_client_components_builtin_global-error_316a03e7.js")
R.c("server/chunks/ssr/[root-of-the-server]__31132813._.js") R.c("server/chunks/ssr/[root-of-the-server]__f4e881ac._.js")
R.c("server/chunks/ssr/549ce_next_dist_client_components_5ea51078._.js") R.c("server/chunks/ssr/549ce_next_dist_client_components_5ea51078._.js")
R.c("server/chunks/ssr/549ce_next_dist_client_components_builtin_forbidden_0318745e.js") R.c("server/chunks/ssr/549ce_next_dist_client_components_builtin_forbidden_0318745e.js")
R.c("server/chunks/ssr/549ce_next_dist_client_components_builtin_unauthorized_5a2cd2c8.js") R.c("server/chunks/ssr/549ce_next_dist_client_components_builtin_unauthorized_5a2cd2c8.js")

File diff suppressed because one or more lines are too long

View File

@@ -50,8 +50,11 @@ __turbopack_context__.s([
"default", "default",
()=>__TURBOPACK__default__export__, ()=>__TURBOPACK__default__export__,
"getDB", "getDB",
()=>getDB ()=>getDB,
"prisma",
()=>prisma
]); ]);
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$server$2d$only$2f$empty$2e$js__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/next/dist/compiled/server-only/empty.js [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$externals$5d2f40$prisma$2f$client__$5b$external$5d$__$2840$prisma$2f$client$2c$__cjs$2c$__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f40$prisma$2f$client$29$__ = __turbopack_context__.i("[externals]/@prisma/client [external] (@prisma/client, cjs, [project]/Documents/00 - projet/plumeia/node_modules/@prisma/client)"); var __TURBOPACK__imported__module__$5b$externals$5d2f40$prisma$2f$client__$5b$external$5d$__$2840$prisma$2f$client$2c$__cjs$2c$__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f40$prisma$2f$client$29$__ = __turbopack_context__.i("[externals]/@prisma/client [external] (@prisma/client, cjs, [project]/Documents/00 - projet/plumeia/node_modules/@prisma/client)");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f40$prisma$2f$adapter$2d$pg$2f$dist$2f$index$2e$mjs__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/@prisma/adapter-pg/dist/index.mjs [app-route] (ecmascript)"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f40$prisma$2f$adapter$2d$pg$2f$dist$2f$index$2e$mjs__$5b$app$2d$route$5d$__$28$ecmascript$29$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/@prisma/adapter-pg/dist/index.mjs [app-route] (ecmascript)");
var __TURBOPACK__imported__module__$5b$externals$5d2f$pg__$5b$external$5d$__$28$pg$2c$__esm_import$2c$__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$pg$29$__ = __turbopack_context__.i("[externals]/pg [external] (pg, esm_import, [project]/Documents/00 - projet/plumeia/node_modules/pg)"); var __TURBOPACK__imported__module__$5b$externals$5d2f$pg__$5b$external$5d$__$28$pg$2c$__esm_import$2c$__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$pg$29$__ = __turbopack_context__.i("[externals]/pg [external] (pg, esm_import, [project]/Documents/00 - projet/plumeia/node_modules/pg)");
@@ -63,6 +66,7 @@ var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__([
; ;
; ;
; ;
;
const globalForPrisma = globalThis; const globalForPrisma = globalThis;
function getDB() { function getDB() {
if (!globalForPrisma.prisma) { if (!globalForPrisma.prisma) {
@@ -77,6 +81,14 @@ function getDB() {
} }
return globalForPrisma.prisma; return globalForPrisma.prisma;
} }
if ("TURBOPACK compile-time truthy", 1) {
globalForPrisma.prisma = getDB();
}
const prisma = new Proxy({}, {
get (target, prop, receiver) {
return Reflect.get(getDB(), prop, receiver);
}
});
const __TURBOPACK__default__export__ = getDB; const __TURBOPACK__default__export__ = getDB;
__turbopack_async_result__(); __turbopack_async_result__();
} catch(e) { __turbopack_async_result__(e); } }, false);}), } catch(e) { __turbopack_async_result__(e); } }, false);}),
@@ -102,8 +114,8 @@ var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__([
const dynamic = 'force-dynamic'; const dynamic = 'force-dynamic';
async function GET() { async function GET() {
try { try {
const prisma = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["default"])(); //const prisma = getDB();
const plans = await prisma.plan.findMany({ const plans = await __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$src$2f$lib$2f$prisma$2e$ts__$5b$app$2d$route$5d$__$28$ecmascript$29$__["prisma"].plan.findMany({
orderBy: { orderBy: {
price: 'asc' price: 'asc'
} }

View File

@@ -2,6 +2,6 @@
"version": 3, "version": 3,
"sources": [], "sources": [],
"sections": [ "sections": [
{"offset": {"line": 48, "column": 0}, "map": {"version":3,"sources":["file:///C:/Users/streaper2/Documents/00%20-%20projet/plumeia/src/lib/prisma.ts"],"sourcesContent":["import { PrismaClient } from '@prisma/client';\r\nimport { PrismaPg } from '@prisma/adapter-pg';\r\nimport { Pool } from 'pg';\r\n\r\nconst globalForPrisma = globalThis as unknown as {\r\n prisma: PrismaClient | undefined;\r\n};\r\n\r\n/**\r\n * Returns a singleton PrismaClient instance using the Prisma v7 adapter pattern.\r\n * Uses @prisma/adapter-pg with a pg Pool for direct PostgreSQL connections.\r\n */\r\nexport function getDB(): PrismaClient {\r\n if (!globalForPrisma.prisma) {\r\n const connectionString = process.env.DATABASE_URL;\r\n const pool = new Pool({ connectionString });\r\n const adapter = new PrismaPg(pool);\r\n\r\n globalForPrisma.prisma = new PrismaClient({ adapter });\r\n }\r\n return globalForPrisma.prisma;\r\n}\r\n\r\nexport default getDB;\r\n"],"names":[],"mappings":";;;;;;AAAA;AACA;AACA;;;;;;;;;AAEA,MAAM,kBAAkB;AAQjB,SAAS;IACZ,IAAI,CAAC,gBAAgB,MAAM,EAAE;QACzB,MAAM,mBAAmB,QAAQ,GAAG,CAAC,YAAY;QACjD,MAAM,OAAO,IAAI,iMAAI,CAAC;YAAE;QAAiB;QACzC,MAAM,UAAU,IAAI,qNAAQ,CAAC;QAE7B,gBAAgB,MAAM,GAAG,IAAI,kPAAY,CAAC;YAAE;QAAQ;IACxD;IACA,OAAO,gBAAgB,MAAM;AACjC;uCAEe"}}, {"offset": {"line": 48, "column": 0}, "map": {"version":3,"sources":["file:///C:/Users/streaper2/Documents/00%20-%20projet/plumeia/src/lib/prisma.ts"],"sourcesContent":["import 'server-only';\r\nimport { PrismaClient } from '@prisma/client';\r\nimport { PrismaPg } from '@prisma/adapter-pg';\r\nimport { Pool } from 'pg';\r\n\r\nconst globalForPrisma = globalThis as unknown as {\r\n prisma: PrismaClient | undefined;\r\n};\r\n\r\nexport function getDB(): PrismaClient {\r\n if (!globalForPrisma.prisma) {\r\n const connectionString = process.env.DATABASE_URL;\r\n const pool = new Pool({ connectionString });\r\n const adapter = new PrismaPg(pool);\r\n\r\n globalForPrisma.prisma = new PrismaClient({ adapter });\r\n }\r\n return globalForPrisma.prisma;\r\n}\r\n\r\nif (process.env.NODE_ENV !== 'production') {\r\n globalForPrisma.prisma = getDB();\r\n}\r\n\r\nexport const prisma = new Proxy({} as any, {\r\n get(target, prop, receiver) {\r\n return Reflect.get(getDB(), prop, receiver);\r\n }\r\n}) as PrismaClient;\r\n\r\nexport default getDB;"],"names":[],"mappings":";;;;;;;;AAAA;AACA;AACA;AACA;;;;;;;;;;AAEA,MAAM,kBAAkB;AAIjB,SAAS;IACZ,IAAI,CAAC,gBAAgB,MAAM,EAAE;QACzB,MAAM,mBAAmB,QAAQ,GAAG,CAAC,YAAY;QACjD,MAAM,OAAO,IAAI,iMAAI,CAAC;YAAE;QAAiB;QACzC,MAAM,UAAU,IAAI,qNAAQ,CAAC;QAE7B,gBAAgB,MAAM,GAAG,IAAI,kPAAY,CAAC;YAAE;QAAQ;IACxD;IACA,OAAO,gBAAgB,MAAM;AACjC;AAEA,wCAA2C;IACvC,gBAAgB,MAAM,GAAG;AAC7B;AAEO,MAAM,SAAS,IAAI,MAAM,CAAC,GAAU;IACvC,KAAI,MAAM,EAAE,IAAI,EAAE,QAAQ;QACtB,OAAO,QAAQ,GAAG,CAAC,SAAS,MAAM;IACtC;AACJ;uCAEe"}},
{"offset": {"line": 87, "column": 0}, "map": {"version":3,"sources":["file:///C:/Users/streaper2/Documents/00%20-%20projet/plumeia/src/app/api/plans/route.ts"],"sourcesContent":["import { NextResponse } from 'next/server';\r\nimport getDB from '@/lib/prisma';\r\n\r\nexport const dynamic = 'force-dynamic';\r\n\r\nexport async function GET() {\r\n try {\r\n const prisma = getDB();\r\n const plans = await prisma.plan.findMany({\r\n orderBy: { price: 'asc' }\r\n });\r\n const response = NextResponse.json(plans);\r\n response.headers.set('Cache-Control', 'no-store, max-age=0');\r\n return response;\r\n } catch (error) {\r\n console.error('Failed to fetch plans', error);\r\n return NextResponse.json({ error: 'Failed to fetch plans' }, { status: 500 });\r\n }\r\n}\r\n"],"names":[],"mappings":";;;;;;AAAA;AACA;;;;;;;AAEO,MAAM,UAAU;AAEhB,eAAe;IAClB,IAAI;QACA,MAAM,SAAS,IAAA,6KAAK;QACpB,MAAM,QAAQ,MAAM,OAAO,IAAI,CAAC,QAAQ,CAAC;YACrC,SAAS;gBAAE,OAAO;YAAM;QAC5B;QACA,MAAM,WAAW,4LAAY,CAAC,IAAI,CAAC;QACnC,SAAS,OAAO,CAAC,GAAG,CAAC,iBAAiB;QACtC,OAAO;IACX,EAAE,OAAO,OAAO;QACZ,QAAQ,KAAK,CAAC,yBAAyB;QACvC,OAAO,4LAAY,CAAC,IAAI,CAAC;YAAE,OAAO;QAAwB,GAAG;YAAE,QAAQ;QAAI;IAC/E;AACJ"}}] {"offset": {"line": 99, "column": 0}, "map": {"version":3,"sources":["file:///C:/Users/streaper2/Documents/00%20-%20projet/plumeia/src/app/api/plans/route.ts"],"sourcesContent":["import { NextResponse } from 'next/server';\r\nimport { prisma } from '@/lib/prisma';\r\n\r\nexport const dynamic = 'force-dynamic';\r\n\r\nexport async function GET() {\r\n try {\r\n //const prisma = getDB();\r\n const plans = await prisma.plan.findMany({\r\n orderBy: { price: 'asc' }\r\n });\r\n const response = NextResponse.json(plans);\r\n response.headers.set('Cache-Control', 'no-store, max-age=0');\r\n return response;\r\n } catch (error) {\r\n console.error('Failed to fetch plans', error);\r\n return NextResponse.json({ error: 'Failed to fetch plans' }, { status: 500 });\r\n }\r\n}\r\n"],"names":[],"mappings":";;;;;;AAAA;AACA;;;;;;;AAEO,MAAM,UAAU;AAEhB,eAAe;IAClB,IAAI;QACA,yBAAyB;QACzB,MAAM,QAAQ,MAAM,4KAAM,CAAC,IAAI,CAAC,QAAQ,CAAC;YACrC,SAAS;gBAAE,OAAO;YAAM;QAC5B;QACA,MAAM,WAAW,4LAAY,CAAC,IAAI,CAAC;QACnC,SAAS,OAAO,CAAC,GAAG,CAAC,iBAAiB;QACtC,OAAO;IACX,EAAE,OAAO,OAAO;QACZ,QAAQ,KAAK,CAAC,yBAAyB;QACvC,OAAO,4LAAY,CAAC,IAAI,CAAC;YAAE,OAAO;QAAwB,GAAG;YAAE,QAAQ;QAAI;IAC/E;AACJ"}}]
} }

View File

@@ -1 +1 @@
self.__NEXT_FONT_MANIFEST="{\n \"app\": {\n \"[project]/Documents/00 - projet/plumeia/src/app/dashboard/page\": [\n \"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2\",\n \"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2\"\n ],\n \"[project]/Documents/00 - projet/plumeia/src/app/login/page\": [\n \"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2\",\n \"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2\"\n ],\n \"[project]/Documents/00 - projet/plumeia/src/app/page\": [\n \"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2\",\n \"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2\"\n ],\n \"[project]/Documents/00 - projet/plumeia/src/app/signup/page\": [\n \"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2\",\n \"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2\"\n ]\n },\n \"appUsingSizeAdjust\": true,\n \"pages\": {},\n \"pagesUsingSizeAdjust\": false\n}" self.__NEXT_FONT_MANIFEST="{\n \"app\": {\n \"[project]/Documents/00 - projet/plumeia/src/app/features/page\": [\n \"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2\",\n \"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2\"\n ],\n \"[project]/Documents/00 - projet/plumeia/src/app/page\": [\n \"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2\",\n \"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2\"\n ],\n \"[project]/Documents/00 - projet/plumeia/src/app/pricing/page\": [\n \"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2\",\n \"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2\"\n ]\n },\n \"appUsingSizeAdjust\": true,\n \"pages\": {},\n \"pagesUsingSizeAdjust\": false\n}"

View File

@@ -1,10 +1,6 @@
{ {
"app": { "app": {
"[project]/Documents/00 - projet/plumeia/src/app/dashboard/page": [ "[project]/Documents/00 - projet/plumeia/src/app/features/page": [
"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2",
"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2"
],
"[project]/Documents/00 - projet/plumeia/src/app/login/page": [
"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2", "static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2",
"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2" "static/media/248e1dc0efc99276-s.p.8a6b2436.woff2"
], ],
@@ -12,7 +8,7 @@
"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2", "static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2",
"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2" "static/media/248e1dc0efc99276-s.p.8a6b2436.woff2"
], ],
"[project]/Documents/00 - projet/plumeia/src/app/signup/page": [ "[project]/Documents/00 - projet/plumeia/src/app/pricing/page": [
"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2", "static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2",
"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2" "static/media/248e1dc0efc99276-s.p.8a6b2436.woff2"
] ]

View File

@@ -150,6 +150,7 @@
--container-md: 28rem; --container-md: 28rem;
--container-lg: 32rem; --container-lg: 32rem;
--container-2xl: 42rem; --container-2xl: 42rem;
--container-3xl: 48rem;
--container-4xl: 56rem; --container-4xl: 56rem;
--container-5xl: 64rem; --container-5xl: 64rem;
--container-6xl: 72rem; --container-6xl: 72rem;
@@ -158,6 +159,8 @@
--text-xs--line-height: calc(1 / .75); --text-xs--line-height: calc(1 / .75);
--text-sm: .875rem; --text-sm: .875rem;
--text-sm--line-height: calc(1.25 / .875); --text-sm--line-height: calc(1.25 / .875);
--text-base: 1rem;
--text-base--line-height: calc(1.5 / 1);
--text-lg: 1.125rem; --text-lg: 1.125rem;
--text-lg--line-height: calc(1.75 / 1.125); --text-lg--line-height: calc(1.75 / 1.125);
--text-xl: 1.25rem; --text-xl: 1.25rem;
@@ -573,6 +576,10 @@
inset-inline-start: var(--spacing); inset-inline-start: var(--spacing);
} }
.end {
inset-inline-end: var(--spacing);
}
.-top-2 { .-top-2 {
top: calc(var(--spacing) * -2); top: calc(var(--spacing) * -2);
} }
@@ -787,6 +794,10 @@
margin-block: calc(var(--spacing) * 4); margin-block: calc(var(--spacing) * 4);
} }
.-mt-20 {
margin-top: calc(var(--spacing) * -20);
}
.mt-0\.5 { .mt-0\.5 {
margin-top: calc(var(--spacing) * .5); margin-top: calc(var(--spacing) * .5);
} }
@@ -949,6 +960,10 @@
height: calc(var(--spacing) * 3); height: calc(var(--spacing) * 3);
} }
.h-3\.5 {
height: calc(var(--spacing) * 3.5);
}
.h-4 { .h-4 {
height: calc(var(--spacing) * 4); height: calc(var(--spacing) * 4);
} }
@@ -1113,6 +1128,10 @@
width: calc(var(--spacing) * 32); width: calc(var(--spacing) * 32);
} }
.w-40 {
width: calc(var(--spacing) * 40);
}
.w-48 { .w-48 {
width: calc(var(--spacing) * 48); width: calc(var(--spacing) * 48);
} }
@@ -1165,6 +1184,10 @@
max-width: var(--container-2xl); max-width: var(--container-2xl);
} }
.max-w-3xl {
max-width: var(--container-3xl);
}
.max-w-4xl { .max-w-4xl {
max-width: var(--container-4xl); max-width: var(--container-4xl);
} }
@@ -1237,6 +1260,10 @@
scale: 1.01; scale: 1.01;
} }
.rotate-180 {
rotate: 180deg;
}
.transform { .transform {
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, ); transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
} }
@@ -1431,6 +1458,10 @@
overflow: hidden; overflow: hidden;
} }
.overflow-x-hidden {
overflow-x: hidden;
}
.overflow-y-auto { .overflow-y-auto {
overflow-y: auto; overflow-y: auto;
} }
@@ -1451,6 +1482,10 @@
border-radius: 2.5rem; border-radius: 2.5rem;
} }
.rounded-\[2px\] {
border-radius: 2px;
}
.rounded-\[2rem\] { .rounded-\[2rem\] {
border-radius: 2rem; border-radius: 2rem;
} }
@@ -1774,6 +1809,16 @@
} }
} }
.bg-blue-500\/20 {
background-color: #3080ff33;
}
@supports (color: color-mix(in lab, red, red)) {
.bg-blue-500\/20 {
background-color: color-mix(in oklab, var(--color-blue-500) 20%, transparent);
}
}
.bg-blue-600 { .bg-blue-600 {
background-color: var(--color-blue-600); background-color: var(--color-blue-600);
} }
@@ -2246,6 +2291,14 @@
padding-top: calc(var(--spacing) * 8); padding-top: calc(var(--spacing) * 8);
} }
.pt-12 {
padding-top: calc(var(--spacing) * 12);
}
.pt-20 {
padding-top: calc(var(--spacing) * 20);
}
.pt-32 { .pt-32 {
padding-top: calc(var(--spacing) * 32); padding-top: calc(var(--spacing) * 32);
} }
@@ -2282,6 +2335,10 @@
padding-bottom: calc(var(--spacing) * 20); padding-bottom: calc(var(--spacing) * 20);
} }
.pb-32 {
padding-bottom: calc(var(--spacing) * 32);
}
.pl-2 { .pl-2 {
padding-left: calc(var(--spacing) * 2); padding-left: calc(var(--spacing) * 2);
} }
@@ -2351,6 +2408,11 @@
line-height: var(--tw-leading, var(--text-6xl--line-height)); line-height: var(--tw-leading, var(--text-6xl--line-height));
} }
.text-base {
font-size: var(--text-base);
line-height: var(--tw-leading, var(--text-base--line-height));
}
.text-lg { .text-lg {
font-size: var(--text-lg); font-size: var(--text-lg);
line-height: var(--tw-leading, var(--text-lg--line-height)); line-height: var(--tw-leading, var(--text-lg--line-height));
@@ -2623,6 +2685,10 @@
color: var(--color-yellow-600); color: var(--color-yellow-600);
} }
.lowercase {
text-transform: lowercase;
}
.uppercase { .uppercase {
text-transform: uppercase; text-transform: uppercase;
} }
@@ -2817,6 +2883,11 @@
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
} }
.blur-\[100px\] {
--tw-blur: blur(100px);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.blur-\[120px\] { .blur-\[120px\] {
--tw-blur: blur(120px); --tw-blur: blur(120px);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
@@ -3444,6 +3515,12 @@
opacity: .5; opacity: .5;
} }
@media (min-width: 40rem) {
.sm\:inline {
display: inline;
}
}
@media (min-width: 40rem) { @media (min-width: 40rem) {
.sm\:w-auto { .sm\:w-auto {
width: auto; width: auto;
@@ -3456,6 +3533,12 @@
} }
} }
@media (min-width: 40rem) {
.sm\:p-12 {
padding: calc(var(--spacing) * 12);
}
}
@media (min-width: 48rem) { @media (min-width: 48rem) {
.md\:flex { .md\:flex {
display: flex; display: flex;
@@ -3516,6 +3599,13 @@
} }
} }
@media (min-width: 48rem) {
.md\:text-5xl {
font-size: var(--text-5xl);
line-height: var(--tw-leading, var(--text-5xl--line-height));
}
}
@media (min-width: 48rem) { @media (min-width: 48rem) {
.md\:text-7xl { .md\:text-7xl {
font-size: var(--text-7xl); font-size: var(--text-7xl);
@@ -3552,6 +3642,58 @@
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
} }
@media (prefers-color-scheme: dark) {
.dark\:border-slate-700 {
border-color: var(--color-slate-700);
}
}
@media (prefers-color-scheme: dark) {
.dark\:bg-blue-900\/20 {
background-color: #1c398e33;
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:bg-blue-900\/20 {
background-color: color-mix(in oklab, var(--color-blue-900) 20%, transparent);
}
}
}
@media (prefers-color-scheme: dark) {
.dark\:bg-slate-800 {
background-color: var(--color-slate-800);
}
}
@media (prefers-color-scheme: dark) {
.dark\:text-blue-400 {
color: var(--color-blue-400);
}
}
@media (prefers-color-scheme: dark) {
.dark\:text-slate-300 {
color: var(--color-slate-300);
}
}
@media (prefers-color-scheme: dark) {
@media (hover: hover) {
.dark\:hover\:bg-slate-700:hover {
background-color: var(--color-slate-700);
}
}
}
@media (prefers-color-scheme: dark) {
@media (hover: hover) {
.dark\:hover\:bg-slate-800:hover {
background-color: var(--color-slate-800);
}
}
}
} }
:root, .theme-light { :root, .theme-light {

View File

@@ -2,7 +2,7 @@
script: typeof document === "object" ? document.currentScript : undefined, script: typeof document === "object" ? document.currentScript : undefined,
chunks: [ chunks: [
"static/chunks/[root-of-the-server]__c391f813._.css", "static/chunks/[root-of-the-server]__c391f813._.css",
"static/chunks/Documents_00 - projet_plumeia_79f2801f._.js" "static/chunks/Documents_00 - projet_plumeia_c15954d6._.js"
], ],
source: "dynamic" source: "dynamic"
}); });

View File

@@ -451,6 +451,7 @@
--container-md: 28rem; --container-md: 28rem;
--container-lg: 32rem; --container-lg: 32rem;
--container-2xl: 42rem; --container-2xl: 42rem;
--container-3xl: 48rem;
--container-4xl: 56rem; --container-4xl: 56rem;
--container-5xl: 64rem; --container-5xl: 64rem;
--container-6xl: 72rem; --container-6xl: 72rem;
@@ -459,6 +460,8 @@
--text-xs--line-height: calc(1 / .75); --text-xs--line-height: calc(1 / .75);
--text-sm: .875rem; --text-sm: .875rem;
--text-sm--line-height: calc(1.25 / .875); --text-sm--line-height: calc(1.25 / .875);
--text-base: 1rem;
--text-base--line-height: calc(1.5 / 1);
--text-lg: 1.125rem; --text-lg: 1.125rem;
--text-lg--line-height: calc(1.75 / 1.125); --text-lg--line-height: calc(1.75 / 1.125);
--text-xl: 1.25rem; --text-xl: 1.25rem;
@@ -874,6 +877,10 @@
inset-inline-start: var(--spacing); inset-inline-start: var(--spacing);
} }
.end {
inset-inline-end: var(--spacing);
}
.-top-2 { .-top-2 {
top: calc(var(--spacing) * -2); top: calc(var(--spacing) * -2);
} }
@@ -1088,6 +1095,10 @@
margin-block: calc(var(--spacing) * 4); margin-block: calc(var(--spacing) * 4);
} }
.-mt-20 {
margin-top: calc(var(--spacing) * -20);
}
.mt-0\.5 { .mt-0\.5 {
margin-top: calc(var(--spacing) * .5); margin-top: calc(var(--spacing) * .5);
} }
@@ -1250,6 +1261,10 @@
height: calc(var(--spacing) * 3); height: calc(var(--spacing) * 3);
} }
.h-3\.5 {
height: calc(var(--spacing) * 3.5);
}
.h-4 { .h-4 {
height: calc(var(--spacing) * 4); height: calc(var(--spacing) * 4);
} }
@@ -1414,6 +1429,10 @@
width: calc(var(--spacing) * 32); width: calc(var(--spacing) * 32);
} }
.w-40 {
width: calc(var(--spacing) * 40);
}
.w-48 { .w-48 {
width: calc(var(--spacing) * 48); width: calc(var(--spacing) * 48);
} }
@@ -1466,6 +1485,10 @@
max-width: var(--container-2xl); max-width: var(--container-2xl);
} }
.max-w-3xl {
max-width: var(--container-3xl);
}
.max-w-4xl { .max-w-4xl {
max-width: var(--container-4xl); max-width: var(--container-4xl);
} }
@@ -1538,6 +1561,10 @@
scale: 1.01; scale: 1.01;
} }
.rotate-180 {
rotate: 180deg;
}
.transform { .transform {
transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, ); transform: var(--tw-rotate-x, ) var(--tw-rotate-y, ) var(--tw-rotate-z, ) var(--tw-skew-x, ) var(--tw-skew-y, );
} }
@@ -1732,6 +1759,10 @@
overflow: hidden; overflow: hidden;
} }
.overflow-x-hidden {
overflow-x: hidden;
}
.overflow-y-auto { .overflow-y-auto {
overflow-y: auto; overflow-y: auto;
} }
@@ -1752,6 +1783,10 @@
border-radius: 2.5rem; border-radius: 2.5rem;
} }
.rounded-\[2px\] {
border-radius: 2px;
}
.rounded-\[2rem\] { .rounded-\[2rem\] {
border-radius: 2rem; border-radius: 2rem;
} }
@@ -2075,6 +2110,16 @@
} }
} }
.bg-blue-500\/20 {
background-color: #3080ff33;
}
@supports (color: color-mix(in lab, red, red)) {
.bg-blue-500\/20 {
background-color: color-mix(in oklab, var(--color-blue-500) 20%, transparent);
}
}
.bg-blue-600 { .bg-blue-600 {
background-color: var(--color-blue-600); background-color: var(--color-blue-600);
} }
@@ -2547,6 +2592,14 @@
padding-top: calc(var(--spacing) * 8); padding-top: calc(var(--spacing) * 8);
} }
.pt-12 {
padding-top: calc(var(--spacing) * 12);
}
.pt-20 {
padding-top: calc(var(--spacing) * 20);
}
.pt-32 { .pt-32 {
padding-top: calc(var(--spacing) * 32); padding-top: calc(var(--spacing) * 32);
} }
@@ -2583,6 +2636,10 @@
padding-bottom: calc(var(--spacing) * 20); padding-bottom: calc(var(--spacing) * 20);
} }
.pb-32 {
padding-bottom: calc(var(--spacing) * 32);
}
.pl-2 { .pl-2 {
padding-left: calc(var(--spacing) * 2); padding-left: calc(var(--spacing) * 2);
} }
@@ -2652,6 +2709,11 @@
line-height: var(--tw-leading, var(--text-6xl--line-height)); line-height: var(--tw-leading, var(--text-6xl--line-height));
} }
.text-base {
font-size: var(--text-base);
line-height: var(--tw-leading, var(--text-base--line-height));
}
.text-lg { .text-lg {
font-size: var(--text-lg); font-size: var(--text-lg);
line-height: var(--tw-leading, var(--text-lg--line-height)); line-height: var(--tw-leading, var(--text-lg--line-height));
@@ -2924,6 +2986,10 @@
color: var(--color-yellow-600); color: var(--color-yellow-600);
} }
.lowercase {
text-transform: lowercase;
}
.uppercase { .uppercase {
text-transform: uppercase; text-transform: uppercase;
} }
@@ -3118,6 +3184,11 @@
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
} }
.blur-\[100px\] {
--tw-blur: blur(100px);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
}
.blur-\[120px\] { .blur-\[120px\] {
--tw-blur: blur(120px); --tw-blur: blur(120px);
filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, ); filter: var(--tw-blur, ) var(--tw-brightness, ) var(--tw-contrast, ) var(--tw-grayscale, ) var(--tw-hue-rotate, ) var(--tw-invert, ) var(--tw-saturate, ) var(--tw-sepia, ) var(--tw-drop-shadow, );
@@ -3745,6 +3816,12 @@
opacity: .5; opacity: .5;
} }
@media (min-width: 40rem) {
.sm\:inline {
display: inline;
}
}
@media (min-width: 40rem) { @media (min-width: 40rem) {
.sm\:w-auto { .sm\:w-auto {
width: auto; width: auto;
@@ -3757,6 +3834,12 @@
} }
} }
@media (min-width: 40rem) {
.sm\:p-12 {
padding: calc(var(--spacing) * 12);
}
}
@media (min-width: 48rem) { @media (min-width: 48rem) {
.md\:flex { .md\:flex {
display: flex; display: flex;
@@ -3817,6 +3900,13 @@
} }
} }
@media (min-width: 48rem) {
.md\:text-5xl {
font-size: var(--text-5xl);
line-height: var(--tw-leading, var(--text-5xl--line-height));
}
}
@media (min-width: 48rem) { @media (min-width: 48rem) {
.md\:text-7xl { .md\:text-7xl {
font-size: var(--text-7xl); font-size: var(--text-7xl);
@@ -3853,6 +3943,58 @@
grid-template-columns: repeat(3, minmax(0, 1fr)); grid-template-columns: repeat(3, minmax(0, 1fr));
} }
} }
@media (prefers-color-scheme: dark) {
.dark\:border-slate-700 {
border-color: var(--color-slate-700);
}
}
@media (prefers-color-scheme: dark) {
.dark\:bg-blue-900\/20 {
background-color: #1c398e33;
}
@supports (color: color-mix(in lab, red, red)) {
.dark\:bg-blue-900\/20 {
background-color: color-mix(in oklab, var(--color-blue-900) 20%, transparent);
}
}
}
@media (prefers-color-scheme: dark) {
.dark\:bg-slate-800 {
background-color: var(--color-slate-800);
}
}
@media (prefers-color-scheme: dark) {
.dark\:text-blue-400 {
color: var(--color-blue-400);
}
}
@media (prefers-color-scheme: dark) {
.dark\:text-slate-300 {
color: var(--color-slate-300);
}
}
@media (prefers-color-scheme: dark) {
@media (hover: hover) {
.dark\:hover\:bg-slate-700:hover {
background-color: var(--color-slate-700);
}
}
}
@media (prefers-color-scheme: dark) {
@media (hover: hover) {
.dark\:hover\:bg-slate-800:hover {
background-color: var(--color-slate-800);
}
}
}
} }
:root, .theme-light { :root, .theme-light {

File diff suppressed because one or more lines are too long

48
RELEASE-v0.0.1.md Normal file
View File

@@ -0,0 +1,48 @@
# 🚀 Pluume - Release Notes v0.0.1 (MVP)
Bienvenue dans la toute première version de **Pluume** (v0.0.1) ! Cette version fondatrice pose les bases d'un environnement de création littéraire de nouvelle génération, intégrant l'intelligence artificielle et des outils visuels avancés pour les romanciers et scénaristes.
Voici un aperçu complet des fonctionnalités intégrées dans cette première release :
---
## ✨ Fonctionnalités Principales
### 1. Gestion des Projets et de l'Écriture
* **Tableau de Bord (Dashboard) :** Vue d'ensemble de vos projets en cours, récapitulatif de votre progression et de vos objectifs quotidiens de mots.
* **Éditeur de Texte Riche (Rich Text Editor) :** Un éditeur performant et sans distraction pour rédiger vos chapitres, avec prise en charge du formatage essentiel.
* **Gestion des Chapitres :** Création, réorganisation (glisser-déposer) et suivi de l'avancement de vos différents chapitres.
* **Objectifs de suivi :** Suivi de l'écriture, séries (streaks) et statistiques de mots pour rester motivé.
### 2. Construction d'Univers (World-Building)
* **La Bible de l'Univers (World Builder) :** Un espace dédié pour répertorier toutes vos Entités (Personnages, Lieux, Factions, Objets magiques, etc.).
* **Attributs Personnalisés :** Création de fiches détaillées avec des caractéristiques sur-mesure pour donner vie à votre univers.
### 3. Planification Visuelle (Story Workflow)
* **Graphe Visuel de l'Intrigue (Plot Nodes & Connections) :** Une interface nodale (mind-mapping) pour visualiser les liens entre les événements, les personnages et les intrigues de votre récit.
* **Mur d'Idées (Idea Board) :** Un système de gestion par cartes (type Kanban) pour capturer, catégoriser et organiser vos fulgurances avant de les intégrer à l'histoire.
### 4. Assistant IA Intégré (Propulsé par Gemini)
* **Panneau IA Interactif :** Discutez avec l'IA directement depuis votre espace de travail pour brainstormer ou résoudre un blocage (writer's block).
* **Génération et Transformation de Texte :** Demandez à l'IA d'étendre une description, de reformuler un paragraphe, ou de générer des idées de rebondissements.
### 5. Personnalisation et Expérience Utilisateur
* **Support Multilingue :** L'interface et les interactions s'adaptent désormais à votre langue de préférence (Français, Anglais, Espagnol, Allemand).
* **Thèmes Dynamiques :** Prise en charge globale des thèmes (Clair, Sombre, Sepia) pour un confort de lecture et d'écriture optimal.
* **Paramètres du Livre (Book Settings) :** Définissez le style, le ton, et le guide de style global de votre livre pour que l'IA respecte votre "Voix".
* **Export (Export Modal) :** Exportez votre manuscrit dans différents formats pour la relecture ou la publication.
### 6. Espace Compte, Sécurité et Monétisation
* **Système d'Authentification :** Inscription et connexion sécurisées via identifiants (NextAuth & Bcrypt).
* **Profil Utilisateur :** Gestion des informations personnelles, pseudonyme et biographie.
* **Abonnements & Limites (Pricing/Checkout) :** Système de forfaits (Free, Pro, Master) qui encadrent la création de projets et le quota d'actions IA autorisées.
---
## 🛠️ Stack Technique & Infrastructure (Pour les Devs)
* **Frontend :** Next.js 16 (Turbopack), React 19, TailwindCSS, Composants UI réactifs.
* **Backend :** API Routes (App Router), Prisma Client v7 (Edge-ready).
* **Base de données :** PostgreSQL via `@prisma/adapter-pg`.
* **Déploiement :** Configuration prête pour production via conteneurs Docker (Nixpacks).
*Merci de faire partie de l'aventure Pluume ! Bonne écriture.* ✍️

Binary file not shown.

View File

@@ -1,64 +0,0 @@
> plumeia@0.1.0 build
> npx prisma generate && next build
[dotenv@17.3.1] injecting env (4) from .env -- tip: ⚙️ specify custom .env file path with { path: '/custom/path/.env' }
[dotenv@17.3.1] injecting env (0) from .env.local -- tip: ⚙️ suppress all logs with { quiet: true }
Loaded Prisma config from prisma.config.ts.
Prisma schema loaded from prisma\schema.prisma.
✔ Generated Prisma Client (v7.4.1) to .\node_modules\@prisma\client in 72ms
Start by importing your Prisma Client (See: https://pris.ly/d/importing-client)
⚠ Warning: Next.js inferred your workspace root, but it may not be correct.
We detected multiple lockfiles and selected the directory of C:\Users\streaper2\package-lock.json as the root directory.
To silence this warning, set `turbopack.root` in your Next.js config, or consider removing one of the lockfiles if it's not needed.
See https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack#root-directory for more information.
Detected additional lockfiles:
* C:\Users\streaper2\Documents\00 - projet\plumeia\package-lock.json
▲ Next.js 16.1.6 (Turbopack)
- Environments: .env
Creating an optimized production build ...
✓ Compiled successfully in 1183.7ms
Skipping validation of types
Collecting page data using 31 workers ...
Error [PrismaClientInitializationError]: `PrismaClient` needs to be constructed with a non-empty, valid `PrismaClientOptions`:
```
new PrismaClient({
...
})
```
or
```
constructor() {
super({ ... });
}
```
at a (C:\Users\streaper2\Documents\00 - projet\plumeia\.next\server\chunks\[root-of-the-server]__bcb19414._.js:1:1488)
at module evaluation (C:\Users\streaper2\Documents\00 - projet\plumeia\.next\server\chunks\[root-of-the-server]__bcb19414._.js:1:1523)
at instantiateModule (C:\Users\streaper2\Documents\00 - projet\plumeia\.next\server\chunks\[turbopack]_runtime.js:740:9)
at getOrInstantiateModuleFromParent (C:\Users\streaper2\Documents\00 - projet\plumeia\.next\server\chunks\[turbopack]_runtime.js:763:12)
at Context.esmImport [as i] (C:\Users\streaper2\Documents\00 - projet\plumeia\.next\server\chunks\[turbopack]_runtime.js:228:20)
at <unknown> (C:\Users\streaper2\Documents\00 - projet\plumeia\.next\server\chunks\[root-of-the-server]__bcb19414._.js:1:2288)
at Context.asyncModule [as a] (C:\Users\streaper2\Documents\00 - projet\plumeia\.next\server\chunks\[turbopack]_runtime.js:455:5)
at module evaluation (C:\Users\streaper2\Documents\00 - projet\plumeia\.next\server\chunks\[root-of-the-server]__bcb19414._.js:1:2235)
at instantiateModule (C:\Users\streaper2\Documents\00 - projet\plumeia\.next\server\chunks\[turbopack]_runtime.js:740:9) {
clientVersion: '7.4.1',
errorCode: undefined,
retryable: undefined
}
> Build error occurred
Error: Failed to collect page data for /api/ai/generate
at ignore-listed frames {
type: 'Error'
}

View File

@@ -1,71 +0,0 @@
> plumeia@0.1.0 build
> npx prisma generate && next build
[dotenv@17.3.1] injecting env (4) from .env -- tip: 🛠️ run anywhere with `dotenvx run -- yourcommand`
[dotenv@17.3.1] injecting env (0) from .env.local -- tip: ⚙️ enable debug logging with { debug: true }
Loaded Prisma config from prisma.config.ts.
Prisma schema loaded from prisma\schema.prisma.
✔ Generated Prisma Client (v7.4.1) to .\node_modules\@prisma\client in 75ms
Start by importing your Prisma Client (See: https://pris.ly/d/importing-client)
⚠ Warning: Next.js inferred your workspace root, but it may not be correct.
We detected multiple lockfiles and selected the directory of C:\Users\streaper2\package-lock.json as the root directory.
To silence this warning, set `turbopack.root` in your Next.js config, or consider removing one of the lockfiles if it's not needed.
See https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack#root-directory for more information.
Detected additional lockfiles:
* C:\Users\streaper2\Documents\00 - projet\plumeia\package-lock.json
▲ Next.js 16.1.6 (Turbopack)
- Environments: .env
Creating an optimized production build ...
✓ Compiled successfully in 1196.6ms
Skipping validation of types
Collecting page data using 31 workers ...
Generating static pages using 31 workers (0/10) ...
Generating static pages using 31 workers (2/10)
Generating static pages using 31 workers (4/10)
Generating static pages using 31 workers (7/10)
✓ Generating static pages using 31 workers (10/10) in 359.6ms
Finalizing page optimization ...
Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /api/ai/generate
├ ƒ /api/ai/transform
├ ƒ /api/auth/[...nextauth]
├ ƒ /api/auth/register
├ ƒ /api/chapters
├ ƒ /api/chapters/[id]
├ ƒ /api/entities
├ ƒ /api/entities/[id]
├ ƒ /api/ideas
├ ƒ /api/ideas/[id]
├ ƒ /api/plans
├ ƒ /api/projects
├ ƒ /api/projects/[id]
├ ƒ /api/projects/[id]/workflow
├ ƒ /api/user/profile
├ ○ /checkout
├ ○ /dashboard
├ ○ /features
├ ○ /login
├ ○ /pricing
├ ○ /profile
├ ƒ /project/[id]
├ ƒ /project/[id]/ideas
├ ƒ /project/[id]/settings
├ ƒ /project/[id]/workflow
├ ƒ /project/[id]/world
└ ○ /signup
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand

View File

@@ -1,71 +0,0 @@
> plumeia@0.1.0 build
> npx prisma generate && next build
[dotenv@17.3.1] injecting env (4) from .env -- tip: ⚙️ load multiple .env files with { path: ['.env.local', '.env'] }
[dotenv@17.3.1] injecting env (0) from .env.local -- tip: 🛠️ run anywhere with `dotenvx run -- yourcommand`
Loaded Prisma config from prisma.config.ts.
Prisma schema loaded from prisma\schema.prisma.
✔ Generated Prisma Client (v7.4.1) to .\node_modules\@prisma\client in 76ms
Start by importing your Prisma Client (See: https://pris.ly/d/importing-client)
⚠ Warning: Next.js inferred your workspace root, but it may not be correct.
We detected multiple lockfiles and selected the directory of C:\Users\streaper2\package-lock.json as the root directory.
To silence this warning, set `turbopack.root` in your Next.js config, or consider removing one of the lockfiles if it's not needed.
See https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack#root-directory for more information.
Detected additional lockfiles:
* C:\Users\streaper2\Documents\00 - projet\plumeia\package-lock.json
▲ Next.js 16.1.6 (Turbopack)
- Environments: .env
Creating an optimized production build ...
✓ Compiled successfully in 1198.1ms
Skipping validation of types
Collecting page data using 31 workers ...
Generating static pages using 31 workers (0/10) ...
Generating static pages using 31 workers (2/10)
Generating static pages using 31 workers (4/10)
Generating static pages using 31 workers (7/10)
✓ Generating static pages using 31 workers (10/10) in 372.5ms
Finalizing page optimization ...
Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /api/ai/generate
├ ƒ /api/ai/transform
├ ƒ /api/auth/[...nextauth]
├ ƒ /api/auth/register
├ ƒ /api/chapters
├ ƒ /api/chapters/[id]
├ ƒ /api/entities
├ ƒ /api/entities/[id]
├ ƒ /api/ideas
├ ƒ /api/ideas/[id]
├ ƒ /api/plans
├ ƒ /api/projects
├ ƒ /api/projects/[id]
├ ƒ /api/projects/[id]/workflow
├ ƒ /api/user/profile
├ ○ /checkout
├ ○ /dashboard
├ ○ /features
├ ○ /login
├ ○ /pricing
├ ○ /profile
├ ƒ /project/[id]
├ ƒ /project/[id]/ideas
├ ƒ /project/[id]/settings
├ ƒ /project/[id]/workflow
├ ƒ /project/[id]/world
└ ○ /signup
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand

View File

@@ -1,71 +0,0 @@
> plumeia@0.1.0 build
> npx prisma generate && next build
[dotenv@17.3.1] injecting env (4) from .env -- tip: ⚙️ load multiple .env files with { path: ['.env.local', '.env'] }
[dotenv@17.3.1] injecting env (0) from .env.local -- tip: 🔐 prevent committing .env to code: https://dotenvx.com/precommit
Loaded Prisma config from prisma.config.ts.
Prisma schema loaded from prisma\schema.prisma.
✔ Generated Prisma Client (v7.4.1) to .\node_modules\@prisma\client in 77ms
Start by importing your Prisma Client (See: https://pris.ly/d/importing-client)
⚠ Warning: Next.js inferred your workspace root, but it may not be correct.
We detected multiple lockfiles and selected the directory of C:\Users\streaper2\package-lock.json as the root directory.
To silence this warning, set `turbopack.root` in your Next.js config, or consider removing one of the lockfiles if it's not needed.
See https://nextjs.org/docs/app/api-reference/config/next-config-js/turbopack#root-directory for more information.
Detected additional lockfiles:
* C:\Users\streaper2\Documents\00 - projet\plumeia\package-lock.json
▲ Next.js 16.1.6 (Turbopack)
- Environments: .env
Creating an optimized production build ...
✓ Compiled successfully in 1233.5ms
Skipping validation of types
Collecting page data using 31 workers ...
Generating static pages using 31 workers (0/10) ...
Generating static pages using 31 workers (2/10)
Generating static pages using 31 workers (4/10)
Generating static pages using 31 workers (7/10)
✓ Generating static pages using 31 workers (10/10) in 348.2ms
Finalizing page optimization ...
Route (app)
┌ ○ /
├ ○ /_not-found
├ ƒ /api/ai/generate
├ ƒ /api/ai/transform
├ ƒ /api/auth/[...nextauth]
├ ƒ /api/auth/register
├ ƒ /api/chapters
├ ƒ /api/chapters/[id]
├ ƒ /api/entities
├ ƒ /api/entities/[id]
├ ƒ /api/ideas
├ ƒ /api/ideas/[id]
├ ƒ /api/plans
├ ƒ /api/projects
├ ƒ /api/projects/[id]
├ ƒ /api/projects/[id]/workflow
├ ƒ /api/user/profile
├ ○ /checkout
├ ○ /dashboard
├ ○ /features
├ ○ /login
├ ○ /pricing
├ ○ /profile
├ ƒ /project/[id]
├ ƒ /project/[id]/ideas
├ ƒ /project/[id]/settings
├ ƒ /project/[id]/workflow
├ ƒ /project/[id]/world
└ ○ /signup
○ (Static) prerendered as static content
ƒ (Dynamic) server-rendered on demand

View File

@@ -1,43 +0,0 @@
> plumeia@0.1.0 build
> next build
node.exe : ÔÜá Warning: Next.js inferred your workspace root, but it
may not be correct.
Au caractère Ligne:1 : 1
+ & "C:\Program Files\nodejs/node.exe" "C:\Program
Files\nodejs/node_mo ...
+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (ÔÜá Warning: Ne...not b
e correct.:String) [], RemoteException
+ FullyQualifiedErrorId : NativeCommandError
We detected multiple lockfiles and selected the directory of
C:\Users\streaper2\package-lock.json as the root directory.
To silence this warning, set `turbopack.root` in your Next.js
config, or consider removing one of the lockfiles if it's not needed.
See https://nextjs.org/docs/app/api-reference/config/next-config-j
s/turbopack#root-directory for more information.
Detected additional lockfiles:
* C:\Users\streaper2\Documents\00 -
projet\plumeia\package-lock.json
Ôû▓ Next.js 16.1.6 (Turbopack)
- Environments: .env.local, .env
Creating an optimized production build ...
Ô£ô Compiled successfully in 1253.2ms
Skipping validation of types
Collecting page data using 31 workers ...
Generating static pages using 31 workers (0/10) ...
Error occurred prerendering page "/pricing". Read more:
https://nextjs.org/docs/messages/prerender-error
ReferenceError: useState is not defined
at h (C:\Users\streaper2\Documents\00 - projet\plumeia\.next\serv
er\chunks\ssr\[root-of-the-server]__1b51e774._.js:1:4513) {
digest: '4248291053'
}
Export encountered an error on /pricing/page: /pricing, exiting the
build.
Ô¿» Next.js build worker exited with code: 1 and signal: null

2
next-env.d.ts vendored
View File

@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -13,7 +13,7 @@ async function main() {
name: 'free', name: 'free',
displayName: 'Gratuit', displayName: 'Gratuit',
price: 0, price: 0,
description: 'Idéal pour découvrir PlumeIA.', description: 'Idéal pour découvrir Pluume.',
maxProjects: 1, maxProjects: 1,
maxAiActions: 10, maxAiActions: 10,
features: ['10 actions IA / mois', '1 projet actif', 'Bible du monde simple'], features: ['10 actions IA / mois', '1 projet actif', 'Bible du monde simple'],
@@ -33,7 +33,7 @@ async function main() {
{ {
id: 'master', id: 'master',
name: 'master', name: 'master',
displayName: 'Maître Plume', displayName: 'Maître Pluume',
price: 29, price: 29,
description: 'Le summum de l\'écriture IA.', description: 'Le summum de l\'écriture IA.',
maxProjects: 20, maxProjects: 20,

38
src/app/cgu/page.tsx Normal file
View File

@@ -0,0 +1,38 @@
'use client';
import React from 'react';
import { useLanguage } from '@/providers/LanguageProvider';
import { ArrowLeft, Book } from 'lucide-react';
import Link from 'next/link';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
export default function CGUPage() {
const { t } = useLanguage();
return (
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
<nav className="bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-4 md:px-8 h-16 flex items-center justify-between sticky top-0">
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<div className="bg-blue-600 p-1.5 rounded-lg">
<Book className="text-white" size={24} />
</div>
<span className="text-xl font-black text-slate-900 tracking-tight">Pluume</span>
</Link>
<div className="flex items-center gap-4">
<LanguageSwitcher />
<Link href="/" className="flex items-center gap-2 text-slate-500 hover:text-blue-600 font-bold transition-colors">
<ArrowLeft size={16} /> {t('common.back')}
</Link>
</div>
</nav>
<main className="max-w-4xl mx-auto py-20 px-4 md:px-8">
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('legal.cgu_title')}</h1>
<div className="bg-white p-6 sm:p-12 rounded-3xl shadow-xl border border-indigo-50 text-slate-600 leading-relaxed space-y-6">
<p>{t('legal.cgu_content')}</p>
<p><i>(Ceci est un document type en attente de la version finale par un conseiller juridique)</i></p>
</div>
</main>
</div>
);
}

38
src/app/cgv/page.tsx Normal file
View File

@@ -0,0 +1,38 @@
'use client';
import React from 'react';
import { useLanguage } from '@/providers/LanguageProvider';
import { ArrowLeft, Book } from 'lucide-react';
import Link from 'next/link';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
export default function CGVPage() {
const { t } = useLanguage();
return (
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
<nav className="bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-4 md:px-8 h-16 flex items-center justify-between sticky top-0">
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<div className="bg-blue-600 p-1.5 rounded-lg">
<Book className="text-white" size={24} />
</div>
<span className="text-xl font-black text-slate-900 tracking-tight">Pluume</span>
</Link>
<div className="flex items-center gap-4">
<LanguageSwitcher />
<Link href="/" className="flex items-center gap-2 text-slate-500 hover:text-blue-600 font-bold transition-colors">
<ArrowLeft size={16} /> {t('common.back')}
</Link>
</div>
</nav>
<main className="max-w-4xl mx-auto py-20 px-4 md:px-8">
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('legal.cgv_title')}</h1>
<div className="bg-white p-6 sm:p-12 rounded-3xl shadow-xl border border-indigo-50 text-slate-600 leading-relaxed space-y-6">
<p>{t('legal.cgv_content')}</p>
<p><i>(Ceci est un document type en attente de la version finale par un conseiller juridique)</i></p>
</div>
</main>
</div>
);
}

View File

@@ -24,7 +24,7 @@ export default function DashboardPage() {
<Loader2 className="animate-spin text-blue-500 mb-4" size={48} /> <Loader2 className="animate-spin text-blue-500 mb-4" size={48} />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen className="text-blue-500" size={20} /> <BookOpen className="text-blue-500" size={20} />
<span className="text-lg font-bold">PlumeIA</span> <span className="text-lg font-bold">Pluume</span>
</div> </div>
</div> </div>
); );

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter, Merriweather } from "next/font/google"; import { Inter, Merriweather } from "next/font/google";
import { AuthProvider } from "@/providers/AuthProvider"; import { AuthProvider } from "@/providers/AuthProvider";
import { LanguageProvider } from "@/providers/LanguageProvider";
import "./globals.css"; import "./globals.css";
const inter = Inter({ const inter = Inter({
@@ -15,7 +16,7 @@ const merriweather = Merriweather({
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "PlumeIA - Éditeur Intelligent", title: "Pluume - Éditeur Intelligent",
description: "Votre assistant éditorial intelligent propulsé par l'IA pour écrire votre prochain roman.", description: "Votre assistant éditorial intelligent propulsé par l'IA pour écrire votre prochain roman.",
}; };
@@ -25,10 +26,12 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="fr"> <html lang="en">
<body className={`${inter.variable} ${merriweather.variable} font-sans h-screen overflow-hidden antialiased bg-theme-bg text-theme-text transition-colors duration-300`}> <body className={`${inter.variable} ${merriweather.variable} font-sans h-screen overflow-x-hidden overflow-y-auto antialiased bg-theme-bg text-theme-text transition-colors duration-300`}>
<AuthProvider> <AuthProvider>
<LanguageProvider>
{children} {children}
</LanguageProvider>
</AuthProvider> </AuthProvider>
</body> </body>
</html> </html>

View File

@@ -62,7 +62,7 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
<Loader2 className="animate-spin text-blue-500 mb-4" size={48} /> <Loader2 className="animate-spin text-blue-500 mb-4" size={48} />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<BookOpen className="text-blue-500" size={20} /> <BookOpen className="text-blue-500" size={20} />
<span className="text-lg font-bold">PlumeIA</span> <span className="text-lg font-bold">Pluume</span>
</div> </div>
</div> </div>
); );

63
src/app/sitemap/page.tsx Normal file
View File

@@ -0,0 +1,63 @@
'use client';
import React from 'react';
import { useLanguage } from '@/providers/LanguageProvider';
import { ArrowLeft, Book, Link as LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
export default function SitemapPage() {
const { t } = useLanguage();
return (
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
<nav className="bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-8 h-16 flex items-center justify-between sticky top-0">
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<div className="bg-blue-600 p-1.5 rounded-lg">
<Book className="text-white" size={24} />
</div>
<span className="text-xl font-black text-slate-900 tracking-tight">Pluume</span>
</Link>
<div className="flex items-center gap-4">
<LanguageSwitcher />
<Link href="/" className="flex items-center gap-2 text-slate-500 hover:text-blue-600 font-bold transition-colors">
<ArrowLeft size={16} /> {t('common.back')}
</Link>
</div>
</nav>
<main className="max-w-4xl mx-auto py-20 px-8">
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('legal.sitemap_title')}</h1>
<div className="bg-white p-8 sm:p-12 rounded-3xl shadow-xl border border-indigo-50">
<ul className="space-y-4">
<li>
<Link href="/" className="flex items-center gap-3 text-lg font-bold text-slate-700 hover:text-blue-600 transition-colors">
<LinkIcon size={18} className="text-slate-400" /> Accueil
</Link>
</li>
<li>
<Link href="/auth" className="flex items-center gap-3 text-lg font-bold text-slate-700 hover:text-blue-600 transition-colors">
<LinkIcon size={18} className="text-slate-400" /> Authentification
</Link>
</li>
<li className="pt-4 mt-4 border-t border-slate-100">
<span className="text-xs font-black uppercase text-slate-400 tracking-widest block mb-4">Légal</span>
<ul className="space-y-4 pl-4">
<li>
<Link href="/cgu" className="flex items-center gap-3 text-base text-slate-600 hover:text-blue-600 transition-colors">
<LinkIcon size={16} className="text-slate-400" /> {t('legal.cgu_title')}
</Link>
</li>
<li>
<Link href="/cgv" className="flex items-center gap-3 text-base text-slate-600 hover:text-blue-600 transition-colors">
<LinkIcon size={16} className="text-slate-400" /> {t('legal.cgv_title')}
</Link>
</li>
</ul>
</li>
</ul>
</div>
</main>
</div>
);
}

View File

@@ -4,6 +4,7 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Sparkles, Send, RefreshCw, BookOpen, Bot, ArrowLeft, BrainCircuit, Zap } from 'lucide-react'; import { Sparkles, Send, RefreshCw, BookOpen, Bot, ArrowLeft, BrainCircuit, Zap } from 'lucide-react';
import { ChatMessage, UserUsage } from '@/lib/types'; import { ChatMessage, UserUsage } from '@/lib/types';
import { useLanguage } from '@/providers/LanguageProvider';
interface AIPanelProps { interface AIPanelProps {
chatHistory: ChatMessage[]; chatHistory: ChatMessage[];
@@ -15,6 +16,7 @@ interface AIPanelProps {
} }
const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertText, selectedText, isGenerating, usage }) => { const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertText, selectedText, isGenerating, usage }) => {
const { t } = useLanguage();
const [input, setInput] = useState(""); const [input, setInput] = useState("");
const messagesEndRef = useRef<HTMLDivElement>(null); const messagesEndRef = useRef<HTMLDivElement>(null);
@@ -37,7 +39,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
<div className="p-4 bg-indigo-600 text-white flex items-center justify-between shadow-md"> <div className="p-4 bg-indigo-600 text-white flex items-center justify-between shadow-md">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Sparkles size={20} className="animate-pulse" /> <Sparkles size={20} className="animate-pulse" />
<h3 className="font-bold tracking-tight">Assistant IA</h3> <h3 className="font-bold tracking-tight">{t('ai_panel.title')}</h3>
</div> </div>
{usage && ( {usage && (
<div className="bg-indigo-900/50 px-2 py-1 rounded text-[10px] font-black flex items-center gap-1"> <div className="bg-indigo-900/50 px-2 py-1 rounded text-[10px] font-black flex items-center gap-1">
@@ -48,7 +50,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
{selectedText && ( {selectedText && (
<div className="bg-indigo-50 p-3 border-b border-indigo-100 text-xs text-indigo-800"> <div className="bg-indigo-50 p-3 border-b border-indigo-100 text-xs text-indigo-800">
<div className="font-bold flex items-center gap-1 mb-1"><BookOpen size={12} /> Contexte :</div> <div className="font-bold flex items-center gap-1 mb-1"><BookOpen size={12} /> {t('ai_panel.context')}</div>
<div className="italic truncate opacity-80">"{selectedText.substring(0, 60)}..."</div> <div className="italic truncate opacity-80">"{selectedText.substring(0, 60)}..."</div>
</div> </div>
)} )}
@@ -57,10 +59,10 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
{chatHistory.length === 0 && ( {chatHistory.length === 0 && (
<div className="text-center text-theme-muted mt-10"> <div className="text-center text-theme-muted mt-10">
<Bot size={48} className="mx-auto mb-2 opacity-50" /> <Bot size={48} className="mx-auto mb-2 opacity-50" />
<p className="text-sm">Bonjour ! Comment puis-je vous aider aujourd'hui ?</p> <p className="text-sm">{t('ai_panel.greeting')}</p>
{isLimitReached && ( {isLimitReached && (
<div className="mt-4 p-4 bg-red-50 border border-red-100 rounded-xl text-red-600 text-xs font-bold uppercase animate-pulse"> <div className="mt-4 p-4 bg-red-50 border border-red-100 rounded-xl text-red-600 text-xs font-bold uppercase animate-pulse">
Limite atteinte ! Améliorez votre plan. {t('ai_panel.limit_reached_upgrade')}
</div> </div>
)} )}
</div> </div>
@@ -70,7 +72,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
<div key={msg.id} className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}> <div key={msg.id} className={`flex flex-col ${msg.role === 'user' ? 'items-end' : 'items-start'}`}>
<div className={`max-w-[85%] rounded-2xl p-4 text-sm shadow-sm transition-colors duration-300 ${msg.role === 'user' ? 'bg-indigo-600 text-white rounded-br-none' : 'bg-theme-panel text-theme-text border border-theme-border rounded-bl-none'}`}> <div className={`max-w-[85%] rounded-2xl p-4 text-sm shadow-sm transition-colors duration-300 ${msg.role === 'user' ? 'bg-indigo-600 text-white rounded-br-none' : 'bg-theme-panel text-theme-text border border-theme-border rounded-bl-none'}`}>
{msg.role === 'model' && msg.responseType === 'reflection' && ( {msg.role === 'model' && msg.responseType === 'reflection' && (
<div className="flex items-center gap-1.5 text-[10px] font-black text-amber-600 mb-1.5 uppercase tracking-wide"><BrainCircuit size={12} /> Réflexion</div> <div className="flex items-center gap-1.5 text-[10px] font-black text-amber-600 mb-1.5 uppercase tracking-wide"><BrainCircuit size={12} /> {t('ai_panel.reflection')}</div>
)} )}
<div className="whitespace-pre-wrap leading-relaxed">{msg.text}</div> <div className="whitespace-pre-wrap leading-relaxed">{msg.text}</div>
</div> </div>
@@ -80,7 +82,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
{isGenerating && ( {isGenerating && (
<div className="flex justify-start"> <div className="flex justify-start">
<div className="bg-theme-panel p-3 rounded-2xl rounded-bl-none shadow-sm border border-theme-border flex items-center gap-2 text-xs text-theme-muted transition-colors duration-300"> <div className="bg-theme-panel p-3 rounded-2xl rounded-bl-none shadow-sm border border-theme-border flex items-center gap-2 text-xs text-theme-muted transition-colors duration-300">
<RefreshCw size={14} className="animate-spin" /> L'IA travaille... <RefreshCw size={14} className="animate-spin" /> {t('ai_panel.ai_working')}
</div> </div>
</div> </div>
)} )}
@@ -93,7 +95,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
type="text" type="text"
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
placeholder={isLimitReached ? "Limite atteinte..." : "Votre message..."} placeholder={isLimitReached ? t('ai_panel.limit_reached') : t('ai_panel.your_message')}
className="w-full pl-4 pr-12 py-3 bg-theme-bg text-theme-text border border-theme-border rounded-2xl text-sm focus:outline-none focus:border-indigo-500 transition-all disabled:opacity-50" className="w-full pl-4 pr-12 py-3 bg-theme-bg text-theme-text border border-theme-border rounded-2xl text-sm focus:outline-none focus:border-indigo-500 transition-all disabled:opacity-50"
disabled={isGenerating || isLimitReached} disabled={isGenerating || isLimitReached}
/> />

View File

@@ -3,6 +3,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Mail, Lock, User, ArrowRight, Loader2, BookOpen, ShieldCheck } from 'lucide-react'; import { Mail, Lock, User, ArrowRight, Loader2, BookOpen, ShieldCheck } from 'lucide-react';
import { useAuthContext } from '@/providers/AuthProvider'; import { useAuthContext } from '@/providers/AuthProvider';
import { useLanguage } from '@/providers/LanguageProvider';
interface AuthPageProps { interface AuthPageProps {
onBack: () => void; onBack: () => void;
@@ -18,6 +19,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
// On récupère les fonctions de connexion directement du hook // On récupère les fonctions de connexion directement du hook
const { user, login, signup } = useAuthContext(); const { user, login, signup } = useAuthContext();
const { t } = useLanguage();
// Redirection automatique dès que l'utilisateur est détecté dans l'état global // Redirection automatique dès que l'utilisateur est détecté dans l'état global
useEffect(() => { useEffect(() => {
@@ -28,7 +30,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
const handleAdminLogin = async () => { const handleAdminLogin = async () => {
const adminData = { email: 'streaper2@gmail.com', password: 'Kency1313' }; const adminData = { email: 'streaper2@gmail.com', password: 'Kency1313' };
setFormData({ name: 'Admin Plume', ...adminData }); setFormData({ name: 'Admin Pluume', ...adminData });
setLoading(true); setLoading(true);
setError(''); setError('');
@@ -69,32 +71,32 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
</div> </div>
<div className="relative z-10 flex items-center gap-2 text-white text-2xl font-black"> <div className="relative z-10 flex items-center gap-2 text-white text-2xl font-black">
<BookOpen className="text-blue-500" /> PlumeIA <BookOpen className="text-blue-500" /> Pluume
</div> </div>
<div className="relative z-10 max-w-lg"> <div className="relative z-10 max-w-lg">
<h2 className="text-5xl font-black text-white leading-tight mb-6"> <h2 className="text-5xl font-black text-white leading-tight mb-6">
L'endroit où vos <span className="text-blue-400">histoires</span> prennent vie. {t('auth.hero_title_part1')} <span className="text-blue-400">{t('auth.hero_title_part2')}</span> {t('auth.hero_title_part3')}
</h2> </h2>
<p className="text-slate-400 text-lg leading-relaxed"> <p className="text-slate-400 text-lg leading-relaxed">
Rejoignez une communauté d'auteurs qui utilisent l'IA pour briser la page blanche. {t('auth.hero_desc')}
</p> </p>
</div> </div>
<div className="relative z-10 text-slate-500 text-sm"> <div className="relative z-10 text-slate-500 text-sm">
© 2024 PlumeIA Ecosystem. © 2024 Pluume Ecosystem.
</div> </div>
</div> </div>
{/* Formulaire */} {/* Formulaire */}
<div className="w-full lg:w-1/2 flex items-center justify-center p-8 bg-white overflow-y-auto"> <div className="w-full lg:w-1/2 flex items-center justify-center p-6 md:p-8 bg-white overflow-y-auto">
<div className="w-full max-w-md animate-in fade-in slide-in-from-right-10 duration-500 py-8"> <div className="w-full max-w-md animate-in fade-in slide-in-from-right-10 duration-500 py-8">
<div className="text-center mb-10"> <div className="text-center mb-10">
<h1 className="text-3xl font-black text-slate-900 mb-2"> <h1 className="text-3xl font-black text-slate-900 mb-2">
{mode === 'signin' ? 'Content de vous revoir' : mode === 'signup' ? "Commencer l'aventure" : 'Récupération'} {mode === 'signin' ? t('auth.welcome_back') : mode === 'signup' ? t('auth.start_adventure') : t('auth.recovery')}
</h1> </h1>
<p className="text-slate-500"> <p className="text-slate-500">
{mode === 'signin' ? 'Entrez vos identifiants pour continuer.' : 'Créez votre compte gratuit en quelques secondes.'} {mode === 'signin' ? t('auth.enter_credentials') : t('auth.create_account_seconds')}
</p> </p>
</div> </div>
@@ -107,7 +109,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
{mode === 'signup' && ( {mode === 'signup' && (
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">Nom complet</label> <label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">{t('auth.full_name')}</label>
<div className="relative"> <div className="relative">
<User className="absolute left-4 top-3.5 text-slate-400" size={18} /> <User className="absolute left-4 top-3.5 text-slate-400" size={18} />
<input <input
@@ -115,7 +117,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
required required
value={formData.name} value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })} onChange={(e) => setFormData({ ...formData, name: e.target.value })}
placeholder="Marc Dupré" placeholder={t('auth.name_placeholder')}
className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium" className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium"
/> />
</div> </div>
@@ -123,7 +125,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
)} )}
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">Email</label> <label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">{t('auth.email')}</label>
<div className="relative"> <div className="relative">
<Mail className="absolute left-4 top-3.5 text-slate-400" size={18} /> <Mail className="absolute left-4 top-3.5 text-slate-400" size={18} />
<input <input
@@ -131,7 +133,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
required required
value={formData.email} value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })} onChange={(e) => setFormData({ ...formData, email: e.target.value })}
placeholder="votre@email.com" placeholder={t('auth.email_placeholder')}
className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium" className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium"
/> />
</div> </div>
@@ -139,7 +141,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
{mode !== 'forgot' && ( {mode !== 'forgot' && (
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">Mot de passe</label> <label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1">{t('auth.password')}</label>
<div className="relative"> <div className="relative">
<Lock className="absolute left-4 top-3.5 text-slate-400" size={18} /> <Lock className="absolute left-4 top-3.5 text-slate-400" size={18} />
<input <input
@@ -147,7 +149,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
required required
value={formData.password} value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })} onChange={(e) => setFormData({ ...formData, password: e.target.value })}
placeholder="" placeholder={t('auth.password_placeholder')}
className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium" className="w-full pl-12 pr-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium"
/> />
</div> </div>
@@ -160,7 +162,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
className="w-full bg-slate-900 text-white py-4 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-blue-600 transition-all shadow-xl disabled:opacity-50 mt-4" className="w-full bg-slate-900 text-white py-4 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-blue-600 transition-all shadow-xl disabled:opacity-50 mt-4"
> >
{loading ? <Loader2 className="animate-spin" /> : ( {loading ? <Loader2 className="animate-spin" /> : (
<>{mode === 'signin' ? 'Se connecter' : mode === 'signup' ? 'Créer mon compte' : 'Envoyer'} <ArrowRight size={18} /></> <>{mode === 'signin' ? t('auth.signin_button') : mode === 'signup' ? t('auth.signup_button') : t('auth.send_button')} <ArrowRight size={18} /></>
)} )}
</button> </button>
</form> </form>
@@ -170,24 +172,24 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
onClick={handleAdminLogin} onClick={handleAdminLogin}
className="w-full mt-4 bg-amber-50 border border-amber-200 text-amber-800 py-3 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-amber-100 transition-all" className="w-full mt-4 bg-amber-50 border border-amber-200 text-amber-800 py-3 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-amber-100 transition-all"
> >
<ShieldCheck size={18} /> Connexion démo (Admin) <ShieldCheck size={18} /> {t('auth.demo_admin')}
</button> </button>
)} )}
<div className="mt-10 text-center"> <div className="mt-10 text-center">
<p className="text-sm text-slate-500"> <p className="text-sm text-slate-500">
{mode === 'signin' ? "Pas de compte ?" : "Déjà membre ?"} {mode === 'signin' ? t('auth.no_account') : t('auth.already_member')}
<button <button
onClick={() => setMode(mode === 'signin' ? 'signup' : 'signin')} onClick={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
className="ml-2 font-bold text-blue-600" className="ml-2 font-bold text-blue-600"
> >
{mode === 'signin' ? "S'inscrire" : "Se connecter"} {mode === 'signin' ? t('auth.signup_link') : t('auth.signin_link')}
</button> </button>
</p> </p>
</div> </div>
<button onClick={onBack} className="mt-8 text-xs text-slate-300 w-full text-center hover:text-slate-500 transition-colors"> <button onClick={onBack} className="mt-8 text-xs text-slate-300 w-full text-center hover:text-slate-500 transition-colors">
Revenir au site {t('auth.back_to_site')}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -4,6 +4,8 @@ import React, { useEffect, useState } from 'react';
import { BookProject, BookSettings } from '@/lib/types'; import { BookProject, BookSettings } from '@/lib/types';
import { GENRES, TONES, POV_OPTIONS, TENSE_OPTIONS } from '@/lib/constants'; import { GENRES, TONES, POV_OPTIONS, TENSE_OPTIONS } from '@/lib/constants';
import { Settings, Book, Feather, Users, Clock, Target, Hash } from 'lucide-react'; import { Settings, Book, Feather, Users, Clock, Target, Hash } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { TranslationKey } from '@/lib/i18n/translations';
interface BookSettingsProps { interface BookSettingsProps {
project: BookProject; project: BookProject;
@@ -23,6 +25,7 @@ const DEFAULT_SETTINGS: BookSettings = {
}; };
const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate, onDeleteProject }) => { const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate, onDeleteProject }) => {
const { t } = useLanguage();
const [settings, setSettings] = useState<BookSettings>(project.settings || DEFAULT_SETTINGS); const [settings, setSettings] = useState<BookSettings>(project.settings || DEFAULT_SETTINGS);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -51,19 +54,19 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<Settings size={24} /> <Settings size={24} />
</div> </div>
<div> <div>
<h2 className="text-2xl font-bold">Paramètres Généraux du Roman</h2> <h2 className="text-2xl font-bold">{t('book_settings.title')}</h2>
<p className="text-slate-400 text-sm">Définissez l'identité, le ton et les règles de votre œuvre pour guider l'IA.</p> <p className="text-slate-400 text-sm">{t('book_settings.subtitle')}</p>
</div> </div>
</div> </div>
<div className="p-8 space-y-8"> <div className="p-8 space-y-8">
<section className="space-y-4"> <section className="space-y-4">
<h3 className="text-lg font-bold text-theme-text flex items-center gap-2 border-b border-theme-border pb-2"> <h3 className="text-lg font-bold text-theme-text flex items-center gap-2 border-b border-theme-border pb-2">
<Book size={18} className="text-blue-600" /> Informations de Base <Book size={18} className="text-blue-600" /> {t('book_settings.basic_info')}
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Titre du Roman</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.novel_title')}</label>
<input <input
type="text" type="text"
value={project.title} value={project.title}
@@ -72,7 +75,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Nom d'Auteur</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.author_name')}</label>
<input <input
type="text" type="text"
value={project.author} value={project.author}
@@ -82,58 +85,58 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Synopsis Global</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.global_synopsis')}</label>
<textarea <textarea
value={settings.synopsis} value={settings.synopsis}
onChange={(e) => handleChange('synopsis', e.target.value)} onChange={(e) => handleChange('synopsis', e.target.value)}
className="w-full p-3 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none h-24 text-sm transition-colors duration-300" className="w-full p-3 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none h-24 text-sm transition-colors duration-300"
placeholder="De quoi parle votre histoire dans les grandes lignes ?" placeholder={t('book_settings.synopsis_placeholder')}
/> />
</div> </div>
</section> </section>
<section className="space-y-4"> <section className="space-y-4">
<h3 className="text-lg font-bold text-theme-text flex items-center gap-2 border-b border-theme-border pb-2"> <h3 className="text-lg font-bold text-theme-text flex items-center gap-2 border-b border-theme-border pb-2">
<Target size={18} className="text-red-500" /> Genre & Public <Target size={18} className="text-red-500" /> {t('book_settings.genre_audience')}
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div> <div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Genre Principal</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.main_genre')}</label>
<input <input
type="text" type="text"
list="genre-suggestions" list="genre-suggestions"
value={settings.genre} value={settings.genre}
onChange={(e) => handleChange('genre', e.target.value)} onChange={(e) => handleChange('genre', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder="Ex: Fantasy" placeholder={t('book_settings.genre_placeholder')}
/> />
<datalist id="genre-suggestions"> <datalist id="genre-suggestions">
{GENRES.map(g => <option key={g} value={g} />)} {GENRES.map(g => <option key={g} value={g} />)}
</datalist> </datalist>
</div> </div>
<div> <div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Sous-Genre</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.sub_genre')}</label>
<input <input
type="text" type="text"
value={settings.subGenre || ''} value={settings.subGenre || ''}
onChange={(e) => handleChange('subGenre', e.target.value)} onChange={(e) => handleChange('subGenre', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder="Ex: Dark Fantasy" placeholder={t('book_settings.subgenre_placeholder')}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Public Cible</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.target_audience')}</label>
<input <input
type="text" type="text"
value={settings.targetAudience} value={settings.targetAudience}
onChange={(e) => handleChange('targetAudience', e.target.value)} onChange={(e) => handleChange('targetAudience', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder="Ex: Jeune Adulte, Adulte..." placeholder={t('book_settings.audience_placeholder')}
/> />
</div> </div>
</div> </div>
<div> <div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Thèmes Clés</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.key_themes')}</label>
<div className="relative"> <div className="relative">
<Hash size={14} className="absolute left-3 top-3 text-theme-muted" /> <Hash size={14} className="absolute left-3 top-3 text-theme-muted" />
<input <input
@@ -141,7 +144,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
value={settings.themes} value={settings.themes}
onChange={(e) => handleChange('themes', e.target.value)} onChange={(e) => handleChange('themes', e.target.value)}
className="w-full pl-9 p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full pl-9 p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder="Ex: Vengeance, Rédemption, Voyage initiatique..." placeholder={t('book_settings.themes_placeholder')}
/> />
</div> </div>
</div> </div>
@@ -149,90 +152,90 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<section className="space-y-4"> <section className="space-y-4">
<h3 className="text-lg font-bold text-theme-text flex items-center gap-2 border-b border-theme-border pb-2"> <h3 className="text-lg font-bold text-theme-text flex items-center gap-2 border-b border-theme-border pb-2">
<Feather size={18} className="text-purple-600" /> Narration & Style <Feather size={18} className="text-purple-600" /> {t('book_settings.narration_style')}
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div> <div>
<label className="block text-sm font-semibold text-theme-muted mb-1 flex items-center gap-1"> <label className="block text-sm font-semibold text-theme-muted mb-1 flex items-center gap-1">
<Users size={14} /> Point de Vue (POV) <Users size={14} /> {t('book_settings.pov')}
</label> </label>
<select <select
value={settings.pov} value={settings.pov}
onChange={(e) => handleChange('pov', e.target.value)} onChange={(e) => handleChange('pov', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
> >
<option value="">Sélectionner...</option> <option value="">{t('book_settings.select')}</option>
{POV_OPTIONS.map(o => <option key={o} value={o}>{o}</option>)} {POV_OPTIONS.map(o => <option key={o} value={o}>{t(`pov_options.${o.toLowerCase().replace(/\s+/g, '_')}` as TranslationKey) || o}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-semibold text-theme-muted mb-1 flex items-center gap-1"> <label className="block text-sm font-semibold text-theme-muted mb-1 flex items-center gap-1">
<Clock size={14} /> Temps du récit <Clock size={14} /> {t('book_settings.tense')}
</label> </label>
<select <select
value={settings.tense} value={settings.tense}
onChange={(e) => handleChange('tense', e.target.value)} onChange={(e) => handleChange('tense', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
> >
<option value="">Sélectionner...</option> <option value="">{t('book_settings.select')}</option>
{TENSE_OPTIONS.map(o => <option key={o} value={o}>{o}</option>)} {TENSE_OPTIONS.map(o => <option key={o} value={o}>{t(`tense_options.${o.toLowerCase().replace(/\s+/g, '_')}` as TranslationKey) || o}</option>)}
</select> </select>
</div> </div>
<div> <div>
<label className="block text-sm font-semibold text-theme-muted mb-1">Ton Général</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.general_tone')}</label>
<input <input
type="text" type="text"
list="tone-suggestions" list="tone-suggestions"
value={settings.tone} value={settings.tone}
onChange={(e) => handleChange('tone', e.target.value)} onChange={(e) => handleChange('tone', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder="Ex: Sombre, Ironique..." placeholder={t('book_settings.tone_placeholder')}
/> />
<datalist id="tone-suggestions"> <datalist id="tone-suggestions">
{TONES.map(t => <option key={t} value={t} />)} {TONES.map(tOption => <option key={tOption} value={tOption} />)}
</datalist> </datalist>
</div> </div>
</div> </div>
<div className="mt-4"> <div className="mt-4">
<label className="block text-sm font-semibold text-theme-muted mb-1"> <label className="block text-sm font-semibold text-theme-muted mb-1">
Guide de Style & Instructions IA (Prompt Système) {t('book_settings.style_guide')}
</label> </label>
<p className="text-xs text-theme-muted mb-2"> <p className="text-xs text-theme-muted mb-2">
Ces instructions seront envoyées à l'IA à chaque génération. Décrivez ici le style d'écriture désiré (ex: "phrases courtes", "vocabulaire soutenu", "beaucoup de métaphores"). {t('book_settings.style_guide_help')}
</p> </p>
<textarea <textarea
value={project.styleGuide || ''} value={project.styleGuide || ''}
onChange={(e) => handleStyleGuideChange(e.target.value)} onChange={(e) => handleStyleGuideChange(e.target.value)}
className="w-full p-3 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none h-32 text-sm font-mono transition-colors duration-300" className="w-full p-3 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none h-32 text-sm font-mono transition-colors duration-300"
placeholder="Ex: Utilise un style descriptif et sensoriel. Évite les adverbes. Le narrateur est cynique." placeholder={t('book_settings.style_guide_placeholder')}
/> />
</div> </div>
</section> </section>
<section className="space-y-4 pt-8 border-t border-red-200"> <section className="space-y-4 pt-8 border-t border-red-200">
<h3 className="text-lg font-bold text-red-600 flex items-center gap-2 pb-2"> <h3 className="text-lg font-bold text-red-600 flex items-center gap-2 pb-2">
<span className="bg-red-100 p-1 rounded"></span> Zone de Danger <span className="bg-red-100 p-1 rounded"></span> {t('book_settings.danger_zone')}
</h3> </h3>
<div className="bg-red-50 border border-red-200 rounded-lg p-6"> <div className="bg-red-50 border border-red-200 rounded-lg p-6">
<h4 className="font-bold text-red-900 mb-2">Supprimer le projet</h4> <h4 className="font-bold text-red-900 mb-2">{t('book_settings.delete_project')}</h4>
<p className="text-sm text-red-700 mb-4"> <p className="text-sm text-red-700 mb-4">
Cette action est irréversible. Toutes les données associées à ce projet (chapitres, entités, idées) seront définitivement effacées. {t('book_settings.delete_warning')}
</p> </p>
{showDeleteConfirm ? ( {showDeleteConfirm ? (
<div className="flex items-center gap-4 bg-theme-panel p-4 rounded border border-red-200"> <div className="flex items-center gap-4 bg-theme-panel p-4 rounded border border-red-200">
<span className="text-sm font-bold text-theme-text">Êtes-vous sûr ?</span> <span className="text-sm font-bold text-theme-text">{t('book_settings.are_you_sure')}</span>
<button <button
onClick={onDeleteProject} onClick={onDeleteProject}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 text-sm font-bold opacity-90 transition-opacity" className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 text-sm font-bold opacity-90 transition-opacity"
> >
Oui, supprimer définitivement {t('book_settings.confirm_delete')}
</button> </button>
<button <button
onClick={() => setShowDeleteConfirm(false)} onClick={() => setShowDeleteConfirm(false)}
className="px-4 py-2 bg-theme-bg text-theme-text border border-theme-border rounded hover:opacity-80 text-sm transition-opacity" className="px-4 py-2 bg-theme-bg text-theme-text border border-theme-border rounded hover:opacity-80 text-sm transition-opacity"
> >
Annuler {t('book_settings.cancel')}
</button> </button>
</div> </div>
) : ( ) : (
@@ -240,7 +243,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
onClick={() => setShowDeleteConfirm(true)} onClick={() => setShowDeleteConfirm(true)}
className="px-4 py-2 bg-theme-panel border border-red-300 text-red-600 rounded hover:bg-red-50 text-sm font-bold transition-colors duration-300" className="px-4 py-2 bg-theme-panel border border-red-300 text-red-600 rounded hover:bg-red-50 text-sm font-bold transition-colors duration-300"
> >
Supprimer ce projet {t('book_settings.delete_button')}
</button> </button>
)} )}
</div> </div>

View File

@@ -3,6 +3,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { CreditCard, Shield, Lock, ArrowRight, Loader2 } from 'lucide-react'; import { CreditCard, Shield, Lock, ArrowRight, Loader2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface CheckoutProps { interface CheckoutProps {
onComplete: () => void; onComplete: () => void;
@@ -11,6 +12,7 @@ interface CheckoutProps {
const Checkout: React.FC<CheckoutProps> = ({ onComplete, onCancel }) => { const Checkout: React.FC<CheckoutProps> = ({ onComplete, onCancel }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { t } = useLanguage();
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -24,19 +26,19 @@ const Checkout: React.FC<CheckoutProps> = ({ onComplete, onCancel }) => {
<div className="min-h-screen bg-[#eef2ff] flex items-center justify-center p-8"> <div className="min-h-screen bg-[#eef2ff] flex items-center justify-center p-8">
<div className="bg-white rounded-3xl shadow-2xl flex flex-col md:flex-row max-w-4xl w-full overflow-hidden animate-in fade-in slide-in-from-bottom-10 duration-500"> <div className="bg-white rounded-3xl shadow-2xl flex flex-col md:flex-row max-w-4xl w-full overflow-hidden animate-in fade-in slide-in-from-bottom-10 duration-500">
<div className="w-full md:w-1/3 bg-slate-900 text-white p-8"> <div className="w-full md:w-1/3 bg-slate-900 text-white p-8">
<h3 className="text-xl font-bold mb-8 flex items-center gap-2"><Lock size={18} className="text-blue-400" /> Commande</h3> <h3 className="text-xl font-bold mb-8 flex items-center gap-2"><Lock size={18} className="text-blue-400" /> {t('checkout.order')}</h3>
<div className="space-y-4"> <div className="space-y-4">
<div className="flex justify-between text-sm"><span>Auteur Pro</span><span>12.00</span></div> <div className="flex justify-between text-sm"><span>{t('checkout.pro_author')}</span><span>12.00</span></div>
<div className="flex justify-between text-sm"><span>TVA (20%)</span><span>2.40</span></div> <div className="flex justify-between text-sm"><span>{t('checkout.vat')}</span><span>2.40</span></div>
<div className="h-px bg-slate-800 my-4" /> <div className="h-px bg-slate-800 my-4" />
<div className="flex justify-between text-xl font-black"><span>Total</span><span className="text-blue-400">14.40</span></div> <div className="flex justify-between text-xl font-black"><span>{t('checkout.total')}</span><span className="text-blue-400">14.40</span></div>
</div> </div>
</div> </div>
<div className="flex-1 p-8 md:p-12"> <div className="flex-1 p-8 md:p-12">
<h2 className="text-2xl font-black text-slate-900 mb-8 text-center">Paiement Sécurisé</h2> <h2 className="text-2xl font-black text-slate-900 mb-8 text-center">{t('checkout.secure_payment')}</h2>
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div> <div>
<label className="block text-xs font-black text-slate-500 uppercase tracking-widest mb-2">Numéro de carte</label> <label className="block text-xs font-black text-slate-500 uppercase tracking-widest mb-2">{t('checkout.card_number')}</label>
<div className="relative"> <div className="relative">
<input type="text" placeholder="4242 4242 4242 4242" className="w-full bg-[#eef2ff] border border-indigo-100 p-4 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-bold" /> <input type="text" placeholder="4242 4242 4242 4242" className="w-full bg-[#eef2ff] border border-indigo-100 p-4 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-bold" />
<CreditCard className="absolute right-4 top-4 text-slate-400" /> <CreditCard className="absolute right-4 top-4 text-slate-400" />
@@ -50,10 +52,10 @@ const Checkout: React.FC<CheckoutProps> = ({ onComplete, onCancel }) => {
disabled={loading} disabled={loading}
className="w-full bg-blue-600 text-white py-5 rounded-2xl font-black text-lg hover:bg-blue-700 transition-all shadow-xl shadow-blue-200 flex items-center justify-center gap-3" className="w-full bg-blue-600 text-white py-5 rounded-2xl font-black text-lg hover:bg-blue-700 transition-all shadow-xl shadow-blue-200 flex items-center justify-center gap-3"
> >
{loading ? <Loader2 className="animate-spin" /> : <>Confirmer le paiement <ArrowRight size={20} /></>} {loading ? <Loader2 className="animate-spin" /> : <>{t('checkout.confirm_payment')} <ArrowRight size={20} /></>}
</button> </button>
<div className="flex items-center justify-center gap-2 text-[10px] text-slate-400 font-bold uppercase"> <div className="flex items-center justify-center gap-2 text-[10px] text-slate-400 font-bold uppercase">
<Shield size={12} /> Traitement chiffré SSL 256-bits <Shield size={12} /> {t('checkout.ssl_encryption')}
</div> </div>
</form> </form>
</div> </div>

View File

@@ -4,6 +4,8 @@
import React from 'react'; import React from 'react';
import { BookProject, UserProfile } from '@/lib/types'; import { BookProject, UserProfile } from '@/lib/types';
import { Plus, Book, Clock, Star, ChevronRight, LogOut, LayoutDashboard, User, Target, Flame, Edit3 } from 'lucide-react'; import { Plus, Book, Clock, Star, ChevronRight, LogOut, LayoutDashboard, User, Target, Flame, Edit3 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
interface DashboardProps { interface DashboardProps {
user: UserProfile; user: UserProfile;
@@ -16,54 +18,57 @@ interface DashboardProps {
} }
const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreate, onLogout, onPricing, onProfile }) => { const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreate, onLogout, onPricing, onProfile }) => {
const { t } = useLanguage();
return ( return (
<div className="min-h-screen bg-theme-bg p-8 font-sans transition-colors duration-300"> <div className="min-h-screen bg-theme-bg p-4 md:p-8 font-sans transition-colors duration-300">
<div className="max-w-6xl mx-auto space-y-8"> <div className="max-w-6xl mx-auto space-y-8">
{/* User Card */} {/* User Card */}
<div className="flex flex-col md:flex-row justify-between items-center bg-theme-panel p-8 rounded-[2rem] shadow-sm border border-theme-border gap-6"> <div className="flex flex-col md:flex-row justify-between items-center bg-theme-panel p-6 md:p-8 rounded-[2rem] shadow-sm border border-theme-border gap-6">
<div className="flex items-center gap-6"> <div className="flex items-center gap-6">
<div className="relative"> <div className="relative">
<img src={user.avatar} className="w-20 h-20 rounded-full border-4 border-slate-50 shadow-lg object-cover" alt="Avatar" /> <img src={user.avatar} className="w-20 h-20 rounded-full border-4 border-slate-50 shadow-lg object-cover" alt="Avatar" />
<div className="absolute -bottom-1 -right-1 bg-green-500 w-5 h-5 rounded-full border-4 border-white" /> <div className="absolute -bottom-1 -right-1 bg-green-500 w-5 h-5 rounded-full border-4 border-white" />
</div> </div>
<div> <div>
<h2 className="text-3xl font-black text-theme-text">Bonjour, {user.name} 👋</h2> <h2 className="text-3xl font-black text-theme-text">{t('dashboard.hello')}, {user.name} 👋</h2>
<div className="flex items-center gap-3 mt-1"> <div className="flex items-center gap-3 mt-1">
<span className="px-3 py-1 rounded-full bg-indigo-100 text-indigo-700 text-[10px] uppercase font-black tracking-widest">{user.subscription.planDetails?.displayName || user.subscription.plan}</span> <span className="px-3 py-1 rounded-full bg-indigo-100 text-indigo-700 text-[10px] uppercase font-black tracking-widest">{user.subscription.planDetails?.displayName || user.subscription.plan}</span>
<span className="text-theme-muted text-xs font-medium">Membre depuis le 24 janv.</span> <span className="text-theme-muted text-xs font-medium">{t('dashboard.member_since')} 24 janv.</span>
</div> </div>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<button onClick={onProfile} className="bg-theme-bg text-theme-text px-5 py-2.5 rounded-xl text-sm font-bold hover:opacity-80 transition-all flex items-center gap-2 border border-theme-border"> <LanguageSwitcher />
<User size={18} /> Mon Profil <button onClick={onProfile} className="bg-theme-bg text-theme-text px-4 md:px-5 py-2 md:py-2.5 rounded-xl text-xs md:text-sm font-bold hover:opacity-80 transition-all flex items-center gap-2 border border-theme-border">
<User size={18} /> {t('dashboard.my_profile')}
</button> </button>
<button onClick={onLogout} className="p-3 text-theme-muted hover:text-red-500 rounded-full hover:bg-red-500/10 transition-colors"><LogOut size={20} /></button> <button onClick={onLogout} title={t('sidebar.logout')} className="p-3 text-theme-muted hover:text-red-500 rounded-full hover:bg-red-500/10 transition-colors"><LogOut size={20} /></button>
</div> </div>
</div> </div>
{/* Stats Section */} {/* Stats Section */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="bg-theme-panel p-6 rounded-3xl shadow-sm border border-theme-border flex items-center gap-4"> <div className="bg-theme-panel p-4 sm:p-6 rounded-3xl shadow-sm border border-theme-border flex items-center gap-4">
<div className="bg-orange-100 p-3 rounded-2xl text-orange-600"><Flame size={24} /></div> <div className="bg-orange-100 p-3 rounded-2xl text-orange-600"><Flame size={24} /></div>
<div> <div>
<p className="text-xs font-bold text-theme-muted uppercase tracking-wider">Série actuelle</p> <p className="text-xs font-bold text-theme-muted uppercase tracking-wider">{t('dashboard.streak')}</p>
<p className="text-2xl font-black text-theme-text">{user.stats.writingStreak} Jours</p> <p className="text-2xl font-black text-theme-text">{user.stats.writingStreak} {t('dashboard.days')}</p>
</div> </div>
</div> </div>
<div className="bg-theme-panel p-6 rounded-3xl shadow-sm border border-theme-border flex items-center gap-4"> <div className="bg-theme-panel p-4 sm:p-6 rounded-3xl shadow-sm border border-theme-border flex items-center gap-4">
<div className="bg-blue-100 p-3 rounded-2xl text-blue-600"><Edit3 size={24} /></div> <div className="bg-blue-100 p-3 rounded-2xl text-blue-600"><Edit3 size={24} /></div>
<div> <div>
<p className="text-xs font-bold text-theme-muted uppercase tracking-wider">Mots écrits</p> <p className="text-xs font-bold text-theme-muted uppercase tracking-wider">{t('dashboard.words_written')}</p>
<p className="text-2xl font-black text-theme-text">{user.stats.totalWordsWritten.toLocaleString()}</p> <p className="text-2xl font-black text-theme-text">{user.stats.totalWordsWritten.toLocaleString()}</p>
</div> </div>
</div> </div>
<div className="bg-theme-panel p-6 rounded-3xl shadow-sm border border-theme-border flex items-center gap-4"> <div className="bg-theme-panel p-4 sm:p-6 rounded-3xl shadow-sm border border-theme-border flex items-center gap-4">
<div className="bg-indigo-100 p-3 rounded-2xl text-indigo-600"><Target size={24} /></div> <div className="bg-indigo-100 p-3 rounded-2xl text-indigo-600"><Target size={24} /></div>
<div> <div>
<p className="text-xs font-bold text-theme-muted uppercase tracking-wider">Objectif du jour</p> <p className="text-xs font-bold text-theme-muted uppercase tracking-wider">{t('dashboard.daily_goal')}</p>
<p className="text-2xl font-black text-theme-text">{user.preferences.dailyWordGoal} Mots</p> <p className="text-2xl font-black text-theme-text">{user.preferences.dailyWordGoal} {t('dashboard.words')}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -72,12 +77,12 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
{/* Project List */} {/* Project List */}
<div className="lg:col-span-2 space-y-4"> <div className="lg:col-span-2 space-y-4">
<div className="flex justify-between items-center mb-6"> <div className="flex justify-between items-center mb-6">
<h3 className="text-2xl font-black text-theme-text">Mes Romans</h3> <h3 className="text-2xl font-black text-theme-text">{t('dashboard.my_novels')}</h3>
<button <button
onClick={onCreate} onClick={onCreate}
className="flex items-center gap-2 bg-blue-600 text-white px-6 py-3 rounded-2xl font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-200" className="flex items-center gap-2 bg-blue-600 text-white px-6 py-3 rounded-2xl font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-200"
> >
<Plus size={20} /> Écrire un nouveau livre <Plus size={20} /> {t('dashboard.write_new')}
</button> </button>
</div> </div>
@@ -86,17 +91,17 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<div <div
key={p.id} key={p.id}
onClick={() => onSelect(p.id)} onClick={() => onSelect(p.id)}
className="bg-theme-panel p-8 rounded-[2.5rem] border border-theme-border shadow-sm hover:shadow-2xl hover:scale-[1.02] transition-all cursor-pointer group flex flex-col justify-between h-64" className="bg-theme-panel p-6 md:p-8 rounded-[2.5rem] border border-theme-border shadow-sm hover:shadow-2xl hover:scale-[1.02] transition-all cursor-pointer group flex flex-col justify-between h-64"
> >
<div> <div>
<div className="bg-blue-500/10 w-12 h-12 rounded-2xl flex items-center justify-center text-blue-500 mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors"> <div className="bg-blue-500/10 w-12 h-12 rounded-2xl flex items-center justify-center text-blue-500 mb-6 group-hover:bg-blue-600 group-hover:text-white transition-colors">
<Book size={24} /> <Book size={24} />
</div> </div>
<h4 className="font-black text-theme-text text-xl truncate mb-1">{p.title}</h4> <h4 className="font-black text-theme-text text-xl truncate mb-1">{p.title}</h4>
<p className="text-theme-muted text-sm">Dernière modification : {new Date(p.lastModified).toLocaleDateString()}</p> <p className="text-theme-muted text-sm">{t('dashboard.last_modified')} : {new Date(p.lastModified).toLocaleDateString()}</p>
</div> </div>
<div className="flex justify-between items-center text-[10px] text-theme-muted font-black uppercase tracking-widest border-t border-theme-border pt-4 mt-auto"> <div className="flex justify-between items-center text-[10px] text-theme-muted font-black uppercase tracking-widest border-t border-theme-border pt-4 mt-auto">
<span>{p.chapters.length} Chapitres</span> <span>{p.chapters.length} {t('nav.chapters')}</span>
<ChevronRight size={20} className="group-hover:text-blue-600 transition-transform group-hover:translate-x-1 duration-300" /> <ChevronRight size={20} className="group-hover:text-blue-600 transition-transform group-hover:translate-x-1 duration-300" />
</div> </div>
</div> </div>
@@ -104,8 +109,8 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
{projects.length === 0 && ( {projects.length === 0 && (
<div className="col-span-2 py-24 bg-theme-panel rounded-[3rem] border-2 border-dashed border-theme-border flex flex-col items-center justify-center text-theme-muted"> <div className="col-span-2 py-24 bg-theme-panel rounded-[3rem] border-2 border-dashed border-theme-border flex flex-col items-center justify-center text-theme-muted">
<Book size={64} className="mb-6 opacity-20" /> <Book size={64} className="mb-6 opacity-20" />
<p className="font-bold text-lg">Prêt à commencer votre premier roman ?</p> <p className="font-bold text-lg">{t('dashboard.empty_projects')}</p>
<button onClick={onCreate} className="mt-4 text-blue-600 font-bold hover:underline">Créer un projet maintenant</button> <button onClick={onCreate} className="mt-4 text-blue-600 font-bold hover:underline">{t('dashboard.create_now')}</button>
</div> </div>
)} )}
</div> </div>
@@ -113,13 +118,13 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
{/* Sidebar Stats & Plan */} {/* Sidebar Stats & Plan */}
<div className="space-y-6"> <div className="space-y-6">
<div className="bg-slate-900 text-white p-8 rounded-[2.5rem] shadow-xl relative overflow-hidden"> <div className="bg-slate-900 text-white p-6 md:p-8 rounded-[2.5rem] shadow-xl relative overflow-hidden">
<div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/20 blur-[60px] -z-1" /> <div className="absolute top-0 right-0 w-32 h-32 bg-indigo-500/20 blur-[60px] -z-1" />
<h3 className="font-black text-xl mb-6 flex items-center gap-2"><Star size={20} className="text-yellow-400" /> Utilisation</h3> <h3 className="font-black text-xl mb-6 flex items-center gap-2"><Star size={20} className="text-yellow-400" /> {t('dashboard.usage')}</h3>
<div className="space-y-8"> <div className="space-y-8">
<div> <div>
<div className="flex justify-between text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2"> <div className="flex justify-between text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2">
<span>Actions IA</span> <span>{t('sidebar.ai_actions')}</span>
<span>{user.usage.aiActionsCurrent} / {user.usage.aiActionsLimit === 999999 ? '∞' : user.usage.aiActionsLimit}</span> <span>{user.usage.aiActionsCurrent} / {user.usage.aiActionsLimit === 999999 ? '∞' : user.usage.aiActionsLimit}</span>
</div> </div>
<div className="h-3 w-full bg-slate-800 rounded-full overflow-hidden"> <div className="h-3 w-full bg-slate-800 rounded-full overflow-hidden">
@@ -131,7 +136,7 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
</div> </div>
<div> <div>
<div className="flex justify-between text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2"> <div className="flex justify-between text-[10px] font-black text-slate-400 uppercase tracking-widest mb-2">
<span>Emplacements Roman</span> <span>{t('dashboard.novel_slots')}</span>
<span>{projects.length} / {user.usage.projectsLimit}</span> <span>{projects.length} / {user.usage.projectsLimit}</span>
</div> </div>
<div className="h-3 w-full bg-slate-800 rounded-full overflow-hidden"> <div className="h-3 w-full bg-slate-800 rounded-full overflow-hidden">
@@ -143,7 +148,7 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
</div> </div>
</div> </div>
<button onClick={onPricing} className="w-full mt-10 bg-white/10 hover:bg-white/20 py-4 rounded-2xl text-sm font-bold transition-all"> <button onClick={onPricing} className="w-full mt-10 bg-white/10 hover:bg-white/20 py-4 rounded-2xl text-sm font-bold transition-all">
Upgrade Plan {t('dashboard.upgrade_plan')}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -3,6 +3,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { BookProject } from '@/lib/types'; import { BookProject } from '@/lib/types';
import { FileText, FileType, Printer, X, Download, Book, FileJson } from 'lucide-react'; import { FileText, FileType, Printer, X, Download, Book, FileJson } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface ExportModalProps { interface ExportModalProps {
isOpen: boolean; isOpen: boolean;
@@ -19,6 +20,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
const [pageSize, setPageSize] = useState<PageSize>('A4'); const [pageSize, setPageSize] = useState<PageSize>('A4');
const [includeCover, setIncludeCover] = useState(true); const [includeCover, setIncludeCover] = useState(true);
const [includeTOC, setIncludeTOC] = useState(true); const [includeTOC, setIncludeTOC] = useState(true);
const { t } = useLanguage();
if (!isOpen) return null; if (!isOpen) return null;
@@ -139,7 +141,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
<div className="bg-slate-900 text-white p-6 flex justify-between items-center"> <div className="bg-slate-900 text-white p-6 flex justify-between items-center">
<div> <div>
<h2 className="text-xl font-bold flex items-center gap-2"> <h2 className="text-xl font-bold flex items-center gap-2">
<Download size={24} /> Exporter le livre <Download size={24} /> {t('export.title')}
</h2> </h2>
<p className="text-slate-400 text-sm mt-1">{project.title}</p> <p className="text-slate-400 text-sm mt-1">{project.title}</p>
</div> </div>
@@ -158,7 +160,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
className={`p-4 rounded-lg border-2 flex flex-col items-center gap-3 transition-all ${format === 'pdf' ? 'border-blue-600 bg-blue-50 text-blue-800' : 'border-slate-200 hover:border-slate-300 text-slate-600'}`} className={`p-4 rounded-lg border-2 flex flex-col items-center gap-3 transition-all ${format === 'pdf' ? 'border-blue-600 bg-blue-50 text-blue-800' : 'border-slate-200 hover:border-slate-300 text-slate-600'}`}
> >
<Printer size={32} /> <Printer size={32} />
<div className="font-semibold">PDF (Impression)</div> <div className="font-semibold">{t('export.pdf_format')}</div>
</button> </button>
<button <button
@@ -166,7 +168,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
className={`p-4 rounded-lg border-2 flex flex-col items-center gap-3 transition-all ${format === 'word' ? 'border-blue-600 bg-blue-50 text-blue-800' : 'border-slate-200 hover:border-slate-300 text-slate-600'}`} className={`p-4 rounded-lg border-2 flex flex-col items-center gap-3 transition-all ${format === 'word' ? 'border-blue-600 bg-blue-50 text-blue-800' : 'border-slate-200 hover:border-slate-300 text-slate-600'}`}
> >
<FileText size={32} /> <FileText size={32} />
<div className="font-semibold">Microsoft Word</div> <div className="font-semibold">{t('export.word_format')}</div>
</button> </button>
<button <button
@@ -174,7 +176,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
className={`p-4 rounded-lg border-2 flex flex-col items-center gap-3 transition-all ${format === 'epub' ? 'border-blue-600 bg-blue-50 text-blue-800' : 'border-slate-200 hover:border-slate-300 text-slate-600'}`} className={`p-4 rounded-lg border-2 flex flex-col items-center gap-3 transition-all ${format === 'epub' ? 'border-blue-600 bg-blue-50 text-blue-800' : 'border-slate-200 hover:border-slate-300 text-slate-600'}`}
> >
<Book size={32} /> <Book size={32} />
<div className="font-semibold">EPUB / Ebook</div> <div className="font-semibold">{t('export.epub_format')}</div>
</button> </button>
<button <button
@@ -182,29 +184,29 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
className={`p-4 rounded-lg border-2 flex flex-col items-center gap-3 transition-all ${format === 'markdown' ? 'border-blue-600 bg-blue-50 text-blue-800' : 'border-slate-200 hover:border-slate-300 text-slate-600'}`} className={`p-4 rounded-lg border-2 flex flex-col items-center gap-3 transition-all ${format === 'markdown' ? 'border-blue-600 bg-blue-50 text-blue-800' : 'border-slate-200 hover:border-slate-300 text-slate-600'}`}
> >
<FileJson size={32} /> <FileJson size={32} />
<div className="font-semibold">Markdown</div> <div className="font-semibold">{t('export.markdown_format')}</div>
</button> </button>
</div> </div>
{/* Options Section */} {/* Options Section */}
<div className="bg-slate-50 rounded-lg p-5 border border-slate-200"> <div className="bg-slate-50 rounded-lg p-5 border border-slate-200">
<h3 className="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4"> <h3 className="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4">
Paramètres d'exportation ({format.toUpperCase()}) {t('export.settings')} ({format.toUpperCase()})
</h3> </h3>
<div className="space-y-4"> <div className="space-y-4">
{format === 'pdf' && ( {format === 'pdf' && (
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex flex-col"> <div className="flex flex-col">
<label className="text-slate-700 font-medium">Format du papier</label> <label className="text-slate-700 font-medium">{t('export.paper_format')}</label>
<span className="text-xs text-slate-400">Géré par l'imprimante (A4, A5...)</span> <span className="text-xs text-slate-400">{t('export.printer_managed')}</span>
</div> </div>
<div className="bg-slate-200 px-3 py-1 rounded text-xs font-mono text-slate-600">Auto</div> <div className="bg-slate-200 px-3 py-1 rounded text-xs font-mono text-slate-600">{t('export.auto')}</div>
</div> </div>
)} )}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-slate-700 font-medium cursor-pointer" htmlFor="cover">Inclure la page de titre</label> <label className="text-slate-700 font-medium cursor-pointer" htmlFor="cover">{t('export.include_cover')}</label>
<input <input
id="cover" id="cover"
type="checkbox" type="checkbox"
@@ -215,7 +217,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<label className="text-slate-700 font-medium cursor-pointer" htmlFor="toc">Générer la table des matières</label> <label className="text-slate-700 font-medium cursor-pointer" htmlFor="toc">{t('export.generate_toc')}</label>
<input <input
id="toc" id="toc"
type="checkbox" type="checkbox"
@@ -227,7 +229,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
{format === 'epub' && ( {format === 'epub' && (
<p className="text-xs text-amber-600 bg-amber-50 p-2 rounded mt-2"> <p className="text-xs text-amber-600 bg-amber-50 p-2 rounded mt-2">
Note: L'export EPUB génère un fichier XHTML optimisé prêt à être converti par Calibre ou Kindle Previewer. {t('export.epub_note')}
</p> </p>
)} )}
</div> </div>
@@ -240,14 +242,14 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
onClick={onClose} onClick={onClose}
className="px-5 py-2 text-slate-600 hover:bg-slate-200 rounded-lg font-medium transition-colors" className="px-5 py-2 text-slate-600 hover:bg-slate-200 rounded-lg font-medium transition-colors"
> >
Annuler {t('export.cancel')}
</button> </button>
<button <button
onClick={handleExport} onClick={handleExport}
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium shadow-md transition-all flex items-center gap-2" className="px-6 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg font-medium shadow-md transition-all flex items-center gap-2"
> >
{format === 'pdf' ? <Printer size={18} /> : <Download size={18} />} {format === 'pdf' ? <Printer size={18} /> : <Download size={18} />}
{format === 'pdf' ? 'Imprimer / Enregistrer PDF' : `Télécharger .${format === 'word' ? 'doc' : format === 'epub' ? 'xhtml' : 'md'}`} {format === 'pdf' ? t('export.print_save_pdf') : `${t('export.download')} .${format === 'word' ? 'doc' : format === 'epub' ? 'xhtml' : 'md'}`}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -2,29 +2,48 @@
import React from 'react'; import React from 'react';
import { Sparkles, Feather, Globe, GitGraph, BookOpen, Download, Lightbulb, Zap, ArrowLeft } from 'lucide-react'; import { ArrowLeft, BookOpen, Brain, Globe, ShieldCheck, Zap, Sparkles, LayoutDashboard, History, MessageSquare, Save, Users, Layers, Workflow, CheckCircle2, GitGraph, Lightbulb, Feather } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import Link from 'next/link';
interface FeaturesPageProps { interface FeaturesPageProps {
onBack: () => void; onBack: () => void;
} }
const FeaturesPage: React.FC<FeaturesPageProps> = ({ onBack }) => { const FeaturesPage: React.FC<FeaturesPageProps> = ({ onBack }) => {
const { t } = useLanguage();
const features = [ const features = [
{ title: "Assistant IA Co-Auteur", icon: Sparkles, desc: "Générez des paragraphes, brainstormez des idées et demandez conseil à une IA qui connaît votre univers." }, { title: t('features.feat1_title'), icon: Sparkles, desc: t('features.feat1_desc') },
{ title: "Bible du Monde Vivante", icon: Globe, desc: "Gérez vos personnages, lieux et objets. L'IA les reconnaît et garde une cohérence absolue." }, { title: t('features.feat2_title'), icon: Globe, desc: t('features.feat2_desc') },
{ title: "Story Workflow", icon: GitGraph, desc: "Visualisez votre intrigue sous forme de nœuds et gérez les embranchements de votre récit." }, { title: t('features.feat3_title'), icon: GitGraph, desc: t('features.feat3_desc') },
{ title: "Boîte à Idées Kanban", icon: Lightbulb, desc: "Notez vos idées fugaces et transformez-les en chapitres quand vous êtes prêt." }, { title: t('features.feat4_title'), icon: Lightbulb, desc: t('features.feat4_desc') },
{ title: "Mise en page Pro", icon: BookOpen, desc: "Exportez au format PDF, Word ou EPUB avec une mise en page soignée et automatique." }, { title: t('features.feat5_title'), icon: BookOpen, desc: t('features.feat5_desc') },
{ title: "Éditeur Riche", icon: Feather, desc: "Un traitement de texte complet avec mode focus et historique des modifications IA." } { title: t('features.feat6_title'), icon: Feather, desc: t('features.feat6_desc') }
]; ];
return ( return (
<div className="min-h-screen bg-[#eef2ff] py-20 px-8"> <div className="min-h-screen bg-[#eef2ff] font-sans">
<div className="max-w-7xl mx-auto"> {/* Header */}
<button onClick={onBack} className="flex items-center gap-2 text-slate-500 hover:text-blue-600 mb-12 font-bold transition-colors"> <div className="bg-slate-900 text-white pt-20 pb-32 px-4 md:px-8 relative overflow-hidden">
<ArrowLeft size={20} /> Retour <div className="absolute top-0 right-0 w-96 h-96 bg-blue-500/20 blur-[100px] rounded-full" />
<div className="max-w-7xl mx-auto relative z-10">
<div className="flex justify-between items-center mb-12">
<button onClick={onBack} className="flex items-center gap-2 text-slate-400 hover:text-white font-bold transition-colors">
<ArrowLeft size={20} /> {t('common.back')}
</button> </button>
<h1 className="text-5xl font-black text-slate-900 mb-12 text-center">Un univers d'outils pour votre créativité.</h1> <LanguageSwitcher />
</div>
<h1 className="text-4xl md:text-5xl font-black text-white mb-4 text-center">{t('features.title')}</h1>
<p className="text-slate-300 text-lg md:text-xl text-center max-w-3xl mx-auto">
{t('features.subtitle')}
</p>
</div>
</div>
{/* Features Grid */}
<div className="max-w-7xl mx-auto px-4 md:px-8 -mt-20 relative z-20">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{features.map((f, i) => ( {features.map((f, i) => (
<div key={i} className="bg-white p-8 rounded-3xl shadow-xl border border-indigo-50 hover:scale-105 transition-transform"> <div key={i} className="bg-white p-8 rounded-3xl shadow-xl border border-indigo-50 hover:scale-105 transition-transform">
@@ -37,6 +56,17 @@ const FeaturesPage: React.FC<FeaturesPageProps> = ({ onBack }) => {
))} ))}
</div> </div>
</div> </div>
{/* Footer */}
<footer className="bg-slate-900 text-slate-400 py-12 px-4 md:px-8 mt-20 text-center relative z-20">
<div className="max-w-7xl mx-auto">
<div className="flex flex-wrap items-center justify-center gap-6 mb-8 text-sm">
<Link href="/cgu" className="hover:text-white transition-colors">{t('footer.cgu')}</Link>
<Link href="/cgv" className="hover:text-white transition-colors">{t('footer.cgv')}</Link>
<Link href="/sitemap" className="hover:text-white transition-colors">{t('footer.sitemap')}</Link>
</div>
<p className="text-sm">{t('landing.copyright')}</p>
</div>
</footer>
</div> </div>
); );
}; };

View File

@@ -3,6 +3,7 @@
import React from 'react'; import React from 'react';
import { X, Keyboard, MousePointerClick, MessageCircle, Sparkles, GitGraph, BookOpen, Command, Globe, Layout, Settings, Lightbulb } from 'lucide-react'; import { X, Keyboard, MousePointerClick, MessageCircle, Sparkles, GitGraph, BookOpen, Command, Globe, Layout, Settings, Lightbulb } from 'lucide-react';
import { ViewMode } from '@/lib/types'; import { ViewMode } from '@/lib/types';
import { useLanguage } from '@/providers/LanguageProvider';
interface HelpModalProps { interface HelpModalProps {
isOpen: boolean; isOpen: boolean;
@@ -17,6 +18,8 @@ const Kbd: React.FC<{ children: React.ReactNode }> = ({ children }) => (
); );
const HelpModal: React.FC<HelpModalProps> = ({ isOpen, onClose, viewMode }) => { const HelpModal: React.FC<HelpModalProps> = ({ isOpen, onClose, viewMode }) => {
const { t } = useLanguage();
if (!isOpen) return null; if (!isOpen) return null;
const renderContent = () => { const renderContent = () => {
@@ -167,10 +170,10 @@ const HelpModal: React.FC<HelpModalProps> = ({ isOpen, onClose, viewMode }) => {
<h4 className="font-bold text-amber-800 mb-2">Menu Contextuel Intelligent</h4> <h4 className="font-bold text-amber-800 mb-2">Menu Contextuel Intelligent</h4>
<p>Sélectionnez du texte et faites un <strong>clic droit</strong> pour :</p> <p>Sélectionnez du texte et faites un <strong>clic droit</strong> pour :</p>
<ul className="grid grid-cols-2 gap-2 mt-2 pl-4"> <ul className="grid grid-cols-2 gap-2 mt-2 pl-4">
<li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400"/>Corriger l'orthographe</li> <li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400" />Corriger l'orthographe</li>
<li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400"/>Reformuler / Améliorer</li> <li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400" />Reformuler / Améliorer</li>
<li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400"/>Développer (Show, don't tell)</li> <li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400" />Développer (Show, don't tell)</li>
<li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400"/>Continuer l'écriture</li> <li className="flex items-center gap-2"><div className="w-1.5 h-1.5 rounded-full bg-amber-400" />Continuer l'écriture</li>
</ul> </ul>
</div> </div>
<p> <p>
@@ -229,15 +232,15 @@ const HelpModal: React.FC<HelpModalProps> = ({ isOpen, onClose, viewMode }) => {
<div className="bg-slate-900 text-white p-6 flex justify-between items-center shrink-0"> <div className="bg-slate-900 text-white p-6 flex justify-between items-center shrink-0">
<div> <div>
<h2 className="text-xl font-bold flex items-center gap-2"> <h2 className="text-xl font-bold flex items-center gap-2">
<BookOpen size={24} className="text-blue-400" /> Aide : { <BookOpen size={24} className="text-blue-400" /> {t('help.title')} : {
viewMode === 'workflow' ? 'Workflow & Dialogues' : viewMode === 'workflow' ? t('help.workflow_title_doc') :
viewMode === 'world_building' ? 'Bible du Monde' : viewMode === 'world_building' ? t('help.world_building_title') :
viewMode === 'settings' ? 'Paramètres' : viewMode === 'settings' ? t('help.settings_title') :
viewMode === 'ideas' ? 'Boîte à Idées' : viewMode === 'ideas' ? t('help.ideas_title') :
'Éditeur & IA' t('help.editor_ai_title')
} }
</h2> </h2>
<p className="text-slate-400 text-sm mt-1">Astuces pour l'écran actuel.</p> <p className="text-slate-400 text-sm mt-1">{t('help.subtitle')}</p>
</div> </div>
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors p-2 hover:bg-slate-800 rounded-full"> <button onClick={onClose} className="text-slate-400 hover:text-white transition-colors p-2 hover:bg-slate-800 rounded-full">
<X size={24} /> <X size={24} />
@@ -252,15 +255,15 @@ const HelpModal: React.FC<HelpModalProps> = ({ isOpen, onClose, viewMode }) => {
{/* General Footer Section (Always visible) */} {/* General Footer Section (Always visible) */}
<div className="border-t border-slate-100 pt-6 mt-6"> <div className="border-t border-slate-100 pt-6 mt-6">
<h4 className="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4">Raccourcis Généraux</h4> <h4 className="text-sm font-bold text-slate-500 uppercase tracking-wider mb-4">{t('help.general_shortcuts')}</h4>
<div className="grid grid-cols-2 gap-4 text-xs text-slate-600"> <div className="grid grid-cols-2 gap-4 text-xs text-slate-600">
<div className="flex justify-between"> <div className="flex justify-between">
<span>Sauvegarde Automatique</span> <span>{t('help.auto_save')}</span>
<span className="font-mono text-slate-400">Permanente</span> <span className="font-mono text-slate-400">{t('help.permanent')}</span>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>Menu Latéral</span> <span>{t('help.side_menu')}</span>
<span>Clic sur le burger</span> <span>{t('help.click_burger')}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -273,7 +276,7 @@ const HelpModal: React.FC<HelpModalProps> = ({ isOpen, onClose, viewMode }) => {
onClick={onClose} onClick={onClose}
className="px-6 py-2 bg-slate-800 text-white rounded-lg hover:bg-slate-900 transition-colors font-medium" className="px-6 py-2 bg-slate-800 text-white rounded-lg hover:bg-slate-900 transition-colors font-medium"
> >
Fermer {t('help.close')}
</button> </button>
</div> </div>

View File

@@ -3,28 +3,31 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Idea, IdeaStatus, IdeaCategory } from '@/lib/types'; import { Idea, IdeaStatus, IdeaCategory } from '@/lib/types';
import { Plus, X, GripVertical, CheckCircle, Circle, Clock, Lightbulb, Search, Trash2, Edit3, Save } from 'lucide-react'; import { Plus, X, GripVertical, CheckCircle, Circle, Clock, Lightbulb, Search, Trash2, Edit3, Save } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { TranslationKey } from '@/lib/i18n/translations';
interface IdeaBoardProps { interface IdeaBoardProps {
ideas: Idea[]; ideas: Idea[];
onUpdate: (ideas: Idea[]) => void; onUpdate: (ideas: Idea[]) => void;
} }
const CATEGORIES: Record<IdeaCategory, { label: string, color: string, icon: any }> = { const CATEGORIES: Record<IdeaCategory, { labelKey: string, color: string, icon: any }> = {
plot: { label: 'Intrigue', color: 'bg-rose-100 text-rose-800 border-rose-200', icon: Lightbulb }, plot: { labelKey: 'ideaboard.cat_plot', color: 'bg-rose-100 text-rose-800 border-rose-200', icon: Lightbulb },
character: { label: 'Personnage', color: 'bg-blue-100 text-blue-800 border-blue-200', icon: Search }, character: { labelKey: 'ideaboard.cat_char', color: 'bg-blue-100 text-blue-800 border-blue-200', icon: Search },
research: { label: 'Recherche', color: 'bg-amber-100 text-amber-800 border-amber-200', icon: Search }, research: { labelKey: 'ideaboard.cat_research', color: 'bg-amber-100 text-amber-800 border-amber-200', icon: Search },
todo: { label: 'À faire', color: 'bg-slate-100 text-slate-800 border-slate-200', icon: CheckCircle }, todo: { labelKey: 'ideaboard.cat_todo', color: 'bg-slate-100 text-slate-800 border-slate-200', icon: CheckCircle },
}; };
const STATUS_LABELS: Record<IdeaStatus, string> = { const STATUS_LABELS: Record<IdeaStatus, string> = {
todo: 'Idées / À faire', todo: 'ideaboard.stat_todo',
progress: 'En cours', progress: 'ideaboard.stat_prog',
done: 'Terminé / Validé' done: 'ideaboard.stat_done'
}; };
const MAX_DESCRIPTION_LENGTH = 500; const MAX_DESCRIPTION_LENGTH = 500;
const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => { const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
const { t } = useLanguage();
const [newIdeaTitle, setNewIdeaTitle] = useState(''); const [newIdeaTitle, setNewIdeaTitle] = useState('');
const [newIdeaCategory, setNewIdeaCategory] = useState<IdeaCategory>('plot'); const [newIdeaCategory, setNewIdeaCategory] = useState<IdeaCategory>('plot');
@@ -54,7 +57,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
}; };
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
if (confirm("Supprimer cette carte ?")) { if (confirm(t('ideaboard.delete') + " ?")) {
onUpdate(ideas.filter(i => i.id !== id)); onUpdate(ideas.filter(i => i.id !== id));
if (editingItem?.id === id) setEditingItem(null); if (editingItem?.id === id) setEditingItem(null);
} }
@@ -128,7 +131,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
onDragOver={handleDragOver} onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, status)} onDrop={(e) => handleDrop(e, status)}
onDoubleClick={() => openQuickAdd(status)} onDoubleClick={() => openQuickAdd(status)}
title="Double-cliquez dans le vide pour ajouter une carte ici" title={t('ideaboard.empty_desc')}
> >
{/* Column Header */} {/* Column Header */}
<div className={`p-4 border-b border-theme-border flex justify-between items-center transition-colors duration-300 ${status === 'todo' ? 'bg-theme-bg' : <div className={`p-4 border-b border-theme-border flex justify-between items-center transition-colors duration-300 ${status === 'todo' ? 'bg-theme-bg' :
@@ -172,7 +175,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
> >
<div className="flex justify-between items-start mb-2"> <div className="flex justify-between items-start mb-2">
<span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded-full flex items-center gap-1 ${CATEGORIES[idea.category].color}`}> <span className={`text-[10px] uppercase font-bold px-2 py-0.5 rounded-full flex items-center gap-1 ${CATEGORIES[idea.category].color}`}>
{CATEGORIES[idea.category].label} {t(CATEGORIES[idea.category].labelKey as TranslationKey)}
</span> </span>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity"> <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button <button
@@ -190,7 +193,6 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
</div> </div>
</div> </div>
{/* CARD CONTENT */}
<div className="mb-2"> <div className="mb-2">
<h4 className="font-bold text-theme-text text-sm mb-1 leading-tight">{idea.title}</h4> <h4 className="font-bold text-theme-text text-sm mb-1 leading-tight">{idea.title}</h4>
{idea.description && ( {idea.description && (
@@ -211,8 +213,8 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
})} })}
{columnIdeas.length === 0 && ( {columnIdeas.length === 0 && (
<div className="h-full flex flex-col items-center justify-center text-slate-300 text-sm italic border-2 border-dashed border-indigo-200 rounded-lg m-1"> <div className="h-full flex flex-col items-center justify-center text-slate-300 text-sm italic border-2 border-dashed border-indigo-200 rounded-lg m-1">
<span className="mb-2">Vide</span> <span className="mb-2">{t('ideaboard.empty')}</span>
<span className="text-xs opacity-70">Double-cliquez pour ajouter</span> <span className="text-xs opacity-70">{t('ideaboard.empty_desc')}</span>
</div> </div>
)} )}
</div> </div>
@@ -227,9 +229,9 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-theme-panel p-4 rounded-xl border border-theme-border shadow-sm shrink-0 transition-colors duration-300"> <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-theme-panel p-4 rounded-xl border border-theme-border shadow-sm shrink-0 transition-colors duration-300">
<div> <div>
<h2 className="text-2xl font-bold text-theme-text flex items-center gap-2"> <h2 className="text-2xl font-bold text-theme-text flex items-center gap-2">
<Lightbulb className="text-yellow-500" /> Boîte à Idées <Lightbulb className="text-yellow-500" /> {t('ideaboard.title')}
</h2> </h2>
<p className="text-theme-muted text-sm">Organisez vos tâches, idées de scènes et recherches.</p> <p className="text-theme-muted text-sm">{t('ideaboard.desc')}</p>
</div> </div>
<form onSubmit={handleAddIdea} className="flex-1 w-full md:w-auto max-w-2xl flex gap-2"> <form onSubmit={handleAddIdea} className="flex-1 w-full md:w-auto max-w-2xl flex gap-2">
@@ -239,14 +241,14 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
className="bg-theme-bg border border-theme-border text-theme-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 outline-none transition-colors duration-300" className="bg-theme-bg border border-theme-border text-theme-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 outline-none transition-colors duration-300"
> >
{Object.entries(CATEGORIES).map(([key, val]) => ( {Object.entries(CATEGORIES).map(([key, val]) => (
<option key={key} value={key}>{val.label}</option> <option key={key} value={key}>{t(val.labelKey as TranslationKey)}</option>
))} ))}
</select> </select>
<input <input
type="text" type="text"
value={newIdeaTitle} value={newIdeaTitle}
onChange={(e) => setNewIdeaTitle(e.target.value)} onChange={(e) => setNewIdeaTitle(e.target.value)}
placeholder="Titre de la nouvelle idée..." placeholder={t('ideaboard.add_idea')}
className="flex-1 bg-theme-bg border border-theme-border text-theme-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 outline-none font-medium transition-colors duration-300" className="flex-1 bg-theme-bg border border-theme-border text-theme-text text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 outline-none font-medium transition-colors duration-300"
/> />
<button <button
@@ -261,9 +263,9 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
{/* Kanban Board */} {/* Kanban Board */}
<div className="flex-1 grid grid-cols-1 md:grid-cols-3 gap-6 min-h-0"> <div className="flex-1 grid grid-cols-1 md:grid-cols-3 gap-6 min-h-0">
<Column title="Idées / À faire" status="todo" icon={Circle} /> <Column title={t('ideaboard.stat_todo')} status="todo" icon={Circle} />
<Column title="En cours" status="progress" icon={Clock} /> <Column title={t('ideaboard.stat_prog')} status="progress" icon={Clock} />
<Column title="Terminé" status="done" icon={CheckCircle} /> <Column title={t('ideaboard.stat_done')} status="done" icon={CheckCircle} />
</div> </div>
{/* EDIT / QUICK ADD MODAL */} {/* EDIT / QUICK ADD MODAL */}
@@ -273,7 +275,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
<div className="bg-theme-bg border-b border-theme-border p-4 flex justify-between items-center"> <div className="bg-theme-bg border-b border-theme-border p-4 flex justify-between items-center">
<h3 className="font-bold text-theme-text flex items-center gap-2"> <h3 className="font-bold text-theme-text flex items-center gap-2">
{editingItem.id ? <Edit3 size={18} /> : <Plus size={18} />} {editingItem.id ? <Edit3 size={18} /> : <Plus size={18} />}
{editingItem.id ? 'Éditer la carte' : 'Ajouter une carte'} {editingItem.id ? t('ideaboard.edit_card') : t('ideaboard.add_card')}
</h3> </h3>
<button onClick={() => setEditingItem(null)} className="text-theme-muted hover:text-theme-text"> <button onClick={() => setEditingItem(null)} className="text-theme-muted hover:text-theme-text">
<X size={20} /> <X size={20} />
@@ -282,19 +284,19 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
<div className="p-6 space-y-4 overflow-y-auto"> <div className="p-6 space-y-4 overflow-y-auto">
<div> <div>
<label className="block text-xs font-bold text-theme-muted uppercase mb-1">Titre</label> <label className="block text-xs font-bold text-theme-muted uppercase mb-1">{t('ideaboard.title_label')}</label>
<input <input
type="text" type="text"
value={editingItem.title} value={editingItem.title}
onChange={(e) => setEditingItem({ ...editingItem, title: e.target.value })} onChange={(e) => setEditingItem({ ...editingItem, title: e.target.value })}
className="w-full p-3 bg-theme-bg border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none font-bold text-theme-text transition-colors duration-300" className="w-full p-3 bg-theme-bg border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none font-bold text-theme-text transition-colors duration-300"
placeholder="Titre de la tâche ou de l'idée..." placeholder={t('ideaboard.add_idea')}
autoFocus autoFocus
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-theme-muted uppercase mb-1">Description</label> <label className="block text-xs font-bold text-theme-muted uppercase mb-1">{t('ideaboard.desc_label')}</label>
<textarea <textarea
value={editingItem.description} value={editingItem.description}
onChange={(e) => setEditingItem({ ...editingItem, description: e.target.value })} onChange={(e) => setEditingItem({ ...editingItem, description: e.target.value })}
@@ -311,26 +313,26 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-bold text-theme-muted uppercase mb-1">Catégorie</label> <label className="block text-xs font-bold text-theme-muted uppercase mb-1">{t('ideaboard.cat_label')}</label>
<select <select
value={editingItem.category} value={editingItem.category}
onChange={(e) => setEditingItem({ ...editingItem, category: e.target.value as IdeaCategory })} onChange={(e) => setEditingItem({ ...editingItem, category: e.target.value as IdeaCategory })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded-lg text-theme-text text-sm outline-none focus:border-blue-500 transition-colors duration-300" className="w-full p-2 bg-theme-bg border border-theme-border rounded-lg text-theme-text text-sm outline-none focus:border-blue-500 transition-colors duration-300"
> >
{Object.entries(CATEGORIES).map(([key, val]) => ( {Object.entries(CATEGORIES).map(([key, val]) => (
<option key={key} value={key}>{val.label}</option> <option key={key} value={key}>{t(val.labelKey as TranslationKey)}</option>
))} ))}
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-bold text-theme-muted uppercase mb-1">Statut</label> <label className="block text-xs font-bold text-theme-muted uppercase mb-1">{t('ideaboard.stat_label')}</label>
<select <select
value={editingItem.status} value={editingItem.status}
onChange={(e) => setEditingItem({ ...editingItem, status: e.target.value as IdeaStatus })} onChange={(e) => setEditingItem({ ...editingItem, status: e.target.value as IdeaStatus })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded-lg text-theme-text text-sm outline-none focus:border-blue-500 transition-colors duration-300" className="w-full p-2 bg-theme-bg border border-theme-border rounded-lg text-theme-text text-sm outline-none focus:border-blue-500 transition-colors duration-300"
> >
{Object.entries(STATUS_LABELS).map(([key, val]) => ( {Object.entries(STATUS_LABELS).map(([key, val]) => (
<option key={key} value={key}>{val}</option> <option key={key} value={key}>{t(val as TranslationKey)}</option>
))} ))}
</select> </select>
</div> </div>
@@ -343,21 +345,21 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
onClick={() => handleDelete(editingItem.id!)} onClick={() => handleDelete(editingItem.id!)}
className="mr-auto text-red-500 hover:text-red-700 text-sm font-medium px-3 py-2" className="mr-auto text-red-500 hover:text-red-700 text-sm font-medium px-3 py-2"
> >
Supprimer {t('ideaboard.delete')}
</button> </button>
)} )}
<button <button
onClick={() => setEditingItem(null)} onClick={() => setEditingItem(null)}
className="px-4 py-2 text-theme-text hover:bg-theme-panel border border-transparent rounded-lg text-sm font-medium transition-colors duration-300" className="px-4 py-2 text-theme-text hover:bg-theme-panel border border-transparent rounded-lg text-sm font-medium transition-colors duration-300"
> >
Annuler {t('ideaboard.cancel')}
</button> </button>
<button <button
onClick={handleSaveEdit} onClick={handleSaveEdit}
disabled={!editingItem.title?.trim()} disabled={!editingItem.title?.trim()}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium shadow-sm disabled:opacity-50 flex items-center gap-2" className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium shadow-sm disabled:opacity-50 flex items-center gap-2"
> >
<Save size={16} /> Enregistrer <Save size={16} /> {t('ideaboard.save')}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,8 +1,10 @@
'use client'; 'use client';
import React from 'react'; import React from 'react';
import { Book, Sparkles, Feather, Globe, ShieldCheck, Zap, ArrowRight, Star } from 'lucide-react'; import { Book, Sparkles, Feather, Globe, ShieldCheck, Zap, ArrowRight, Star } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import Link from 'next/link';
interface LandingPageProps { interface LandingPageProps {
onLogin: () => void; onLogin: () => void;
@@ -11,45 +13,49 @@ interface LandingPageProps {
} }
const LandingPage: React.FC<LandingPageProps> = ({ onLogin, onPricing, onFeatures }) => { const LandingPage: React.FC<LandingPageProps> = ({ onLogin, onPricing, onFeatures }) => {
const { t } = useLanguage();
return ( return (
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200"> <div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
{/* Navbar */} {/* Navbar */}
<nav className="fixed top-0 w-full bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-8 h-16 flex items-center justify-between"> <nav className="fixed top-0 w-full bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-4 md:px-8 h-16 flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="bg-blue-600 p-1.5 rounded-lg"> <div className="bg-blue-600 p-1.5 rounded-lg">
<Book className="text-white" size={24} /> <Book className="text-white" size={24} />
</div> </div>
<span className="text-xl font-black text-slate-900 tracking-tight">PlumeIA</span> <span className="text-xl font-black text-slate-900 tracking-tight">Pluume</span>
</div> </div>
<div className="hidden md:flex items-center gap-8 text-sm font-medium text-slate-600"> <div className="hidden md:flex items-center gap-8 text-sm font-medium text-slate-600">
<button onClick={onFeatures} className="hover:text-blue-600 transition-colors">Fonctionnalités</button> <button onClick={onFeatures} className="hover:text-blue-600 transition-colors">{t('landing.nav_features')}</button>
<button onClick={onPricing} className="hover:text-blue-600 transition-colors">Tarifs</button> <button onClick={onPricing} className="hover:text-blue-600 transition-colors">{t('landing.nav_pricing')}</button>
<a href="#" className="hover:text-blue-600 transition-colors">Blog</a> <a href="#" className="hover:text-blue-600 transition-colors">{t('landing.nav_blog')}</a>
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<button onClick={onLogin} className="text-sm font-bold text-slate-700 hover:text-blue-600 px-4 py-2">Connexion</button> <LanguageSwitcher />
<button onClick={onLogin} className="bg-slate-900 text-white px-5 py-2.5 rounded-full text-sm font-bold hover:bg-blue-600 transition-all shadow-lg hover:shadow-blue-200">Essai Gratuit</button> <button onClick={onLogin} className="hidden sm:block text-sm font-bold text-slate-700 hover:text-blue-600 px-4 py-2">{t('landing.login')}</button>
<button onClick={onLogin} className="bg-slate-900 text-white px-4 sm:px-5 py-2 sm:py-2.5 rounded-full text-xs sm:text-sm font-bold hover:bg-blue-600 transition-all shadow-lg hover:shadow-blue-200">{t('landing.free_trial')}</button>
</div> </div>
</nav> </nav>
{/* Hero Section */} {/* Hero Section */}
<header className="pt-32 pb-20 px-8 max-w-7xl mx-auto text-center"> <header className="pt-32 pb-20 px-4 md:px-8 max-w-7xl mx-auto text-center">
<div className="inline-flex items-center gap-2 bg-white border border-indigo-100 px-4 py-2 rounded-full text-xs font-bold text-blue-600 mb-8 shadow-sm"> <div className="inline-flex items-center gap-2 bg-white border border-indigo-100 px-4 py-2 rounded-full text-xs font-bold text-blue-600 mb-8 shadow-sm">
<Sparkles size={14} className="animate-pulse" /> NOUVEAUTÉ : GÉNÉRATION DE BIBLE DU MONDE PAR IA <Sparkles size={14} className="animate-pulse" /> {t('landing.new_feature')}
</div> </div>
<h1 className="text-5xl md:text-7xl font-black text-slate-900 leading-[1.1] mb-6"> <h1 className="text-4xl md:text-7xl font-black text-slate-900 leading-[1.1] mb-6">
L'écriture d'un roman, <br /> <span dangerouslySetInnerHTML={{ __html: t('landing.hero_title') }}></span>
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-500">augmentée par l'IA.</span> <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-500">{t('landing.hero_subtitle')}</span>
</h1> </h1>
<p className="text-xl text-slate-600 max-w-2xl mx-auto mb-10 leading-relaxed"> <p className="text-lg md:text-xl text-slate-600 max-w-2xl mx-auto mb-10 leading-relaxed">
PlumeIA est le premier éditeur intelligent qui comprend votre univers, vos personnages et votre style pour vous aider à franchir la page blanche. {t('landing.hero_description')}
</p> </p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4"> <div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<button onClick={onLogin} className="w-full sm:w-auto bg-blue-600 text-white px-8 py-4 rounded-full text-lg font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-200 flex items-center gap-2 justify-center"> <button onClick={onLogin} className="w-full sm:w-auto bg-blue-600 text-white px-8 py-4 rounded-full text-lg font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-200 flex items-center gap-2 justify-center">
Commencer mon livre <ArrowRight size={20} /> {t('landing.start_book')} <ArrowRight size={20} />
</button> </button>
<button onClick={onFeatures} className="w-full sm:w-auto bg-white text-slate-900 border border-slate-200 px-8 py-4 rounded-full text-lg font-bold hover:bg-slate-50 transition-all"> <button onClick={onFeatures} className="w-full sm:w-auto bg-white text-slate-900 border border-slate-200 px-8 py-4 rounded-full text-lg font-bold hover:bg-slate-50 transition-all">
Voir la démo {t('landing.see_demo')}
</button> </button>
</div> </div>
@@ -59,17 +65,17 @@ const LandingPage: React.FC<LandingPageProps> = ({ onLogin, onPricing, onFeature
<img <img
src="https://images.unsplash.com/photo-1455390582262-044cdead277a?auto=format&fit=crop&q=80&w=2000" src="https://images.unsplash.com/photo-1455390582262-044cdead277a?auto=format&fit=crop&q=80&w=2000"
alt="Editor Preview" alt="Editor Preview"
className="rounded-xl object-cover h-[500px] w-full" className="rounded-xl object-cover h-64 sm:h-[400px] md:h-[500px] w-full"
/> />
</div> </div>
</div> </div>
</header> </header>
{/* Social Proof */} {/* Social Proof */}
<section className="bg-white py-24 px-8 border-y border-indigo-100"> <section className="bg-white py-16 md:py-24 px-4 md:px-8 border-y border-indigo-100">
<div className="max-w-7xl mx-auto text-center"> <div className="max-w-7xl mx-auto text-center">
<h2 className="text-slate-400 text-sm font-bold uppercase tracking-widest mb-12">Utilisé par les auteurs de demain</h2> <h2 className="text-slate-400 text-sm font-bold uppercase tracking-widest mb-12">{t('landing.used_by')}</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-12 items-center grayscale opacity-60"> <div className="grid grid-cols-2 md:grid-cols-4 gap-8 md:gap-12 items-center grayscale opacity-60">
<span className="text-3xl font-serif font-black italic">FantasyMag</span> <span className="text-3xl font-serif font-black italic">FantasyMag</span>
<span className="text-2xl font-sans font-bold">Writer's Hub</span> <span className="text-2xl font-sans font-bold">Writer's Hub</span>
<span className="text-3xl font-serif">L'Éditeur</span> <span className="text-3xl font-serif">L'Éditeur</span>
@@ -79,13 +85,18 @@ const LandingPage: React.FC<LandingPageProps> = ({ onLogin, onPricing, onFeature
</section> </section>
{/* Footer */} {/* Footer */}
<footer className="bg-slate-900 text-slate-400 py-12 px-8 text-center"> <footer className="bg-slate-900 text-slate-400 py-12 px-4 md:px-8 text-center">
<div className="max-w-7xl mx-auto"> <div className="max-w-7xl mx-auto">
<div className="flex items-center justify-center gap-2 text-white mb-6"> <div className="flex items-center justify-center gap-2 text-white mb-6">
<Book className="text-blue-500" size={24} /> <Book className="text-blue-500" size={24} />
<span className="text-xl font-bold">PlumeIA</span> <span className="text-xl font-bold">Pluume</span>
</div> </div>
<p className="text-sm">© 2024 PlumeIA. Tous droits réservés.</p> <div className="flex flex-wrap items-center justify-center gap-6 mb-8 text-sm">
<Link href="/cgu" className="hover:text-blue-400 transition-colors">{t('footer.cgu')}</Link>
<Link href="/cgv" className="hover:text-blue-400 transition-colors">{t('footer.cgv')}</Link>
<Link href="/sitemap" className="hover:text-blue-400 transition-colors">{t('footer.sitemap')}</Link>
</div>
<p className="text-sm">{t('landing.copyright')}</p>
</div> </div>
</footer> </footer>
</div> </div>

View File

@@ -0,0 +1,67 @@
'use client';
import React, { useState, useRef, useEffect } from 'react';
import { useLanguage } from '@/providers/LanguageProvider';
import { SupportedLanguage } from '@/lib/i18n/translations';
import { Globe, ChevronDown } from 'lucide-react';
const languages: { code: SupportedLanguage; label: string; flag: string }[] = [
{ code: 'fr', label: 'Français', flag: 'https://flagcdn.com/fr.svg' },
{ code: 'en', label: 'English', flag: 'https://flagcdn.com/gb.svg' },
{ code: 'es', label: 'Español', flag: 'https://flagcdn.com/es.svg' },
{ code: 'de', label: 'Deutsch', flag: 'https://flagcdn.com/de.svg' },
];
export const LanguageSwitcher: React.FC = () => {
const { language, setLanguage } = useLanguage();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const currentLang = languages.find(l => l.code === language) || languages[0];
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2 px-3 py-1.5 rounded-lg text-sm font-medium hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-600 dark:text-slate-300 transition-colors"
title="Changer de langue"
>
<img src={currentLang.flag} alt={currentLang.label} className="w-5 h-3.5 object-cover rounded-[2px] shadow-sm" />
<span className="hidden sm:inline font-bold">{currentLang.code.toUpperCase()}</span>
<ChevronDown size={14} className={`transition-transform text-slate-400 ${isOpen ? 'rotate-180' : ''}`} />
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-40 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-xl shadow-lg z-50 overflow-hidden">
<div className="py-1">
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => {
setLanguage(lang.code);
setIsOpen(false);
}}
className={`w-full text-left px-4 py-2 text-sm flex items-center gap-3 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors
${language === lang.code ? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-medium' : 'text-slate-700 dark:text-slate-300'}
`}
>
<img src={lang.flag} alt={lang.label} className="w-5 h-3.5 object-cover rounded-[2px] shadow-sm" />
{lang.label}
</button>
))}
</div>
</div>
)}
</div>
);
};

View File

@@ -3,6 +3,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useAuthContext } from '@/providers/AuthProvider'; import { useAuthContext } from '@/providers/AuthProvider';
import { Loader2, AlertCircle, ArrowRight } from 'lucide-react'; import { Loader2, AlertCircle, ArrowRight } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface LoginPageProps { interface LoginPageProps {
onSuccess: () => void; onSuccess: () => void;
@@ -17,6 +18,7 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
// Use the global auth context // Use the global auth context
const { login } = useAuthContext(); const { login } = useAuthContext();
const { t } = useLanguage();
async function handleSubmit(e: React.FormEvent) { async function handleSubmit(e: React.FormEvent) {
e.preventDefault(); e.preventDefault();
@@ -35,10 +37,10 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
return ( return (
<div className="min-h-screen bg-slate-50 flex overflow-hidden font-sans text-slate-900 items-center justify-center p-4"> <div className="min-h-screen bg-slate-50 flex overflow-hidden font-sans text-slate-900 items-center justify-center p-4">
{/* Using styles similar to AuthPage for consistency */} {/* Using styles similar to AuthPage for consistency */}
<div className="w-full max-w-md bg-white rounded-2xl shadow-xl overflow-hidden p-8 animate-in fade-in zoom-in duration-300"> <div className="w-full max-w-md bg-white rounded-2xl shadow-xl overflow-hidden p-6 md:p-8 animate-in fade-in zoom-in duration-300">
<div className="text-center mb-8"> <div className="text-center mb-8">
<h1 className="text-3xl font-black text-slate-900 mb-2">Connexion</h1> <h1 className="text-3xl font-black text-slate-900 mb-2">{t('auth.login_title')}</h1>
<p className="text-slate-500">Bienvenue ! Connectez-vous à votre compte</p> <p className="text-slate-500">{t('auth.login_subtitle')}</p>
</div> </div>
{error && ( {error && (
@@ -50,12 +52,12 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1" htmlFor="email">Email</label> <label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1" htmlFor="email">{t('auth.email')}</label>
<input <input
id="email" id="email"
type="email" type="email"
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium transition-all" className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium transition-all"
placeholder="votre@email.com" placeholder={t('auth.email_placeholder')}
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
required required
@@ -63,12 +65,12 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1" htmlFor="password">Mot de passe</label> <label className="text-xs font-black text-slate-500 uppercase tracking-widest ml-1" htmlFor="password">{t('auth.password')}</label>
<input <input
id="password" id="password"
type="password" type="password"
className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium transition-all" className="w-full px-4 py-3 bg-slate-50 border border-slate-200 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-medium transition-all"
placeholder="••••••••" placeholder={t('auth.password_placeholder')}
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
required required
@@ -80,17 +82,17 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
className="w-full bg-slate-900 text-white py-4 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-blue-600 transition-all shadow-xl disabled:opacity-50 mt-6" className="w-full bg-slate-900 text-white py-4 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-blue-600 transition-all shadow-xl disabled:opacity-50 mt-6"
disabled={loading} disabled={loading}
> >
{loading ? <Loader2 className="animate-spin" /> : "Se connecter"} <ArrowRight size={18} /> {loading ? <Loader2 className="animate-spin" /> : t('auth.login_submit')} <ArrowRight size={18} />
</button> </button>
</form> </form>
<div className="mt-8 text-center text-sm text-slate-500"> <div className="mt-8 text-center text-sm text-slate-500">
Pas encore de compte ?{" "} {t('auth.not_registered')}{" "}
<button <button
onClick={onRegister} onClick={onRegister}
className="font-bold text-blue-600 hover:text-blue-800 transition-colors ml-1" className="font-bold text-blue-600 hover:text-blue-800 transition-colors ml-1"
> >
Créer un compte {t('auth.create_account')}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -3,6 +3,9 @@
import React from 'react'; import React from 'react';
import { Check, ArrowLeft } from 'lucide-react'; import { Check, ArrowLeft } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import Link from 'next/link';
interface PlanData { interface PlanData {
id: string; id: string;
@@ -23,23 +26,28 @@ interface PricingProps {
} }
const Pricing: React.FC<PricingProps> = ({ plans, currentPlan, onBack, onSelectPlan, isLoading }) => { const Pricing: React.FC<PricingProps> = ({ plans, currentPlan, onBack, onSelectPlan, isLoading }) => {
const { t } = useLanguage();
return ( return (
<div className="min-h-screen bg-[#eef2ff] py-20 px-8"> <div className="min-h-screen bg-[#eef2ff] py-20 px-4 md:px-8">
<div className="max-w-6xl mx-auto"> <div className="max-w-6xl mx-auto">
<button onClick={onBack} className="flex items-center gap-2 text-slate-500 hover:text-blue-600 mb-12 font-bold transition-colors"> <div className="flex justify-between items-center mb-12">
<ArrowLeft size={20} /> Retour <button onClick={onBack} className="flex items-center gap-2 text-slate-500 hover:text-blue-600 font-bold transition-colors">
<ArrowLeft size={20} /> {t('common.back')}
</button> </button>
<LanguageSwitcher />
</div>
<div className="text-center mb-16"> <div className="text-center mb-16">
<h2 className="text-4xl font-black text-slate-900 mb-4">Choisissez votre destin d'écrivain.</h2> <h2 className="text-3xl md:text-4xl font-black text-slate-900 mb-4">{t('pricing.title')}</h2>
<p className="text-slate-500">Passez au plan supérieur pour libérer toute la puissance de l'IA.</p> <p className="text-slate-500">{t('pricing.subtitle')}</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8"> <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{isLoading && <p className="text-center col-span-3 py-10">Chargement des offres...</p>} {isLoading && <p className="text-center col-span-3 py-10">{t('pricing.loading')}</p>}
{!isLoading && plans.map((p) => ( {!isLoading && plans.map((p) => (
<div key={p.id} className={`bg-white rounded-3xl p-8 border transition-all ${p.isPopular ? 'border-blue-500 shadow-2xl scale-105 z-10' : 'border-indigo-100 shadow-xl'}`}> <div key={p.id} className={`bg-white rounded-3xl p-8 border transition-all ${p.isPopular ? 'border-blue-500 shadow-2xl scale-105 z-10' : 'border-indigo-100 shadow-xl'}`}>
<div className="mb-8"> <div className="mb-8">
<h4 className="text-xl font-bold text-slate-900 mb-2">{p.displayName}</h4> <h4 className="text-xl font-bold text-slate-900 mb-2">{p.displayName}</h4>
<div className="text-4xl font-black text-slate-900 mb-2">{p.price}<span className="text-sm font-normal text-slate-400">/mois</span></div> <div className="text-4xl font-black text-slate-900 mb-2">{p.price}<span className="text-sm font-normal text-slate-400">{t('pricing.per_month')}</span></div>
<p className="text-sm text-slate-500">{p.description}</p> <p className="text-sm text-slate-500">{p.description}</p>
</div> </div>
<ul className="space-y-4 mb-10"> <ul className="space-y-4 mb-10">
@@ -54,12 +62,23 @@ const Pricing: React.FC<PricingProps> = ({ plans, currentPlan, onBack, onSelectP
onClick={() => onSelectPlan(p.id)} onClick={() => onSelectPlan(p.id)}
className={`w-full py-4 rounded-2xl font-black transition-all ${p.id === currentPlan ? 'bg-slate-100 text-slate-400 cursor-default' : p.isPopular ? 'bg-blue-600 text-white hover:bg-blue-700' : 'bg-slate-900 text-white hover:bg-slate-800'}`} className={`w-full py-4 rounded-2xl font-black transition-all ${p.id === currentPlan ? 'bg-slate-100 text-slate-400 cursor-default' : p.isPopular ? 'bg-blue-600 text-white hover:bg-blue-700' : 'bg-slate-900 text-white hover:bg-slate-800'}`}
> >
{p.id === currentPlan ? 'Plan Actuel' : 'Sélectionner'} {p.id === currentPlan ? t('pricing.current_plan') : t('pricing.select')}
</button> </button>
</div> </div>
))} ))}
</div> </div>
</div> </div>
{/* Footer */}
<footer className="mt-20 text-center border-t border-indigo-100 pt-12 relative z-20">
<div className="max-w-6xl mx-auto">
<div className="flex flex-wrap items-center justify-center gap-6 mb-8 text-sm">
<Link href="/cgu" className="text-slate-500 hover:text-blue-600 font-medium transition-colors">{t('footer.cgu')}</Link>
<Link href="/cgv" className="text-slate-500 hover:text-blue-600 font-medium transition-colors">{t('footer.cgv')}</Link>
<Link href="/sitemap" className="text-slate-500 hover:text-blue-600 font-medium transition-colors">{t('footer.sitemap')}</Link>
</div>
<p className="text-sm text-slate-400">{t('landing.copyright')}</p>
</div>
</footer>
</div> </div>
); );
}; };

View File

@@ -4,6 +4,7 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { WorkflowData, PlotNode, PlotConnection, PlotNodeType, Entity, EntityType } from '@/lib/types'; import { WorkflowData, PlotNode, PlotConnection, PlotNodeType, Entity, EntityType } from '@/lib/types';
import { Plus, Trash2, ArrowRight, BookOpen, MessageCircle, Zap, Palette, Save, Link2 } from 'lucide-react'; import { Plus, Trash2, ArrowRight, BookOpen, MessageCircle, Zap, Palette, Save, Link2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface StoryWorkflowProps { interface StoryWorkflowProps {
data: WorkflowData; data: WorkflowData;
@@ -24,8 +25,8 @@ const INITIAL_COLORS = [
'#f3e8ff', // Purple '#f3e8ff', // Purple
]; ];
const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id: string) => void) => { const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id: string) => void, t: any) => {
if (!text) return <span className="text-slate-400 italic">Description...</span>; if (!text) return <span className="text-slate-400 italic">{t('sw.desc_ph')}</span>;
const parts: (string | React.ReactNode)[] = [text]; const parts: (string | React.ReactNode)[] = [text];
@@ -45,7 +46,7 @@ const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id:
key={`${entity.id}-${idx}`} key={`${entity.id}-${idx}`}
onClick={(e) => { e.stopPropagation(); onNavigate(entity.id); }} onClick={(e) => { e.stopPropagation(); onNavigate(entity.id); }}
className="text-indigo-600 hover:text-indigo-800 underline decoration-indigo-300 hover:decoration-indigo-600 cursor-pointer font-medium bg-indigo-50 px-0.5 rounded transition-all" className="text-indigo-600 hover:text-indigo-800 underline decoration-indigo-300 hover:decoration-indigo-600 cursor-pointer font-medium bg-indigo-50 px-0.5 rounded transition-all"
title={`Voir la fiche de ${entity.name}`} title={t('sw.see_sheet') + entity.name}
> >
{s} {s}
</span> </span>
@@ -92,12 +93,12 @@ const StoryNode = React.memo(({
onToggleColorPicker, onSaveColor, onNavigateToEntity, onToggleColorPicker, onSaveColor, onNavigateToEntity,
onInputFocus, onInputCheckAutocomplete, onKeyDownInInput onInputFocus, onInputCheckAutocomplete, onKeyDownInInput
}: StoryNodeProps) => { }: StoryNodeProps) => {
const { t } = useLanguage();
const [showTypePicker, setShowTypePicker] = useState(false); const [showTypePicker, setShowTypePicker] = useState(false);
const richDescription = useMemo(() => { const richDescription = useMemo(() => {
return renderTextWithLinks(node.description, entities, onNavigateToEntity); return renderTextWithLinks(node.description, entities, onNavigateToEntity, t);
}, [node.description, entities, onNavigateToEntity]); }, [node.description, entities, onNavigateToEntity, t]);
return ( return (
<div <div
@@ -170,7 +171,7 @@ const StoryNode = React.memo(({
onClick={() => onSaveColor(node.color || '#ffffff')} onClick={() => onSaveColor(node.color || '#ffffff')}
className="text-[10px] font-bold text-indigo-600 hover:text-indigo-800 hover:underline flex-1 text-right" className="text-[10px] font-bold text-indigo-600 hover:text-indigo-800 hover:underline flex-1 text-right"
> >
+ SAUVER {t('sw.save_color')}
</button> </button>
</div> </div>
</div> </div>
@@ -181,7 +182,7 @@ const StoryNode = React.memo(({
{isEditing ? ( {isEditing ? (
<textarea <textarea
className={`w-full h-full bg-white/70 resize-none outline-none text-xs leading-relaxed p-2 rounded border border-indigo-100 shadow-inner ${node.type === 'dialogue' ? 'font-mono text-slate-700' : 'text-slate-600'}`} className={`w-full h-full bg-white/70 resize-none outline-none text-xs leading-relaxed p-2 rounded border border-indigo-100 shadow-inner ${node.type === 'dialogue' ? 'font-mono text-slate-700' : 'text-slate-600'}`}
placeholder={node.type === 'dialogue' ? "Héros: Salut !\nGuide: ..." : "Résumé de l'intrigue..."} placeholder={node.type === 'dialogue' ? t('sw.dialogue_ph') : t('sw.plot_ph')}
value={node.description} value={node.description}
onChange={(e) => onInputCheckAutocomplete(e, node.id, 'description')} onChange={(e) => onInputCheckAutocomplete(e, node.id, 'description')}
onKeyDown={(e) => onKeyDownInInput(e, node.id)} onKeyDown={(e) => onKeyDownInInput(e, node.id)}
@@ -204,21 +205,21 @@ const StoryNode = React.memo(({
<button <button
onClick={(e) => { e.stopPropagation(); onUpdate(node.id, { type: 'story' }); setShowTypePicker(false); }} onClick={(e) => { e.stopPropagation(); onUpdate(node.id, { type: 'story' }); setShowTypePicker(false); }}
className={`p-1.5 rounded hover:bg-slate-100 ${node.type === 'story' ? 'bg-indigo-50 ring-1 ring-indigo-200' : ''}`} className={`p-1.5 rounded hover:bg-slate-100 ${node.type === 'story' ? 'bg-indigo-50 ring-1 ring-indigo-200' : ''}`}
title="Narration" title={t('sw.type_story')}
> >
<BookOpen size={14} className="text-slate-500" /> <BookOpen size={14} className="text-slate-500" />
</button> </button>
<button <button
onClick={(e) => { e.stopPropagation(); onUpdate(node.id, { type: 'action' }); setShowTypePicker(false); }} onClick={(e) => { e.stopPropagation(); onUpdate(node.id, { type: 'action' }); setShowTypePicker(false); }}
className={`p-1.5 rounded hover:bg-amber-50 ${node.type === 'action' ? 'bg-amber-50 ring-1 ring-amber-200' : ''}`} className={`p-1.5 rounded hover:bg-amber-50 ${node.type === 'action' ? 'bg-amber-50 ring-1 ring-amber-200' : ''}`}
title="Action" title={t('sw.type_action')}
> >
<Zap size={14} className="text-amber-500" /> <Zap size={14} className="text-amber-500" />
</button> </button>
<button <button
onClick={(e) => { e.stopPropagation(); onUpdate(node.id, { type: 'dialogue' }); setShowTypePicker(false); }} onClick={(e) => { e.stopPropagation(); onUpdate(node.id, { type: 'dialogue' }); setShowTypePicker(false); }}
className={`p-1.5 rounded hover:bg-blue-50 ${node.type === 'dialogue' ? 'bg-blue-50 ring-1 ring-blue-200' : ''}`} className={`p-1.5 rounded hover:bg-blue-50 ${node.type === 'dialogue' ? 'bg-blue-50 ring-1 ring-blue-200' : ''}`}
title="Dialogue" title={t('sw.type_dialogue')}
> >
<MessageCircle size={14} className="text-blue-500" /> <MessageCircle size={14} className="text-blue-500" />
</button> </button>
@@ -267,6 +268,7 @@ interface SuggestionState {
} }
const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities, onNavigateToEntity }) => { const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities, onNavigateToEntity }) => {
const { t } = useLanguage();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const rafRef = useRef<number | null>(null); const rafRef = useRef<number | null>(null);
@@ -569,7 +571,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
id: `node-${Date.now()}`, id: `node-${Date.now()}`,
x, x,
y, y,
title: 'Nouvel événement', title: t('sw.new_event'),
description: '', description: '',
color: INITIAL_COLORS[0], color: INITIAL_COLORS[0],
type: 'story' type: 'story'
@@ -598,7 +600,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
id: `node-${Date.now()}`, id: `node-${Date.now()}`,
x: scrollLeft + clientWidth / 2 - CARD_WIDTH / 2, x: scrollLeft + clientWidth / 2 - CARD_WIDTH / 2,
y: scrollTop + clientHeight / 2 - CARD_HEIGHT / 2, y: scrollTop + clientHeight / 2 - CARD_HEIGHT / 2,
title: 'Nouveau point d\'intrigue', title: t('sw.new_plot_point'),
description: '', description: '',
color: INITIAL_COLORS[0], color: INITIAL_COLORS[0],
type: 'story' type: 'story'
@@ -613,15 +615,15 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
<div className="h-12 bg-theme-panel border-b border-theme-border flex items-center justify-between px-4 z-10 shadow-sm shrink-0 transition-colors duration-300"> <div className="h-12 bg-theme-panel border-b border-theme-border flex items-center justify-between px-4 z-10 shadow-sm shrink-0 transition-colors duration-300">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button onClick={handleAddNodeCenter} className="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-xs font-bold transition-all shadow-md shadow-indigo-100"> <button onClick={handleAddNodeCenter} className="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 text-xs font-bold transition-all shadow-md shadow-indigo-100">
<Plus size={14} /> AJOUTER NŒUD <Plus size={14} /> {t('sw.add_node')}
</button> </button>
<div className="w-px h-6 bg-theme-border mx-2" /> <div className="w-px h-6 bg-theme-border mx-2" />
<div className="text-[10px] uppercase font-bold text-theme-muted tracking-wider"> <div className="text-[10px] uppercase font-bold text-theme-muted tracking-wider">
{selectedNodeIds.size > 0 ? `${selectedNodeIds.size} SÉLECTIONNÉ(S)` : 'Double-cliquez sur le canvas pour créer'} {selectedNodeIds.size > 0 ? `${selectedNodeIds.size} ${t('sw.selected')}` : t('sw.double_click_create')}
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button onClick={handleDeleteSelected} disabled={selectedNodeIds.size === 0} className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg disabled:opacity-30 transition-colors" title="Supprimer"> <button onClick={handleDeleteSelected} disabled={selectedNodeIds.size === 0} className="p-2 text-red-500 hover:bg-red-500/10 rounded-lg disabled:opacity-30 transition-colors" title={t('sw.delete')}>
<Trash2 size={16} /> <Trash2 size={16} />
</button> </button>
</div> </div>
@@ -698,7 +700,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
{activeSuggestion && ( {activeSuggestion && (
<div className="fixed z-50 bg-white rounded-xl shadow-2xl border border-indigo-100 w-64 max-h-48 overflow-y-auto" style={{ left: '50%', top: '50%', transform: 'translate(-50%, -50%)' }}> <div className="fixed z-50 bg-white rounded-xl shadow-2xl border border-indigo-100 w-64 max-h-48 overflow-y-auto" style={{ left: '50%', top: '50%', transform: 'translate(-50%, -50%)' }}>
<div className="px-3 py-2 bg-indigo-600 text-white text-[10px] font-black uppercase tracking-widest"> <div className="px-3 py-2 bg-indigo-600 text-white text-[10px] font-black uppercase tracking-widest">
Insérer {activeSuggestion.trigger === '@' ? 'Personnage' : activeSuggestion.trigger === '#' ? 'Lieu' : 'Objet'} {activeSuggestion.trigger === '@' ? t('sw.insert_char') : activeSuggestion.trigger === '#' ? t('sw.insert_loc') : t('sw.insert_obj')}
</div> </div>
<div className="divide-y divide-slate-50"> <div className="divide-y divide-slate-50">
{activeSuggestion.filteredEntities.length > 0 ? ( {activeSuggestion.filteredEntities.length > 0 ? (
@@ -712,7 +714,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
</button> </button>
)) ))
) : ( ) : (
<div className="p-4 text-xs text-slate-400 italic text-center">Aucun résultat</div> <div className="p-4 text-xs text-slate-400 italic text-center">{t('sw.no_result')}</div>
)} )}
</div> </div>
</div> </div>

View File

@@ -4,6 +4,8 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { UserProfile, UserPreferences } from '@/lib/types'; import { UserProfile, UserPreferences } from '@/lib/types';
import { User, Settings, Globe, Shield, Bell, Save, Camera, Target, Flame, Layout } from 'lucide-react'; import { User, Settings, Globe, Shield, Bell, Save, Camera, Target, Flame, Layout } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
interface UserProfileSettingsProps { interface UserProfileSettingsProps {
user: UserProfile; user: UserProfile;
@@ -20,6 +22,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
hasOnBack: !!onBack hasOnBack: !!onBack
}); });
const { t } = useLanguage();
const [activeTab, setActiveTab] = useState<'profile' | 'preferences' | 'account'>('profile'); const [activeTab, setActiveTab] = useState<'profile' | 'preferences' | 'account'>('profile');
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: user.name, name: user.name,
@@ -36,7 +39,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
const file = event.target.files?.[0]; const file = event.target.files?.[0];
if (!file) return; if (!file) return;
if (!file.type.startsWith('image/')) { if (!file.type.startsWith('image/')) {
alert('Veuillez sélectionner une image valide.'); alert(t('profile.invalid_image') || 'Veuillez sélectionner une image valide.');
return; return;
} }
@@ -83,7 +86,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
dailyWordGoal: formData.dailyWordGoal dailyWordGoal: formData.dailyWordGoal
} }
}); });
alert("Profil mis à jour !"); alert(t('profile.saved_success') || "Profil mis à jour !");
}; };
const isDark = formData.theme === 'dark'; const isDark = formData.theme === 'dark';
@@ -102,10 +105,13 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="max-w-4xl mx-auto"> <div className="max-w-4xl mx-auto">
<div className="flex justify-between items-center mb-10"> <div className="flex justify-between items-center mb-10">
<div> <div>
<h1 className={`text-3xl font-black ${themeTextHeading}`}>Mon Compte</h1> <h1 className={`text-3xl font-black ${themeTextHeading}`}>{t('profile.title')}</h1>
<p className={themeTextMuted}>Gérez vos informations personnelles et préférences d'écriture.</p> <p className={themeTextMuted}>{t('profile.subtitle')}</p>
</div>
<div className="flex items-center gap-4">
<LanguageSwitcher />
<button onClick={onBack} className={`${themeInnerClass} px-4 py-2 rounded-lg text-sm font-bold opacity-80 hover:opacity-100 transition-opacity`}>{t('profile.close')}</button>
</div> </div>
<button onClick={onBack} className={`${themeInnerClass} px-4 py-2 rounded-lg text-sm font-bold opacity-80 hover:opacity-100 transition-opacity`}>Fermer</button>
</div> </div>
<div className="flex flex-col md:flex-row gap-8"> <div className="flex flex-col md:flex-row gap-8">
@@ -115,19 +121,19 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
onClick={() => setActiveTab('profile')} onClick={() => setActiveTab('profile')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all ${activeTab === 'profile' ? themeTabActive : themeTabInactive}`} className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all ${activeTab === 'profile' ? themeTabActive : themeTabInactive}`}
> >
<User size={18} /> Profil Public <User size={18} /> {t('profile.tab_public')}
</button> </button>
<button <button
onClick={() => setActiveTab('preferences')} onClick={() => setActiveTab('preferences')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all ${activeTab === 'preferences' ? themeTabActive : themeTabInactive}`} className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all ${activeTab === 'preferences' ? themeTabActive : themeTabInactive}`}
> >
<Layout size={18} /> Interface & Écriture <Layout size={18} /> {t('profile.tab_interface')}
</button> </button>
<button <button
onClick={() => setActiveTab('account')} onClick={() => setActiveTab('account')}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all ${activeTab === 'account' ? themeTabActive : themeTabInactive}`} className={`w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-bold transition-all ${activeTab === 'account' ? themeTabActive : themeTabInactive}`}
> >
<Shield size={18} /> Sécurité & Plan <Shield size={18} /> {t('profile.tab_security')}
</button> </button>
</div> </div>
@@ -145,16 +151,16 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
className="hidden" className="hidden"
/> />
<img src={formData.avatar || 'https://via.placeholder.com/150'} className={`w-24 h-24 rounded-full object-cover border-4 shadow-md ${isDark ? 'border-slate-800' : isSepia ? 'border-[#f4ecd8]' : 'border-slate-50'}`} alt="Avatar" /> <img src={formData.avatar || 'https://via.placeholder.com/150'} className={`w-24 h-24 rounded-full object-cover border-4 shadow-md ${isDark ? 'border-slate-800' : isSepia ? 'border-[#f4ecd8]' : 'border-slate-50'}`} alt="Avatar" />
<div className="absolute inset-0 bg-black/40 text-white rounded-full opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity" title="Changer d'avatar"> <div className="absolute inset-0 bg-black/40 text-white rounded-full opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity" title={t('profile.change_avatar')}>
<Camera size={20} /> <Camera size={20} />
</div> </div>
</div> </div>
<div> <div>
<h3 className={`font-bold text-lg ${themeTextHeading}`}>{user.name}</h3> <h3 className={`font-bold text-lg ${themeTextHeading}`}>{user.name}</h3>
<p className={`text-sm ${themeTextMuted}`}>Membre depuis {new Date(user.subscription.startDate).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}</p> <p className={`text-sm ${themeTextMuted}`}>{t('dashboard.member_since')} {new Date(user.subscription.startDate).toLocaleDateString('fr-FR', { month: 'long', year: 'numeric' })}</p>
<div className="mt-2 flex gap-4"> <div className="mt-2 flex gap-4">
<div className="flex items-center gap-1.5 text-xs font-bold text-orange-500"> <div className="flex items-center gap-1.5 text-xs font-bold text-orange-500">
<Flame size={14} fill="currentColor" /> {user.stats.writingStreak} jours de streak <Flame size={14} fill="currentColor" /> {user.stats.writingStreak} {t('dashboard.days')}
</div> </div>
</div> </div>
</div> </div>
@@ -162,7 +168,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="grid grid-cols-1 gap-6"> <div className="grid grid-cols-1 gap-6">
<div className="space-y-1"> <div className="space-y-1">
<label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>Nom affiché</label> <label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>{t('profile.display_name')}</label>
<input <input
type="text" type="text"
value={formData.name} value={formData.name}
@@ -172,12 +178,12 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>Bio / Citation inspirante</label> <label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>{t('profile.bio')}</label>
<textarea <textarea
value={formData.bio} value={formData.bio}
onChange={(e) => setFormData({ ...formData, bio: e.target.value })} onChange={(e) => setFormData({ ...formData, bio: e.target.value })}
className={`w-full p-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 h-24 resize-none ${themeInputBg}`} className={`w-full p-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 h-24 resize-none ${themeInputBg}`}
placeholder="Partagez quelques mots sur votre style..." placeholder={t('profile.bio_placeholder')}
/> />
</div> </div>
</div> </div>
@@ -189,7 +195,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="grid grid-cols-1 gap-8"> <div className="grid grid-cols-1 gap-8">
<div className="space-y-3"> <div className="space-y-3">
<label className={`text-xs font-black uppercase tracking-widest flex items-center gap-2 ${themeTextMuted}`}> <label className={`text-xs font-black uppercase tracking-widest flex items-center gap-2 ${themeTextMuted}`}>
<Target size={14} /> Objectif quotidien de mots <Target size={14} /> {t('profile.daily_goal')}
</label> </label>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<input <input
@@ -204,7 +210,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="space-y-3"> <div className="space-y-3">
<label className={`text-xs font-black uppercase tracking-widest flex items-center gap-2 ${themeTextMuted}`}> <label className={`text-xs font-black uppercase tracking-widest flex items-center gap-2 ${themeTextMuted}`}>
Thème de l'éditeur {t('profile.editor_theme')}
</label> </label>
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
{['light', 'sepia', 'dark'].map((t) => ( {['light', 'sepia', 'dark'].map((t) => (
@@ -227,14 +233,14 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-300"> <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-300">
<div className="p-4 bg-blue-50 border border-blue-100 rounded-xl flex justify-between items-center"> <div className="p-4 bg-blue-50 border border-blue-100 rounded-xl flex justify-between items-center">
<div> <div>
<h4 className="font-bold text-blue-900">Plan {(user.subscription.planDetails?.displayName || user.subscription.plan).toUpperCase()}</h4> <h4 className="font-bold text-blue-900">{t('profile.plan_title')} {(user.subscription.planDetails?.displayName || user.subscription.plan).toUpperCase()}</h4>
<p className="text-xs text-blue-700">Abonnement actif</p> <p className="text-xs text-blue-700">{t('profile.active_sub')}</p>
</div> </div>
<button className="bg-blue-600 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-blue-700 shadow-md shadow-blue-200">Gérer</button> <button className="bg-blue-600 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-blue-700 shadow-md shadow-blue-200">{t('profile.manage')}</button>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>Email du compte</label> <label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>{t('profile.account_email')}</label>
<input <input
type="email" type="email"
value={formData.email} value={formData.email}
@@ -244,7 +250,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
</div> </div>
<div className="pt-4"> <div className="pt-4">
<button className="text-red-500 text-sm font-bold hover:underline">Supprimer mon compte définitivement</button> <button className="text-red-500 text-sm font-bold hover:underline">{t('profile.delete_account')}</button>
</div> </div>
</div> </div>
)} )}
@@ -254,7 +260,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
onClick={handleSave} onClick={handleSave}
className={`px-8 py-3 rounded-xl font-bold flex items-center gap-2 transition-all shadow-xl hover:shadow-blue-200 ${isDark ? 'bg-white text-slate-900 hover:bg-blue-500 hover:text-white' : isSepia ? 'bg-[#5c4731] text-white hover:bg-blue-600' : 'bg-slate-900 text-white hover:bg-blue-600'}`} className={`px-8 py-3 rounded-xl font-bold flex items-center gap-2 transition-all shadow-xl hover:shadow-blue-200 ${isDark ? 'bg-white text-slate-900 hover:bg-blue-500 hover:text-white' : isSepia ? 'bg-[#5c4731] text-white hover:bg-blue-600' : 'bg-slate-900 text-white hover:bg-blue-600'}`}
> >
<Save size={18} /> Sauvegarder les modifications <Save size={18} /> {t('profile.save_changes')}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import React, { useState, useMemo, useEffect } from 'react'; import React, { useState, useMemo, useEffect } from 'react';
import { useLanguage } from '@/providers/LanguageProvider';
import { Entity, EntityType, CharacterAttributes, EntityTemplate, CustomFieldDefinition, CustomFieldType } from '@/lib/types'; import { Entity, EntityType, CharacterAttributes, EntityTemplate, CustomFieldDefinition, CustomFieldType } from '@/lib/types';
import { Plus, Trash2, Save, X, Sparkles, User, Activity, Brain, Ruler, Settings, Layout, List, ToggleLeft } from 'lucide-react'; import { Plus, Trash2, Save, X, Sparkles, User, Activity, Brain, Ruler, Settings, Layout, List, ToggleLeft } from 'lucide-react';
import { ENTITY_ICONS, ENTITY_COLORS, HAIR_COLORS, EYE_COLORS, ARCHETYPES } from '@/lib/constants'; import { ENTITY_ICONS, ENTITY_COLORS, HAIR_COLORS, EYE_COLORS, ARCHETYPES } from '@/lib/constants';
@@ -32,6 +33,7 @@ const DEFAULT_CHAR_ATTRIBUTES: CharacterAttributes = {
}; };
const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdate, onDelete, templates, onUpdateTemplates, initialSelectedId }) => { const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdate, onDelete, templates, onUpdateTemplates, initialSelectedId }) => {
const { t } = useLanguage();
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState<string | null>(null);
const [tempEntity, setTempEntity] = useState<Entity | null>(null); const [tempEntity, setTempEntity] = useState<Entity | null>(null);
const [mode, setMode] = useState<'entities' | 'templates'>('entities'); const [mode, setMode] = useState<'entities' | 'templates'>('entities');
@@ -102,7 +104,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
}; };
const handleDelete = (id: string) => { const handleDelete = (id: string) => {
if (confirm('Supprimer cet élément ?')) { if (confirm(t('wb.delete_confirm'))) {
onDelete(id); onDelete(id);
if (editingId === id) { if (editingId === id) {
setEditingId(null); setEditingId(null);
@@ -149,7 +151,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
const addCustomField = (type: EntityType) => { const addCustomField = (type: EntityType) => {
const newField: CustomFieldDefinition = { const newField: CustomFieldDefinition = {
id: `field-${Date.now()}`, id: `field-${Date.now()}`,
label: 'Nouveau Champ', label: t('wb.new_field'),
type: 'text', type: 'text',
placeholder: '' placeholder: ''
}; };
@@ -209,31 +211,31 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
{/* SECTION 1: ROLE & ARCHETYPE */} {/* SECTION 1: ROLE & ARCHETYPE */}
<div className="bg-theme-bg p-4 rounded-lg border border-theme-border"> <div className="bg-theme-bg p-4 rounded-lg border border-theme-border">
<h3 className="text-sm font-bold text-theme-text uppercase mb-4 flex items-center gap-2"> <h3 className="text-sm font-bold text-theme-text uppercase mb-4 flex items-center gap-2">
<User size={16} /> Identité Narrative <User size={16} /> {t('wb.id_narrative')}
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div> <div>
<label className="block text-xs font-semibold text-theme-muted mb-2">Archétype</label> <label className="block text-xs font-semibold text-theme-muted mb-2">{t('wb.archetype')}</label>
<input <input
type="text" type="text"
list="archetype-suggestions" list="archetype-suggestions"
value={attrs.archetype} value={attrs.archetype}
onChange={(e) => updateAttribute('archetype', e.target.value)} onChange={(e) => updateAttribute('archetype', e.target.value)}
className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm outline-none focus:border-blue-500" className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm outline-none focus:border-blue-500"
placeholder="Ex: Le Héros, Le Sage..." placeholder={t('wb.archetype_ph')}
/> />
<datalist id="archetype-suggestions"> <datalist id="archetype-suggestions">
{allArchetypes.map(a => <option key={a} value={a} />)} {allArchetypes.map(a => <option key={a} value={a} />)}
</datalist> </datalist>
</div> </div>
<div> <div>
<label className="block text-xs font-semibold text-theme-muted mb-2">Rôle dans l'histoire</label> <label className="block text-xs font-semibold text-theme-muted mb-2">{t('wb.role')}</label>
<div className="flex gap-2 flex-wrap"> <div className="flex gap-2 flex-wrap">
{[ {[
{ val: 'protagonist', label: 'Protagoniste' }, { val: 'protagonist', label: t('wb.role_protagonist') },
{ val: 'antagonist', label: 'Antagoniste' }, { val: 'antagonist', label: t('wb.role_antagonist') },
{ val: 'support', label: 'Secondaire' }, { val: 'support', label: t('wb.role_support') },
{ val: 'extra', label: 'Figurant' } { val: 'extra', label: t('wb.role_extra') }
].map(opt => ( ].map(opt => (
<label key={opt.val} className={`cursor-pointer px-3 py-1.5 rounded text-xs border transition-colors ${attrs.role === opt.val ? 'bg-indigo-100 border-indigo-300 text-indigo-700 font-bold' : 'bg-theme-bg border-theme-border text-theme-muted hover:bg-theme-border'}`}> <label key={opt.val} className={`cursor-pointer px-3 py-1.5 rounded text-xs border transition-colors ${attrs.role === opt.val ? 'bg-indigo-100 border-indigo-300 text-indigo-700 font-bold' : 'bg-theme-bg border-theme-border text-theme-muted hover:bg-theme-border'}`}>
<input <input
@@ -255,13 +257,13 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
{/* SECTION 2: PHYSIQUE */} {/* SECTION 2: PHYSIQUE */}
<div className="bg-theme-bg p-4 rounded-lg border border-theme-border"> <div className="bg-theme-bg p-4 rounded-lg border border-theme-border">
<h3 className="text-sm font-bold text-theme-text uppercase mb-4 flex items-center gap-2"> <h3 className="text-sm font-bold text-theme-text uppercase mb-4 flex items-center gap-2">
<Ruler size={16} /> Apparence Physique <Ruler size={16} /> {t('wb.appearance')}
</h3> </h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-6"> <div className="space-y-6">
<div> <div>
<div className="flex justify-between text-xs mb-1"> <div className="flex justify-between text-xs mb-1">
<label className="font-semibold text-theme-muted">Âge (ans)</label> <label className="font-semibold text-theme-muted">{t('wb.age')}</label>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<input <input
@@ -281,7 +283,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div> <div>
<div className="flex justify-between text-xs mb-1"> <div className="flex justify-between text-xs mb-1">
<label className="font-semibold text-theme-muted">Taille (cm)</label> <label className="font-semibold text-theme-muted">{t('wb.height')}</label>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<input <input
@@ -303,7 +305,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="space-y-4"> <div className="space-y-4">
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-semibold text-theme-muted mb-1">Cheveux</label> <label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.hair')}</label>
<select <select
value={attrs.hair} value={attrs.hair}
onChange={(e) => updateAttribute('hair', e.target.value)} onChange={(e) => updateAttribute('hair', e.target.value)}
@@ -313,7 +315,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</select> </select>
</div> </div>
<div> <div>
<label className="block text-xs font-semibold text-theme-muted mb-1">Yeux</label> <label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.eyes')}</label>
<select <select
value={attrs.eyes} value={attrs.eyes}
onChange={(e) => updateAttribute('eyes', e.target.value)} onChange={(e) => updateAttribute('eyes', e.target.value)}
@@ -324,12 +326,12 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div> </div>
</div> </div>
<div> <div>
<label className="block text-xs font-semibold text-theme-muted mb-1">Signe distinctif</label> <label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.physical_quirk')}</label>
<input <input
type="text" type="text"
value={attrs.physicalQuirk} value={attrs.physicalQuirk}
onChange={(e) => updateAttribute('physicalQuirk', e.target.value)} onChange={(e) => updateAttribute('physicalQuirk', e.target.value)}
placeholder="Cicatrice, tatouage..." placeholder={t('wb.physical_quirk_ph')}
className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm outline-none focus:border-indigo-400" className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm outline-none focus:border-indigo-400"
/> />
</div> </div>
@@ -340,15 +342,15 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
{/* SECTION 3: PSYCHOLOGIE */} {/* SECTION 3: PSYCHOLOGIE */}
<div className="bg-theme-bg p-4 rounded-lg border border-theme-border"> <div className="bg-theme-bg p-4 rounded-lg border border-theme-border">
<h3 className="text-sm font-bold text-theme-text uppercase mb-4 flex items-center gap-2"> <h3 className="text-sm font-bold text-theme-text uppercase mb-4 flex items-center gap-2">
<Brain size={16} /> Psychologie & Comportement <Brain size={16} /> {t('wb.psychology')}
</h3> </h3>
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-4 px-2"> <div className="space-y-4 px-2">
<div className="relative pt-1"> <div className="relative pt-1">
<div className="flex justify-between text-[10px] uppercase font-bold text-theme-muted mb-1"> <div className="flex justify-between text-[10px] uppercase font-bold text-theme-muted mb-1">
<span>Introverti</span> <span>{t('wb.introvert')}</span>
<span>Extraverti</span> <span>{t('wb.extravert')}</span>
</div> </div>
<input <input
type="range" min="0" max="100" type="range" min="0" max="100"
@@ -359,8 +361,8 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div> </div>
<div className="relative pt-1"> <div className="relative pt-1">
<div className="flex justify-between text-[10px] uppercase font-bold text-theme-muted mb-1"> <div className="flex justify-between text-[10px] uppercase font-bold text-theme-muted mb-1">
<span>Émotionnel</span> <span>{t('wb.emotional')}</span>
<span>Rationnel</span> <span>{t('wb.rational')}</span>
</div> </div>
<input <input
type="range" min="0" max="100" type="range" min="0" max="100"
@@ -371,8 +373,8 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div> </div>
<div className="relative pt-1"> <div className="relative pt-1">
<div className="flex justify-between text-[10px] uppercase font-bold text-theme-muted mb-1"> <div className="flex justify-between text-[10px] uppercase font-bold text-theme-muted mb-1">
<span>Chaotique</span> <span>{t('wb.chaotic')}</span>
<span>Loyal</span> <span>{t('wb.lawful')}</span>
</div> </div>
<input <input
type="range" min="0" max="100" type="range" min="0" max="100"
@@ -384,12 +386,12 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div> </div>
<div className="border-t border-theme-border pt-4"> <div className="border-t border-theme-border pt-4">
<label className="block text-xs font-semibold text-theme-muted mb-1">Toc ou habitude comportementale</label> <label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.behavioral_quirk')}</label>
<input <input
type="text" type="text"
value={attrs.behavioralQuirk} value={attrs.behavioralQuirk}
onChange={(e) => updateAttribute('behavioralQuirk', e.target.value)} onChange={(e) => updateAttribute('behavioralQuirk', e.target.value)}
placeholder="Joue avec sa bague, bégaie quand il ment..." placeholder={t('wb.behavioral_quirk_ph')}
className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm outline-none focus:border-indigo-400" className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm outline-none focus:border-indigo-400"
/> />
</div> </div>
@@ -406,7 +408,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
return ( return (
<div className="bg-theme-bg p-4 rounded-lg border border-theme-border mt-6"> <div className="bg-theme-bg p-4 rounded-lg border border-theme-border mt-6">
<h3 className="text-sm font-bold text-theme-text uppercase mb-4 flex items-center gap-2"> <h3 className="text-sm font-bold text-theme-text uppercase mb-4 flex items-center gap-2">
<List size={16} /> Champs Personnalisés <List size={16} /> {t('wb.custom_fields')}
</h3> </h3>
<div className="grid grid-cols-1 gap-4"> <div className="grid grid-cols-1 gap-4">
{currentTemplate.fields.map(field => { {currentTemplate.fields.map(field => {
@@ -429,7 +431,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
onChange={(e) => updateCustomValue(field.id, e.target.value)} onChange={(e) => updateCustomValue(field.id, e.target.value)}
className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm outline-none focus:border-indigo-400" className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm outline-none focus:border-indigo-400"
> >
<option value="">Sélectionner...</option> <option value="">{t('wb.select')}</option>
{field.options?.map(opt => ( {field.options?.map(opt => (
<option key={opt} value={opt}>{opt}</option> <option key={opt} value={opt}>{opt}</option>
))} ))}
@@ -442,7 +444,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
onChange={(e) => updateCustomValue(field.id, e.target.checked)} onChange={(e) => updateCustomValue(field.id, e.target.checked)}
className="w-4 h-4 text-indigo-600 rounded border-theme-border focus:ring-indigo-500" className="w-4 h-4 text-indigo-600 rounded border-theme-border focus:ring-indigo-500"
/> />
<span className="text-sm text-theme-text">Activé / Oui</span> <span className="text-sm text-theme-text">{t('wb.active_yes')}</span>
</label> </label>
) : ( ) : (
<input <input
@@ -469,10 +471,10 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="flex justify-between items-start mb-6"> <div className="flex justify-between items-start mb-6">
<div> <div>
<h2 className="text-2xl font-bold text-theme-text flex items-center gap-2"> <h2 className="text-2xl font-bold text-theme-text flex items-center gap-2">
<Layout size={24} className="text-indigo-600" /> Éditeur de Modèles <Layout size={24} className="text-indigo-600" /> {t('wb.template_editor')}
</h2> </h2>
<p className="text-theme-muted text-sm mt-1"> <p className="text-theme-muted text-sm mt-1">
Configurez les champs personnalisés pour chaque type de fiche. {t('wb.template_editor_desc')}
</p> </p>
</div> </div>
<button onClick={() => setMode('entities')} className="p-2 text-theme-muted hover:bg-theme-border rounded-full"> <button onClick={() => setMode('entities')} className="p-2 text-theme-muted hover:bg-theme-border rounded-full">
@@ -500,7 +502,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div key={field.id} className="bg-theme-bg border border-theme-border rounded-lg p-4 flex gap-4 items-start group"> <div key={field.id} className="bg-theme-bg border border-theme-border rounded-lg p-4 flex gap-4 items-start group">
<div className="flex-1 grid grid-cols-2 gap-4"> <div className="flex-1 grid grid-cols-2 gap-4">
<div> <div>
<label className="block text-xs font-semibold text-theme-muted mb-1">Nom du champ</label> <label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.field_name')}</label>
<input <input
type="text" type="text"
value={field.label} value={field.label}
@@ -509,28 +511,28 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-semibold text-theme-muted mb-1">Type</label> <label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.field_type')}</label>
<select <select
value={field.type} value={field.type}
onChange={(e) => updateCustomField(activeTemplateType, field.id, { type: e.target.value as CustomFieldType })} onChange={(e) => updateCustomField(activeTemplateType, field.id, { type: e.target.value as CustomFieldType })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm" className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm"
> >
<option value="text">Texte court</option> <option value="text">{t('wb.type_text')}</option>
<option value="textarea">Texte long</option> <option value="textarea">{t('wb.type_textarea')}</option>
<option value="number">Nombre</option> <option value="number">{t('wb.type_num')}</option>
<option value="boolean">Case à cocher</option> <option value="boolean">{t('wb.type_bool')}</option>
<option value="select">Liste déroulante</option> <option value="select">{t('wb.type_select')}</option>
</select> </select>
</div> </div>
{field.type === 'select' && ( {field.type === 'select' && (
<div className="col-span-2"> <div className="col-span-2">
<label className="block text-xs font-semibold text-theme-muted mb-1">Options (séparées par des virgules)</label> <label className="block text-xs font-semibold text-theme-muted mb-1">{t('wb.options_desc')}</label>
<input <input
type="text" type="text"
value={field.options?.join(',') || ''} value={field.options?.join(',') || ''}
onChange={(e) => updateCustomField(activeTemplateType, field.id, { options: e.target.value.split(',').map(s => s.trim()) })} onChange={(e) => updateCustomField(activeTemplateType, field.id, { options: e.target.value.split(',').map(s => s.trim()) })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm" className="w-full p-2 bg-theme-bg border border-theme-border rounded text-sm"
placeholder="Option A, Option B, Option C" placeholder={t('wb.options_ph')}
/> />
</div> </div>
)} )}
@@ -548,7 +550,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
onClick={() => addCustomField(activeTemplateType)} onClick={() => addCustomField(activeTemplateType)}
className="w-full py-3 border-2 border-dashed border-theme-border rounded-lg text-theme-muted hover:border-indigo-400 hover:text-indigo-600 hover:bg-indigo-500/10 transition-all flex items-center justify-center gap-2" className="w-full py-3 border-2 border-dashed border-theme-border rounded-lg text-theme-muted hover:border-indigo-400 hover:text-indigo-600 hover:bg-indigo-500/10 transition-all flex items-center justify-center gap-2"
> >
<Plus size={20} /> Ajouter un champ <Plus size={20} /> {t('wb.add_field')}
</button> </button>
</div> </div>
</div> </div>
@@ -557,10 +559,10 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
if (mode === 'templates') { if (mode === 'templates') {
return ( return (
<div className="flex h-full gap-6 p-6 bg-theme-bg"> <div className="flex flex-col md:flex-row h-full gap-4 md:gap-6 p-4 md:p-6 bg-theme-bg">
<div className="w-1/3 opacity-50 pointer-events-none filter blur-[1px]"> <div className="hidden md:block w-1/3 opacity-50 pointer-events-none filter blur-[1px]">
<div className="bg-theme-panel rounded-lg p-6 shadow-sm border border-theme-border"> <div className="bg-theme-panel rounded-lg p-6 shadow-sm border border-theme-border">
<h3 className="font-bold text-theme-text mb-4">Aperçu Fiches</h3> <h3 className="font-bold text-theme-text mb-4">{t('wb.preview_cards')}</h3>
<div className="space-y-2"> <div className="space-y-2">
<div className="h-10 bg-indigo-500/10 rounded"></div> <div className="h-10 bg-indigo-500/10 rounded"></div>
<div className="h-10 bg-indigo-500/10 rounded"></div> <div className="h-10 bg-indigo-500/10 rounded"></div>
@@ -574,16 +576,16 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
} }
return ( return (
<div className="flex h-full gap-6 p-6 bg-theme-bg"> <div className="flex flex-col md:flex-row h-full gap-4 md:gap-6 p-4 md:p-6 bg-theme-bg">
<div className="w-1/3 flex flex-col gap-4"> <div className={`${(editingId) ? 'hidden md:flex' : 'flex'} w-full md:w-1/3 flex-col gap-4 h-full`}>
<div className="flex justify-between items-center px-1"> <div className="flex justify-between items-center px-1">
<h2 className="text-lg font-bold text-theme-text">Explorateur</h2> <h2 className="text-lg font-bold text-theme-text">{t('wb.explorer')}</h2>
<button <button
onClick={() => setMode('templates')} onClick={() => setMode('templates')}
className="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-100 text-indigo-700 hover:bg-indigo-200 rounded text-xs font-medium transition-colors" className="flex items-center gap-1.5 px-3 py-1.5 bg-indigo-100 text-indigo-700 hover:bg-indigo-200 rounded text-xs font-medium transition-colors"
title="Gérer les modèles de fiches" title={t('wb.manage_templates')}
> >
<Settings size={14} /> Modèles <Settings size={14} /> {t('wb.templates')}
</button> </button>
</div> </div>
@@ -603,7 +605,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div> </div>
<div className="divide-y divide-slate-100"> <div className="divide-y divide-slate-100">
{filterByType(type).length === 0 && ( {filterByType(type).length === 0 && (
<p className="p-4 text-sm text-theme-muted italic text-center">Aucun élément</p> <p className="p-4 text-sm text-theme-muted italic text-center">{t('wb.no_element')}</p>
)} )}
{filterByType(type).map(entity => ( {filterByType(type).map(entity => (
<div <div
@@ -629,7 +631,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div> </div>
</div> </div>
<div className="flex-1 bg-theme-panel rounded-xl shadow-lg border border-theme-border p-8 overflow-y-auto"> <div className={`${(!editingId) ? 'hidden md:block' : 'block'} flex-1 bg-theme-panel rounded-xl shadow-lg border border-theme-border p-4 sm:p-6 md:p-8 overflow-y-auto h-full`}>
{editingId && tempEntity ? ( {editingId && tempEntity ? (
<div className="space-y-6 animate-in fade-in duration-200"> <div className="space-y-6 animate-in fade-in duration-200">
<div className="flex justify-between items-start"> <div className="flex justify-between items-start">
@@ -638,7 +640,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
{tempEntity.type} {tempEntity.type}
</span> </span>
<h2 className="text-2xl font-bold text-theme-text"> <h2 className="text-2xl font-bold text-theme-text">
{tempEntity.type === EntityType.CHARACTER ? 'Fiche Personnage' : 'Édition de la fiche'} {tempEntity.type === EntityType.CHARACTER ? t('wb.char_sheet') : t('wb.edit_sheet')}
</h2> </h2>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
@@ -650,23 +652,23 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<label className="block text-sm font-medium text-theme-text mb-1">Nom</label> <label className="block text-sm font-medium text-theme-text mb-1">{t('wb.name')}</label>
<input <input
type="text" type="text"
value={tempEntity.name} value={tempEntity.name}
onChange={e => setTempEntity({ ...tempEntity, name: e.target.value })} onChange={e => setTempEntity({ ...tempEntity, name: e.target.value })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none font-serif text-lg" className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none font-serif text-lg"
placeholder="Ex: Gandalf le Gris" placeholder={t('wb.name_ph')}
/> />
</div> </div>
<div> <div>
<label className="block text-sm font-medium text-theme-text mb-1">Description Courte (pour l'IA)</label> <label className="block text-sm font-medium text-theme-text mb-1">{t('wb.short_desc')}</label>
<textarea <textarea
value={tempEntity.description} value={tempEntity.description}
onChange={e => setTempEntity({ ...tempEntity, description: e.target.value })} onChange={e => setTempEntity({ ...tempEntity, description: e.target.value })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none text-sm h-20" className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none text-sm h-20"
placeholder="Un magicien puissant qui guide la communauté..." placeholder={t('wb.short_desc_ph')}
/> />
</div> </div>
@@ -677,23 +679,23 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="mt-6 border-t border-theme-border pt-6"> <div className="mt-6 border-t border-theme-border pt-6">
<div> <div>
<label className="block text-sm font-medium text-indigo-700 mb-1 flex items-center gap-2"> <label className="block text-sm font-medium text-indigo-700 mb-1 flex items-center gap-2">
<Sparkles size={14} /> Contexte Narratif (Auto-généré) <Sparkles size={14} /> {t('wb.story_context')}
</label> </label>
<textarea <textarea
value={tempEntity.storyContext || ''} value={tempEntity.storyContext || ''}
onChange={e => setTempEntity({ ...tempEntity, storyContext: e.target.value })} onChange={e => setTempEntity({ ...tempEntity, storyContext: e.target.value })}
className="w-full p-2 border border-indigo-200 bg-indigo-500/10 rounded focus:ring-2 focus:ring-blue-500 outline-none text-sm h-24 italic text-theme-muted" className="w-full p-2 border border-indigo-200 bg-indigo-500/10 rounded focus:ring-2 focus:ring-blue-500 outline-none text-sm h-24 italic text-theme-muted"
placeholder="Les événements vécus par ce personnage apparaîtront ici..." placeholder={t('wb.story_context_ph')}
/> />
</div> </div>
<div className="mt-4"> <div className="mt-4">
<label className="block text-sm font-medium text-theme-text mb-1">Notes & Biographie Complète</label> <label className="block text-sm font-medium text-theme-text mb-1">{t('wb.notes_bio')}</label>
<textarea <textarea
value={tempEntity.details} value={tempEntity.details}
onChange={e => setTempEntity({ ...tempEntity, details: e.target.value })} onChange={e => setTempEntity({ ...tempEntity, details: e.target.value })}
className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none h-48 font-serif" className="w-full p-2 bg-theme-bg border border-theme-border rounded focus:ring-2 focus:ring-blue-500 outline-none h-48 font-serif"
placeholder="Histoire détaillée, secrets, origines..." placeholder={t('wb.notes_bio_ph')}
/> />
</div> </div>
</div> </div>
@@ -704,7 +706,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 transition-colors shadow-md" className="bg-blue-600 hover:bg-blue-700 text-white px-6 py-2 rounded-lg flex items-center gap-2 transition-colors shadow-md"
> >
<Save size={18} /> <Save size={18} />
Enregistrer la fiche {t('wb.save')}
</button> </button>
</div> </div>
</div> </div>
@@ -712,8 +714,8 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
) : ( ) : (
<div className="h-full flex flex-col items-center justify-center text-theme-muted"> <div className="h-full flex flex-col items-center justify-center text-theme-muted">
<div className="text-6xl mb-4 opacity-20">🌍</div> <div className="text-6xl mb-4 opacity-20">🌍</div>
<p className="text-lg">Sélectionnez ou créez une fiche pour commencer.</p> <p className="text-lg">{t('wb.select_start')}</p>
<p className="text-sm">Ces informations aideront l'IA à rester cohérente.</p> <p className="text-sm">{t('wb.ai_help')}</p>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,10 +1,12 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { BookProject, UserProfile, ViewMode, ChatMessage } from '@/lib/types'; import { BookProject, UserProfile, ViewMode, ChatMessage } from '@/lib/types';
import AIPanel from '@/components/AIPanel'; import AIPanel from '@/components/AIPanel';
import { Book, FileText, Globe, GitGraph, Lightbulb, Settings, Menu, ChevronRight, ChevronLeft, Share2, HelpCircle, LogOut, LayoutDashboard, User, Plus, Trash2 } from 'lucide-react'; import { Book, FileText, Globe, GitGraph, Lightbulb, Settings, Menu, ChevronRight, ChevronLeft, Share2, HelpCircle, LogOut, LayoutDashboard, User, Plus, Trash2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
interface EditorShellProps { interface EditorShellProps {
project: BookProject; project: BookProject;
@@ -30,33 +32,53 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
const { project, user, viewMode, currentChapterId, children } = props; const { project, user, viewMode, currentChapterId, children } = props;
const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [isAiPanelOpen, setIsAiPanelOpen] = useState(true); const [isAiPanelOpen, setIsAiPanelOpen] = useState(true);
const { t } = useLanguage();
const currentChapter = project.chapters.find(c => c.id === currentChapterId); const currentChapter = project.chapters.find(c => c.id === currentChapterId);
// Auto-close sidebars on mobile when navigating
useEffect(() => {
if (typeof window !== 'undefined' && window.innerWidth < 1024) {
setIsSidebarOpen(false);
setIsAiPanelOpen(false);
}
}, [viewMode, currentChapterId]);
return ( return (
<div className="flex h-screen overflow-hidden no-print bg-theme-bg text-theme-text transition-colors duration-300"> <div className="flex h-screen overflow-hidden no-print bg-theme-bg text-theme-text transition-colors duration-300 relative">
{/* Mobile Sidebar Overlay */}
{isSidebarOpen && (
<div
className="fixed inset-0 bg-black/50 z-30 lg:hidden"
onClick={() => setIsSidebarOpen(false)}
/>
)}
{/* SIDEBAR */} {/* SIDEBAR */}
<aside className={`${isSidebarOpen ? 'w-64' : 'w-0'} bg-slate-900 text-slate-300 flex-shrink-0 transition-all duration-300 overflow-hidden flex flex-col border-r border-slate-800`}> <aside className={`${isSidebarOpen ? 'w-64' : 'w-0'} bg-slate-900 text-slate-300 flex-shrink-0 transition-all duration-300 overflow-hidden flex flex-col border-r border-slate-800 absolute lg:relative z-40 h-full shadow-2xl lg:shadow-none`}>
<div className="p-4 border-b border-slate-700"> <div className="p-4 border-b border-slate-700">
<h1 className="text-white font-bold flex items-center gap-2 mb-4 cursor-pointer" onClick={() => props.onViewModeChange('dashboard')}> <h1 className="text-white font-bold flex items-center gap-2 mb-4 cursor-pointer" onClick={() => props.onViewModeChange('dashboard')}>
<Book className="text-blue-400" /> PlumeIA <Book className="text-blue-400" /> Pluume
</h1> </h1>
<input <input
type="text" type="text"
value={project.title} value={project.title}
onChange={(e) => props.onUpdateProject({ title: e.target.value })} onChange={(e) => props.onUpdateProject({ title: e.target.value })}
className="w-full bg-transparent font-serif font-bold text-white text-lg mb-1 focus:outline-none focus:border-b focus:border-blue-500 truncate" className="w-full bg-transparent font-serif font-bold text-white text-lg mb-1 focus:outline-none focus:border-b focus:border-blue-500 truncate"
placeholder="Titre du livre" placeholder={t('header.title_placeholder')}
/> />
<button onClick={() => props.onViewModeChange('dashboard')} className="w-full flex items-center gap-2 text-xs hover:bg-slate-800 p-2 rounded transition-colors text-slate-400"> <button onClick={() => props.onViewModeChange('dashboard')} className="w-full flex items-center gap-2 text-xs hover:bg-slate-800 p-2 rounded transition-colors text-slate-400">
<LayoutDashboard size={14} /> Retour au Dashboard <LayoutDashboard size={14} /> {t('nav.dashboard')}
</button> </button>
<div className="mt-2 text-slate-400">
<LanguageSwitcher />
</div>
</div> </div>
<div className="flex-1 overflow-y-auto py-2"> <div className="flex-1 overflow-y-auto py-2">
<div className="px-4 py-2 text-xs font-semibold text-slate-500 uppercase flex justify-between items-center"> <div className="px-4 py-2 text-xs font-semibold text-slate-500 uppercase flex justify-between items-center">
Chapitres <button onClick={props.onAddChapter} className="hover:text-blue-400"><Plus size={14} /></button> {t('nav.chapters')} <button onClick={props.onAddChapter} className="hover:text-blue-400"><Plus size={14} /></button>
</div> </div>
{project.chapters.map((chap, idx) => ( {project.chapters.map((chap, idx) => (
<div key={chap.id} className="group relative"> <div key={chap.id} className="group relative">
@@ -70,26 +92,26 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
</div> </div>
))} ))}
<div className="mt-6 px-4 py-2 text-xs font-semibold text-slate-500 uppercase">Outils & Bible</div> <div className="mt-6 px-4 py-2 text-xs font-semibold text-slate-500 uppercase">{t('nav.tools')}</div>
<button onClick={() => props.onViewModeChange('write')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'write' ? 'bg-blue-900 text-white' : 'hover:bg-slate-800'}`}><FileText size={16} /> Retour à la rédaction</button> <button onClick={() => props.onViewModeChange('write')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'write' ? 'bg-blue-900 text-white' : 'hover:bg-slate-800'}`}><FileText size={16} /> {t('sidebar.write')}</button>
<button onClick={() => props.onViewModeChange('world_building')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'world_building' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><Globe size={16} /> Bible du Monde</button> <button onClick={() => props.onViewModeChange('world_building')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'world_building' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><Globe size={16} /> {t('sidebar.world_building')}</button>
<button onClick={() => props.onViewModeChange('workflow')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'workflow' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><GitGraph size={16} /> Workflow</button> <button onClick={() => props.onViewModeChange('workflow')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'workflow' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><GitGraph size={16} /> {t('sidebar.workflow')}</button>
<button onClick={() => props.onViewModeChange('ideas')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'ideas' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><Lightbulb size={16} /> Boîte à Idées</button> <button onClick={() => props.onViewModeChange('ideas')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'ideas' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><Lightbulb size={16} /> {t('sidebar.ideas')}</button>
<button onClick={() => props.onViewModeChange('settings')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'settings' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><Settings size={16} /> Paramètres</button> <button onClick={() => props.onViewModeChange('settings')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'settings' ? 'bg-indigo-900 text-white' : 'hover:bg-slate-800'}`}><Settings size={16} /> {t('sidebar.settings')}</button>
</div> </div>
<div className="p-4 border-t border-slate-800"> <div className="p-4 border-t border-slate-800">
<div className="bg-slate-800 rounded-lg p-3 mb-4"> <div className="bg-slate-800 rounded-lg p-3 mb-4">
<div className="flex justify-between text-[10px] text-slate-400 uppercase font-bold mb-1"> <div className="flex justify-between text-[10px] text-slate-400 uppercase font-bold mb-1">
<span>Actions IA</span> <span>{t('sidebar.ai_actions')}</span>
<span>{user.usage.aiActionsCurrent} / {user.usage.aiActionsLimit === 999999 ? '∞' : user.usage.aiActionsLimit}</span> <span>{user.usage.aiActionsCurrent} / {user.usage.aiActionsLimit === 999999 ? '∞' : user.usage.aiActionsLimit}</span>
</div> </div>
<div className="h-1.5 w-full bg-slate-700 rounded-full overflow-hidden"> <div className="h-1.5 w-full bg-slate-700 rounded-full overflow-hidden">
<div className="h-full bg-blue-500" style={{ width: `${Math.min(100, (user.usage.aiActionsCurrent / user.usage.aiActionsLimit) * 100)}%` }} /> <div className="h-full bg-blue-500" style={{ width: `${Math.min(100, (user.usage.aiActionsCurrent / user.usage.aiActionsLimit) * 100)}%` }} />
</div> </div>
</div> </div>
<button onClick={() => props.onViewModeChange('profile')} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-slate-400 hover:bg-slate-800 rounded mb-2"><User size={14} /> Mon Compte</button> <button onClick={() => props.onViewModeChange('profile')} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-slate-400 hover:bg-slate-800 rounded mb-2"><User size={14} /> {t('sidebar.account')}</button>
<button onClick={props.onLogout} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-red-400 hover:bg-red-900/20 rounded"><LogOut size={14} /> Déconnexion</button> <button onClick={props.onLogout} className="w-full flex items-center gap-2 px-3 py-2 text-xs text-red-400 hover:bg-red-900/20 rounded"><LogOut size={14} /> {t('sidebar.logout')}</button>
</div> </div>
</aside> </aside>
@@ -123,8 +145,16 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
</main> </main>
</div> </div>
{/* Mobile AI Panel Overlay */}
{isAiPanelOpen && (
<div
className="fixed inset-0 bg-black/50 z-30 lg:hidden"
onClick={() => setIsAiPanelOpen(false)}
/>
)}
{/* AI PANEL */} {/* AI PANEL */}
<div className={`${isAiPanelOpen ? 'w-80 lg:w-96' : 'w-0'} transition-all duration-300 flex-shrink-0 h-full border-l border-theme-border relative bg-theme-panel`}> <div className={`${isAiPanelOpen ? 'w-80 lg:w-96' : 'w-0'} transition-all duration-300 flex-shrink-0 h-full border-l border-theme-border flex flex-col bg-theme-panel absolute right-0 lg:relative z-40 shadow-2xl lg:shadow-none`}>
{isAiPanelOpen && <AIPanel chatHistory={props.chatHistory} onSendMessage={props.onSendMessage} onInsertText={props.onInsertText} selectedText="" isGenerating={props.isGenerating} usage={user.usage} />} {isAiPanelOpen && <AIPanel chatHistory={props.chatHistory} onSendMessage={props.onSendMessage} onInsertText={props.onInsertText} selectedText="" isGenerating={props.isGenerating} usage={user.usage} />}
</div> </div>
</div> </div>

1318
src/lib/i18n/translations.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
import { translations, SupportedLanguage, TranslationKey } from '@/lib/i18n/translations';
interface LanguageContextType {
language: SupportedLanguage;
setLanguage: (lang: SupportedLanguage) => void;
t: (key: TranslationKey) => string;
}
const LanguageContext = createContext<LanguageContextType | undefined>(undefined);
export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [language, setLanguageState] = useState<SupportedLanguage>('fr');
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
setIsMounted(true);
const storedLang = localStorage.getItem('pluume_language') as SupportedLanguage;
if (storedLang && Object.keys(translations).includes(storedLang)) {
setLanguageState(storedLang);
}
}, []);
const setLanguage = (lang: SupportedLanguage) => {
setLanguageState(lang);
localStorage.setItem('pluume_language', lang);
};
const t = (key: TranslationKey): string => {
if (!isMounted) return translations.fr[key] || key; // SSR fallback
return translations[language][key] || key;
};
return (
<LanguageContext.Provider value={{ language, setLanguage, t }}>
{children}
</LanguageContext.Provider>
);
};
export const useLanguage = () => {
const context = useContext(LanguageContext);
if (context === undefined) {
throw new Error('useLanguage must be used within a LanguageProvider');
}
return context;
};