feat: implement core application structure, UI components, internationalization, and database seeding.
This commit is contained in:
BIN
.next/dev/cache/turbopack/23c46498/CURRENT
vendored
BIN
.next/dev/cache/turbopack/23c46498/CURRENT
vendored
Binary file not shown.
60
.next/dev/cache/turbopack/23c46498/LOG
vendored
60
.next/dev/cache/turbopack/23c46498/LOG
vendored
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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)")
|
||||
|
||||
@@ -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
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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"}}]
|
||||
}
|
||||
@@ -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}"
|
||||
@@ -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"
|
||||
]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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"
|
||||
});
|
||||
|
||||
@@ -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
48
RELEASE-v0.0.1.md
Normal 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.* ✍️
|
||||
BIN
build_output.txt
BIN
build_output.txt
Binary file not shown.
@@ -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'
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
2
next-env.d.ts
vendored
@@ -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.
|
||||
|
||||
@@ -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
38
src/app/cgu/page.tsx
Normal 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
38
src/app/cgv/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
63
src/app/sitemap/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
67
src/components/LanguageSwitcher.tsx
Normal file
67
src/components/LanguageSwitcher.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
1318
src/lib/i18n/translations.ts
Normal file
File diff suppressed because it is too large
Load Diff
49
src/providers/LanguageProvider.tsx
Normal file
49
src/providers/LanguageProvider.tsx
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user