feat: implement core application structure, UI components, internationalization, and database seeding.

This commit is contained in:
2026-03-04 13:50:37 +01:00
parent 85642b4672
commit 5101f39ba0
49 changed files with 2732 additions and 980 deletions

Binary file not shown.

View File

@@ -2048,3 +2048,63 @@ FAM | META SEQ | SST SEQ | RANGE
0 | 00001833 | 00001832 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00001834 | 00001831 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)

View File

@@ -1,10 +1,7 @@
{
"/api/auth/[...nextauth]/route": "app/api/auth/[...nextauth]/route.js",
"/api/auth/register/route": "app/api/auth/register/route.js",
"/api/projects/route": "app/api/projects/route.js",
"/api/user/profile/route": "app/api/user/profile/route.js",
"/dashboard/page": "app/dashboard/page.js",
"/login/page": "app/login/page.js",
"/api/plans/route": "app/api/plans/route.js",
"/features/page": "app/features/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")
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/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)")

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/[externals]__7f148858._.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_builtin_forbidden_0318745e.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",
()=>__TURBOPACK__default__export__,
"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$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)");
@@ -63,6 +66,7 @@ var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__([
;
;
;
;
const globalForPrisma = globalThis;
function getDB() {
if (!globalForPrisma.prisma) {
@@ -77,6 +81,14 @@ function getDB() {
}
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;
__turbopack_async_result__();
} catch(e) { __turbopack_async_result__(e); } }, false);}),
@@ -102,8 +114,8 @@ var __turbopack_async_dependencies__ = __turbopack_handle_async_dependencies__([
const dynamic = 'force-dynamic';
async function GET() {
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 plans = await prisma.plan.findMany({
//const prisma = getDB();
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: {
price: 'asc'
}

View File

@@ -2,6 +2,6 @@
"version": 3,
"sources": [],
"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": 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": 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": 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": {
"[project]/Documents/00 - projet/plumeia/src/app/dashboard/page": [
"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2",
"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2"
],
"[project]/Documents/00 - projet/plumeia/src/app/login/page": [
"[project]/Documents/00 - projet/plumeia/src/app/features/page": [
"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2",
"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2"
],
@@ -12,7 +8,7 @@
"static/media/83afe278b6a6bb3c-s.p.3a6ba036.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/248e1dc0efc99276-s.p.8a6b2436.woff2"
]

View File

@@ -150,6 +150,7 @@
--container-md: 28rem;
--container-lg: 32rem;
--container-2xl: 42rem;
--container-3xl: 48rem;
--container-4xl: 56rem;
--container-5xl: 64rem;
--container-6xl: 72rem;
@@ -158,6 +159,8 @@
--text-xs--line-height: calc(1 / .75);
--text-sm: .875rem;
--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--line-height: calc(1.75 / 1.125);
--text-xl: 1.25rem;
@@ -573,6 +576,10 @@
inset-inline-start: var(--spacing);
}
.end {
inset-inline-end: var(--spacing);
}
.-top-2 {
top: calc(var(--spacing) * -2);
}
@@ -787,6 +794,10 @@
margin-block: calc(var(--spacing) * 4);
}
.-mt-20 {
margin-top: calc(var(--spacing) * -20);
}
.mt-0\.5 {
margin-top: calc(var(--spacing) * .5);
}
@@ -949,6 +960,10 @@
height: calc(var(--spacing) * 3);
}
.h-3\.5 {
height: calc(var(--spacing) * 3.5);
}
.h-4 {
height: calc(var(--spacing) * 4);
}
@@ -1113,6 +1128,10 @@
width: calc(var(--spacing) * 32);
}
.w-40 {
width: calc(var(--spacing) * 40);
}
.w-48 {
width: calc(var(--spacing) * 48);
}
@@ -1165,6 +1184,10 @@
max-width: var(--container-2xl);
}
.max-w-3xl {
max-width: var(--container-3xl);
}
.max-w-4xl {
max-width: var(--container-4xl);
}
@@ -1237,6 +1260,10 @@
scale: 1.01;
}
.rotate-180 {
rotate: 180deg;
}
.transform {
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-x-hidden {
overflow-x: hidden;
}
.overflow-y-auto {
overflow-y: auto;
}
@@ -1451,6 +1482,10 @@
border-radius: 2.5rem;
}
.rounded-\[2px\] {
border-radius: 2px;
}
.rounded-\[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 {
background-color: var(--color-blue-600);
}
@@ -2246,6 +2291,14 @@
padding-top: calc(var(--spacing) * 8);
}
.pt-12 {
padding-top: calc(var(--spacing) * 12);
}
.pt-20 {
padding-top: calc(var(--spacing) * 20);
}
.pt-32 {
padding-top: calc(var(--spacing) * 32);
}
@@ -2282,6 +2335,10 @@
padding-bottom: calc(var(--spacing) * 20);
}
.pb-32 {
padding-bottom: calc(var(--spacing) * 32);
}
.pl-2 {
padding-left: calc(var(--spacing) * 2);
}
@@ -2351,6 +2408,11 @@
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 {
font-size: var(--text-lg);
line-height: var(--tw-leading, var(--text-lg--line-height));
@@ -2623,6 +2685,10 @@
color: var(--color-yellow-600);
}
.lowercase {
text-transform: lowercase;
}
.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, );
}
.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\] {
--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, );
@@ -3444,6 +3515,12 @@
opacity: .5;
}
@media (min-width: 40rem) {
.sm\:inline {
display: inline;
}
}
@media (min-width: 40rem) {
.sm\:w-auto {
width: auto;
@@ -3456,6 +3533,12 @@
}
}
@media (min-width: 40rem) {
.sm\:p-12 {
padding: calc(var(--spacing) * 12);
}
}
@media (min-width: 48rem) {
.md\: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) {
.md\:text-7xl {
font-size: var(--text-7xl);
@@ -3552,6 +3642,58 @@
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 {

View File

@@ -2,7 +2,7 @@
script: typeof document === "object" ? document.currentScript : undefined,
chunks: [
"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"
});

View File

@@ -451,6 +451,7 @@
--container-md: 28rem;
--container-lg: 32rem;
--container-2xl: 42rem;
--container-3xl: 48rem;
--container-4xl: 56rem;
--container-5xl: 64rem;
--container-6xl: 72rem;
@@ -459,6 +460,8 @@
--text-xs--line-height: calc(1 / .75);
--text-sm: .875rem;
--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--line-height: calc(1.75 / 1.125);
--text-xl: 1.25rem;
@@ -874,6 +877,10 @@
inset-inline-start: var(--spacing);
}
.end {
inset-inline-end: var(--spacing);
}
.-top-2 {
top: calc(var(--spacing) * -2);
}
@@ -1088,6 +1095,10 @@
margin-block: calc(var(--spacing) * 4);
}
.-mt-20 {
margin-top: calc(var(--spacing) * -20);
}
.mt-0\.5 {
margin-top: calc(var(--spacing) * .5);
}
@@ -1250,6 +1261,10 @@
height: calc(var(--spacing) * 3);
}
.h-3\.5 {
height: calc(var(--spacing) * 3.5);
}
.h-4 {
height: calc(var(--spacing) * 4);
}
@@ -1414,6 +1429,10 @@
width: calc(var(--spacing) * 32);
}
.w-40 {
width: calc(var(--spacing) * 40);
}
.w-48 {
width: calc(var(--spacing) * 48);
}
@@ -1466,6 +1485,10 @@
max-width: var(--container-2xl);
}
.max-w-3xl {
max-width: var(--container-3xl);
}
.max-w-4xl {
max-width: var(--container-4xl);
}
@@ -1538,6 +1561,10 @@
scale: 1.01;
}
.rotate-180 {
rotate: 180deg;
}
.transform {
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-x-hidden {
overflow-x: hidden;
}
.overflow-y-auto {
overflow-y: auto;
}
@@ -1752,6 +1783,10 @@
border-radius: 2.5rem;
}
.rounded-\[2px\] {
border-radius: 2px;
}
.rounded-\[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 {
background-color: var(--color-blue-600);
}
@@ -2547,6 +2592,14 @@
padding-top: calc(var(--spacing) * 8);
}
.pt-12 {
padding-top: calc(var(--spacing) * 12);
}
.pt-20 {
padding-top: calc(var(--spacing) * 20);
}
.pt-32 {
padding-top: calc(var(--spacing) * 32);
}
@@ -2583,6 +2636,10 @@
padding-bottom: calc(var(--spacing) * 20);
}
.pb-32 {
padding-bottom: calc(var(--spacing) * 32);
}
.pl-2 {
padding-left: calc(var(--spacing) * 2);
}
@@ -2652,6 +2709,11 @@
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 {
font-size: var(--text-lg);
line-height: var(--tw-leading, var(--text-lg--line-height));
@@ -2924,6 +2986,10 @@
color: var(--color-yellow-600);
}
.lowercase {
text-transform: lowercase;
}
.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, );
}
.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\] {
--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, );
@@ -3745,6 +3816,12 @@
opacity: .5;
}
@media (min-width: 40rem) {
.sm\:inline {
display: inline;
}
}
@media (min-width: 40rem) {
.sm\:w-auto {
width: auto;
@@ -3757,6 +3834,12 @@
}
}
@media (min-width: 40rem) {
.sm\:p-12 {
padding: calc(var(--spacing) * 12);
}
}
@media (min-width: 48rem) {
.md\: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) {
.md\:text-7xl {
font-size: var(--text-7xl);
@@ -3853,6 +3943,58 @@
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 {

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/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -13,7 +13,7 @@ async function main() {
name: 'free',
displayName: 'Gratuit',
price: 0,
description: 'Idéal pour découvrir PlumeIA.',
description: 'Idéal pour découvrir Pluume.',
maxProjects: 1,
maxAiActions: 10,
features: ['10 actions IA / mois', '1 projet actif', 'Bible du monde simple'],
@@ -33,7 +33,7 @@ async function main() {
{
id: 'master',
name: 'master',
displayName: 'Maître Plume',
displayName: 'Maître Pluume',
price: 29,
description: 'Le summum de l\'écriture IA.',
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-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.cgu_title')}</h1>
<div className="bg-white p-8 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-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.cgv_title')}</h1>
<div className="bg-white p-8 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} />
<div className="flex items-center gap-2">
<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>
);

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Inter, Merriweather } from "next/font/google";
import { AuthProvider } from "@/providers/AuthProvider";
import { LanguageProvider } from "@/providers/LanguageProvider";
import "./globals.css";
const inter = Inter({
@@ -15,7 +16,7 @@ const merriweather = Merriweather({
});
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.",
};
@@ -25,10 +26,12 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
<html lang="fr">
<body className={`${inter.variable} ${merriweather.variable} font-sans h-screen overflow-hidden antialiased bg-theme-bg text-theme-text transition-colors duration-300`}>
<html lang="en">
<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>
{children}
<LanguageProvider>
{children}
</LanguageProvider>
</AuthProvider>
</body>
</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} />
<div className="flex items-center gap-2">
<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>
);

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 { Sparkles, Send, RefreshCw, BookOpen, Bot, ArrowLeft, BrainCircuit, Zap } from 'lucide-react';
import { ChatMessage, UserUsage } from '@/lib/types';
import { useLanguage } from '@/providers/LanguageProvider';
interface AIPanelProps {
chatHistory: ChatMessage[];
@@ -15,6 +16,7 @@ interface AIPanelProps {
}
const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertText, selectedText, isGenerating, usage }) => {
const { t } = useLanguage();
const [input, setInput] = useState("");
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="flex items-center gap-2">
<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>
{usage && (
<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 && (
<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>
)}
@@ -57,10 +59,10 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
{chatHistory.length === 0 && (
<div className="text-center text-theme-muted mt-10">
<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 && (
<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>
@@ -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 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' && (
<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>
@@ -80,7 +82,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
{isGenerating && (
<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">
<RefreshCw size={14} className="animate-spin" /> L'IA travaille...
<RefreshCw size={14} className="animate-spin" /> {t('ai_panel.ai_working')}
</div>
</div>
)}
@@ -93,7 +95,7 @@ const AIPanel: React.FC<AIPanelProps> = ({ chatHistory, onSendMessage, onInsertT
type="text"
value={input}
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"
disabled={isGenerating || isLimitReached}
/>

View File

@@ -3,6 +3,7 @@
import React, { useState, useEffect } from 'react';
import { Mail, Lock, User, ArrowRight, Loader2, BookOpen, ShieldCheck } from 'lucide-react';
import { useAuthContext } from '@/providers/AuthProvider';
import { useLanguage } from '@/providers/LanguageProvider';
interface AuthPageProps {
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
const { user, login, signup } = useAuthContext();
const { t } = useLanguage();
// Redirection automatique dès que l'utilisateur est détecté dans l'état global
useEffect(() => {
@@ -28,7 +30,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
const handleAdminLogin = async () => {
const adminData = { email: 'streaper2@gmail.com', password: 'Kency1313' };
setFormData({ name: 'Admin Plume', ...adminData });
setFormData({ name: 'Admin Pluume', ...adminData });
setLoading(true);
setError('');
@@ -69,20 +71,20 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
</div>
<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 className="relative z-10 max-w-lg">
<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>
<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>
</div>
<div className="relative z-10 text-slate-500 text-sm">
© 2024 PlumeIA Ecosystem.
© 2024 Pluume Ecosystem.
</div>
</div>
@@ -91,10 +93,10 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
<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">
<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>
<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>
</div>
@@ -107,7 +109,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
<form onSubmit={handleSubmit} className="space-y-4">
{mode === 'signup' && (
<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">
<User className="absolute left-4 top-3.5 text-slate-400" size={18} />
<input
@@ -115,7 +117,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
required
value={formData.name}
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"
/>
</div>
@@ -123,7 +125,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
)}
<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">
<Mail className="absolute left-4 top-3.5 text-slate-400" size={18} />
<input
@@ -131,7 +133,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
required
value={formData.email}
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"
/>
</div>
@@ -139,7 +141,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
{mode !== 'forgot' && (
<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">
<Lock className="absolute left-4 top-3.5 text-slate-400" size={18} />
<input
@@ -147,7 +149,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
required
value={formData.password}
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"
/>
</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"
>
{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>
</form>
@@ -170,24 +172,24 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
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"
>
<ShieldCheck size={18} /> Connexion démo (Admin)
<ShieldCheck size={18} /> {t('auth.demo_admin')}
</button>
)}
<div className="mt-10 text-center">
<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
onClick={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
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>
</p>
</div>
<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>
</div>
</div>

View File

@@ -4,6 +4,8 @@ import React, { useEffect, useState } from 'react';
import { BookProject, BookSettings } from '@/lib/types';
import { GENRES, TONES, POV_OPTIONS, TENSE_OPTIONS } from '@/lib/constants';
import { Settings, Book, Feather, Users, Clock, Target, Hash } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { TranslationKey } from '@/lib/i18n/translations';
interface BookSettingsProps {
project: BookProject;
@@ -23,6 +25,7 @@ const DEFAULT_SETTINGS: BookSettings = {
};
const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate, onDeleteProject }) => {
const { t } = useLanguage();
const [settings, setSettings] = useState<BookSettings>(project.settings || DEFAULT_SETTINGS);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
@@ -51,19 +54,19 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<Settings size={24} />
</div>
<div>
<h2 className="text-2xl font-bold">Paramètres Généraux du Roman</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>
<h2 className="text-2xl font-bold">{t('book_settings.title')}</h2>
<p className="text-slate-400 text-sm">{t('book_settings.subtitle')}</p>
</div>
</div>
<div className="p-8 space-y-8">
<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">
<Book size={18} className="text-blue-600" /> Informations de Base
<Book size={18} className="text-blue-600" /> {t('book_settings.basic_info')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<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
type="text"
value={project.title}
@@ -72,7 +75,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
/>
</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
type="text"
value={project.author}
@@ -82,58 +85,58 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
</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
value={settings.synopsis}
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"
placeholder="De quoi parle votre histoire dans les grandes lignes ?"
placeholder={t('book_settings.synopsis_placeholder')}
/>
</div>
</section>
<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">
<Target size={18} className="text-red-500" /> Genre & Public
<Target size={18} className="text-red-500" /> {t('book_settings.genre_audience')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<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
type="text"
list="genre-suggestions"
value={settings.genre}
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"
placeholder="Ex: Fantasy"
placeholder={t('book_settings.genre_placeholder')}
/>
<datalist id="genre-suggestions">
{GENRES.map(g => <option key={g} value={g} />)}
</datalist>
</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
type="text"
value={settings.subGenre || ''}
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"
placeholder="Ex: Dark Fantasy"
placeholder={t('book_settings.subgenre_placeholder')}
/>
</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
type="text"
value={settings.targetAudience}
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"
placeholder="Ex: Jeune Adulte, Adulte..."
placeholder={t('book_settings.audience_placeholder')}
/>
</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">
<Hash size={14} className="absolute left-3 top-3 text-theme-muted" />
<input
@@ -141,7 +144,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
value={settings.themes}
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"
placeholder="Ex: Vengeance, Rédemption, Voyage initiatique..."
placeholder={t('book_settings.themes_placeholder')}
/>
</div>
</div>
@@ -149,90 +152,90 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<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">
<Feather size={18} className="text-purple-600" /> Narration & Style
<Feather size={18} className="text-purple-600" /> {t('book_settings.narration_style')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<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>
<select
value={settings.pov}
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"
>
<option value="">Sélectionner...</option>
{POV_OPTIONS.map(o => <option key={o} value={o}>{o}</option>)}
<option value="">{t('book_settings.select')}</option>
{POV_OPTIONS.map(o => <option key={o} value={o}>{t(`pov_options.${o.toLowerCase().replace(/\s+/g, '_')}` as TranslationKey) || o}</option>)}
</select>
</div>
<div>
<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>
<select
value={settings.tense}
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"
>
<option value="">Sélectionner...</option>
{TENSE_OPTIONS.map(o => <option key={o} value={o}>{o}</option>)}
<option value="">{t('book_settings.select')}</option>
{TENSE_OPTIONS.map(o => <option key={o} value={o}>{t(`tense_options.${o.toLowerCase().replace(/\s+/g, '_')}` as TranslationKey) || o}</option>)}
</select>
</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
type="text"
list="tone-suggestions"
value={settings.tone}
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"
placeholder="Ex: Sombre, Ironique..."
placeholder={t('book_settings.tone_placeholder')}
/>
<datalist id="tone-suggestions">
{TONES.map(t => <option key={t} value={t} />)}
{TONES.map(tOption => <option key={tOption} value={tOption} />)}
</datalist>
</div>
</div>
<div className="mt-4">
<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>
<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>
<textarea
value={project.styleGuide || ''}
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"
placeholder="Ex: Utilise un style descriptif et sensoriel. Évite les adverbes. Le narrateur est cynique."
placeholder={t('book_settings.style_guide_placeholder')}
/>
</div>
</section>
<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">
<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>
<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">
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>
{showDeleteConfirm ? (
<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
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"
>
Oui, supprimer définitivement
{t('book_settings.confirm_delete')}
</button>
<button
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"
>
Annuler
{t('book_settings.cancel')}
</button>
</div>
) : (
@@ -240,7 +243,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
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"
>
Supprimer ce projet
{t('book_settings.delete_button')}
</button>
)}
</div>

View File

@@ -3,6 +3,7 @@
import React, { useState } from 'react';
import { CreditCard, Shield, Lock, ArrowRight, Loader2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface CheckoutProps {
onComplete: () => void;
@@ -11,12 +12,13 @@ interface CheckoutProps {
const Checkout: React.FC<CheckoutProps> = ({ onComplete, onCancel }) => {
const [loading, setLoading] = useState(false);
const { t } = useLanguage();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setTimeout(() => {
onComplete();
onComplete();
}, 2000);
};
@@ -24,38 +26,38 @@ const Checkout: React.FC<CheckoutProps> = ({ onComplete, onCancel }) => {
<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="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>
<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>TVA (20%)</span><span>2.40</span></div>
<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>
<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="flex justify-between text-sm"><span>{t('checkout.pro_author')}</span><span>12.00</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="flex justify-between text-xl font-black"><span>{t('checkout.total')}</span><span className="text-blue-400">14.40</span></div>
</div>
</div>
<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>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="block text-xs font-black text-slate-500 uppercase tracking-widest mb-2">Numéro de carte</label>
<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" />
<CreditCard className="absolute right-4 top-4 text-slate-400" />
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<input type="text" placeholder="MM / YY" 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="CVC" className="w-full bg-[#eef2ff] border border-indigo-100 p-4 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-bold" />
</div>
<button
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"
>
{loading ? <Loader2 className="animate-spin" /> : <>Confirmer le paiement <ArrowRight size={20} /></>}
</button>
<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
</div>
</form>
<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">
<div>
<label className="block text-xs font-black text-slate-500 uppercase tracking-widest mb-2">{t('checkout.card_number')}</label>
<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" />
<CreditCard className="absolute right-4 top-4 text-slate-400" />
</div>
</div>
<div className="grid grid-cols-2 gap-6">
<input type="text" placeholder="MM / YY" 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="CVC" className="w-full bg-[#eef2ff] border border-indigo-100 p-4 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-bold" />
</div>
<button
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"
>
{loading ? <Loader2 className="animate-spin" /> : <>{t('checkout.confirm_payment')} <ArrowRight size={20} /></>}
</button>
<div className="flex items-center justify-center gap-2 text-[10px] text-slate-400 font-bold uppercase">
<Shield size={12} /> {t('checkout.ssl_encryption')}
</div>
</form>
</div>
</div>
</div>

View File

@@ -4,6 +4,8 @@
import React from 'react';
import { BookProject, UserProfile } from '@/lib/types';
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 {
user: UserProfile;
@@ -16,6 +18,8 @@ interface DashboardProps {
}
const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreate, onLogout, onPricing, onProfile }) => {
const { t } = useLanguage();
return (
<div className="min-h-screen bg-theme-bg p-8 font-sans transition-colors duration-300">
<div className="max-w-6xl mx-auto space-y-8">
@@ -28,18 +32,19 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<div className="absolute -bottom-1 -right-1 bg-green-500 w-5 h-5 rounded-full border-4 border-white" />
</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">
<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 className="flex items-center gap-3">
<LanguageSwitcher />
<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">
<User size={18} /> Mon Profil
<User size={18} /> {t('dashboard.my_profile')}
</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>
@@ -48,22 +53,22 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<div className="bg-theme-panel 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>
<p className="text-xs font-bold text-theme-muted uppercase tracking-wider">Série actuelle</p>
<p className="text-2xl font-black text-theme-text">{user.stats.writingStreak} Jours</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} {t('dashboard.days')}</p>
</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-blue-100 p-3 rounded-2xl text-blue-600"><Edit3 size={24} /></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>
</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-indigo-100 p-3 rounded-2xl text-indigo-600"><Target size={24} /></div>
<div>
<p className="text-xs font-bold text-theme-muted uppercase tracking-wider">Objectif du jour</p>
<p className="text-2xl font-black text-theme-text">{user.preferences.dailyWordGoal} Mots</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} {t('dashboard.words')}</p>
</div>
</div>
</div>
@@ -72,12 +77,12 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
{/* Project List */}
<div className="lg:col-span-2 space-y-4">
<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
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"
>
<Plus size={20} /> Écrire un nouveau livre
<Plus size={20} /> {t('dashboard.write_new')}
</button>
</div>
@@ -93,10 +98,10 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<Book size={24} />
</div>
<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 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" />
</div>
</div>
@@ -104,8 +109,8 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
{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">
<Book size={64} className="mb-6 opacity-20" />
<p className="font-bold text-lg">Prêt à commencer votre premier roman ?</p>
<button onClick={onCreate} className="mt-4 text-blue-600 font-bold hover:underline">Créer un projet maintenant</button>
<p className="font-bold text-lg">{t('dashboard.empty_projects')}</p>
<button onClick={onCreate} className="mt-4 text-blue-600 font-bold hover:underline">{t('dashboard.create_now')}</button>
</div>
)}
</div>
@@ -115,11 +120,11 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<div className="space-y-6">
<div className="bg-slate-900 text-white 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" />
<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>
<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>
</div>
<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 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>
</div>
<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>
<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>
</div>
</div>

View File

@@ -3,6 +3,7 @@
import React, { useState } from 'react';
import { BookProject } from '@/lib/types';
import { FileText, FileType, Printer, X, Download, Book, FileJson } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface ExportModalProps {
isOpen: boolean;
@@ -19,6 +20,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
const [pageSize, setPageSize] = useState<PageSize>('A4');
const [includeCover, setIncludeCover] = useState(true);
const [includeTOC, setIncludeTOC] = useState(true);
const { t } = useLanguage();
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>
<h2 className="text-xl font-bold flex items-center gap-2">
<Download size={24} /> Exporter le livre
<Download size={24} /> {t('export.title')}
</h2>
<p className="text-slate-400 text-sm mt-1">{project.title}</p>
</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'}`}
>
<Printer size={32} />
<div className="font-semibold">PDF (Impression)</div>
<div className="font-semibold">{t('export.pdf_format')}</div>
</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'}`}
>
<FileText size={32} />
<div className="font-semibold">Microsoft Word</div>
<div className="font-semibold">{t('export.word_format')}</div>
</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'}`}
>
<Book size={32} />
<div className="font-semibold">EPUB / Ebook</div>
<div className="font-semibold">{t('export.epub_format')}</div>
</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'}`}
>
<FileJson size={32} />
<div className="font-semibold">Markdown</div>
<div className="font-semibold">{t('export.markdown_format')}</div>
</button>
</div>
{/* Options Section */}
<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">
Paramètres d'exportation ({format.toUpperCase()})
{t('export.settings')} ({format.toUpperCase()})
</h3>
<div className="space-y-4">
{format === 'pdf' && (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<label className="text-slate-700 font-medium">Format du papier</label>
<span className="text-xs text-slate-400">Géré par l'imprimante (A4, A5...)</span>
<label className="text-slate-700 font-medium">{t('export.paper_format')}</label>
<span className="text-xs text-slate-400">{t('export.printer_managed')}</span>
</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 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
id="cover"
type="checkbox"
@@ -215,7 +217,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
</div>
<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
id="toc"
type="checkbox"
@@ -227,7 +229,7 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
{format === 'epub' && (
<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>
)}
</div>
@@ -240,14 +242,14 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
onClick={onClose}
className="px-5 py-2 text-slate-600 hover:bg-slate-200 rounded-lg font-medium transition-colors"
>
Annuler
{t('export.cancel')}
</button>
<button
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"
>
{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>
</div>
</div>

View File

@@ -2,41 +2,71 @@
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 {
onBack: () => void;
}
const FeaturesPage: React.FC<FeaturesPageProps> = ({ onBack }) => {
const { t } = useLanguage();
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: "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: "Story Workflow", icon: GitGraph, desc: "Visualisez votre intrigue sous forme de nœuds et gérez les embranchements de votre récit." },
{ 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: "Mise en page Pro", icon: BookOpen, desc: "Exportez au format PDF, Word ou EPUB avec une mise en page soignée et automatique." },
{ title: "Éditeur Riche", icon: Feather, desc: "Un traitement de texte complet avec mode focus et historique des modifications IA." }
{ title: t('features.feat1_title'), icon: Sparkles, desc: t('features.feat1_desc') },
{ title: t('features.feat2_title'), icon: Globe, desc: t('features.feat2_desc') },
{ title: t('features.feat3_title'), icon: GitGraph, desc: t('features.feat3_desc') },
{ title: t('features.feat4_title'), icon: Lightbulb, desc: t('features.feat4_desc') },
{ title: t('features.feat5_title'), icon: BookOpen, desc: t('features.feat5_desc') },
{ title: t('features.feat6_title'), icon: Feather, desc: t('features.feat6_desc') }
];
return (
<div className="min-h-screen bg-[#eef2ff] py-20 px-8">
<div className="max-w-7xl mx-auto">
<button onClick={onBack} className="flex items-center gap-2 text-slate-500 hover:text-blue-600 mb-12 font-bold transition-colors">
<ArrowLeft size={20} /> Retour
</button>
<h1 className="text-5xl font-black text-slate-900 mb-12 text-center">Un univers d'outils pour votre créativité.</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{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 className="w-12 h-12 bg-indigo-100 rounded-2xl flex items-center justify-center text-indigo-600 mb-6">
<f.icon size={24} />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-4">{f.title}</h3>
<p className="text-slate-600 leading-relaxed">{f.desc}</p>
</div>
))}
<div className="min-h-screen bg-[#eef2ff] font-sans">
{/* Header */}
<div className="bg-slate-900 text-white pt-20 pb-32 px-8 relative overflow-hidden">
<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>
<LanguageSwitcher />
</div>
<h1 className="text-5xl font-black text-white mb-4 text-center">{t('features.title')}</h1>
<p className="text-slate-300 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-8 -mt-20 relative z-20">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{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 className="w-12 h-12 bg-indigo-100 rounded-2xl flex items-center justify-center text-indigo-600 mb-6">
<f.icon size={24} />
</div>
<h3 className="text-xl font-bold text-slate-900 mb-4">{f.title}</h3>
<p className="text-slate-600 leading-relaxed">{f.desc}</p>
</div>
))}
</div>
</div>
{/* Footer */}
<footer className="bg-slate-900 text-slate-400 py-12 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>
);
};

View File

@@ -3,147 +3,150 @@
import React from 'react';
import { X, Keyboard, MousePointerClick, MessageCircle, Sparkles, GitGraph, BookOpen, Command, Globe, Layout, Settings, Lightbulb } from 'lucide-react';
import { ViewMode } from '@/lib/types';
import { useLanguage } from '@/providers/LanguageProvider';
interface HelpModalProps {
isOpen: boolean;
onClose: () => void;
viewMode: ViewMode;
isOpen: boolean;
onClose: () => void;
viewMode: ViewMode;
}
const Kbd: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<kbd className="px-2 py-1 text-xs font-semibold text-slate-800 bg-slate-100 border border-slate-300 rounded-md shadow-[0px_2px_0px_0px_rgba(203,213,225,1)] mx-1 font-mono">
{children}
</kbd>
<kbd className="px-2 py-1 text-xs font-semibold text-slate-800 bg-slate-100 border border-slate-300 rounded-md shadow-[0px_2px_0px_0px_rgba(203,213,225,1)] mx-1 font-mono">
{children}
</kbd>
);
const HelpModal: React.FC<HelpModalProps> = ({ isOpen, onClose, viewMode }) => {
if (!isOpen) return null;
const { t } = useLanguage();
const renderContent = () => {
switch (viewMode) {
case 'ideas':
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-yellow-600 flex items-center gap-2 border-b border-yellow-100 pb-2 mb-4">
<Lightbulb size={20} /> Boîte à Idées & Tâches
</h3>
<div className="text-sm text-slate-600 space-y-4">
<p>
Un espace de type Kanban pour ne rien oublier. Utilisez-le pour noter des idées fugaces, planifier des recherches ou lister les scènes à écrire.
</p>
<ul className="space-y-3">
<li className="flex items-start gap-2">
<MousePointerClick size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Glisser-Déposer :</span> Déplacez les cartes d'une colonne à l'autre (À faire En cours Validé) pour suivre votre progression.
</span>
</li>
<li className="flex items-start gap-2">
<Layout size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Catégories :</span> Utilisez les catégories (Intrigue, Personnage, Recherche) pour filtrer visuellement vos tâches grâce aux codes couleurs.
</span>
</li>
</ul>
</div>
</section>
);
if (!isOpen) return null;
case 'workflow':
return (
<>
{/* Workflow Section */}
const renderContent = () => {
switch (viewMode) {
case 'ideas':
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-indigo-700 flex items-center gap-2 border-b border-indigo-100 pb-2 mb-4">
<GitGraph size={20} /> Organisation Narrative
<h3 className="text-lg font-bold text-yellow-600 flex items-center gap-2 border-b border-yellow-100 pb-2 mb-4">
<Lightbulb size={20} /> Boîte à Idées & Tâches
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm text-slate-600">
<ul className="space-y-3">
<li className="flex items-start gap-2">
<MousePointerClick size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Sélection :</span> <Kbd>Ctrl</Kbd> + Clic pour sélectionner plusieurs cartes. Glissez pour déplacer tout un groupe.
</span>
</li>
<li className="flex items-start gap-2">
<Command size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Copier / Coller :</span> <Kbd>Ctrl</Kbd> + <Kbd>C</Kbd> pour copier les nœuds sélectionnés, <Kbd>Ctrl</Kbd> + <Kbd>V</Kbd> pour coller.
</span>
</li>
<li className="flex items-start gap-2">
<Layout size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Connexions :</span> Tirez depuis le cercle à droite d'une carte pour lier les événements.
</span>
</li>
</ul>
<div className="text-sm text-slate-600 space-y-4">
<p>
Un espace de type Kanban pour ne rien oublier. Utilisez-le pour noter des idées fugaces, planifier des recherches ou lister les scènes à écrire.
</p>
<ul className="space-y-3">
<li className="flex items-start gap-2">
<MousePointerClick size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Glisser-Déposer :</span> Déplacez les cartes d'une colonne à l'autre (À faire En cours Validé) pour suivre votre progression.
</span>
</li>
<li className="flex items-start gap-2">
<Layout size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Catégories :</span> Utilisez les catégories (Intrigue, Personnage, Recherche) pour filtrer visuellement vos tâches grâce aux codes couleurs.
</span>
</li>
</ul>
</div>
</section>
);
{/* Dialogue Intelligent */}
<section className="bg-blue-50 p-6 rounded-xl border border-blue-100 mb-8">
<h3 className="text-lg font-bold text-blue-800 flex items-center gap-2 border-b border-blue-200 pb-2 mb-4">
<MessageCircle size={20} /> Mode Dialogue (Workflow)
case 'workflow':
return (
<>
{/* Workflow Section */}
<section className="mb-8">
<h3 className="text-lg font-bold text-indigo-700 flex items-center gap-2 border-b border-indigo-100 pb-2 mb-4">
<GitGraph size={20} /> Organisation Narrative
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm text-slate-600">
<ul className="space-y-3">
<li className="flex items-start gap-2">
<MousePointerClick size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Sélection :</span> <Kbd>Ctrl</Kbd> + Clic pour sélectionner plusieurs cartes. Glissez pour déplacer tout un groupe.
</span>
</li>
<li className="flex items-start gap-2">
<Command size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Copier / Coller :</span> <Kbd>Ctrl</Kbd> + <Kbd>C</Kbd> pour copier les nœuds sélectionnés, <Kbd>Ctrl</Kbd> + <Kbd>V</Kbd> pour coller.
</span>
</li>
<li className="flex items-start gap-2">
<Layout size={16} className="mt-0.5 shrink-0" />
<span>
<span className="font-semibold text-slate-800">Connexions :</span> Tirez depuis le cercle à droite d'une carte pour lier les événements.
</span>
</li>
</ul>
</div>
</section>
{/* Dialogue Intelligent */}
<section className="bg-blue-50 p-6 rounded-xl border border-blue-100 mb-8">
<h3 className="text-lg font-bold text-blue-800 flex items-center gap-2 border-b border-blue-200 pb-2 mb-4">
<MessageCircle size={20} /> Mode Dialogue (Workflow)
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm">
<div>
<div className="font-semibold text-slate-800 mb-1">Écriture Rapide</div>
<p className="text-slate-600 leading-relaxed mb-3">
Tapez un nom et <Kbd>Entrée</Kbd> : le formatage <code>Nom: </code> s'ajoute seul.
</p>
<p className="text-slate-600 leading-relaxed">
Dans un dialogue, <Kbd>Entrée</Kbd> change de ligne et <strong>devine le prochain interlocuteur</strong> automatiquement.
</p>
</div>
<div>
<div className="font-semibold text-slate-800 mb-1">Rotation & Insertion</div>
<p className="text-slate-600 leading-relaxed mb-3">
<Kbd>Tab</Kbd> permute instantanément entre les personnages présents dans la scène.
</p>
<p className="text-slate-600 leading-relaxed">
Utilisez <Kbd>@</Kbd> pour insérer un personnage, <Kbd>#</Kbd> pour un lieu.
</p>
</div>
</div>
</section>
</>
);
case 'world_building':
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-green-700 flex items-center gap-2 border-b border-green-100 pb-2 mb-4">
<Globe size={20} /> Bible du Monde
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 text-sm">
<div>
<div className="font-semibold text-slate-800 mb-1">Écriture Rapide</div>
<p className="text-slate-600 leading-relaxed mb-3">
Tapez un nom et <Kbd>Entrée</Kbd> : le formatage <code>Nom: </code> s'ajoute seul.
<div className="text-sm text-slate-600 space-y-4">
<p>
La bible du monde permet de centraliser toutes les informations sur vos personnages et lieux.
Ces informations sont <strong>lues par l'IA</strong> pour assurer la cohérence de l'histoire.
</p>
<p className="text-slate-600 leading-relaxed">
Dans un dialogue, <Kbd>Entrée</Kbd> change de ligne et <strong>devine le prochain interlocuteur</strong> automatiquement.
</p>
</div>
<div>
<div className="font-semibold text-slate-800 mb-1">Rotation & Insertion</div>
<p className="text-slate-600 leading-relaxed mb-3">
<Kbd>Tab</Kbd> permute instantanément entre les personnages présents dans la scène.
</p>
<p className="text-slate-600 leading-relaxed">
Utilisez <Kbd>@</Kbd> pour insérer un personnage, <Kbd>#</Kbd> pour un lieu.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
<div className="bg-slate-50 p-4 rounded-lg">
<h4 className="font-bold text-slate-800 mb-2">Modèles Personnalisés</h4>
<p>
Cliquez sur le bouton "Modèles" pour ajouter des champs spécifiques (ex: "Type de Magie", "Allégeance") à tous vos personnages ou lieux.
</p>
</div>
<div className="bg-slate-50 p-4 rounded-lg">
<h4 className="font-bold text-slate-800 mb-2">Contexte Automatique</h4>
<p>
Le champ "Contexte Narratif" se remplit automatiquement au fur et à mesure que vous écrivez votre histoire et que l'IA détecte l'évolution des personnages.
</p>
</div>
</div>
</div>
</section>
</>
);
case 'world_building':
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-green-700 flex items-center gap-2 border-b border-green-100 pb-2 mb-4">
<Globe size={20} /> Bible du Monde
</h3>
<div className="text-sm text-slate-600 space-y-4">
<p>
La bible du monde permet de centraliser toutes les informations sur vos personnages et lieux.
Ces informations sont <strong>lues par l'IA</strong> pour assurer la cohérence de l'histoire.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mt-4">
<div className="bg-slate-50 p-4 rounded-lg">
<h4 className="font-bold text-slate-800 mb-2">Modèles Personnalisés</h4>
<p>
Cliquez sur le bouton "Modèles" pour ajouter des champs spécifiques (ex: "Type de Magie", "Allégeance") à tous vos personnages ou lieux.
</p>
</div>
<div className="bg-slate-50 p-4 rounded-lg">
<h4 className="font-bold text-slate-800 mb-2">Contexte Automatique</h4>
<p>
Le champ "Contexte Narratif" se remplit automatiquement au fur et à mesure que vous écrivez votre histoire et que l'IA détecte l'évolution des personnages.
</p>
</div>
</div>
</div>
</section>
);
);
case 'settings':
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-slate-700 flex items-center gap-2 border-b border-slate-100 pb-2 mb-4">
<Settings size={20} /> Paramètres du Livre
<Settings size={20} /> Paramètres du Livre
</h3>
<p className="text-sm text-slate-600 mb-4">
Ces réglages sont cruciaux pour l'Assistant IA. Ils définissent le "ton" de toutes les générations de texte.
@@ -155,131 +158,131 @@ const HelpModal: React.FC<HelpModalProps> = ({ isOpen, onClose, viewMode }) => {
</section>
);
case 'write':
default:
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-amber-600 flex items-center gap-2 border-b border-amber-100 pb-2 mb-4">
<Sparkles size={20} /> Éditeur & Assistant IA
</h3>
<div className="space-y-4 text-sm text-slate-600">
<div className="bg-amber-50 p-4 rounded-lg border border-amber-100">
<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>
<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"/>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"/>Continuer l'écriture</li>
</ul>
case 'write':
default:
return (
<section className="mb-8">
<h3 className="text-lg font-bold text-amber-600 flex items-center gap-2 border-b border-amber-100 pb-2 mb-4">
<Sparkles size={20} /> Éditeur & Assistant IA
</h3>
<div className="space-y-4 text-sm text-slate-600">
<div className="bg-amber-50 p-4 rounded-lg border border-amber-100">
<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>
<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" />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" />Continuer l'écriture</li>
</ul>
</div>
<p>
<span className="font-semibold text-slate-800">Historique des versions :</span> Activez la marge de droite (icône horloge) pour voir toutes les interventions de l'IA et revenir en arrière si nécessaire.
</p>
<p>
<span className="font-semibold text-slate-800">Chat Latéral :</span> Posez des questions sur votre histoire, demandez des résumés ou des idées de rebondissements. L'IA connaît le contexte de vos chapitres précédents et de vos fiches personnages.
</p>
<div className="mt-6 border-t border-slate-100 pt-4">
<h4 className="font-bold text-slate-700 mb-3 flex items-center gap-2">
<Keyboard size={16} /> Raccourcis Clavier (Éditeur)
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-slate-50 p-4 rounded-lg border border-slate-100">
<div className="space-y-3">
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Mettre en Gras</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>B</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Mettre en Italique</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>I</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Souligner</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>U</Kbd></span>
</div>
</div>
<div className="space-y-3">
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Tout sélectionner</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>A</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Annuler</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>Z</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Rétablir</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>Shift</Kbd> + <Kbd>Z</Kbd></span>
</div>
</div>
</div>
</div>
</div>
</section>
);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-white rounded-xl shadow-2xl w-[800px] max-h-[90vh] flex flex-col overflow-hidden">
{/* Header */}
<div className="bg-slate-900 text-white p-6 flex justify-between items-center shrink-0">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<BookOpen size={24} className="text-blue-400" /> {t('help.title')} : {
viewMode === 'workflow' ? t('help.workflow_title_doc') :
viewMode === 'world_building' ? t('help.world_building_title') :
viewMode === 'settings' ? t('help.settings_title') :
viewMode === 'ideas' ? t('help.ideas_title') :
t('help.editor_ai_title')
}
</h2>
<p className="text-slate-400 text-sm mt-1">{t('help.subtitle')}</p>
</div>
<p>
<span className="font-semibold text-slate-800">Historique des versions :</span> Activez la marge de droite (icône horloge) pour voir toutes les interventions de l'IA et revenir en arrière si nécessaire.
</p>
<p>
<span className="font-semibold text-slate-800">Chat Latéral :</span> Posez des questions sur votre histoire, demandez des résumés ou des idées de rebondissements. L'IA connaît le contexte de vos chapitres précédents et de vos fiches personnages.
</p>
<div className="mt-6 border-t border-slate-100 pt-4">
<h4 className="font-bold text-slate-700 mb-3 flex items-center gap-2">
<Keyboard size={16} /> Raccourcis Clavier (Éditeur)
</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-slate-50 p-4 rounded-lg border border-slate-100">
<div className="space-y-3">
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Mettre en Gras</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>B</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Mettre en Italique</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>I</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Souligner</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>U</Kbd></span>
</div>
</div>
<div className="space-y-3">
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Tout sélectionner</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>A</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Annuler</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>Z</Kbd></span>
</div>
<div className="flex justify-between items-center text-xs">
<span className="text-slate-600">Rétablir</span>
<span><Kbd>Ctrl</Kbd> + <Kbd>Shift</Kbd> + <Kbd>Z</Kbd></span>
</div>
</div>
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors p-2 hover:bg-slate-800 rounded-full">
<X size={24} />
</button>
</div>
{/* Content */}
<div className="overflow-y-auto p-8">
{/* Context Specific Content */}
{renderContent()}
{/* General Footer Section (Always visible) */}
<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">{t('help.general_shortcuts')}</h4>
<div className="grid grid-cols-2 gap-4 text-xs text-slate-600">
<div className="flex justify-between">
<span>{t('help.auto_save')}</span>
<span className="font-mono text-slate-400">{t('help.permanent')}</span>
</div>
<div className="flex justify-between">
<span>{t('help.side_menu')}</span>
<span>{t('help.click_burger')}</span>
</div>
</div>
</div>
</div>
</section>
);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-in fade-in duration-200">
<div className="bg-white rounded-xl shadow-2xl w-[800px] max-h-[90vh] flex flex-col overflow-hidden">
{/* Header */}
<div className="bg-slate-900 text-white p-6 flex justify-between items-center shrink-0">
<div>
<h2 className="text-xl font-bold flex items-center gap-2">
<BookOpen size={24} className="text-blue-400" /> Aide : {
viewMode === 'workflow' ? 'Workflow & Dialogues' :
viewMode === 'world_building' ? 'Bible du Monde' :
viewMode === 'settings' ? 'Paramètres' :
viewMode === 'ideas' ? 'Boîte à Idées' :
'Éditeur & IA'
}
</h2>
<p className="text-slate-400 text-sm mt-1">Astuces pour l'écran actuel.</p>
</div>
<button onClick={onClose} className="text-slate-400 hover:text-white transition-colors p-2 hover:bg-slate-800 rounded-full">
<X size={24} />
</button>
</div>
{/* Content */}
<div className="overflow-y-auto p-8">
{/* Context Specific Content */}
{renderContent()}
{/* General Footer Section (Always visible) */}
<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>
<div className="grid grid-cols-2 gap-4 text-xs text-slate-600">
<div className="flex justify-between">
<span>Sauvegarde Automatique</span>
<span className="font-mono text-slate-400">Permanente</span>
</div>
<div className="flex justify-between">
<span>Menu Latéral</span>
<span>Clic sur le burger</span>
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-slate-200 bg-slate-50 flex justify-end">
<button
onClick={onClose}
className="px-6 py-2 bg-slate-800 text-white rounded-lg hover:bg-slate-900 transition-colors font-medium"
>
{t('help.close')}
</button>
</div>
</div>
</div>
{/* Footer */}
<div className="p-4 border-t border-slate-200 bg-slate-50 flex justify-end">
<button
onClick={onClose}
className="px-6 py-2 bg-slate-800 text-white rounded-lg hover:bg-slate-900 transition-colors font-medium"
>
Fermer
</button>
</div>
</div>
</div>
);
);
};
export default HelpModal;

View File

@@ -3,28 +3,31 @@
import React, { useState } from 'react';
import { Idea, IdeaStatus, IdeaCategory } from '@/lib/types';
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 {
ideas: Idea[];
onUpdate: (ideas: Idea[]) => void;
}
const CATEGORIES: Record<IdeaCategory, { label: string, color: string, icon: any }> = {
plot: { label: 'Intrigue', 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 },
research: { label: 'Recherche', 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 },
const CATEGORIES: Record<IdeaCategory, { labelKey: string, color: string, icon: any }> = {
plot: { labelKey: 'ideaboard.cat_plot', color: 'bg-rose-100 text-rose-800 border-rose-200', icon: Lightbulb },
character: { labelKey: 'ideaboard.cat_char', color: 'bg-blue-100 text-blue-800 border-blue-200', icon: Search },
research: { labelKey: 'ideaboard.cat_research', color: 'bg-amber-100 text-amber-800 border-amber-200', icon: Search },
todo: { labelKey: 'ideaboard.cat_todo', color: 'bg-slate-100 text-slate-800 border-slate-200', icon: CheckCircle },
};
const STATUS_LABELS: Record<IdeaStatus, string> = {
todo: 'Idées / À faire',
progress: 'En cours',
done: 'Terminé / Validé'
todo: 'ideaboard.stat_todo',
progress: 'ideaboard.stat_prog',
done: 'ideaboard.stat_done'
};
const MAX_DESCRIPTION_LENGTH = 500;
const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
const { t } = useLanguage();
const [newIdeaTitle, setNewIdeaTitle] = useState('');
const [newIdeaCategory, setNewIdeaCategory] = useState<IdeaCategory>('plot');
@@ -54,7 +57,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
};
const handleDelete = (id: string) => {
if (confirm("Supprimer cette carte ?")) {
if (confirm(t('ideaboard.delete') + " ?")) {
onUpdate(ideas.filter(i => i.id !== id));
if (editingItem?.id === id) setEditingItem(null);
}
@@ -128,12 +131,12 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, status)}
onDoubleClick={() => openQuickAdd(status)}
title="Double-cliquez dans le vide pour ajouter une carte ici"
title={t('ideaboard.empty_desc')}
>
{/* 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' :
status === 'progress' ? 'bg-indigo-500/10' :
'bg-green-500/10'
status === 'progress' ? 'bg-indigo-500/10' :
'bg-green-500/10'
}`}>
<div className="flex items-center gap-2 font-bold text-theme-text">
<Icon size={18} />
@@ -172,7 +175,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
>
<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}`}>
{CATEGORIES[idea.category].label}
{t(CATEGORIES[idea.category].labelKey as TranslationKey)}
</span>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button
@@ -190,7 +193,6 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
</div>
</div>
{/* CARD CONTENT */}
<div className="mb-2">
<h4 className="font-bold text-theme-text text-sm mb-1 leading-tight">{idea.title}</h4>
{idea.description && (
@@ -211,8 +213,8 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
})}
{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">
<span className="mb-2">Vide</span>
<span className="text-xs opacity-70">Double-cliquez pour ajouter</span>
<span className="mb-2">{t('ideaboard.empty')}</span>
<span className="text-xs opacity-70">{t('ideaboard.empty_desc')}</span>
</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>
<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>
<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>
<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"
>
{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>
<input
type="text"
value={newIdeaTitle}
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"
/>
<button
@@ -261,9 +263,9 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
{/* Kanban Board */}
<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="En cours" status="progress" icon={Clock} />
<Column title="Terminé" status="done" icon={CheckCircle} />
<Column title={t('ideaboard.stat_todo')} status="todo" icon={Circle} />
<Column title={t('ideaboard.stat_prog')} status="progress" icon={Clock} />
<Column title={t('ideaboard.stat_done')} status="done" icon={CheckCircle} />
</div>
{/* 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">
<h3 className="font-bold text-theme-text flex items-center gap-2">
{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>
<button onClick={() => setEditingItem(null)} className="text-theme-muted hover:text-theme-text">
<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>
<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
type="text"
value={editingItem.title}
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"
placeholder="Titre de la tâche ou de l'idée..."
placeholder={t('ideaboard.add_idea')}
autoFocus
/>
</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
value={editingItem.description}
onChange={(e) => setEditingItem({ ...editingItem, description: e.target.value })}
@@ -303,7 +305,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
placeholder="Détails, notes, liens..."
/>
<div className={`text-right text-xs mt-1 transition-colors ${(editingItem.description?.length || 0) >= MAX_DESCRIPTION_LENGTH ? 'text-red-500 font-bold' :
(editingItem.description?.length || 0) > MAX_DESCRIPTION_LENGTH * 0.9 ? 'text-orange-500' : 'text-slate-400'
(editingItem.description?.length || 0) > MAX_DESCRIPTION_LENGTH * 0.9 ? 'text-orange-500' : 'text-slate-400'
}`}>
{editingItem.description?.length || 0} / {MAX_DESCRIPTION_LENGTH} caractères
</div>
@@ -311,26 +313,26 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
<div className="grid grid-cols-2 gap-4">
<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
value={editingItem.category}
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"
>
{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>
</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
value={editingItem.status}
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"
>
{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>
</div>
@@ -343,21 +345,21 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
onClick={() => handleDelete(editingItem.id!)}
className="mr-auto text-red-500 hover:text-red-700 text-sm font-medium px-3 py-2"
>
Supprimer
{t('ideaboard.delete')}
</button>
)}
<button
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"
>
Annuler
{t('ideaboard.cancel')}
</button>
<button
onClick={handleSaveEdit}
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"
>
<Save size={16} /> Enregistrer
<Save size={16} /> {t('ideaboard.save')}
</button>
</div>
</div>

View File

@@ -1,95 +1,106 @@
'use client';
import React from '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 {
onLogin: () => void;
onPricing: () => void;
onFeatures: () => void;
onLogin: () => void;
onPricing: () => void;
onFeatures: () => void;
}
const LandingPage: React.FC<LandingPageProps> = ({ onLogin, onPricing, onFeatures }) => {
return (
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
{/* 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">
<div className="flex items-center gap-2">
<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">PlumeIA</span>
</div>
<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={onPricing} className="hover:text-blue-600 transition-colors">Tarifs</button>
<a href="#" className="hover:text-blue-600 transition-colors">Blog</a>
</div>
<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>
<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>
</div>
</nav>
const { t } = useLanguage();
{/* Hero Section */}
<header className="pt-32 pb-20 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">
<Sparkles size={14} className="animate-pulse" /> NOUVEAUTÉ : GÉNÉRATION DE BIBLE DU MONDE PAR IA
</div>
<h1 className="text-5xl md:text-7xl font-black text-slate-900 leading-[1.1] mb-6">
L'écriture d'un roman, <br />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-600 to-indigo-500">augmentée par l'IA.</span>
</h1>
<p className="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.
</p>
<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">
Commencer mon livre <ArrowRight size={20} />
</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">
Voir la démo
</button>
</div>
<div className="mt-20 relative">
<div className="absolute -inset-4 bg-gradient-to-r from-blue-500/20 to-indigo-500/20 blur-3xl -z-10 rounded-full" />
<div className="bg-white rounded-2xl shadow-2xl border border-indigo-100 p-2 overflow-hidden max-w-5xl mx-auto">
<img
src="https://images.unsplash.com/photo-1455390582262-044cdead277a?auto=format&fit=crop&q=80&w=2000"
alt="Editor Preview"
className="rounded-xl object-cover h-[500px] w-full"
/>
</div>
</div>
</header>
return (
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
{/* 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">
<div className="flex items-center gap-2">
<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>
</div>
<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">{t('landing.nav_features')}</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">{t('landing.nav_blog')}</a>
</div>
<div className="flex items-center gap-4">
<LanguageSwitcher />
<button onClick={onLogin} className="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-5 py-2.5 rounded-full text-sm font-bold hover:bg-blue-600 transition-all shadow-lg hover:shadow-blue-200">{t('landing.free_trial')}</button>
</div>
</nav>
{/* Social Proof */}
<section className="bg-white py-24 px-8 border-y border-indigo-100">
<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>
<div className="grid grid-cols-2 md:grid-cols-4 gap-12 items-center grayscale opacity-60">
<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-3xl font-serif">L'Éditeur</span>
<span className="text-2xl font-sans font-black tracking-tight underline underline-offset-4 decoration-blue-500">Novelty</span>
</div>
</div>
</section>
{/* Hero Section */}
<header className="pt-32 pb-20 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">
<Sparkles size={14} className="animate-pulse" /> {t('landing.new_feature')}
</div>
<h1 className="text-5xl md:text-7xl font-black text-slate-900 leading-[1.1] mb-6">
<span dangerouslySetInnerHTML={{ __html: t('landing.hero_title') }}></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>
<p className="text-xl text-slate-600 max-w-2xl mx-auto mb-10 leading-relaxed">
{t('landing.hero_description')}
</p>
<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">
{t('landing.start_book')} <ArrowRight size={20} />
</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">
{t('landing.see_demo')}
</button>
</div>
{/* Footer */}
<footer className="bg-slate-900 text-slate-400 py-12 px-8 text-center">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-center gap-2 text-white mb-6">
<Book className="text-blue-500" size={24} />
<span className="text-xl font-bold">PlumeIA</span>
</div>
<p className="text-sm">© 2024 PlumeIA. Tous droits réservés.</p>
<div className="mt-20 relative">
<div className="absolute -inset-4 bg-gradient-to-r from-blue-500/20 to-indigo-500/20 blur-3xl -z-10 rounded-full" />
<div className="bg-white rounded-2xl shadow-2xl border border-indigo-100 p-2 overflow-hidden max-w-5xl mx-auto">
<img
src="https://images.unsplash.com/photo-1455390582262-044cdead277a?auto=format&fit=crop&q=80&w=2000"
alt="Editor Preview"
className="rounded-xl object-cover h-[500px] w-full"
/>
</div>
</div>
</header>
{/* Social Proof */}
<section className="bg-white py-24 px-8 border-y border-indigo-100">
<div className="max-w-7xl mx-auto text-center">
<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">
<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-3xl font-serif">L'Éditeur</span>
<span className="text-2xl font-sans font-black tracking-tight underline underline-offset-4 decoration-blue-500">Novelty</span>
</div>
</div>
</section>
{/* Footer */}
<footer className="bg-slate-900 text-slate-400 py-12 px-8 text-center">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-center gap-2 text-white mb-6">
<Book className="text-blue-500" size={24} />
<span className="text-xl font-bold">Pluume</span>
</div>
<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>
</footer>
</div>
</footer>
</div>
);
);
};
export default LandingPage;

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 { useAuthContext } from '@/providers/AuthProvider';
import { Loader2, AlertCircle, ArrowRight } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface LoginPageProps {
onSuccess: () => void;
@@ -17,6 +18,7 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
// Use the global auth context
const { login } = useAuthContext();
const { t } = useLanguage();
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
@@ -37,8 +39,8 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
{/* 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="text-center mb-8">
<h1 className="text-3xl font-black text-slate-900 mb-2">Connexion</h1>
<p className="text-slate-500">Bienvenue ! Connectez-vous à votre compte</p>
<h1 className="text-3xl font-black text-slate-900 mb-2">{t('auth.login_title')}</h1>
<p className="text-slate-500">{t('auth.login_subtitle')}</p>
</div>
{error && (
@@ -50,12 +52,12 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
<form onSubmit={handleSubmit} className="space-y-4">
<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
id="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"
placeholder="votre@email.com"
placeholder={t('auth.email_placeholder')}
value={email}
onChange={(e) => setEmail(e.target.value)}
required
@@ -63,12 +65,12 @@ const LoginPage: React.FC<LoginPageProps> = ({ onSuccess, onRegister }) => {
</div>
<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
id="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"
placeholder="••••••••"
placeholder={t('auth.password_placeholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
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"
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>
</form>
<div className="mt-8 text-center text-sm text-slate-500">
Pas encore de compte ?{" "}
{t('auth.not_registered')}{" "}
<button
onClick={onRegister}
className="font-bold text-blue-600 hover:text-blue-800 transition-colors ml-1"
>
Créer un compte
{t('auth.create_account')}
</button>
</div>
</div>

View File

@@ -3,6 +3,9 @@
import React from 'react';
import { Check, ArrowLeft } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import Link from 'next/link';
interface PlanData {
id: string;
@@ -23,23 +26,28 @@ interface PricingProps {
}
const Pricing: React.FC<PricingProps> = ({ plans, currentPlan, onBack, onSelectPlan, isLoading }) => {
const { t } = useLanguage();
return (
<div className="min-h-screen bg-[#eef2ff] py-20 px-8">
<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">
<ArrowLeft size={20} /> Retour
</button>
<div className="flex justify-between items-center mb-12">
<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>
<LanguageSwitcher />
</div>
<div className="text-center mb-16">
<h2 className="text-4xl font-black text-slate-900 mb-4">Choisissez votre destin d'écrivain.</h2>
<p className="text-slate-500">Passez au plan supérieur pour libérer toute la puissance de l'IA.</p>
<h2 className="text-4xl font-black text-slate-900 mb-4">{t('pricing.title')}</h2>
<p className="text-slate-500">{t('pricing.subtitle')}</p>
</div>
<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) => (
<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">
<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>
</div>
<ul className="space-y-4 mb-10">
@@ -54,12 +62,23 @@ const Pricing: React.FC<PricingProps> = ({ plans, currentPlan, onBack, onSelectP
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'}`}
>
{p.id === currentPlan ? 'Plan Actuel' : 'Sélectionner'}
{p.id === currentPlan ? t('pricing.current_plan') : t('pricing.select')}
</button>
</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>
);
};

View File

@@ -4,6 +4,7 @@
import React, { useState, useRef, useEffect, useCallback, useMemo } from 'react';
import { WorkflowData, PlotNode, PlotConnection, PlotNodeType, Entity, EntityType } from '@/lib/types';
import { Plus, Trash2, ArrowRight, BookOpen, MessageCircle, Zap, Palette, Save, Link2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface StoryWorkflowProps {
data: WorkflowData;
@@ -24,8 +25,8 @@ const INITIAL_COLORS = [
'#f3e8ff', // Purple
];
const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id: string) => void) => {
if (!text) return <span className="text-slate-400 italic">Description...</span>;
const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id: string) => void, t: any) => {
if (!text) return <span className="text-slate-400 italic">{t('sw.desc_ph')}</span>;
const parts: (string | React.ReactNode)[] = [text];
@@ -45,7 +46,7 @@ const renderTextWithLinks = (text: string, entities: Entity[], onNavigate: (id:
key={`${entity.id}-${idx}`}
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"
title={`Voir la fiche de ${entity.name}`}
title={t('sw.see_sheet') + entity.name}
>
{s}
</span>
@@ -92,12 +93,12 @@ const StoryNode = React.memo(({
onToggleColorPicker, onSaveColor, onNavigateToEntity,
onInputFocus, onInputCheckAutocomplete, onKeyDownInInput
}: StoryNodeProps) => {
const { t } = useLanguage();
const [showTypePicker, setShowTypePicker] = useState(false);
const richDescription = useMemo(() => {
return renderTextWithLinks(node.description, entities, onNavigateToEntity);
}, [node.description, entities, onNavigateToEntity]);
return renderTextWithLinks(node.description, entities, onNavigateToEntity, t);
}, [node.description, entities, onNavigateToEntity, t]);
return (
<div
@@ -170,7 +171,7 @@ const StoryNode = React.memo(({
onClick={() => onSaveColor(node.color || '#ffffff')}
className="text-[10px] font-bold text-indigo-600 hover:text-indigo-800 hover:underline flex-1 text-right"
>
+ SAUVER
{t('sw.save_color')}
</button>
</div>
</div>
@@ -181,7 +182,7 @@ const StoryNode = React.memo(({
{isEditing ? (
<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'}`}
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}
onChange={(e) => onInputCheckAutocomplete(e, node.id, 'description')}
onKeyDown={(e) => onKeyDownInInput(e, node.id)}
@@ -204,21 +205,21 @@ const StoryNode = React.memo(({
<button
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' : ''}`}
title="Narration"
title={t('sw.type_story')}
>
<BookOpen size={14} className="text-slate-500" />
</button>
<button
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' : ''}`}
title="Action"
title={t('sw.type_action')}
>
<Zap size={14} className="text-amber-500" />
</button>
<button
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' : ''}`}
title="Dialogue"
title={t('sw.type_dialogue')}
>
<MessageCircle size={14} className="text-blue-500" />
</button>
@@ -267,6 +268,7 @@ interface SuggestionState {
}
const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities, onNavigateToEntity }) => {
const { t } = useLanguage();
const containerRef = useRef<HTMLDivElement>(null);
const rafRef = useRef<number | null>(null);
@@ -569,7 +571,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
id: `node-${Date.now()}`,
x,
y,
title: 'Nouvel événement',
title: t('sw.new_event'),
description: '',
color: INITIAL_COLORS[0],
type: 'story'
@@ -598,7 +600,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
id: `node-${Date.now()}`,
x: scrollLeft + clientWidth / 2 - CARD_WIDTH / 2,
y: scrollTop + clientHeight / 2 - CARD_HEIGHT / 2,
title: 'Nouveau point d\'intrigue',
title: t('sw.new_plot_point'),
description: '',
color: INITIAL_COLORS[0],
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="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">
<Plus size={14} /> AJOUTER NŒUD
<Plus size={14} /> {t('sw.add_node')}
</button>
<div className="w-px h-6 bg-theme-border mx-2" />
<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 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} />
</button>
</div>
@@ -698,7 +700,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
{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="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 className="divide-y divide-slate-50">
{activeSuggestion.filteredEntities.length > 0 ? (
@@ -712,7 +714,7 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
</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>

View File

@@ -4,6 +4,8 @@
import React, { useState } from 'react';
import { UserProfile, UserPreferences } from '@/lib/types';
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 {
user: UserProfile;
@@ -20,6 +22,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
hasOnBack: !!onBack
});
const { t } = useLanguage();
const [activeTab, setActiveTab] = useState<'profile' | 'preferences' | 'account'>('profile');
const [formData, setFormData] = useState({
name: user.name,
@@ -36,7 +39,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
const file = event.target.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
alert('Veuillez sélectionner une image valide.');
alert(t('profile.invalid_image') || 'Veuillez sélectionner une image valide.');
return;
}
@@ -83,7 +86,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
dailyWordGoal: formData.dailyWordGoal
}
});
alert("Profil mis à jour !");
alert(t('profile.saved_success') || "Profil mis à jour !");
};
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="flex justify-between items-center mb-10">
<div>
<h1 className={`text-3xl font-black ${themeTextHeading}`}>Mon Compte</h1>
<p className={themeTextMuted}>Gérez vos informations personnelles et préférences d'écriture.</p>
<h1 className={`text-3xl font-black ${themeTextHeading}`}>{t('profile.title')}</h1>
<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>
<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 className="flex flex-col md:flex-row gap-8">
@@ -115,19 +121,19 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
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}`}
>
<User size={18} /> Profil Public
<User size={18} /> {t('profile.tab_public')}
</button>
<button
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}`}
>
<Layout size={18} /> Interface & Écriture
<Layout size={18} /> {t('profile.tab_interface')}
</button>
<button
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}`}
>
<Shield size={18} /> Sécurité & Plan
<Shield size={18} /> {t('profile.tab_security')}
</button>
</div>
@@ -145,16 +151,16 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
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" />
<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} />
</div>
</div>
<div>
<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="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>
@@ -162,7 +168,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="grid grid-cols-1 gap-6">
<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
type="text"
value={formData.name}
@@ -172,12 +178,12 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
</div>
<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
value={formData.bio}
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}`}
placeholder="Partagez quelques mots sur votre style..."
placeholder={t('profile.bio_placeholder')}
/>
</div>
</div>
@@ -189,7 +195,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="grid grid-cols-1 gap-8">
<div className="space-y-3">
<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>
<div className="flex items-center gap-4">
<input
@@ -204,7 +210,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className="space-y-3">
<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>
<div className="grid grid-cols-3 gap-3">
{['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="p-4 bg-blue-50 border border-blue-100 rounded-xl flex justify-between items-center">
<div>
<h4 className="font-bold text-blue-900">Plan {(user.subscription.planDetails?.displayName || user.subscription.plan).toUpperCase()}</h4>
<p className="text-xs text-blue-700">Abonnement actif</p>
<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">{t('profile.active_sub')}</p>
</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 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
type="email"
value={formData.email}
@@ -244,7 +250,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
</div>
<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>
)}
@@ -254,7 +260,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
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'}`}
>
<Save size={18} /> Sauvegarder les modifications
<Save size={18} /> {t('profile.save_changes')}
</button>
</div>
</div>

View File

@@ -1,6 +1,7 @@
'use client';
import React, { useState, useMemo, useEffect } from 'react';
import { useLanguage } from '@/providers/LanguageProvider';
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 { 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 { t } = useLanguage();
const [editingId, setEditingId] = useState<string | null>(null);
const [tempEntity, setTempEntity] = useState<Entity | null>(null);
const [mode, setMode] = useState<'entities' | 'templates'>('entities');
@@ -102,7 +104,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
};
const handleDelete = (id: string) => {
if (confirm('Supprimer cet élément ?')) {
if (confirm(t('wb.delete_confirm'))) {
onDelete(id);
if (editingId === id) {
setEditingId(null);
@@ -149,7 +151,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
const addCustomField = (type: EntityType) => {
const newField: CustomFieldDefinition = {
id: `field-${Date.now()}`,
label: 'Nouveau Champ',
label: t('wb.new_field'),
type: 'text',
placeholder: ''
};
@@ -209,31 +211,31 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
{/* SECTION 1: ROLE & ARCHETYPE */}
<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">
<User size={16} /> Identité Narrative
<User size={16} /> {t('wb.id_narrative')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<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
type="text"
list="archetype-suggestions"
value={attrs.archetype}
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"
placeholder="Ex: Le Héros, Le Sage..."
placeholder={t('wb.archetype_ph')}
/>
<datalist id="archetype-suggestions">
{allArchetypes.map(a => <option key={a} value={a} />)}
</datalist>
</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">
{[
{ val: 'protagonist', label: 'Protagoniste' },
{ val: 'antagonist', label: 'Antagoniste' },
{ val: 'support', label: 'Secondaire' },
{ val: 'extra', label: 'Figurant' }
{ val: 'protagonist', label: t('wb.role_protagonist') },
{ val: 'antagonist', label: t('wb.role_antagonist') },
{ val: 'support', label: t('wb.role_support') },
{ val: 'extra', label: t('wb.role_extra') }
].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'}`}>
<input
@@ -255,13 +257,13 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
{/* SECTION 2: PHYSIQUE */}
<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">
<Ruler size={16} /> Apparence Physique
<Ruler size={16} /> {t('wb.appearance')}
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-6">
<div>
<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 className="flex items-center gap-3">
<input
@@ -281,7 +283,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div>
<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 className="flex items-center gap-3">
<input
@@ -303,7 +305,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<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
value={attrs.hair}
onChange={(e) => updateAttribute('hair', e.target.value)}
@@ -313,7 +315,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</select>
</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
value={attrs.eyes}
onChange={(e) => updateAttribute('eyes', e.target.value)}
@@ -324,12 +326,12 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</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
type="text"
value={attrs.physicalQuirk}
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"
/>
</div>
@@ -340,15 +342,15 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
{/* SECTION 3: PSYCHOLOGIE */}
<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">
<Brain size={16} /> Psychologie & Comportement
<Brain size={16} /> {t('wb.psychology')}
</h3>
<div className="space-y-6">
<div className="space-y-4 px-2">
<div className="relative pt-1">
<div className="flex justify-between text-[10px] uppercase font-bold text-theme-muted mb-1">
<span>Introverti</span>
<span>Extraverti</span>
<span>{t('wb.introvert')}</span>
<span>{t('wb.extravert')}</span>
</div>
<input
type="range" min="0" max="100"
@@ -359,8 +361,8 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div>
<div className="relative pt-1">
<div className="flex justify-between text-[10px] uppercase font-bold text-theme-muted mb-1">
<span>Émotionnel</span>
<span>Rationnel</span>
<span>{t('wb.emotional')}</span>
<span>{t('wb.rational')}</span>
</div>
<input
type="range" min="0" max="100"
@@ -371,8 +373,8 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div>
<div className="relative pt-1">
<div className="flex justify-between text-[10px] uppercase font-bold text-theme-muted mb-1">
<span>Chaotique</span>
<span>Loyal</span>
<span>{t('wb.chaotic')}</span>
<span>{t('wb.lawful')}</span>
</div>
<input
type="range" min="0" max="100"
@@ -384,12 +386,12 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div>
<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
type="text"
value={attrs.behavioralQuirk}
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"
/>
</div>
@@ -406,7 +408,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
return (
<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">
<List size={16} /> Champs Personnalisés
<List size={16} /> {t('wb.custom_fields')}
</h3>
<div className="grid grid-cols-1 gap-4">
{currentTemplate.fields.map(field => {
@@ -429,7 +431,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
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"
>
<option value="">Sélectionner...</option>
<option value="">{t('wb.select')}</option>
{field.options?.map(opt => (
<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)}
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>
) : (
<input
@@ -469,10 +471,10 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="flex justify-between items-start mb-6">
<div>
<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>
<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>
</div>
<button onClick={() => setMode('entities')} className="p-2 text-theme-muted hover:bg-theme-border rounded-full">
@@ -486,8 +488,8 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
key={type}
onClick={() => setActiveTemplateType(type)}
className={`px-4 py-2 text-sm font-medium rounded-t-lg transition-colors ${activeTemplateType === type
? 'bg-indigo-500/10 text-indigo-700 border-b-2 border-indigo-600'
: 'text-theme-muted hover:text-theme-text hover:bg-theme-panel/50'
? 'bg-indigo-500/10 text-indigo-700 border-b-2 border-indigo-600'
: 'text-theme-muted hover:text-theme-text hover:bg-theme-panel/50'
}`}
>
{type}
@@ -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 className="flex-1 grid grid-cols-2 gap-4">
<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
type="text"
value={field.label}
@@ -509,28 +511,28 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
/>
</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
value={field.type}
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"
>
<option value="text">Texte court</option>
<option value="textarea">Texte long</option>
<option value="number">Nombre</option>
<option value="boolean">Case à cocher</option>
<option value="select">Liste déroulante</option>
<option value="text">{t('wb.type_text')}</option>
<option value="textarea">{t('wb.type_textarea')}</option>
<option value="number">{t('wb.type_num')}</option>
<option value="boolean">{t('wb.type_bool')}</option>
<option value="select">{t('wb.type_select')}</option>
</select>
</div>
{field.type === 'select' && (
<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
type="text"
value={field.options?.join(',') || ''}
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"
placeholder="Option A, Option B, Option C"
placeholder={t('wb.options_ph')}
/>
</div>
)}
@@ -548,7 +550,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
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"
>
<Plus size={20} /> Ajouter un champ
<Plus size={20} /> {t('wb.add_field')}
</button>
</div>
</div>
@@ -560,7 +562,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="flex h-full gap-6 p-6 bg-theme-bg">
<div className="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">
<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="h-10 bg-indigo-500/10 rounded"></div>
<div className="h-10 bg-indigo-500/10 rounded"></div>
@@ -577,13 +579,13 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="flex h-full gap-6 p-6 bg-theme-bg">
<div className="w-1/3 flex flex-col gap-4">
<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
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"
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>
</div>
@@ -603,7 +605,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div>
<div className="divide-y divide-slate-100">
{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 => (
<div
@@ -638,7 +640,7 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
{tempEntity.type}
</span>
<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>
</div>
<div className="flex gap-2">
@@ -650,23 +652,23 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
<div className="space-y-4">
<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
type="text"
value={tempEntity.name}
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"
placeholder="Ex: Gandalf le Gris"
placeholder={t('wb.name_ph')}
/>
</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
value={tempEntity.description}
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"
placeholder="Un magicien puissant qui guide la communauté..."
placeholder={t('wb.short_desc_ph')}
/>
</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>
<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>
<textarea
value={tempEntity.storyContext || ''}
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"
placeholder="Les événements vécus par ce personnage apparaîtront ici..."
placeholder={t('wb.story_context_ph')}
/>
</div>
<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
value={tempEntity.details}
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"
placeholder="Histoire détaillée, secrets, origines..."
placeholder={t('wb.notes_bio_ph')}
/>
</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"
>
<Save size={18} />
Enregistrer la fiche
{t('wb.save')}
</button>
</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="text-6xl mb-4 opacity-20">🌍</div>
<p className="text-lg">Sélectionnez ou créez une fiche pour commencer.</p>
<p className="text-sm">Ces informations aideront l'IA à rester cohérente.</p>
<p className="text-lg">{t('wb.select_start')}</p>
<p className="text-sm">{t('wb.ai_help')}</p>
</div>
)}
</div>

View File

@@ -5,6 +5,8 @@ import React, { useState } from 'react';
import { BookProject, UserProfile, ViewMode, ChatMessage } from '@/lib/types';
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 { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher';
interface EditorShellProps {
project: BookProject;
@@ -30,6 +32,7 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
const { project, user, viewMode, currentChapterId, children } = props;
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [isAiPanelOpen, setIsAiPanelOpen] = useState(true);
const { t } = useLanguage();
const currentChapter = project.chapters.find(c => c.id === currentChapterId);
@@ -40,23 +43,26 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
<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`}>
<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')}>
<Book className="text-blue-400" /> PlumeIA
<Book className="text-blue-400" /> Pluume
</h1>
<input
type="text"
value={project.title}
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"
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">
<LayoutDashboard size={14} /> Retour au Dashboard
<LayoutDashboard size={14} /> {t('nav.dashboard')}
</button>
<div className="mt-2 text-slate-400">
<LanguageSwitcher />
</div>
</div>
<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">
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>
{project.chapters.map((chap, idx) => (
<div key={chap.id} className="group relative">
@@ -70,26 +76,26 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
</div>
))}
<div className="mt-6 px-4 py-2 text-xs font-semibold text-slate-500 uppercase">Outils & Bible</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('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('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('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('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>
<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} /> {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} /> {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} /> {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} /> {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} /> {t('sidebar.settings')}</button>
</div>
<div className="p-4 border-t border-slate-800">
<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">
<span>Actions IA</span>
<span>{t('sidebar.ai_actions')}</span>
<span>{user.usage.aiActionsCurrent} / {user.usage.aiActionsLimit === 999999 ? '∞' : user.usage.aiActionsLimit}</span>
</div>
<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>
</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.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.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} /> {t('sidebar.logout')}</button>
</div>
</aside>

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