Compare commits

..

13 Commits

75 changed files with 3765 additions and 700 deletions

1
.env
View File

@@ -5,3 +5,4 @@ BETTER_AUTH_SECRET=cbe18fc18cd18fa590bd8be51204fac2ee0b1cc704adf8fa0192f77d42b26
AUTH_SECRET=cbe18fc18cd18fa590bd8be51204fac2ee0b1cc704adf8fa0192f77d42b262dd AUTH_SECRET=cbe18fc18cd18fa590bd8be51204fac2ee0b1cc704adf8fa0192f77d42b262dd
# Server-only # Server-only
GEMINI_API_KEY=AIzaSyBjMxaRq4psBbvtdks0iYGkv-r9midKSh4 GEMINI_API_KEY=AIzaSyBjMxaRq4psBbvtdks0iYGkv-r9midKSh4
STRIPE_SECRET_KEY=sk_test_51T8SBN2WIo50AZ0wy5YbNVscHHw6y10z1aB0Ho54iqHHJ4ROYAQRMQEi42sgI8dknnGsHOxgo5tpw9UZA9QE2xIR00R5WD9BjB

77
.gitignore vendored
View File

@@ -6,7 +6,7 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log* pnpm-debug.log*
lerna-debug.log* lerna-debug.log*
*.next .next/
.next/* .next/*
node_modules node_modules
@@ -26,82 +26,9 @@ dist-ssr
*.sw? *.sw?
/src/generated/prisma /src/generated/prisma
.next/dev/prerender-manifest.json .next/dev/*
.next/dev/trace .next/dev/trace
.next/dev/cache/turbopack/23c46498/CURRENT .next/dev/cache/turbopack/23c46498/CURRENT
.next/dev/cache/turbopack/23c46498/LOG .next/dev/cache/turbopack/23c46498/LOG
.next/dev/server/app-paths-manifest.json
.next/dev/server/next-font-manifest.js
.next/dev/server/next-font-manifest.json
.next/dev/server/chunks/\[root-of-the-server]__43f27a2c._.js
.next/dev/server/chunks/\[root-of-the-server]__43f27a2c._.js.map
.next/dev/server/chunks/\[root-of-the-server]__630db118._.js
.next/dev/server/chunks/\[root-of-the-server]__630db118._.js.map
.next/dev/server/chunks/\[root-of-the-server]__f694870c._.js
.next/dev/server/chunks/\[root-of-the-server]__f694870c._.js.map
.next/dev/server/chunks/ssr/\[root-of-the-server]__2aea0639._.js
.next/dev/server/chunks/ssr/\[root-of-the-server]__2aea0639._.js.map
.next/dev/server/chunks/ssr/\[root-of-the-server]__661e4e50._.js.map
.next/dev/server/chunks/ssr/\[root-of-the-server]__bcada481._.js
.next/dev/server/chunks/ssr/\[root-of-the-server]__bcada481._.js.map
.next/dev/server/chunks/ssr/\[root-of-the-server]__de10d535._.js
.next/dev/server/chunks/ssr/\[root-of-the-server]__de10d535._.js.map
.next/dev/static/chunks/\[root-of-the-server]__c391f813._.css
.next/dev/static/chunks/\[root-of-the-server]__c391f813._.css.map
.next/dev/static/chunks/Documents_00 - projet_plumeia_0ae2c1c3._.js
.next/dev/static/chunks/Documents_00 - projet_plumeia_0ae2c1c3._.js.map
.next/dev/static/chunks/Documents_00 - projet_plumeia_48e545ad._.js
.next/dev/static/chunks/Documents_00 - projet_plumeia_48e545ad._.js.map
.next/dev/static/chunks/Documents_00 - projet_plumeia_c4c2fd93._.js
.next/dev/static/chunks/Documents_00 - projet_plumeia_c4c2fd93._.js.map
.next/dev/static/chunks/Documents_00 - projet_plumeia_src_74b79b3f._.js.map
.next/dev/static/chunks/Documents_00 - projet_plumeia_src_app_globals_css_bad6b30c._.single.css
.next/dev/static/chunks/Documents_00 - projet_plumeia_src_app_globals_css_bad6b30c._.single.css.map
.next/dev/types/routes.d.ts
.next/dev/types/validator.ts
.next/dev/server/chunks/\[root-of-the-server]__43f27a2c._.js
.next/dev/server/chunks/\[root-of-the-server]__43f27a2c._.js.map
.next/dev/server/chunks/\[root-of-the-server]__630db118._.js
.next/dev/server/chunks/\[root-of-the-server]__630db118._.js.map
.next/dev/server/chunks/\[root-of-the-server]__f694870c._.js
.next/dev/server/chunks/\[root-of-the-server]__f694870c._.js.map
.next/dev/server/chunks/ssr/\[root-of-the-server]__2aea0639._.js
.next/dev/server/chunks/ssr/\[root-of-the-server]__2aea0639._.js.map
.next/dev/server/chunks/ssr/\[root-of-the-server]__661e4e50._.js.map
.next/dev/server/chunks/ssr/\[root-of-the-server]__bcada481._.js
.next/dev/server/chunks/ssr/\[root-of-the-server]__bcada481._.js.map
.next/dev/server/chunks/ssr/\[root-of-the-server]__de10d535._.js
.next/dev/server/chunks/ssr/\[root-of-the-server]__de10d535._.js.map
.next/dev/prerender-manifest.json
.next/dev/trace
.next/dev/cache/turbopack/23c46498/CURRENT .next/dev/cache/turbopack/23c46498/CURRENT
.next/dev/cache/turbopack/23c46498/LOG .next/dev/cache/turbopack/23c46498/LOG
.next/dev/server/app-paths-manifest.json
.next/dev/server/next-font-manifest.js
.next/dev/server/next-font-manifest.json
.next/dev/server/chunks/\[root-of-the-server]__43f27a2c._.js
.next/dev/server/chunks/\[root-of-the-server]__43f27a2c._.js.map
.next/dev/server/chunks/\[root-of-the-server]__630db118._.js
.next/dev/server/chunks/\[root-of-the-server]__630db118._.js.map
.next/dev/server/chunks/\[root-of-the-server]__f694870c._.js
.next/dev/server/chunks/\[root-of-the-server]__f694870c._.js.map
.next/dev/server/chunks/ssr/\[root-of-the-server]__2aea0639._.js
.next/dev/server/chunks/ssr/\[root-of-the-server]__2aea0639._.js.map
.next/dev/server/chunks/ssr/\[root-of-the-server]__661e4e50._.js.map
.next/dev/server/chunks/ssr/\[root-of-the-server]__bcada481._.js
.next/dev/server/chunks/ssr/\[root-of-the-server]__bcada481._.js.map
.next/dev/server/chunks/ssr/\[root-of-the-server]__de10d535._.js
.next/dev/server/chunks/ssr/\[root-of-the-server]__de10d535._.js.map
.next/dev/static/chunks/\[root-of-the-server]__c391f813._.css
.next/dev/static/chunks/\[root-of-the-server]__c391f813._.css.map
.next/dev/static/chunks/Documents_00 - projet_plumeia_0ae2c1c3._.js
.next/dev/static/chunks/Documents_00 - projet_plumeia_0ae2c1c3._.js.map
.next/dev/static/chunks/Documents_00 - projet_plumeia_48e545ad._.js
.next/dev/static/chunks/Documents_00 - projet_plumeia_48e545ad._.js.map
.next/dev/static/chunks/Documents_00 - projet_plumeia_c4c2fd93._.js
.next/dev/static/chunks/Documents_00 - projet_plumeia_c4c2fd93._.js.map
.next/dev/static/chunks/Documents_00 - projet_plumeia_src_74b79b3f._.js.map
.next/dev/static/chunks/Documents_00 - projet_plumeia_src_app_globals_css_bad6b30c._.single.css
.next/dev/static/chunks/Documents_00 - projet_plumeia_src_app_globals_css_bad6b30c._.single.css.map
.next/dev/types/routes.d.ts
.next/dev/types/validator.ts

Binary file not shown.

View File

@@ -5834,3 +5834,303 @@ FAM | META SEQ | SST SEQ | RANGE
0 | 00013503 | 00013502 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh) 0 | 00013503 | 00013502 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013504 | 00013500 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) 1 | 00013504 | 00013500 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
2 | 00013505 | 00013501 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh) 2 | 00013505 | 00013501 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
Time 2026-03-05T09:12:24.6778725Z
Commit 00013529 4 keys in 16ms 819µs 800ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013527 | 00013526 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013528 | 00013524 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
2 | 00013529 | 00013525 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
Time 2026-03-05T09:26:50.6243723Z
Commit 00013535 4 keys in 7ms 324µs 500ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013533 | 00013532 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
2 | 00013534 | 00013531 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
1 | 00013535 | 00013530 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
Time 2026-03-05T09:28:50.6367929Z
Commit 00013541 4 keys in 7ms 682µs 500ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013539 | 00013538 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013540 | 00013536 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
2 | 00013541 | 00013537 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
Time 2026-03-05T09:30:50.6331668Z
Commit 00013547 4 keys in 6ms 493µs 600ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013545 | 00013544 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013546 | 00013542 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
2 | 00013547 | 00013543 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
Time 2026-03-05T09:32:50.6240317Z
Commit 00013553 4 keys in 16ms 750µs 700ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013551 | 00013550 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013552 | 00013548 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
2 | 00013553 | 00013549 SST | O | b294a4237ccef201-b294a4237ccef201 (0 MiB, fresh)
Time 2026-03-05T09:40:38.5061297Z
Commit 00013559 149 keys in 6ms 249µs
FAM | META SEQ | SST SEQ | RANGE
0 | 00013557 | 00013556 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013558 | 00013554 SST | [================================================================================================] | 0667c7b664635738-fd8767054879a7cc (0 MiB, fresh)
2 | 00013559 | 00013555 SST | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc (0 MiB, fresh)
Time 2026-03-05T09:40:42.3191423Z
Commit 00013565 139 keys in 6ms 488µs 100ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013563 | 00013562 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013564 | 00013560 SST | [================================================================================================] | 0667c7b664635738-fd8767054879a7cc (0 MiB, fresh)
2 | 00013565 | 00013561 SST | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc (0 MiB, fresh)
Time 2026-03-05T09:41:53.2990348Z
Commit 00013571 139 keys in 7ms 775µs 800ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013569 | 00013568 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013570 | 00013567 SST | [================================================================================================] | 0667c7b664635738-fd8767054879a7cc (0 MiB, fresh)
2 | 00013571 | 00013566 SST | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc (0 MiB, fresh)
Time 2026-03-05T09:42:03.9130042Z
Commit 00013577 4 keys in 7ms 403µs 300ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013575 | 00013574 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013576 | 00013572 SST | O | 7c65b158fbf615ea-7c65b158fbf615ea (0 MiB, fresh)
2 | 00013577 | 00013573 SST | O | 7c65b158fbf615ea-7c65b158fbf615ea (0 MiB, fresh)
Time 2026-03-05T09:42:16.5822981Z
Commit 00013583 59 keys in 8ms 342µs 800ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013581 | 00013580 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013582 | 00013579 SST | [==============================================================================================] | 040dc7eeb774bec4-f5d5f2d6bd5d47cb (0 MiB, fresh)
2 | 00013583 | 00013578 SST | [============================================================================================] | 09b7999c944d458c-f5d5f2d6bd5d47cb (0 MiB, fresh)
Time 2026-03-05T09:42:30.0633616Z
Commit 00013589 143 keys in 6ms 652µs 200ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013587 | 00013586 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013588 | 00013584 SST | [================================================================================================] | 0667c7b664635738-fd8767054879a7cc (0 MiB, fresh)
2 | 00013589 | 00013585 SST | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc (0 MiB, fresh)
Time 2026-03-05T09:42:39.863664Z
Commit 00013595 4 keys in 16ms 629µs 600ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013593 | 00013592 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013594 | 00013590 SST | O | 7c65b158fbf615ea-7c65b158fbf615ea (0 MiB, fresh)
2 | 00013595 | 00013591 SST | O | 7c65b158fbf615ea-7c65b158fbf615ea (0 MiB, fresh)
Time 2026-03-05T09:43:46.7973842Z
Commit 00013601 536 keys in 11ms 659µs 800ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013599 | 00013598 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013600 | 00013597 SST | [==================================================================================================] | 00c875afcd53ecac-fff8031d5d519426 (0 MiB, fresh)
2 | 00013601 | 00013596 SST | [==================================================================================================] | 00c875afcd53ecac-ff1ad0c236dc7dd8 (2 MiB, fresh)
Time 2026-03-05T09:44:14.8946301Z
Commit 00013611 3633 keys in 10ms 570µs 100ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013607 | 00013604 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
4 | 00013608 | 00013606 SST | [=============================================================================================] | 0d17912fd410ae98-fede697cf988e470 (0 MiB, fresh)
3 | 00013609 | 00013605 SST | [============================================================================================] | 094fc48477aa1157-f76d1016977e8665 (0 MiB, fresh)
1 | 00013610 | 00013603 SST | [==================================================================================================] | 000d168784b3a904-ffd966598c8595fa (1 MiB, fresh)
2 | 00013611 | 00013602 SST | [==================================================================================================] | 000d168784b3a904-ffd966598c8595fa (6 MiB, fresh)
2 | 00013614 | Compaction:
2 | 00013614 | MERGE (8273 keys):
2 | 00013614 | 00013189 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013195 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013200 INPUT | [==================================================================================================] | 000d168784b3a904-fffb6b73b51e00c8
2 | 00013614 | 00013210 INPUT | [==================================================================================================] | 000d168784b3a904-ffd966598c8595fa
2 | 00013614 | 00013221 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013227 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013233 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013239 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013245 INPUT | [====================================================================] | 3ffdfb3b7d50fcf1-ef311d8b965c9633
2 | 00013614 | 00013251 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013257 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013263 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013269 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013275 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013281 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013287 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013293 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013299 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013305 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013311 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013317 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013323 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013328 INPUT | [==================================================================================================] | 000d168784b3a904-ffd966598c8595fa
2 | 00013614 | 00013338 INPUT | [==================================================================================================] | 000d168784b3a904-fff068465974022c
2 | 00013614 | 00013349 INPUT | [==================================================================================================] | 000d168784b3a904-ffd966598c8595fa
2 | 00013614 | 00013359 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013364 INPUT | [==================================================================================================] | 000d168784b3a904-ffd966598c8595fa
2 | 00013614 | 00013371 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013377 INPUT | [================================================================================] | 20ff59e1772d8bcf-ef311d8b965c9633
2 | 00013614 | 00013387 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013393 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013399 INPUT | [==========================================================================================] | 09a1a5601bffdc2e-f123127f3b6f4541
2 | 00013614 | 00013404 INPUT | [==================================================================================================] | 000d168784b3a904-ffd966598c8595fa
2 | 00013614 | 00013415 INPUT | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc
2 | 00013614 | 00013420 INPUT | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc
2 | 00013614 | 00013426 INPUT | [==================================================================================================] | 00eac999f8125084-ffe64bbd36bacfc8
2 | 00013614 | 00013433 INPUT | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc
2 | 00013614 | 00013438 INPUT | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc
2 | 00013614 | 00013444 INPUT | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc
2 | 00013614 | 00013451 INPUT | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc
2 | 00013614 | 00013457 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013462 INPUT | [==================================================================================================] | 000b1fbdee8aaa62-fff49d0b9a706a34
2 | 00013614 | 00013473 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013479 INPUT | [===========================] | 3922e28834dec7a3-801410113b08be83
2 | 00013614 | 00013485 INPUT | O | 3e454b68eb86960f-3e454b68eb86960f
2 | 00013614 | 00013490 INPUT | [==================================================================================================] | 001159b6a37810de-ff870422b220f2d9
2 | 00013614 | 00013501 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013507 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013513 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013519 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013525 INPUT | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1
2 | 00013614 | 00013531 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013537 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013543 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013549 INPUT | O | b294a4237ccef201-b294a4237ccef201
2 | 00013614 | 00013555 INPUT | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc
2 | 00013614 | 00013561 INPUT | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc
2 | 00013614 | 00013566 INPUT | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc
2 | 00013614 | 00013573 INPUT | O | 7c65b158fbf615ea-7c65b158fbf615ea
2 | 00013614 | 00013578 INPUT | [============================================================================================] | 09b7999c944d458c-f5d5f2d6bd5d47cb
2 | 00013614 | 00013585 INPUT | [================================================================================================] | 06945e35a566c40d-fd8767054879a7cc
2 | 00013614 | 00013591 INPUT | O | 7c65b158fbf615ea-7c65b158fbf615ea
2 | 00013614 | 00013596 INPUT | [==================================================================================================] | 00c875afcd53ecac-ff1ad0c236dc7dd8
2 | 00013614 | 00013602 INPUT | [==================================================================================================] | 000d168784b3a904-ffd966598c8595fa
2 | 00013614 | 00013613 OUTPUT | [==================================================================================================] | 007b83f40de780ac-ffe8beb2fab5240b (cold)
2 | 00013614 | 00013612 OUTPUT | [==================================================================================================] | 000b1fbdee8aaa62-fffb6b73b51e00c8 (warm)
Time 2026-03-05T09:44:15.0737579Z
Commit 00013615 8273 keys in 32ms 476µs 100ns
FAM | META SEQ | SST SEQ | RANGE
2 | 00013614 | 00013613 SST | [==================================================================================================] | 007b83f40de780ac-ffe8beb2fab5240b (0 MiB, cold)
2 | 00013614 | 00013612 SST | [==================================================================================================] | 000b1fbdee8aaa62-fffb6b73b51e00c8 (16 MiB, warm)
2 | 00013614 | 00013189 00013195 00013200 00013210 00013221 00013227 00013233 00013239 00013245 00013251 00013257 00013263 00013269 00013275 00013281 OBSOLETE SST
2 | 00013614 | 00013287 00013293 00013299 00013305 00013311 00013317 00013323 00013328 00013338 00013349 00013359 00013364 00013371 00013377 00013387 OBSOLETE SST
2 | 00013614 | 00013393 00013399 00013404 00013415 00013420 00013426 00013433 00013438 00013444 00013451 00013457 00013462 00013473 00013479 00013485 OBSOLETE SST
2 | 00013614 | 00013490 00013501 00013507 00013513 00013519 00013525 00013531 00013537 00013543 00013549 00013555 00013561 00013566 00013573 00013578 OBSOLETE SST
2 | 00013614 | 00013585 00013591 00013596 00013602 OBSOLETE SST
| | 00013189 00013195 00013200 00013210 00013221 00013227 00013233 00013239 00013245 00013251 00013257 00013263 00013269 00013275 00013281 SST DELETED
| | 00013287 00013293 00013299 00013305 00013311 00013317 00013323 00013328 00013338 00013349 00013359 00013364 00013371 00013377 00013387 SST DELETED
| | 00013393 00013399 00013404 00013415 00013420 00013426 00013433 00013438 00013444 00013451 00013457 00013462 00013473 00013479 00013485 SST DELETED
| | 00013490 00013501 00013507 00013513 00013519 00013525 00013531 00013537 00013543 00013549 00013555 00013561 00013566 00013573 00013578 SST DELETED
| | 00013585 00013591 00013596 00013602 SST DELETED
| | 00013193 00013199 00013208 00013217 00013225 00013231 00013237 00013243 00013249 00013255 00013261 00013267 00013273 00013279 00013285 META DELETED
| | 00013291 00013297 00013303 00013309 00013315 00013321 00013327 00013335 00013346 00013355 00013363 00013368 00013375 00013383 00013391 META DELETED
| | 00013397 00013403 00013413 00013419 00013425 00013431 00013437 00013443 00013449 00013455 00013461 00013471 00013477 00013482 00013489 META DELETED
| | 00013498 00013505 00013511 00013517 00013522 00013529 00013534 00013541 00013547 00013553 00013559 00013565 00013571 00013577 00013583 META DELETED
| | 00013589 00013595 00013601 00013611 META DELETED
Time 2026-03-05T09:45:04.2738421Z
Commit 00013625 1904 keys in 10ms 729µs 100ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013621 | 00013618 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
4 | 00013622 | 00013620 SST | [====================================================================================] | 02cea76613175246-ddcd9a48d7354f8a (0 MiB, fresh)
3 | 00013623 | 00013619 SST | [=============================================================================] | 1053e97e66b456e8-d8133ee902cc8c89 (0 MiB, fresh)
2 | 00013624 | 00013616 SST | [==================================================================================================] | 0051cad80c47e3ca-fff3b56bbe0e6b22 (2 MiB, fresh)
1 | 00013625 | 00013617 SST | [==================================================================================================] | 0051cad80c47e3ca-fff3b56bbe0e6b22 (0 MiB, fresh)
Time 2026-03-05T09:45:22.6919165Z
Commit 00013631 4 keys in 7ms 467µs 100ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013629 | 00013628 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013630 | 00013626 SST | O | 801410113b08be83-801410113b08be83 (0 MiB, fresh)
2 | 00013631 | 00013627 SST | O | 801410113b08be83-801410113b08be83 (0 MiB, fresh)
Time 2026-03-05T09:45:38.0730767Z
Commit 00013637 81 keys in 6ms 305µs 800ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013635 | 00013634 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013636 | 00013633 SST | [==============================================================================================] | 040dc7eeb774bec4-f5d5f2d6bd5d47cb (0 MiB, fresh)
2 | 00013637 | 00013632 SST | [==============================================================================================] | 040dc7eeb774bec4-f5d5f2d6bd5d47cb (0 MiB, fresh)
Time 2026-03-05T09:45:43.2260573Z
Commit 00013647 2146 keys in 9ms 217µs 500ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013643 | 00013640 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
4 | 00013644 | 00013642 SST | [==================================================================================================] | 006fbb6ec017bf69-ff2f104f8736cc76 (0 MiB, fresh)
3 | 00013645 | 00013641 SST | [==================================================================================================] | 00722fe17a68306b-fffaa2b20140affd (0 MiB, fresh)
2 | 00013646 | 00013638 SST | [==================================================================================================] | 001159b6a37810de-ffb36b555eaa2e36 (2 MiB, fresh)
1 | 00013647 | 00013639 SST | [==================================================================================================] | 001159b6a37810de-fff88b53b5ad4b9a (0 MiB, fresh)
Time 2026-03-05T09:45:49.5803448Z
Commit 00013653 2390 keys in 8ms 273µs 200ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013651 | 00013650 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
2 | 00013652 | 00013648 SST | [==================================================================================================] | 015aa7af8c46be57-ff1373558b528b52 (0 MiB, fresh)
1 | 00013653 | 00013649 SST | [==================================================================================================] | 0013a9e4f34335e5-fff187cd7cce0e80 (1 MiB, fresh)
Time 2026-03-05T09:45:51.864302Z
Commit 00013663 681 keys in 11ms 141µs 800ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013659 | 00013656 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013660 | 00013654 SST | [==================================================================================================] | 01e217d129c5cd8b-ffd2128295feedf0 (0 MiB, fresh)
2 | 00013661 | 00013655 SST | [==================================================================================================] | 0205db9f6b04596e-ff1763b6f45cd288 (0 MiB, fresh)
3 | 00013662 | 00013657 SST | [=============================================================================] | 0a12f0dc4d221852-d125bd8c7ccdb585 (0 MiB, fresh)
4 | 00013663 | 00013658 SST | [==========================================================] | 2559e4611f5c0172-bd0c62044625aa6c (0 MiB, fresh)
Time 2026-03-05T09:46:36.3307863Z
Commit 00013673 198 keys in 10ms 222µs 100ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013669 | 00013666 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
2 | 00013670 | 00013664 SST | [==================================================================================================] | 0234350df6afdb76-ffd95e498f1850a5 (0 MiB, fresh)
1 | 00013671 | 00013665 SST | [==================================================================================================] | 0234350df6afdb76-ffd95e498f1850a5 (0 MiB, fresh)
4 | 00013672 | 00013667 SST | [===================================================] | 03b6a334a1ed1265-87fe61d16c4e336a (0 MiB, fresh)
3 | 00013673 | 00013668 SST | [====================================================================] | 379ed994a6f89a0e-e8e89e1b79c84a13 (0 MiB, fresh)
Time 2026-03-05T09:47:18.7149438Z
Commit 00013679 77 keys in 7ms 369µs 500ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013677 | 00013676 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
2 | 00013678 | 00013675 SST | [=================================================================================================] | 024fd2c66c04979c-fbb97280b2255708 (0 MiB, fresh)
1 | 00013679 | 00013674 SST | [=================================================================================================] | 024fd2c66c04979c-fbb97280b2255708 (0 MiB, fresh)
Time 2026-03-05T09:47:38.9509265Z
Commit 00013685 78 keys in 7ms 841µs 600ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013683 | 00013682 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013684 | 00013680 SST | [=============================================================================================] | 0302206520c18bf0-f366a041d02d9210 (0 MiB, fresh)
2 | 00013685 | 00013681 SST | [=============================================================================================] | 0302206520c18bf0-f366a041d02d9210 (0 MiB, fresh)
Time 2026-03-05T09:51:11.5129028Z
Commit 00013695 5718 keys in 11ms 538µs 800ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013691 | 00013688 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
4 | 00013692 | 00013690 SST | [==================================================================================================] | 014468475edc4608-ff789f220176697f (0 MiB, fresh)
3 | 00013693 | 00013689 SST | [==================================================================================================] | 00138ab5334ca35e-fffe1a2d422e4868 (0 MiB, fresh)
2 | 00013694 | 00013686 SST | [==================================================================================================] | 00457a46f5c1adcf-fff068465974022c (3 MiB, fresh)
1 | 00013695 | 00013687 SST | [==================================================================================================] | 00457a46f5c1adcf-fff068465974022c (1 MiB, fresh)
Time 2026-03-05T09:51:17.0554241Z
Commit 00013701 4 keys in 8ms 153µs 700ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013699 | 00013698 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013700 | 00013696 SST | O | 801410113b08be83-801410113b08be83 (0 MiB, fresh)
2 | 00013701 | 00013697 SST | O | 801410113b08be83-801410113b08be83 (0 MiB, fresh)
Time 2026-03-05T09:51:20.21688Z
Commit 00013707 59 keys in 7ms 408µs
FAM | META SEQ | SST SEQ | RANGE
0 | 00013705 | 00013704 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013706 | 00013703 SST | [============================================================================================] | 0cf8b834dd50cb16-fcf23e4059cdfb0b (0 MiB, fresh)
2 | 00013707 | 00013702 SST | [============================================================================================] | 0e53f9aa3f2e5fa6-fcf23e4059cdfb0b (0 MiB, fresh)
Time 2026-03-05T09:51:26.6173918Z
Commit 00013717 242 keys in 11ms 469µs
FAM | META SEQ | SST SEQ | RANGE
0 | 00013713 | 00013710 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
2 | 00013714 | 00013709 SST | [==================================================================================================] | 024a51d48220e7dd-fff068465974022c (0 MiB, fresh)
3 | 00013715 | 00013711 SST | [=] | 094aadbbd0c62e5e-0e452ca0afd31391 (0 MiB, fresh)
1 | 00013716 | 00013708 SST | [==================================================================================================] | 024a51d48220e7dd-fff068465974022c (0 MiB, fresh)
4 | 00013717 | 00013712 SST | [=======] | 6351cf276af0408c-776c88e811462325 (0 MiB, fresh)
Time 2026-03-05T09:53:31.4657146Z
Commit 00013723 188 keys in 6ms 995µs 800ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013721 | 00013720 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013722 | 00013718 SST | [=================================================================================================] | 040dc7eeb774bec4-fede697cf988e470 (0 MiB, fresh)
2 | 00013723 | 00013719 SST | [=============================================================================================] | 06945e35a566c40d-f5d5f2d6bd5d47cb (0 MiB, fresh)
Time 2026-03-05T09:54:08.9292929Z
Commit 00013729 56 keys in 7ms 280µs 100ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013727 | 00013726 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
2 | 00013728 | 00013724 SST | [=================================================================================================] | 024fd2c66c04979c-fbb97280b2255708 (0 MiB, fresh)
1 | 00013729 | 00013725 SST | [=================================================================================================] | 024fd2c66c04979c-fbb97280b2255708 (0 MiB, fresh)
Time 2026-03-05T11:47:26.3903621Z
Commit 00013987 4 keys in 9ms 216µs 300ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00013985 | 00013984 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00013986 | 00013982 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
2 | 00013987 | 00013983 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
Time 2026-03-05T17:21:32.7902721Z
Commit 00014935 4 keys in 16ms 322µs 500ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00014933 | 00014932 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00014934 | 00014930 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
2 | 00014935 | 00014931 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
Time 2026-03-06T10:28:56.8886436Z
Commit 00015705 4 keys in 7ms 974µs 900ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00015703 | 00015702 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00015704 | 00015700 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
2 | 00015705 | 00015701 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
Time 2026-03-14T19:42:53.4660378Z
Commit 00001865 4 keys in 5ms 888µs 400ns
FAM | META SEQ | SST SEQ | RANGE
0 | 00001863 | 00001862 SST | [=======================================================================] | 3aefa6fd5cf2deb4-f42f94001fcb5351 (0 MiB, fresh)
1 | 00001864 | 00001860 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)
2 | 00001865 | 00001861 SST | O | 3ffdfb3b7d50fcf1-3ffdfb3b7d50fcf1 (0 MiB, fresh)

View File

@@ -4,8 +4,8 @@
"dynamicRoutes": {}, "dynamicRoutes": {},
"notFoundRoutes": [], "notFoundRoutes": [],
"preview": { "preview": {
"previewModeId": "196862b1bfff2ba012281ebc734bd07b", "previewModeId": "81c5b99488ca3ef406c0ad8b4a86d3ca",
"previewModeSigningKey": "3745933161423febbc580e5730495b53049213e7a37a27fdeb1d4a5c7215d2d3", "previewModeSigningKey": "5c3f99d29cba2cf6c1d0b5f06b0c3bcb717bd8a4cabfcfa05667c35a362a414f",
"previewModeEncryptionKey": "58cb94e204a1d02b3f8a529ba81bba219c2cfe8d0b7b36eff707ff947755d2d5" "previewModeEncryptionKey": "b589b6ec1c70eaeb79d83dbfbb549733febd58291914ffc41c9e19c4d4f59510"
} }
} }

View File

@@ -1,13 +1,13 @@
{ {
"/api/auth/[...nextauth]/route": "app/api/auth/[...nextauth]/route.js", "/api/auth/[...nextauth]/route": "app/api/auth/[...nextauth]/route.js",
"/api/chapters/[id]/route": "app/api/chapters/[id]/route.js",
"/api/projects/[id]/route": "app/api/projects/[id]/route.js", "/api/projects/[id]/route": "app/api/projects/[id]/route.js",
"/api/projects/route": "app/api/projects/route.js", "/api/projects/route": "app/api/projects/route.js",
"/api/user/profile/route": "app/api/user/profile/route.js", "/api/user/profile/route": "app/api/user/profile/route.js",
"/dashboard/page": "app/dashboard/page.js", "/dashboard/page": "app/dashboard/page.js",
"/login/page": "app/login/page.js", "/login/page": "app/login/page.js",
"/project/[id]/ideas/page": "app/project/[id]/ideas/page.js", "/profile/page": "app/profile/page.js",
"/project/[id]/page": "app/project/[id]/page.js", "/project/[id]/page": "app/project/[id]/page.js",
"/project/[id]/settings/page": "app/project/[id]/settings/page.js", "/project/[id]/settings/page": "app/project/[id]/settings/page.js",
"/project/[id]/workflow/page": "app/project/[id]/workflow/page.js",
"/project/[id]/world/page": "app/project/[id]/world/page.js" "/project/[id]/world/page": "app/project/[id]/world/page.js"
} }

View File

@@ -1,10 +1,9 @@
var R=require("../../../../chunks/[turbopack]_runtime.js")("server/app/api/chapters/[id]/route.js") var R=require("../../../../chunks/[turbopack]_runtime.js")("server/app/api/chapters/[id]/route.js")
R.c("server/chunks/Documents_00 - projet_plumeia_src_lib_prisma_ts_d7a1a866._.js") R.c("server/chunks/549ce_next_d909a8c3._.js")
R.c("server/chunks/549ce_next_dcda18ca._.js")
R.c("server/chunks/549ce_@auth_core_cb936014._.js") R.c("server/chunks/549ce_@auth_core_cb936014._.js")
R.c("server/chunks/549ce_jose_dist_webapi_61916537._.js") R.c("server/chunks/549ce_jose_dist_webapi_61916537._.js")
R.c("server/chunks/[root-of-the-server]__40bf2c68._.js") R.c("server/chunks/[root-of-the-server]__b06aea5c._.js")
R.c("server/chunks/[root-of-the-server]__27e12fd5._.js") R.c("server/chunks/[root-of-the-server]__94fb2294._.js")
R.c("server/chunks/b79dd_plumeia__next-internal_server_app_api_chapters_[id]_route_actions_62885031.js") R.c("server/chunks/b79dd_plumeia__next-internal_server_app_api_chapters_[id]_route_actions_62885031.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/chapters/[id]/route.ts [app-route] (ecmascript)\" } [app-route] (ecmascript)") R.m("[project]/Documents/00 - projet/plumeia/node_modules/next/dist/esm/build/templates/app-route.js { INNER_APP_ROUTE => \"[project]/Documents/00 - projet/plumeia/src/app/api/chapters/[id]/route.ts [app-route] (ecmascript)\" } [app-route] (ecmascript)")
module.exports=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/chapters/[id]/route.ts [app-route] (ecmascript)\" } [app-route] (ecmascript)").exports module.exports=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/chapters/[id]/route.ts [app-route] (ecmascript)\" } [app-route] (ecmascript)").exports

View File

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -20,6 +20,7 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__proje
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$copy$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Copy$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/copy.js [app-ssr] (ecmascript) <export default as Copy>"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$copy$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Copy$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/copy.js [app-ssr] (ecmascript) <export default as Copy>");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$wand$2d$sparkles$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Wand2$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/wand-sparkles.js [app-ssr] (ecmascript) <export default as Wand2>"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$wand$2d$sparkles$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Wand2$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/wand-sparkles.js [app-ssr] (ecmascript) <export default as Wand2>");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$check$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Check$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/check.js [app-ssr] (ecmascript) <export default as Check>"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$check$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Check$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/check.js [app-ssr] (ecmascript) <export default as Check>");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$check$2d$check$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__CheckCheck$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/check-check.js [app-ssr] (ecmascript) <export default as CheckCheck>");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$refresh$2d$cw$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__RefreshCw$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/refresh-cw.js [app-ssr] (ecmascript) <export default as RefreshCw>"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$refresh$2d$cw$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__RefreshCw$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/refresh-cw.js [app-ssr] (ecmascript) <export default as RefreshCw>");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$maximize$2d$2$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Maximize2$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/maximize-2.js [app-ssr] (ecmascript) <export default as Maximize2>"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$maximize$2d$2$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Maximize2$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/maximize-2.js [app-ssr] (ecmascript) <export default as Maximize2>");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$loader$2d$circle$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Loader2$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/loader-circle.js [app-ssr] (ecmascript) <export default as Loader2>"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$loader$2d$circle$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Loader2$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/loader-circle.js [app-ssr] (ecmascript) <export default as Loader2>");
@@ -33,12 +34,12 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__proje
; ;
; ;
; ;
const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["forwardRef"])(({ initialContent, onChange, onSave, onSelectionChange, onAiTransform }, ref)=>{ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["forwardRef"])(({ editorId, initialContent, onChange, onSave, onSelectionChange, onAiTransform }, ref)=>{
const contentRef = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useRef"])(null); const contentRef = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useRef"])(null);
const scrollContainerRef = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useRef"])(null); const scrollContainerRef = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useRef"])(null);
const [isFocused, setIsFocused] = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useState"])(false); const [isFocused, setIsFocused] = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useState"])(false);
// Auto-Save State // Auto-Save State
const [saveStatus, setSaveStatus] = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useState"])('saved'); const [saveStatus, setSaveStatus] = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useState"])('saved_db');
const saveTimeoutRef = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useRef"])(null); const saveTimeoutRef = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useRef"])(null);
// Track sync state to avoid autosave loopbacks wiping current edits // Track sync state to avoid autosave loopbacks wiping current edits
// Start as null so the initial useEffect ALWAYS writes initialContent to the div // Start as null so the initial useEffect ALWAYS writes initialContent to the div
@@ -151,27 +152,43 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
// --- Effects --- // --- Effects ---
(0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useEffect"])(()=>{ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useEffect"])(()=>{
if (!contentRef.current || initialContent === undefined) return; if (!contentRef.current || initialContent === undefined) return;
// Ignore exact loopbacks from our own saves let contentToLoad = initialContent;
if (initialContent === syncRef.current) return; let hasLocalDraft = false;
// Safety: never overwrite real content with an empty string from a stale/placeholder source // Check localStorage for a newer draft
const hasRealContent = latestContentRef.current && latestContentRef.current.trim().length > 0; if (editorId) {
if (!initialContent && hasRealContent) return; const localDraft = localStorage.getItem(`draft_${editorId}`);
// We reached here, so initialContent is genuinely NEW data we didn't know about. if (localDraft && localDraft !== initialContent) {
// E.g. clicked another chapter, or data was modified in another tab/device. contentToLoad = localDraft;
contentRef.current.innerHTML = initialContent; hasLocalDraft = true;
syncRef.current = initialContent; setSaveStatus('saved_local');
latestContentRef.current = initialContent; }
}
// 1. Si le contenu entrant est identique à ce qu'on a déjà, on ne touche à rien
if (contentToLoad === contentRef.current.innerHTML) return;
// 2. LOGIQUE CRUCIALE : On ne met à jour le DOM que si :
// - L'éditeur est vide (premier chargement)
// - OU le document a changé (si vous gérez des IDs de documents)
// - OU si l'utilisateur n'est PAS en train de focus l'éditeur
const isUserEditing = document.activeElement === contentRef.current;
if (!isUserEditing || contentRef.current.innerHTML === "" && contentToLoad !== "") {
contentRef.current.innerHTML = contentToLoad;
syncRef.current = contentToLoad;
latestContentRef.current = contentToLoad;
}
}, [ }, [
initialContent initialContent,
editorId
]); ]);
// Flush pending save on unmount // Flush pending save on unmount
(0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useEffect"])(()=>{ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["useEffect"])(()=>{
return ()=>{ return ()=>{
if (saveTimeoutRef.current) { if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current); clearTimeout(saveTimeoutRef.current);
if (latestContentRef.current !== syncRef.current && onSave) { }
onSave(latestContentRef.current); // Always save if there are unsaved changes, regardless of timer
} if (latestContentRef.current !== syncRef.current && onSave) {
syncRef.current = latestContentRef.current;
onSave(latestContentRef.current);
} }
}; };
}, [ }, [
@@ -188,16 +205,34 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
const currentHtml = contentRef.current.innerHTML; const currentHtml = contentRef.current.innerHTML;
latestContentRef.current = currentHtml; latestContentRef.current = currentHtml;
if (onChange) onChange(currentHtml); if (onChange) onChange(currentHtml);
// Auto-Save Debounce // 1. Save locally immediately
if (onSave) { if (editorId) {
localStorage.setItem(`draft_${editorId}`, currentHtml);
setSaveStatus('saved_local');
} else {
setSaveStatus('unsaved'); setSaveStatus('unsaved');
}
// 2. Auto-Save Debounce for DB
if (onSave) {
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = setTimeout(async ()=>{ saveTimeoutRef.current = setTimeout(async ()=>{
setSaveStatus('saving'); setSaveStatus('saving');
const htmlToSave = latestContentRef.current; const htmlToSave = latestContentRef.current;
await onSave(htmlToSave); // Update syncRef BEFORE calling onSave, because onSave triggers setProjects
syncRef.current = htmlToSave; // Record that we've synced this exact string to the server // which causes a re-render. The useEffect must see the updated syncRef
setSaveStatus('saved'); // to avoid re-writing innerHTML unnecessarily.
syncRef.current = htmlToSave;
try {
await onSave(htmlToSave);
setSaveStatus('saved_db');
if (editorId) {
// Once saved to DB, we can consider the local draft synced if we want,
// or just keep it there. It will be overwritten on next load.
}
} catch (err) {
console.error('Auto-save failed:', err);
setSaveStatus('saved_local'); // Revert to local save status on error
}
}, 2000); // 2 seconds }, 2000); // 2 seconds
} }
} }
@@ -305,12 +340,12 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
size: 18 size: 18
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 340, lineNumber: 377,
columnNumber: 7 columnNumber: 7
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 327, lineNumber: 364,
columnNumber: 5 columnNumber: 5
}, ("TURBOPACK compile-time value", void 0)); }, ("TURBOPACK compile-time value", void 0));
const hasSelection = savedRange.current && !savedRange.current.collapsed; const hasSelection = savedRange.current && !savedRange.current.collapsed;
@@ -328,7 +363,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
` `
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 348, lineNumber: 385,
columnNumber: 7 columnNumber: 7
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -340,7 +375,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
label: "Gras" label: "Gras"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 359, lineNumber: 396,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -349,7 +384,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
label: "Italique" label: "Italique"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 360, lineNumber: 397,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -358,14 +393,14 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
label: "Souligné" label: "Souligné"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 361, lineNumber: 398,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
className: "w-px h-6 bg-slate-300 mx-1" className: "w-px h-6 bg-slate-300 mx-1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 362, lineNumber: 399,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -375,7 +410,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
label: "Titre 1" label: "Titre 1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 363, lineNumber: 400,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -385,14 +420,14 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
label: "Titre 2" label: "Titre 2"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 364, lineNumber: 401,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
className: "w-px h-6 bg-slate-300 mx-1" className: "w-px h-6 bg-slate-300 mx-1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 365, lineNumber: 402,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -401,7 +436,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
label: "Aligner à gauche" label: "Aligner à gauche"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 366, lineNumber: 403,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -410,7 +445,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
label: "Centrer" label: "Centrer"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 367, lineNumber: 404,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -419,14 +454,14 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
label: "Aligner à droite" label: "Aligner à droite"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 368, lineNumber: 405,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
className: "w-px h-6 bg-slate-300 mx-1" className: "w-px h-6 bg-slate-300 mx-1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 369, lineNumber: 406,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -435,14 +470,14 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
label: "Liste" label: "Liste"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 370, lineNumber: 407,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
className: "flex-1" className: "flex-1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 372, lineNumber: 409,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -452,47 +487,84 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
children: [ children: [
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$loader$2d$circle$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Loader2$3e$__["Loader2"], { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$loader$2d$circle$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Loader2$3e$__["Loader2"], {
size: 12, size: 12,
className: "animate-spin" className: "animate-spin text-blue-500"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 376, lineNumber: 413,
columnNumber: 41 columnNumber: 41
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Sauvegarde..." " ",
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
className: "text-blue-500 hidden sm:inline",
children: "Sauvegarde en cours..."
}, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 413,
columnNumber: 102
}, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true), }, void 0, true),
saveStatus === 'saved' && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["Fragment"], { saveStatus === 'saved_local' && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["Fragment"], {
children: [ children: [
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$check$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Check$3e$__["Check"], { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$check$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__Check$3e$__["Check"], {
size: 12, size: 14,
className: "text-green-500" className: "text-green-500"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 377, lineNumber: 414,
columnNumber: 40 columnNumber: 46
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Sauvegardé" " ",
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
className: "text-green-500 hidden sm:inline",
children: "Brouillon local"
}, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 414,
columnNumber: 93
}, ("TURBOPACK compile-time value", void 0))
]
}, void 0, true),
saveStatus === 'saved_db' && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["Fragment"], {
children: [
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$check$2d$check$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__CheckCheck$3e$__["CheckCheck"], {
size: 14,
className: "text-emerald-600"
}, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 415,
columnNumber: 43
}, ("TURBOPACK compile-time value", void 0)),
" ",
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
className: "text-emerald-600 hidden sm:inline",
children: "Sauvegardé"
}, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 415,
columnNumber: 97
}, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true), }, void 0, true),
saveStatus === 'unsaved' && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("span", { saveStatus === 'unsaved' && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
className: "text-amber-500", className: "text-amber-500",
children: "Modifications non enregistrées..." children: "Non sauvegardé..."
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 378, lineNumber: 416,
columnNumber: 40 columnNumber: 40
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 375, lineNumber: 412,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
className: "w-px h-6 bg-slate-300 mx-1" className: "w-px h-6 bg-slate-300 mx-1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 381, lineNumber: 419,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -502,13 +574,13 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
isActive: showHistoryMargin isActive: showHistoryMargin
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 382, lineNumber: 420,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 358, lineNumber: 395,
columnNumber: 7 columnNumber: 7
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -538,7 +610,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
"data-placeholder": "Commencez à écrire votre chef-d'œuvre... (Clic droit pour outils IA)" "data-placeholder": "Commencez à écrire votre chef-d'œuvre... (Clic droit pour outils IA)"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 398, lineNumber: 436,
columnNumber: 11 columnNumber: 11
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
showHistoryMargin && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { showHistoryMargin && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -552,7 +624,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
className: "mx-auto mb-2 opacity-20" className: "mx-auto mb-2 opacity-20"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 422, lineNumber: 460,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("p", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("p", {
@@ -560,13 +632,13 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
children: "L'historique des modifications IA apparaîtra ici, aligné avec votre texte." children: "L'historique des modifications IA apparaîtra ici, aligné avec votre texte."
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 423, lineNumber: 461,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 421, lineNumber: 459,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
versionGroups.map((group)=>{ versionGroups.map((group)=>{
@@ -585,7 +657,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
className: "absolute inset-0 bg-white border border-indigo-100 rounded-lg transform -translate-x-1 -translate-y-1 -z-10 shadow-sm" className: "absolute inset-0 bg-white border border-indigo-100 rounded-lg transform -translate-x-1 -translate-y-1 -z-10 shadow-sm"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 443, lineNumber: 481,
columnNumber: 25 columnNumber: 25
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -600,7 +672,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
className: "text-indigo-500" className: "text-indigo-500"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 453, lineNumber: 491,
columnNumber: 29 columnNumber: 29
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("span", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
@@ -608,13 +680,13 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
children: latest.type children: latest.type
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 455, lineNumber: 493,
columnNumber: 27 columnNumber: 27
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 451, lineNumber: 489,
columnNumber: 25 columnNumber: 25
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -628,7 +700,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
}) })
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 463, lineNumber: 501,
columnNumber: 27 columnNumber: 27
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
isStack && (isExpanded ? /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$chevron$2d$up$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__ChevronUp$3e$__["ChevronUp"], { isStack && (isExpanded ? /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$chevron$2d$up$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__ChevronUp$3e$__["ChevronUp"], {
@@ -636,26 +708,26 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
className: "text-slate-400" className: "text-slate-400"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 467, lineNumber: 505,
columnNumber: 42 columnNumber: 42
}, ("TURBOPACK compile-time value", void 0)) : /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$chevron$2d$down$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__ChevronDown$3e$__["ChevronDown"], { }, ("TURBOPACK compile-time value", void 0)) : /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$chevron$2d$down$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__$3c$export__default__as__ChevronDown$3e$__["ChevronDown"], {
size: 14, size: 14,
className: "text-slate-400" className: "text-slate-400"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 467, lineNumber: 505,
columnNumber: 95 columnNumber: 95
}, ("TURBOPACK compile-time value", void 0))) }, ("TURBOPACK compile-time value", void 0)))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 462, lineNumber: 500,
columnNumber: 25 columnNumber: 25
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 447, lineNumber: 485,
columnNumber: 23 columnNumber: 23
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
!isExpanded && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { !isExpanded && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -670,7 +742,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 475, lineNumber: 513,
columnNumber: 27 columnNumber: 27
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -681,20 +753,20 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
size: 10 size: 10
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 482, lineNumber: 520,
columnNumber: 29 columnNumber: 29
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Restaurer" " Restaurer"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 478, lineNumber: 516,
columnNumber: 27 columnNumber: 27
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 474, lineNumber: 512,
columnNumber: 25 columnNumber: 25
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
isExpanded && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { isExpanded && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -710,7 +782,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
children: i === 0 ? 'Dernière version' : `Version -${i}` children: i === 0 ? 'Dernière version' : `Version -${i}`
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 493, lineNumber: 531,
columnNumber: 33 columnNumber: 33
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("span", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
@@ -722,13 +794,13 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
}) })
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 496, lineNumber: 534,
columnNumber: 33 columnNumber: 33
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 492, lineNumber: 530,
columnNumber: 31 columnNumber: 31
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -740,7 +812,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 500, lineNumber: 538,
columnNumber: 31 columnNumber: 31
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -751,54 +823,54 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
size: 10 size: 10
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 507, lineNumber: 545,
columnNumber: 33 columnNumber: 33
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Restaurer cette version" " Restaurer cette version"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 503, lineNumber: 541,
columnNumber: 31 columnNumber: 31
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, v.id, true, { }, v.id, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 491, lineNumber: 529,
columnNumber: 29 columnNumber: 29
}, ("TURBOPACK compile-time value", void 0))) }, ("TURBOPACK compile-time value", void 0)))
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 489, lineNumber: 527,
columnNumber: 25 columnNumber: 25
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 439, lineNumber: 477,
columnNumber: 21 columnNumber: 21
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
}, group.id, false, { }, group.id, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 434, lineNumber: 472,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)); }, ("TURBOPACK compile-time value", void 0));
}) })
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 418, lineNumber: 456,
columnNumber: 13 columnNumber: 13
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 395, lineNumber: 433,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 391, lineNumber: 429,
columnNumber: 7 columnNumber: 7
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
contextMenu && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["Fragment"], { contextMenu && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["Fragment"], {
@@ -812,7 +884,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
} }
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 525, lineNumber: 563,
columnNumber: 11 columnNumber: 11
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -829,7 +901,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
size: 24 size: 24
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 536, lineNumber: 574,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("span", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
@@ -837,13 +909,13 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
children: "L'IA travaille..." children: "L'IA travaille..."
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 537, lineNumber: 575,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 535, lineNumber: 573,
columnNumber: 15 columnNumber: 15
}, ("TURBOPACK compile-time value", void 0)) : /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["Fragment"], { }, ("TURBOPACK compile-time value", void 0)) : /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["Fragment"], {
children: [ children: [
@@ -852,7 +924,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
children: "Outils IA" children: "Outils IA"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 541, lineNumber: 579,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -864,14 +936,14 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
size: 14 size: 14
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 550, lineNumber: 588,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Corriger l'orthographe" " Corriger l'orthographe"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 545, lineNumber: 583,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -883,14 +955,14 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
size: 14 size: 14
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 558, lineNumber: 596,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Reformuler" " Reformuler"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 553, lineNumber: 591,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -902,14 +974,14 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
size: 14 size: 14
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 566, lineNumber: 604,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Développer" " Développer"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 561, lineNumber: 599,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -920,21 +992,21 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
size: 14 size: 14
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 573, lineNumber: 611,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Continuer l'écriture" " Continuer l'écriture"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 569, lineNumber: 607,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
className: "h-px bg-slate-100 my-1" className: "h-px bg-slate-100 my-1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 576, lineNumber: 614,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -942,7 +1014,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
children: "Édition" children: "Édition"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 578, lineNumber: 616,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -954,14 +1026,14 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
size: 14 size: 14
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 587, lineNumber: 625,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Copier" " Copier"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 582, lineNumber: 620,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -972,21 +1044,21 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
size: 14 size: 14
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 594, lineNumber: 632,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Tout sélectionner" " Tout sélectionner"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 590, lineNumber: 628,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true) }, void 0, true)
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 530, lineNumber: 568,
columnNumber: 11 columnNumber: 11
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
@@ -994,7 +1066,7 @@ const RichTextEditor = /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$proj
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 347, lineNumber: 384,
columnNumber: 5 columnNumber: 5
}, ("TURBOPACK compile-time value", void 0)); }, ("TURBOPACK compile-time value", void 0));
}); });
@@ -1025,6 +1097,7 @@ function WritePage() {
const currentChapter = project.chapters?.find((c)=>c.id === currentChapterId); const currentChapter = project.chapters?.find((c)=>c.id === currentChapterId);
return /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$src$2f$components$2f$RichTextEditor$2e$tsx__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["default"], { return /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$server$2f$route$2d$modules$2f$app$2d$page$2f$vendored$2f$ssr$2f$react$2d$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$src$2f$components$2f$RichTextEditor$2e$tsx__$5b$app$2d$ssr$5d$__$28$ecmascript$29$__["default"], {
ref: editorRef, ref: editorRef,
editorId: currentChapterId,
initialContent: currentChapter?.content || '', initialContent: currentChapter?.content || '',
onSave: (html)=>updateChapter(currentChapterId, { onSave: (html)=>updateChapter(currentChapterId, {
content: html content: html

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
self.__NEXT_FONT_MANIFEST="{\n \"app\": {\n \"[project]/Documents/00 - projet/plumeia/src/app/dashboard/page\": [\n \"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2\",\n \"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2\"\n ],\n \"[project]/Documents/00 - projet/plumeia/src/app/login/page\": [\n \"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2\",\n \"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2\"\n ],\n \"[project]/Documents/00 - projet/plumeia/src/app/project/[id]/ideas/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/project/[id]/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/project/[id]/settings/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/project/[id]/workflow/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/project/[id]/world/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/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/profile/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/project/[id]/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/project/[id]/settings/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/project/[id]/world/page\": [\n \"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2\",\n \"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2\"\n ]\n },\n \"appUsingSizeAdjust\": true,\n \"pages\": {},\n \"pagesUsingSizeAdjust\": false\n}"

View File

@@ -8,7 +8,7 @@
"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2", "static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2",
"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2" "static/media/248e1dc0efc99276-s.p.8a6b2436.woff2"
], ],
"[project]/Documents/00 - projet/plumeia/src/app/project/[id]/ideas/page": [ "[project]/Documents/00 - projet/plumeia/src/app/profile/page": [
"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2", "static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2",
"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2" "static/media/248e1dc0efc99276-s.p.8a6b2436.woff2"
], ],
@@ -20,10 +20,6 @@
"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2", "static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2",
"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2" "static/media/248e1dc0efc99276-s.p.8a6b2436.woff2"
], ],
"[project]/Documents/00 - projet/plumeia/src/app/project/[id]/workflow/page": [
"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2",
"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2"
],
"[project]/Documents/00 - projet/plumeia/src/app/project/[id]/world/page": [ "[project]/Documents/00 - projet/plumeia/src/app/project/[id]/world/page": [
"static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2", "static/media/83afe278b6a6bb3c-s.p.3a6ba036.woff2",
"static/media/248e1dc0efc99276-s.p.8a6b2436.woff2" "static/media/248e1dc0efc99276-s.p.8a6b2436.woff2"

View File

@@ -104,6 +104,7 @@
--color-green-500: #00c758; --color-green-500: #00c758;
--color-green-700: #008138; --color-green-700: #008138;
--color-green-800: #016630; --color-green-800: #016630;
--color-emerald-600: #009767;
--color-blue-50: #eff6ff; --color-blue-50: #eff6ff;
--color-blue-100: #dbeafe; --color-blue-100: #dbeafe;
--color-blue-200: #bedbff; --color-blue-200: #bedbff;
@@ -126,8 +127,10 @@
--color-indigo-900: #312c85; --color-indigo-900: #312c85;
--color-purple-100: #f3e8ff; --color-purple-100: #f3e8ff;
--color-purple-200: #e9d5ff; --color-purple-200: #e9d5ff;
--color-purple-500: #ac4bff;
--color-purple-600: #9810fa; --color-purple-600: #9810fa;
--color-purple-700: #8200da; --color-purple-700: #8200da;
--color-pink-500: #f6339a;
--color-rose-100: #ffe4e6; --color-rose-100: #ffe4e6;
--color-rose-200: #ffccd3; --color-rose-200: #ffccd3;
--color-rose-800: #a30037; --color-rose-800: #a30037;
@@ -243,6 +246,7 @@
--color-green-500: lab(70.5521% -66.5147 45.8072); --color-green-500: lab(70.5521% -66.5147 45.8072);
--color-green-700: lab(47.0329% -47.0239 31.4788); --color-green-700: lab(47.0329% -47.0239 31.4788);
--color-green-800: lab(37.4616% -36.7971 22.9692); --color-green-800: lab(37.4616% -36.7971 22.9692);
--color-emerald-600: lab(55.0481% -49.9246 15.93);
--color-blue-50: lab(96.492% -1.14647 -5.11479); --color-blue-50: lab(96.492% -1.14647 -5.11479);
--color-blue-100: lab(92.0301% -2.24757 -11.6453); --color-blue-100: lab(92.0301% -2.24757 -11.6453);
--color-blue-200: lab(86.15% -4.04379 -21.0797); --color-blue-200: lab(86.15% -4.04379 -21.0797);
@@ -265,8 +269,10 @@
--color-indigo-900: lab(23.3911% 24.6978 -50.4719); --color-indigo-900: lab(23.3911% 24.6978 -50.4719);
--color-purple-100: lab(93.3333% 6.9744 -9.83434); --color-purple-100: lab(93.3333% 6.9744 -9.83434);
--color-purple-200: lab(87.8405% 13.4282 -18.7159); --color-purple-200: lab(87.8405% 13.4282 -18.7159);
--color-purple-500: lab(52.0183% 66.11 -78.2316);
--color-purple-600: lab(43.0295% 75.21 -86.5669); --color-purple-600: lab(43.0295% 75.21 -86.5669);
--color-purple-700: lab(36.1758% 69.8525 -80.0381); --color-purple-700: lab(36.1758% 69.8525 -80.0381);
--color-pink-500: lab(56.9303% 76.8162 -8.07021);
--color-rose-100: lab(92.8221% 9.86832 2.60077); --color-rose-100: lab(92.8221% 9.86832 2.60077);
--color-rose-200: lab(86.806% 19.1909 4.07754); --color-rose-200: lab(86.806% 19.1909 4.07754);
--color-rose-800: lab(34.6481% 60.802 20.1957); --color-rose-800: lab(34.6481% 60.802 20.1957);
@@ -644,6 +650,10 @@
right: calc(var(--spacing) * 4); right: calc(var(--spacing) * 4);
} }
.right-6 {
right: calc(var(--spacing) * 6);
}
.right-10 { .right-10 {
right: calc(var(--spacing) * 10); right: calc(var(--spacing) * 10);
} }
@@ -664,6 +674,10 @@
bottom: calc(var(--spacing) * 2); bottom: calc(var(--spacing) * 2);
} }
.bottom-6 {
bottom: calc(var(--spacing) * 6);
}
.bottom-10 { .bottom-10 {
bottom: calc(var(--spacing) * 10); bottom: calc(var(--spacing) * 10);
} }
@@ -680,6 +694,10 @@
left: calc(var(--spacing) * 0); left: calc(var(--spacing) * 0);
} }
.left-2 {
left: calc(var(--spacing) * 2);
}
.left-3 { .left-3 {
left: calc(var(--spacing) * 3); left: calc(var(--spacing) * 3);
} }
@@ -1316,10 +1334,6 @@
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-cols-4 { .grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
@@ -1589,6 +1603,11 @@
border-style: dashed; border-style: dashed;
} }
.border-none {
--tw-border-style: none;
border-style: none;
}
.border-\[\#dfcdae\] { .border-\[\#dfcdae\] {
border-color: #dfcdae; border-color: #dfcdae;
} }
@@ -1709,6 +1728,14 @@
border-color: var(--color-theme-border); border-color: var(--color-theme-border);
} }
.border-theme-panel {
border-color: var(--color-theme-panel);
}
.border-theme-text {
border-color: var(--color-theme-text);
}
.border-transparent { .border-transparent {
border-color: #0000; border-color: #0000;
} }
@@ -1987,6 +2014,10 @@
background-color: var(--color-theme-panel); background-color: var(--color-theme-panel);
} }
.bg-theme-text {
background-color: var(--color-theme-text);
}
.bg-transparent { .bg-transparent {
background-color: #0000; background-color: #0000;
} }
@@ -2045,6 +2076,11 @@
background-image: linear-gradient(var(--tw-gradient-stops)); background-image: linear-gradient(var(--tw-gradient-stops));
} }
.bg-gradient-to-tr {
--tw-gradient-position: to top right in oklab;
background-image: linear-gradient(var(--tw-gradient-stops));
}
.from-blue-500 { .from-blue-500 {
--tw-gradient-from: var(--color-blue-500); --tw-gradient-from: var(--color-blue-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@@ -2074,6 +2110,11 @@
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
} }
.from-pink-500 {
--tw-gradient-from: var(--color-pink-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-red-200 { .from-red-200 {
--tw-gradient-from: var(--color-red-200); --tw-gradient-from: var(--color-red-200);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@@ -2096,6 +2137,12 @@
--tw-gradient-stops: var(--tw-gradient-via-stops); --tw-gradient-stops: var(--tw-gradient-via-stops);
} }
.via-purple-500 {
--tw-gradient-via: var(--color-purple-500);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-yellow-100 { .via-yellow-100 {
--tw-gradient-via: var(--color-yellow-100); --tw-gradient-via: var(--color-yellow-100);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
@@ -2568,6 +2615,10 @@
color: var(--color-blue-900); color: var(--color-blue-900);
} }
.text-emerald-600 {
color: var(--color-emerald-600);
}
.text-gray-800 { .text-gray-800 {
color: var(--color-gray-800); color: var(--color-gray-800);
} }
@@ -2668,6 +2719,10 @@
color: var(--color-slate-900); color: var(--color-slate-900);
} }
.text-theme-bg {
color: var(--color-theme-bg);
}
.text-theme-editor-text { .text-theme-editor-text {
color: var(--color-theme-editor-text); color: var(--color-theme-editor-text);
} }
@@ -2826,6 +2881,16 @@
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
} }
.shadow-\[\#dfcdae\] {
--tw-shadow-color: #dfcdae;
}
@supports (color: color-mix(in lab, red, red)) {
.shadow-\[\#dfcdae\] {
--tw-shadow-color: color-mix(in oklab, #dfcdae var(--tw-shadow-alpha), transparent);
}
}
.shadow-blue-200 { .shadow-blue-200 {
--tw-shadow-color: #bedbff; --tw-shadow-color: #bedbff;
} }
@@ -2858,6 +2923,48 @@
} }
} }
.shadow-slate-200 {
--tw-shadow-color: #e2e8f0;
}
@supports (color: lab(0% 0 0)) {
.shadow-slate-200 {
--tw-shadow-color: lab(91.7353% -.998765 -4.76968);
}
}
@supports (color: color-mix(in lab, red, red)) {
.shadow-slate-200 {
--tw-shadow-color: color-mix(in oklab, var(--color-slate-200) var(--tw-shadow-alpha), transparent);
}
}
.shadow-slate-900 {
--tw-shadow-color: #0f172b;
}
@supports (color: lab(0% 0 0)) {
.shadow-slate-900 {
--tw-shadow-color: lab(7.78673% 1.82346 -15.0537);
}
}
@supports (color: color-mix(in lab, red, red)) {
.shadow-slate-900 {
--tw-shadow-color: color-mix(in oklab, var(--color-slate-900) var(--tw-shadow-alpha), transparent);
}
}
.shadow-theme-border {
--tw-shadow-color: var(--color-theme-border);
}
@supports (color: color-mix(in lab, red, red)) {
.shadow-theme-border {
--tw-shadow-color: color-mix(in oklab, var(--color-theme-border) var(--tw-shadow-alpha), transparent);
}
}
.ring-amber-200 { .ring-amber-200 {
--tw-ring-color: var(--color-amber-200); --tw-ring-color: var(--color-amber-200);
} }
@@ -3033,6 +3140,13 @@
background-color: var(--color-blue-200); background-color: var(--color-blue-200);
} }
@media (hover: hover) {
.hover\:-translate-y-1:hover {
--tw-translate-y: calc(var(--spacing) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
}
@media (hover: hover) { @media (hover: hover) {
.hover\:scale-105:hover { .hover\:scale-105:hover {
--tw-scale-x: 105%; --tw-scale-x: 105%;
@@ -3087,6 +3201,12 @@
} }
} }
@media (hover: hover) {
.hover\:border-red-200:hover {
border-color: var(--color-red-200);
}
}
@media (hover: hover) { @media (hover: hover) {
.hover\:border-slate-200:hover { .hover\:border-slate-200:hover {
border-color: var(--color-slate-200); border-color: var(--color-slate-200);
@@ -3105,6 +3225,18 @@
} }
} }
@media (hover: hover) {
.hover\:border-theme-text:hover {
border-color: var(--color-theme-text);
}
}
@media (hover: hover) {
.hover\:bg-\[\#433422\]:hover {
background-color: #433422;
}
}
@media (hover: hover) { @media (hover: hover) {
.hover\:bg-\[\#eaddc4\]:hover { .hover\:bg-\[\#eaddc4\]:hover {
background-color: #eaddc4; background-color: #eaddc4;
@@ -3141,12 +3273,6 @@
} }
} }
@media (hover: hover) {
.hover\:bg-blue-500:hover {
background-color: var(--color-blue-500);
}
}
@media (hover: hover) { @media (hover: hover) {
.hover\:bg-blue-500\/10:hover { .hover\:bg-blue-500\/10:hover {
background-color: #3080ff1a; background-color: #3080ff1a;

View File

@@ -20,6 +20,7 @@ var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__proje
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$copy$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Copy$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/copy.js [app-client] (ecmascript) <export default as Copy>"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$copy$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Copy$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/copy.js [app-client] (ecmascript) <export default as Copy>");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$wand$2d$sparkles$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Wand2$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/wand-sparkles.js [app-client] (ecmascript) <export default as Wand2>"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$wand$2d$sparkles$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Wand2$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/wand-sparkles.js [app-client] (ecmascript) <export default as Wand2>");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$check$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Check$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/check.js [app-client] (ecmascript) <export default as Check>"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$check$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Check$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/check.js [app-client] (ecmascript) <export default as Check>");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$check$2d$check$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__CheckCheck$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/check-check.js [app-client] (ecmascript) <export default as CheckCheck>");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$refresh$2d$cw$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__RefreshCw$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/refresh-cw.js [app-client] (ecmascript) <export default as RefreshCw>"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$refresh$2d$cw$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__RefreshCw$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/refresh-cw.js [app-client] (ecmascript) <export default as RefreshCw>");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$maximize$2d$2$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Maximize2$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/maximize-2.js [app-client] (ecmascript) <export default as Maximize2>"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$maximize$2d$2$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Maximize2$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/maximize-2.js [app-client] (ecmascript) <export default as Maximize2>");
var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$loader$2d$circle$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Loader2$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/loader-circle.js [app-client] (ecmascript) <export default as Loader2>"); var __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$loader$2d$circle$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Loader2$3e$__ = __turbopack_context__.i("[project]/Documents/00 - projet/plumeia/node_modules/lucide-react/dist/esm/icons/loader-circle.js [app-client] (ecmascript) <export default as Loader2>");
@@ -34,13 +35,13 @@ var _s = __turbopack_context__.k.signature();
'use client'; 'use client';
; ;
; ;
const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["forwardRef"])(_c = _s(({ initialContent, onChange, onSave, onSelectionChange, onAiTransform }, ref)=>{ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["forwardRef"])(_c = _s(({ editorId, initialContent, onChange, onSave, onSelectionChange, onAiTransform }, ref)=>{
_s(); _s();
const contentRef = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useRef"])(null); const contentRef = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useRef"])(null);
const scrollContainerRef = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useRef"])(null); const scrollContainerRef = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useRef"])(null);
const [isFocused, setIsFocused] = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useState"])(false); const [isFocused, setIsFocused] = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useState"])(false);
// Auto-Save State // Auto-Save State
const [saveStatus, setSaveStatus] = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useState"])('saved'); const [saveStatus, setSaveStatus] = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useState"])('saved_db');
const saveTimeoutRef = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useRef"])(null); const saveTimeoutRef = (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useRef"])(null);
// Track sync state to avoid autosave loopbacks wiping current edits // Track sync state to avoid autosave loopbacks wiping current edits
// Start as null so the initial useEffect ALWAYS writes initialContent to the div // Start as null so the initial useEffect ALWAYS writes initialContent to the div
@@ -168,19 +169,33 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
(0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useEffect"])({ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useEffect"])({
"RichTextEditor.useEffect": ()=>{ "RichTextEditor.useEffect": ()=>{
if (!contentRef.current || initialContent === undefined) return; if (!contentRef.current || initialContent === undefined) return;
// Ignore exact loopbacks from our own saves let contentToLoad = initialContent;
if (initialContent === syncRef.current) return; let hasLocalDraft = false;
// Safety: never overwrite real content with an empty string from a stale/placeholder source // Check localStorage for a newer draft
const hasRealContent = latestContentRef.current && latestContentRef.current.trim().length > 0; if (editorId) {
if (!initialContent && hasRealContent) return; const localDraft = localStorage.getItem(`draft_${editorId}`);
// We reached here, so initialContent is genuinely NEW data we didn't know about. if (localDraft && localDraft !== initialContent) {
// E.g. clicked another chapter, or data was modified in another tab/device. contentToLoad = localDraft;
contentRef.current.innerHTML = initialContent; hasLocalDraft = true;
syncRef.current = initialContent; setSaveStatus('saved_local');
latestContentRef.current = initialContent; }
}
// 1. Si le contenu entrant est identique à ce qu'on a déjà, on ne touche à rien
if (contentToLoad === contentRef.current.innerHTML) return;
// 2. LOGIQUE CRUCIALE : On ne met à jour le DOM que si :
// - L'éditeur est vide (premier chargement)
// - OU le document a changé (si vous gérez des IDs de documents)
// - OU si l'utilisateur n'est PAS en train de focus l'éditeur
const isUserEditing = document.activeElement === contentRef.current;
if (!isUserEditing || contentRef.current.innerHTML === "" && contentToLoad !== "") {
contentRef.current.innerHTML = contentToLoad;
syncRef.current = contentToLoad;
latestContentRef.current = contentToLoad;
}
} }
}["RichTextEditor.useEffect"], [ }["RichTextEditor.useEffect"], [
initialContent initialContent,
editorId
]); ]);
// Flush pending save on unmount // Flush pending save on unmount
(0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useEffect"])({ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$index$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["useEffect"])({
@@ -189,9 +204,11 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
"RichTextEditor.useEffect": ()=>{ "RichTextEditor.useEffect": ()=>{
if (saveTimeoutRef.current) { if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current); clearTimeout(saveTimeoutRef.current);
if (latestContentRef.current !== syncRef.current && onSave) { }
onSave(latestContentRef.current); // Always save if there are unsaved changes, regardless of timer
} if (latestContentRef.current !== syncRef.current && onSave) {
syncRef.current = latestContentRef.current;
onSave(latestContentRef.current);
} }
} }
})["RichTextEditor.useEffect"]; })["RichTextEditor.useEffect"];
@@ -210,16 +227,34 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
const currentHtml = contentRef.current.innerHTML; const currentHtml = contentRef.current.innerHTML;
latestContentRef.current = currentHtml; latestContentRef.current = currentHtml;
if (onChange) onChange(currentHtml); if (onChange) onChange(currentHtml);
// Auto-Save Debounce // 1. Save locally immediately
if (onSave) { if (editorId) {
localStorage.setItem(`draft_${editorId}`, currentHtml);
setSaveStatus('saved_local');
} else {
setSaveStatus('unsaved'); setSaveStatus('unsaved');
}
// 2. Auto-Save Debounce for DB
if (onSave) {
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = setTimeout(async ()=>{ saveTimeoutRef.current = setTimeout(async ()=>{
setSaveStatus('saving'); setSaveStatus('saving');
const htmlToSave = latestContentRef.current; const htmlToSave = latestContentRef.current;
await onSave(htmlToSave); // Update syncRef BEFORE calling onSave, because onSave triggers setProjects
syncRef.current = htmlToSave; // Record that we've synced this exact string to the server // which causes a re-render. The useEffect must see the updated syncRef
setSaveStatus('saved'); // to avoid re-writing innerHTML unnecessarily.
syncRef.current = htmlToSave;
try {
await onSave(htmlToSave);
setSaveStatus('saved_db');
if (editorId) {
// Once saved to DB, we can consider the local draft synced if we want,
// or just keep it there. It will be overwritten on next load.
}
} catch (err) {
console.error('Auto-save failed:', err);
setSaveStatus('saved_local'); // Revert to local save status on error
}
}, 2000); // 2 seconds }, 2000); // 2 seconds
} }
} }
@@ -327,12 +362,12 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
size: 18 size: 18
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 340, lineNumber: 377,
columnNumber: 7 columnNumber: 7
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 327, lineNumber: 364,
columnNumber: 5 columnNumber: 5
}, ("TURBOPACK compile-time value", void 0)); }, ("TURBOPACK compile-time value", void 0));
const hasSelection = savedRange.current && !savedRange.current.collapsed; const hasSelection = savedRange.current && !savedRange.current.collapsed;
@@ -350,7 +385,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
` `
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 348, lineNumber: 385,
columnNumber: 7 columnNumber: 7
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -362,7 +397,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
label: "Gras" label: "Gras"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 359, lineNumber: 396,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -371,7 +406,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
label: "Italique" label: "Italique"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 360, lineNumber: 397,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -380,14 +415,14 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
label: "Souligné" label: "Souligné"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 361, lineNumber: 398,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
className: "w-px h-6 bg-slate-300 mx-1" className: "w-px h-6 bg-slate-300 mx-1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 362, lineNumber: 399,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -397,7 +432,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
label: "Titre 1" label: "Titre 1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 363, lineNumber: 400,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -407,14 +442,14 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
label: "Titre 2" label: "Titre 2"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 364, lineNumber: 401,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
className: "w-px h-6 bg-slate-300 mx-1" className: "w-px h-6 bg-slate-300 mx-1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 365, lineNumber: 402,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -423,7 +458,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
label: "Aligner à gauche" label: "Aligner à gauche"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 366, lineNumber: 403,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -432,7 +467,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
label: "Centrer" label: "Centrer"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 367, lineNumber: 404,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -441,14 +476,14 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
label: "Aligner à droite" label: "Aligner à droite"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 368, lineNumber: 405,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
className: "w-px h-6 bg-slate-300 mx-1" className: "w-px h-6 bg-slate-300 mx-1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 369, lineNumber: 406,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -457,14 +492,14 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
label: "Liste" label: "Liste"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 370, lineNumber: 407,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
className: "flex-1" className: "flex-1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 372, lineNumber: 409,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -474,47 +509,84 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
children: [ children: [
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$loader$2d$circle$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Loader2$3e$__["Loader2"], { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$loader$2d$circle$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Loader2$3e$__["Loader2"], {
size: 12, size: 12,
className: "animate-spin" className: "animate-spin text-blue-500"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 376, lineNumber: 413,
columnNumber: 41 columnNumber: 41
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Sauvegarde..." " ",
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
className: "text-blue-500 hidden sm:inline",
children: "Sauvegarde en cours..."
}, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 413,
columnNumber: 102
}, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true), }, void 0, true),
saveStatus === 'saved' && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["Fragment"], { saveStatus === 'saved_local' && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["Fragment"], {
children: [ children: [
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$check$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Check$3e$__["Check"], { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$check$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__Check$3e$__["Check"], {
size: 12, size: 14,
className: "text-green-500" className: "text-green-500"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 377, lineNumber: 414,
columnNumber: 40 columnNumber: 46
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Sauvegardé" " ",
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
className: "text-green-500 hidden sm:inline",
children: "Brouillon local"
}, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 414,
columnNumber: 93
}, ("TURBOPACK compile-time value", void 0))
]
}, void 0, true),
saveStatus === 'saved_db' && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["Fragment"], {
children: [
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$check$2d$check$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__CheckCheck$3e$__["CheckCheck"], {
size: 14,
className: "text-emerald-600"
}, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 415,
columnNumber: 43
}, ("TURBOPACK compile-time value", void 0)),
" ",
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
className: "text-emerald-600 hidden sm:inline",
children: "Sauvegardé"
}, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 415,
columnNumber: 97
}, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true), }, void 0, true),
saveStatus === 'unsaved' && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("span", { saveStatus === 'unsaved' && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
className: "text-amber-500", className: "text-amber-500",
children: "Modifications non enregistrées..." children: "Non sauvegardé..."
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 378, lineNumber: 416,
columnNumber: 40 columnNumber: 40
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 375, lineNumber: 412,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
className: "w-px h-6 bg-slate-300 mx-1" className: "w-px h-6 bg-slate-300 mx-1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 381, lineNumber: 419,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(ToolbarButton, {
@@ -524,13 +596,13 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
isActive: showHistoryMargin isActive: showHistoryMargin
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 382, lineNumber: 420,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 358, lineNumber: 395,
columnNumber: 7 columnNumber: 7
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -560,7 +632,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
"data-placeholder": "Commencez à écrire votre chef-d'œuvre... (Clic droit pour outils IA)" "data-placeholder": "Commencez à écrire votre chef-d'œuvre... (Clic droit pour outils IA)"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 398, lineNumber: 436,
columnNumber: 11 columnNumber: 11
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
showHistoryMargin && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { showHistoryMargin && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -574,7 +646,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
className: "mx-auto mb-2 opacity-20" className: "mx-auto mb-2 opacity-20"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 422, lineNumber: 460,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("p", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("p", {
@@ -582,13 +654,13 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
children: "L'historique des modifications IA apparaîtra ici, aligné avec votre texte." children: "L'historique des modifications IA apparaîtra ici, aligné avec votre texte."
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 423, lineNumber: 461,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 421, lineNumber: 459,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
versionGroups.map((group)=>{ versionGroups.map((group)=>{
@@ -607,7 +679,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
className: "absolute inset-0 bg-white border border-indigo-100 rounded-lg transform -translate-x-1 -translate-y-1 -z-10 shadow-sm" className: "absolute inset-0 bg-white border border-indigo-100 rounded-lg transform -translate-x-1 -translate-y-1 -z-10 shadow-sm"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 443, lineNumber: 481,
columnNumber: 25 columnNumber: 25
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -622,7 +694,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
className: "text-indigo-500" className: "text-indigo-500"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 453, lineNumber: 491,
columnNumber: 29 columnNumber: 29
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("span", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
@@ -630,13 +702,13 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
children: latest.type children: latest.type
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 455, lineNumber: 493,
columnNumber: 27 columnNumber: 27
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 451, lineNumber: 489,
columnNumber: 25 columnNumber: 25
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -650,7 +722,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
}) })
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 463, lineNumber: 501,
columnNumber: 27 columnNumber: 27
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
isStack && (isExpanded ? /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$chevron$2d$up$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__ChevronUp$3e$__["ChevronUp"], { isStack && (isExpanded ? /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$chevron$2d$up$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__ChevronUp$3e$__["ChevronUp"], {
@@ -658,26 +730,26 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
className: "text-slate-400" className: "text-slate-400"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 467, lineNumber: 505,
columnNumber: 42 columnNumber: 42
}, ("TURBOPACK compile-time value", void 0)) : /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$chevron$2d$down$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__ChevronDown$3e$__["ChevronDown"], { }, ("TURBOPACK compile-time value", void 0)) : /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$lucide$2d$react$2f$dist$2f$esm$2f$icons$2f$chevron$2d$down$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__$3c$export__default__as__ChevronDown$3e$__["ChevronDown"], {
size: 14, size: 14,
className: "text-slate-400" className: "text-slate-400"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 467, lineNumber: 505,
columnNumber: 95 columnNumber: 95
}, ("TURBOPACK compile-time value", void 0))) }, ("TURBOPACK compile-time value", void 0)))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 462, lineNumber: 500,
columnNumber: 25 columnNumber: 25
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 447, lineNumber: 485,
columnNumber: 23 columnNumber: 23
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
!isExpanded && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { !isExpanded && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -692,7 +764,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 475, lineNumber: 513,
columnNumber: 27 columnNumber: 27
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -703,20 +775,20 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
size: 10 size: 10
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 482, lineNumber: 520,
columnNumber: 29 columnNumber: 29
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Restaurer" " Restaurer"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 478, lineNumber: 516,
columnNumber: 27 columnNumber: 27
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 474, lineNumber: 512,
columnNumber: 25 columnNumber: 25
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
isExpanded && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { isExpanded && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -732,7 +804,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
children: i === 0 ? 'Dernière version' : `Version -${i}` children: i === 0 ? 'Dernière version' : `Version -${i}`
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 493, lineNumber: 531,
columnNumber: 33 columnNumber: 33
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("span", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
@@ -744,13 +816,13 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
}) })
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 496, lineNumber: 534,
columnNumber: 33 columnNumber: 33
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 492, lineNumber: 530,
columnNumber: 31 columnNumber: 31
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -762,7 +834,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 500, lineNumber: 538,
columnNumber: 31 columnNumber: 31
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -773,54 +845,54 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
size: 10 size: 10
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 507, lineNumber: 545,
columnNumber: 33 columnNumber: 33
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Restaurer cette version" " Restaurer cette version"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 503, lineNumber: 541,
columnNumber: 31 columnNumber: 31
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, v.id, true, { }, v.id, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 491, lineNumber: 529,
columnNumber: 29 columnNumber: 29
}, ("TURBOPACK compile-time value", void 0))) }, ("TURBOPACK compile-time value", void 0)))
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 489, lineNumber: 527,
columnNumber: 25 columnNumber: 25
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 439, lineNumber: 477,
columnNumber: 21 columnNumber: 21
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
}, group.id, false, { }, group.id, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 434, lineNumber: 472,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)); }, ("TURBOPACK compile-time value", void 0));
}) })
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 418, lineNumber: 456,
columnNumber: 13 columnNumber: 13
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 395, lineNumber: 433,
columnNumber: 9 columnNumber: 9
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 391, lineNumber: 429,
columnNumber: 7 columnNumber: 7
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
contextMenu && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["Fragment"], { contextMenu && /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["Fragment"], {
@@ -834,7 +906,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
} }
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 525, lineNumber: 563,
columnNumber: 11 columnNumber: 11
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -851,7 +923,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
size: 24 size: 24
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 536, lineNumber: 574,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("span", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("span", {
@@ -859,13 +931,13 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
children: "L'IA travaille..." children: "L'IA travaille..."
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 537, lineNumber: 575,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 535, lineNumber: 573,
columnNumber: 15 columnNumber: 15
}, ("TURBOPACK compile-time value", void 0)) : /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["Fragment"], { }, ("TURBOPACK compile-time value", void 0)) : /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["Fragment"], {
children: [ children: [
@@ -874,7 +946,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
children: "Outils IA" children: "Outils IA"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 541, lineNumber: 579,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -886,14 +958,14 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
size: 14 size: 14
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 550, lineNumber: 588,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Corriger l'orthographe" " Corriger l'orthographe"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 545, lineNumber: 583,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -905,14 +977,14 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
size: 14 size: 14
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 558, lineNumber: 596,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Reformuler" " Reformuler"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 553, lineNumber: 591,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -924,14 +996,14 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
size: 14 size: 14
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 566, lineNumber: 604,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Développer" " Développer"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 561, lineNumber: 599,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -942,21 +1014,21 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
size: 14 size: 14
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 573, lineNumber: 611,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Continuer l'écriture" " Continuer l'écriture"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 569, lineNumber: 607,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
className: "h-px bg-slate-100 my-1" className: "h-px bg-slate-100 my-1"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 576, lineNumber: 614,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("div", {
@@ -964,7 +1036,7 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
children: "Édition" children: "Édition"
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 578, lineNumber: 616,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -976,14 +1048,14 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
size: 14 size: 14
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 587, lineNumber: 625,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Copier" " Copier"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 582, lineNumber: 620,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
/*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", { /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])("button", {
@@ -994,21 +1066,21 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
size: 14 size: 14
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 594, lineNumber: 632,
columnNumber: 19 columnNumber: 19
}, ("TURBOPACK compile-time value", void 0)), }, ("TURBOPACK compile-time value", void 0)),
" Tout sélectionner" " Tout sélectionner"
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 590, lineNumber: 628,
columnNumber: 17 columnNumber: 17
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
}, void 0, true) }, void 0, true)
}, void 0, false, { }, void 0, false, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 530, lineNumber: 568,
columnNumber: 11 columnNumber: 11
}, ("TURBOPACK compile-time value", void 0)) }, ("TURBOPACK compile-time value", void 0))
] ]
@@ -1016,10 +1088,10 @@ const RichTextEditor = /*#__PURE__*/ _s((0, __TURBOPACK__imported__module__$5b$p
] ]
}, void 0, true, { }, void 0, true, {
fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx", fileName: "[project]/Documents/00 - projet/plumeia/src/components/RichTextEditor.tsx",
lineNumber: 347, lineNumber: 384,
columnNumber: 5 columnNumber: 5
}, ("TURBOPACK compile-time value", void 0)); }, ("TURBOPACK compile-time value", void 0));
}, "1keE8cf732OZ6jTNRl46BlQ/eZo=")), "1keE8cf732OZ6jTNRl46BlQ/eZo="); }, "cWpHoTw7wOYNa/FuW7N+8oRfoPQ=")), "cWpHoTw7wOYNa/FuW7N+8oRfoPQ=");
_c1 = RichTextEditor; _c1 = RichTextEditor;
const __TURBOPACK__default__export__ = RichTextEditor; const __TURBOPACK__default__export__ = RichTextEditor;
var _c, _c1; var _c, _c1;
@@ -1056,6 +1128,7 @@ function WritePage() {
const currentChapter = project.chapters?.find((c)=>c.id === currentChapterId); const currentChapter = project.chapters?.find((c)=>c.id === currentChapterId);
return /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$src$2f$components$2f$RichTextEditor$2e$tsx__$5b$app$2d$client$5d$__$28$ecmascript$29$__["default"], { return /*#__PURE__*/ (0, __TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$node_modules$2f$next$2f$dist$2f$compiled$2f$react$2f$jsx$2d$dev$2d$runtime$2e$js__$5b$app$2d$client$5d$__$28$ecmascript$29$__["jsxDEV"])(__TURBOPACK__imported__module__$5b$project$5d2f$Documents$2f$00__$2d$__projet$2f$plumeia$2f$src$2f$components$2f$RichTextEditor$2e$tsx__$5b$app$2d$client$5d$__$28$ecmascript$29$__["default"], {
ref: editorRef, ref: editorRef,
editorId: currentChapterId,
initialContent: currentChapter?.content || '', initialContent: currentChapter?.content || '',
onSave: (html)=>updateChapter(currentChapterId, { onSave: (html)=>updateChapter(currentChapterId, {
content: html content: html

File diff suppressed because one or more lines are too long

View File

@@ -405,6 +405,7 @@
--color-green-500: #00c758; --color-green-500: #00c758;
--color-green-700: #008138; --color-green-700: #008138;
--color-green-800: #016630; --color-green-800: #016630;
--color-emerald-600: #009767;
--color-blue-50: #eff6ff; --color-blue-50: #eff6ff;
--color-blue-100: #dbeafe; --color-blue-100: #dbeafe;
--color-blue-200: #bedbff; --color-blue-200: #bedbff;
@@ -427,8 +428,10 @@
--color-indigo-900: #312c85; --color-indigo-900: #312c85;
--color-purple-100: #f3e8ff; --color-purple-100: #f3e8ff;
--color-purple-200: #e9d5ff; --color-purple-200: #e9d5ff;
--color-purple-500: #ac4bff;
--color-purple-600: #9810fa; --color-purple-600: #9810fa;
--color-purple-700: #8200da; --color-purple-700: #8200da;
--color-pink-500: #f6339a;
--color-rose-100: #ffe4e6; --color-rose-100: #ffe4e6;
--color-rose-200: #ffccd3; --color-rose-200: #ffccd3;
--color-rose-800: #a30037; --color-rose-800: #a30037;
@@ -544,6 +547,7 @@
--color-green-500: lab(70.5521% -66.5147 45.8072); --color-green-500: lab(70.5521% -66.5147 45.8072);
--color-green-700: lab(47.0329% -47.0239 31.4788); --color-green-700: lab(47.0329% -47.0239 31.4788);
--color-green-800: lab(37.4616% -36.7971 22.9692); --color-green-800: lab(37.4616% -36.7971 22.9692);
--color-emerald-600: lab(55.0481% -49.9246 15.93);
--color-blue-50: lab(96.492% -1.14647 -5.11479); --color-blue-50: lab(96.492% -1.14647 -5.11479);
--color-blue-100: lab(92.0301% -2.24757 -11.6453); --color-blue-100: lab(92.0301% -2.24757 -11.6453);
--color-blue-200: lab(86.15% -4.04379 -21.0797); --color-blue-200: lab(86.15% -4.04379 -21.0797);
@@ -566,8 +570,10 @@
--color-indigo-900: lab(23.3911% 24.6978 -50.4719); --color-indigo-900: lab(23.3911% 24.6978 -50.4719);
--color-purple-100: lab(93.3333% 6.9744 -9.83434); --color-purple-100: lab(93.3333% 6.9744 -9.83434);
--color-purple-200: lab(87.8405% 13.4282 -18.7159); --color-purple-200: lab(87.8405% 13.4282 -18.7159);
--color-purple-500: lab(52.0183% 66.11 -78.2316);
--color-purple-600: lab(43.0295% 75.21 -86.5669); --color-purple-600: lab(43.0295% 75.21 -86.5669);
--color-purple-700: lab(36.1758% 69.8525 -80.0381); --color-purple-700: lab(36.1758% 69.8525 -80.0381);
--color-pink-500: lab(56.9303% 76.8162 -8.07021);
--color-rose-100: lab(92.8221% 9.86832 2.60077); --color-rose-100: lab(92.8221% 9.86832 2.60077);
--color-rose-200: lab(86.806% 19.1909 4.07754); --color-rose-200: lab(86.806% 19.1909 4.07754);
--color-rose-800: lab(34.6481% 60.802 20.1957); --color-rose-800: lab(34.6481% 60.802 20.1957);
@@ -945,6 +951,10 @@
right: calc(var(--spacing) * 4); right: calc(var(--spacing) * 4);
} }
.right-6 {
right: calc(var(--spacing) * 6);
}
.right-10 { .right-10 {
right: calc(var(--spacing) * 10); right: calc(var(--spacing) * 10);
} }
@@ -965,6 +975,10 @@
bottom: calc(var(--spacing) * 2); bottom: calc(var(--spacing) * 2);
} }
.bottom-6 {
bottom: calc(var(--spacing) * 6);
}
.bottom-10 { .bottom-10 {
bottom: calc(var(--spacing) * 10); bottom: calc(var(--spacing) * 10);
} }
@@ -981,6 +995,10 @@
left: calc(var(--spacing) * 0); left: calc(var(--spacing) * 0);
} }
.left-2 {
left: calc(var(--spacing) * 2);
}
.left-3 { .left-3 {
left: calc(var(--spacing) * 3); left: calc(var(--spacing) * 3);
} }
@@ -1617,10 +1635,6 @@
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
.grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.grid-cols-4 { .grid-cols-4 {
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));
} }
@@ -1890,6 +1904,11 @@
border-style: dashed; border-style: dashed;
} }
.border-none {
--tw-border-style: none;
border-style: none;
}
.border-\[\#dfcdae\] { .border-\[\#dfcdae\] {
border-color: #dfcdae; border-color: #dfcdae;
} }
@@ -2010,6 +2029,14 @@
border-color: var(--color-theme-border); border-color: var(--color-theme-border);
} }
.border-theme-panel {
border-color: var(--color-theme-panel);
}
.border-theme-text {
border-color: var(--color-theme-text);
}
.border-transparent { .border-transparent {
border-color: #0000; border-color: #0000;
} }
@@ -2288,6 +2315,10 @@
background-color: var(--color-theme-panel); background-color: var(--color-theme-panel);
} }
.bg-theme-text {
background-color: var(--color-theme-text);
}
.bg-transparent { .bg-transparent {
background-color: #0000; background-color: #0000;
} }
@@ -2346,6 +2377,11 @@
background-image: linear-gradient(var(--tw-gradient-stops)); background-image: linear-gradient(var(--tw-gradient-stops));
} }
.bg-gradient-to-tr {
--tw-gradient-position: to top right in oklab;
background-image: linear-gradient(var(--tw-gradient-stops));
}
.from-blue-500 { .from-blue-500 {
--tw-gradient-from: var(--color-blue-500); --tw-gradient-from: var(--color-blue-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@@ -2375,6 +2411,11 @@
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
} }
.from-pink-500 {
--tw-gradient-from: var(--color-pink-500);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
}
.from-red-200 { .from-red-200 {
--tw-gradient-from: var(--color-red-200); --tw-gradient-from: var(--color-red-200);
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
@@ -2397,6 +2438,12 @@
--tw-gradient-stops: var(--tw-gradient-via-stops); --tw-gradient-stops: var(--tw-gradient-via-stops);
} }
.via-purple-500 {
--tw-gradient-via: var(--color-purple-500);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
--tw-gradient-stops: var(--tw-gradient-via-stops);
}
.via-yellow-100 { .via-yellow-100 {
--tw-gradient-via: var(--color-yellow-100); --tw-gradient-via: var(--color-yellow-100);
--tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position);
@@ -2869,6 +2916,10 @@
color: var(--color-blue-900); color: var(--color-blue-900);
} }
.text-emerald-600 {
color: var(--color-emerald-600);
}
.text-gray-800 { .text-gray-800 {
color: var(--color-gray-800); color: var(--color-gray-800);
} }
@@ -2969,6 +3020,10 @@
color: var(--color-slate-900); color: var(--color-slate-900);
} }
.text-theme-bg {
color: var(--color-theme-bg);
}
.text-theme-editor-text { .text-theme-editor-text {
color: var(--color-theme-editor-text); color: var(--color-theme-editor-text);
} }
@@ -3127,6 +3182,16 @@
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
} }
.shadow-\[\#dfcdae\] {
--tw-shadow-color: #dfcdae;
}
@supports (color: color-mix(in lab, red, red)) {
.shadow-\[\#dfcdae\] {
--tw-shadow-color: color-mix(in oklab, #dfcdae var(--tw-shadow-alpha), transparent);
}
}
.shadow-blue-200 { .shadow-blue-200 {
--tw-shadow-color: #bedbff; --tw-shadow-color: #bedbff;
} }
@@ -3159,6 +3224,48 @@
} }
} }
.shadow-slate-200 {
--tw-shadow-color: #e2e8f0;
}
@supports (color: lab(0% 0 0)) {
.shadow-slate-200 {
--tw-shadow-color: lab(91.7353% -.998765 -4.76968);
}
}
@supports (color: color-mix(in lab, red, red)) {
.shadow-slate-200 {
--tw-shadow-color: color-mix(in oklab, var(--color-slate-200) var(--tw-shadow-alpha), transparent);
}
}
.shadow-slate-900 {
--tw-shadow-color: #0f172b;
}
@supports (color: lab(0% 0 0)) {
.shadow-slate-900 {
--tw-shadow-color: lab(7.78673% 1.82346 -15.0537);
}
}
@supports (color: color-mix(in lab, red, red)) {
.shadow-slate-900 {
--tw-shadow-color: color-mix(in oklab, var(--color-slate-900) var(--tw-shadow-alpha), transparent);
}
}
.shadow-theme-border {
--tw-shadow-color: var(--color-theme-border);
}
@supports (color: color-mix(in lab, red, red)) {
.shadow-theme-border {
--tw-shadow-color: color-mix(in oklab, var(--color-theme-border) var(--tw-shadow-alpha), transparent);
}
}
.ring-amber-200 { .ring-amber-200 {
--tw-ring-color: var(--color-amber-200); --tw-ring-color: var(--color-amber-200);
} }
@@ -3334,6 +3441,13 @@
background-color: var(--color-blue-200); background-color: var(--color-blue-200);
} }
@media (hover: hover) {
.hover\:-translate-y-1:hover {
--tw-translate-y: calc(var(--spacing) * -1);
translate: var(--tw-translate-x) var(--tw-translate-y);
}
}
@media (hover: hover) { @media (hover: hover) {
.hover\:scale-105:hover { .hover\:scale-105:hover {
--tw-scale-x: 105%; --tw-scale-x: 105%;
@@ -3388,6 +3502,12 @@
} }
} }
@media (hover: hover) {
.hover\:border-red-200:hover {
border-color: var(--color-red-200);
}
}
@media (hover: hover) { @media (hover: hover) {
.hover\:border-slate-200:hover { .hover\:border-slate-200:hover {
border-color: var(--color-slate-200); border-color: var(--color-slate-200);
@@ -3406,6 +3526,18 @@
} }
} }
@media (hover: hover) {
.hover\:border-theme-text:hover {
border-color: var(--color-theme-text);
}
}
@media (hover: hover) {
.hover\:bg-\[\#433422\]:hover {
background-color: #433422;
}
}
@media (hover: hover) { @media (hover: hover) {
.hover\:bg-\[\#eaddc4\]:hover { .hover\:bg-\[\#eaddc4\]:hover {
background-color: #eaddc4; background-color: #eaddc4;
@@ -3442,12 +3574,6 @@
} }
} }
@media (hover: hover) {
.hover\:bg-blue-500:hover {
background-color: var(--color-blue-500);
}
}
@media (hover: hover) { @media (hover: hover) {
.hover\:bg-blue-500\/10:hover { .hover\:bg-blue-500\/10:hover {
background-color: #3080ff1a; background-color: #3080ff1a;

File diff suppressed because one or more lines are too long

View File

@@ -9,6 +9,11 @@ const nextConfig: NextConfig = {
ignoreBuildErrors: true, ignoreBuildErrors: true,
}, },
output: "standalone", output: "standalone",
experimental: {
turbopack: {
root: './',
},
},
}; };
export default nextConfig; export default nextConfig;

View File

@@ -10,13 +10,12 @@ cmds = ["npm ci"]
# Le "&&" est important pour que tout se passe dans la même couche logicielle # Le "&&" est important pour que tout se passe dans la même couche logicielle
cmds = [ cmds = [
"npm run build", "npm run build",
"cp -r public .next/standalone/public", # On vérifie si 'public' existe avant de copier, sinon on passe à la suite sans erreur
"cp -r .next/static .next/standalone/.next/static", "[ -d public ] && cp -r public .next/standalone/public || echo 'Pas de dossier public trouvé'",
"echo 'Nettoyage en cours...'", # Idem pour static
"rm -rf node_modules", "[ -d .next/static ] && cp -r .next/static .next/standalone/.next/static || echo 'Pas de dossier static trouvé'",
"rm -rf .next/cache" "find . -maxdepth 1 ! -name '.next' ! -name '.' -exec rm -rf {} +"
] ]
[start] [start]
# On pointe vers le serveur optimisé # On pointe vers le serveur optimisé
cmd = "node .next/standalone/server.js" cmd = "node .next/standalone/server.js"

93
package-lock.json generated
View File

@@ -8,6 +8,7 @@
"name": "plumeia", "name": "plumeia",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.78.0",
"@google/genai": "^1.38.0", "@google/genai": "^1.38.0",
"@prisma/adapter-pg": "^7.4.2", "@prisma/adapter-pg": "^7.4.2",
"@prisma/client": "^7.4.1", "@prisma/client": "^7.4.1",
@@ -17,11 +18,13 @@
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"next": "16.1.6", "next": "16.1.6",
"next-auth": "^5.0.0-beta.30", "next-auth": "^5.0.0-beta.30",
"openai": "^6.25.0",
"pg": "^8.19.0", "pg": "^8.19.0",
"prisma": "^7.4.1", "prisma": "^7.4.1",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"server-only": "^0.0.1" "server-only": "^0.0.1",
"stripe": "^20.4.1"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",
@@ -52,6 +55,26 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/@anthropic-ai/sdk": {
"version": "0.78.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.78.0.tgz",
"integrity": "sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w==",
"license": "MIT",
"dependencies": {
"json-schema-to-ts": "^3.1.1"
},
"bin": {
"anthropic-ai-sdk": "bin/cli"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/@auth/core": { "node_modules/@auth/core": {
"version": "0.41.0", "version": "0.41.0",
"resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.0.tgz", "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.0.tgz",
@@ -273,6 +296,15 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@babel/runtime": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz",
"integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.28.6", "version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@@ -6008,6 +6040,19 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/json-schema-to-ts": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz",
"integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.3",
"ts-algebra": "^2.0.0"
},
"engines": {
"node": ">=16"
}
},
"node_modules/json-schema-traverse": { "node_modules/json-schema-traverse": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
@@ -6962,6 +7007,27 @@
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/openai": {
"version": "6.25.0",
"resolved": "https://registry.npmjs.org/openai/-/openai-6.25.0.tgz",
"integrity": "sha512-mEh6VZ2ds2AGGokWARo18aPISI1OhlgdEIC1ewhkZr8pSIT31dec0ecr9Nhxx0JlybyOgoAT1sWeKtwPZzJyww==",
"license": "Apache-2.0",
"bin": {
"openai": "bin/cli"
},
"peerDependencies": {
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
"ws": {
"optional": true
},
"zod": {
"optional": true
}
}
},
"node_modules/optionator": { "node_modules/optionator": {
"version": "0.9.4", "version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -8332,6 +8398,23 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/stripe": {
"version": "20.4.1",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-20.4.1.tgz",
"integrity": "sha512-axCguHItc8Sxt0HC6aSkdVRPffjYPV7EQqZRb2GkIa8FzWDycE7nHJM19C6xAIynH1Qp1/BHiopSi96jGBxT0w==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"peerDependencies": {
"@types/node": ">=16"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/styled-jsx": { "node_modules/styled-jsx": {
"version": "5.1.6", "version": "5.1.6",
"resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz",
@@ -8472,6 +8555,12 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/ts-algebra": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz",
"integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==",
"license": "MIT"
},
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz",
@@ -9056,7 +9145,7 @@
"version": "4.3.6", "version": "4.3.6",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

View File

@@ -12,6 +12,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@anthropic-ai/sdk": "^0.78.0",
"@google/genai": "^1.38.0", "@google/genai": "^1.38.0",
"@prisma/adapter-pg": "^7.4.2", "@prisma/adapter-pg": "^7.4.2",
"@prisma/client": "^7.4.1", "@prisma/client": "^7.4.1",
@@ -21,11 +22,13 @@
"lucide-react": "^0.563.0", "lucide-react": "^0.563.0",
"next": "16.1.6", "next": "16.1.6",
"next-auth": "^5.0.0-beta.30", "next-auth": "^5.0.0-beta.30",
"openai": "^6.25.0",
"pg": "^8.19.0", "pg": "^8.19.0",
"prisma": "^7.4.1", "prisma": "^7.4.1",
"react": "19.2.3", "react": "19.2.3",
"react-dom": "19.2.3", "react-dom": "19.2.3",
"server-only": "^0.0.1" "server-only": "^0.0.1",
"stripe": "^20.4.1"
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/postcss": "^4", "@tailwindcss/postcss": "^4",

View File

@@ -40,10 +40,22 @@ model User {
planId String? @default("free") planId String? @default("free")
subscriptionPlan Plan? @relation(fields: [planId], references: [id]) subscriptionPlan Plan? @relation(fields: [planId], references: [id])
// Bring Your Own Key
customApiProvider String?
customApiKey String?
aiActionsUsed Int @default(0) aiActionsUsed Int @default(0)
dailyWordGoal Int @default(500) dailyWordGoal Int @default(500)
dailyWordCount Int @default(0)
writingStreak Int @default(0) writingStreak Int @default(0)
lastWriteDate DateTime? lastWriteDate DateTime?
// Stripe Integration
stripeCustomerId String? @unique
stripeSubscriptionId String? @unique
stripePriceId String?
stripeCurrentPeriodEnd DateTime?
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
@@ -96,6 +108,7 @@ model Entity {
id String @id @default(cuid()) id String @id @default(cuid())
type String type String
name String name String
avatar String? @db.Text // Base64 image or URL
description String @default("") description String @default("")
details String @default("") details String @default("")
storyContext String? storyContext String?

View File

@@ -1,8 +1,13 @@
import { config } from 'dotenv'; import { config } from 'dotenv';
config({ path: '.env.local' }); config();
import getDB from '../src/lib/prisma'; import { PrismaClient } from '@prisma/client';
import { PrismaPg } from '@prisma/adapter-pg';
import { Pool } from 'pg';
const prisma = getDB(); const connectionString = process.env.DATABASE_URL;
const pool = new Pool({ connectionString });
const adapter = new PrismaPg(pool);
const prisma = new PrismaClient({ adapter });
async function main() { async function main() {
console.log('Seeding plans...'); console.log('Seeding plans...');
@@ -41,6 +46,17 @@ async function main() {
features: ['250 actions IA / mois', 'Accès Gemini 3 Pro', 'Bible du monde avancée', 'Outils de révision avancés'], features: ['250 actions IA / mois', 'Accès Gemini 3 Pro', 'Bible du monde avancée', 'Outils de révision avancés'],
isPopular: false, isPopular: false,
}, },
{
id: 'byok',
name: 'byok',
displayName: 'Clé Perso (BYOK)',
price: 4.99,
description: 'Utilisez vos propres clés API (ChatGPT, Claude, Gemini).',
maxProjects: -1,
maxAiActions: -1,
features: ['Tokens illimités via votre clé', 'Mode Bring Your Own Key', 'Choix du modèle IA', 'Projets illimités'],
isPopular: false,
},
]; ];
for (const plan of plans) { for (const plan of plans) {

11
scripts/test-prisma.ts Normal file
View File

@@ -0,0 +1,11 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
console.log(Object.keys(prisma.entity.fields));
const e = await prisma.entity.findFirst();
console.log(e);
}
main().catch(console.error).finally(() => prisma.$disconnect());

View File

@@ -36,7 +36,21 @@ export async function PUT(
}, },
}); });
return NextResponse.json(updated); // Update writing streak if content was explicitly updated
let userStats = null;
if (body.content !== undefined) {
const oldWords = (chapter.content || '').replace(/<[^>]*>/g, ' ').trim().split(/\s+/).filter(Boolean).length;
const newWords = (body.content || '').replace(/<[^>]*>/g, ' ').trim().split(/\s+/).filter(Boolean).length;
const wordDelta = Math.max(0, newWords - oldWords);
const { updateWritingStreak } = await import('@/lib/streak');
userStats = await updateWritingStreak(session.user.id, wordDelta);
}
return NextResponse.json({
...updated,
_userStats: userStats
});
} }
// DELETE /api/chapters/[id] // DELETE /api/chapters/[id]

View File

@@ -34,5 +34,11 @@ export async function POST(request: NextRequest) {
}, },
}); });
// Update writing streak
const wordCount = (body.content || '').replace(/<[^>]*>/g, ' ').trim().split(/\s+/).filter(Boolean).length;
import('@/lib/streak').then(({ updateWritingStreak }) => {
updateWritingStreak(session.user.id, wordCount).catch(console.error);
});
return NextResponse.json(chapter, { status: 201 }); return NextResponse.json(chapter, { status: 201 });
} }

View File

@@ -0,0 +1,76 @@
import { NextResponse } from 'next/server';
import { stripe } from '@/lib/stripe';
import { prisma } from '@/lib/prisma';
import { auth } from '@/lib/auth';
export async function POST(req: Request) {
try {
const session = await auth();
if (!session?.user?.email) {
return new NextResponse('Unauthorized', { status: 401 });
}
const { planId } = await req.json();
if (!planId) {
return new NextResponse('Plan ID is required', { status: 400 });
}
const plan = await prisma.plan.findUnique({
where: { id: planId },
});
if (!plan) {
return new NextResponse('Plan not found', { status: 404 });
}
// Free plan doesn't need checkout
if (plan.id === 'free') {
return new NextResponse('Free plan does not require payment', { status: 400 });
}
const user = await prisma.user.findUnique({
where: { email: session.user.email },
});
if (!user) {
return new NextResponse('User not found', { status: 404 });
}
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000';
const checkoutSession = await stripe.checkout.sessions.create({
mode: 'subscription',
payment_method_types: ['card'],
customer_email: session.user.email,
line_items: [
{
price_data: {
currency: 'eur',
product_data: {
name: plan.displayName,
description: plan.description,
},
unit_amount: Math.round(plan.price * 100),
recurring: {
interval: 'month',
},
},
quantity: 1,
},
],
metadata: {
userId: user.id,
planId: plan.id,
},
success_url: `${baseUrl}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${baseUrl}/pricing`,
});
return NextResponse.json({ url: checkoutSession.url });
} catch (error) {
console.error('[STRIPE_CHECKOUT_ERROR]', error);
return new NextResponse('Internal Error', { status: 500 });
}
}

View File

@@ -30,6 +30,7 @@ export async function PUT(
data: { data: {
...(body.name !== undefined && { name: body.name }), ...(body.name !== undefined && { name: body.name }),
...(body.type !== undefined && { type: body.type }), ...(body.type !== undefined && { type: body.type }),
...(body.avatar !== undefined && { avatar: body.avatar }),
...(body.description !== undefined && { description: body.description }), ...(body.description !== undefined && { description: body.description }),
...(body.details !== undefined && { details: body.details }), ...(body.details !== undefined && { details: body.details }),
...(body.storyContext !== undefined && { storyContext: body.storyContext }), ...(body.storyContext !== undefined && { storyContext: body.storyContext }),

View File

@@ -24,6 +24,7 @@ export async function POST(request: NextRequest) {
data: { data: {
type: body.type, type: body.type,
name: body.name || 'Nouvelle entité', name: body.name || 'Nouvelle entité',
avatar: body.avatar || null,
description: body.description || '', description: body.description || '',
details: body.details || '', details: body.details || '',
storyContext: body.storyContext || null, storyContext: body.storyContext || null,

View File

@@ -0,0 +1,49 @@
export const dynamic = 'force-dynamic';
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth';
import { prisma } from '@/lib/prisma';
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Non autorisé' }, { status: 401 });
}
const { id: projectId } = await params;
const body = await request.json();
const { chapters } = body as { chapters: { id: string, orderIndex: number }[] };
if (!Array.isArray(chapters)) {
return NextResponse.json({ error: 'Format invalide' }, { status: 400 });
}
// Verify ownership
const existing = await prisma.project.findFirst({
where: { id: projectId, userId: session.user.id },
});
if (!existing) {
return NextResponse.json({ error: 'Projet non trouvé' }, { status: 404 });
}
try {
// Bulk update chapters' orderIndex within a transaction
await prisma.$transaction(
chapters.map((chapter) =>
prisma.chapter.update({
where: { id: chapter.id, projectId },
data: { orderIndex: chapter.orderIndex },
})
)
);
return NextResponse.json({ success: true });
} catch (error) {
console.error('Erreur lors de la réorganisation des chapitres:', error);
return NextResponse.json({ error: 'Erreur interne' }, { status: 500 });
}
}

View File

@@ -54,6 +54,9 @@ export async function GET() {
dailyWordGoal: user.dailyWordGoal, dailyWordGoal: user.dailyWordGoal,
writingStreak: user.writingStreak, writingStreak: user.writingStreak,
lastWriteDate: user.lastWriteDate, lastWriteDate: user.lastWriteDate,
dailyWordCount: user.dailyWordCount || 0,
customApiProvider: user.customApiProvider,
customApiKey: user.customApiKey,
createdAt: user.createdAt, createdAt: user.createdAt,
totalWords, totalWords,
}); });
@@ -76,8 +79,15 @@ export async function PUT(request: NextRequest) {
if (body.avatar !== undefined) data.avatar = body.avatar; if (body.avatar !== undefined) data.avatar = body.avatar;
if (body.bio !== undefined) data.bio = body.bio; if (body.bio !== undefined) data.bio = body.bio;
if (body.dailyWordGoal !== undefined) data.dailyWordGoal = body.dailyWordGoal; if (body.dailyWordGoal !== undefined) data.dailyWordGoal = body.dailyWordGoal;
if (body.dailyWordCount !== undefined) data.dailyWordCount = body.dailyWordCount;
if (body.writingStreak !== undefined) data.writingStreak = body.writingStreak; if (body.writingStreak !== undefined) data.writingStreak = body.writingStreak;
if (body.lastWriteDate !== undefined) data.lastWriteDate = body.lastWriteDate ? new Date(body.lastWriteDate) : null; if (body.lastWriteDate !== undefined) data.lastWriteDate = body.lastWriteDate ? new Date(body.lastWriteDate) : null;
if (body.customApiProvider !== undefined) data.customApiProvider = body.customApiProvider;
if (body.customApiKey !== undefined) data.customApiKey = body.customApiKey;
if (body.planId !== undefined) {
data.planId = body.planId;
data.plan = body.planId; // legacy sync
}
const updated = await prisma.user.update({ const updated = await prisma.user.update({
where: { id: session.user.id }, where: { id: session.user.id },

View File

@@ -0,0 +1,70 @@
import { NextResponse } from 'next/server';
import { headers } from 'next/headers';
import { stripe } from '@/lib/stripe';
import { prisma } from '@/lib/prisma';
import Stripe from 'stripe';
export async function POST(req: Request) {
const body = await req.text();
const headersList = await headers();
const signature = headersList.get('Stripe-Signature') as string;
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
);
} catch (error: any) {
return new NextResponse(`Webhook Error: ${error.message}`, { status: 400 });
}
const session = event.data.object as Stripe.Checkout.Session;
if (event.type === 'checkout.session.completed') {
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
) as any;
if (!session?.metadata?.userId) {
return new NextResponse('User id is required', { status: 400 });
}
await prisma.user.update({
where: {
id: session.metadata.userId,
},
data: {
stripeSubscriptionId: subscription.id,
stripeCustomerId: subscription.customer as string,
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(
subscription.current_period_end * 1000
),
planId: session.metadata.planId,
},
});
}
if (event.type === 'invoice.payment_succeeded') {
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
) as any;
await prisma.user.update({
where: {
stripeSubscriptionId: subscription.id,
},
data: {
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(
subscription.current_period_end * 1000
),
},
});
}
return new NextResponse(null, { status: 200 });
}

View File

@@ -27,10 +27,148 @@ export default function CGUPage() {
</nav> </nav>
<main className="max-w-4xl mx-auto py-20 px-4 md:px-8"> <main className="max-w-4xl mx-auto py-20 px-4 md:px-8">
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('legal.cgu_title')}</h1> <h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('cgu.title')}</h1>
<div className="bg-white p-6 sm:p-12 rounded-3xl shadow-xl border border-indigo-50 text-slate-600 leading-relaxed space-y-6"> <div className="bg-white p-6 sm:p-12 rounded-3xl shadow-xl border border-indigo-50 text-slate-600 leading-relaxed space-y-8">
<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>
<p className="font-semibold text-slate-500 mb-6">{t('cgu.version')}</p>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgu.preamble_title')}</h2>
<p className="mb-4">
{t('cgu.preamble_text1')}
</p>
<p>
{t('cgu.preamble_text2')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgu.art1_title')}</h2>
<ul className="list-disc pl-5 space-y-2">
<li>{t('cgu.art1_user')}</li>
<li>{t('cgu.art1_service')}</li>
<li>{t('cgu.art1_content')}</li>
<li>{t('cgu.art1_prompt')}</li>
</ul>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgu.art2_title')}</h2>
<p>
{t('cgu.art2_text')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgu.art3_title')}</h2>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgu.art3_1_title')}</h3>
<p className="mb-2">
{t('cgu.art3_1_text')}
</p>
<ul className="list-disc pl-5 space-y-2 mb-4">
<li>{t('cgu.art3_1_sub1')}</li>
<li>{t('cgu.art3_1_sub2')}</li>
</ul>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgu.art3_2_title')}</h3>
<p>
{t('cgu.art3_2_text')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgu.art4_title')}</h2>
<p className="mb-4">{t('cgu.art4_text')}</p>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgu.art4_1_title')}</h3>
<p className="mb-2">{t('cgu.art4_1_text')}</p>
<ul className="list-disc pl-5 space-y-2 mb-4">
<li>{t('cgu.art4_1_item1')}</li>
<li>{t('cgu.art4_1_item2')}</li>
<li>{t('cgu.art4_1_item3')}</li>
</ul>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgu.art4_2_title')}</h3>
<p>
{t('cgu.art4_2_text')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgu.art5_title')}</h2>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgu.art5_1_title')}</h3>
<p className="mb-2">
{t('cgu.art5_1_text')}
</p>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-200 tex-sm my-4">
{t('cgu.art5_1_note')}
</div>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgu.art5_2_title')}</h3>
<p>
{t('cgu.art5_2_text')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgu.art6_title')}</h2>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgu.art6_1_title')}</h3>
<p className="mb-2">
{t('cgu.art6_1_text1')}
</p>
<p className="mb-2">
{t('cgu.art6_1_text2')}
</p>
<p className="mb-4">
{t('cgu.art6_1_text3')}
</p>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgu.art6_2_title')}</h3>
<p>
{t('cgu.art6_2_text')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgu.art7_title')}</h2>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgu.art7_1_title')}</h3>
<p className="mb-4">
{t('cgu.art7_1_text')}
</p>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgu.art7_2_title')}</h3>
<p>
{t('cgu.art7_2_text')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgu.art8_title')}</h2>
<p>
{t('cgu.art8_text')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgu.art9_title')}</h2>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgu.art9_1_title')}</h3>
<p className="mb-4">
{t('cgu.art9_1_text')}
</p>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgu.art9_2_title')}</h3>
<p>
{t('cgu.art9_2_text')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgu.art10_title')}</h2>
<p>
{t('cgu.art10_text')}
</p>
</div>
</div> </div>
</main> </main>
</div> </div>

View File

@@ -27,10 +27,114 @@ export default function CGVPage() {
</nav> </nav>
<main className="max-w-4xl mx-auto py-20 px-4 md:px-8"> <main className="max-w-4xl mx-auto py-20 px-4 md:px-8">
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('legal.cgv_title')}</h1> <h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('cgv.title')}</h1>
<div className="bg-white p-6 sm:p-12 rounded-3xl shadow-xl border border-indigo-50 text-slate-600 leading-relaxed space-y-6"> <div className="bg-white p-6 sm:p-12 rounded-3xl shadow-xl border border-indigo-50 text-slate-600 leading-relaxed space-y-8">
<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>
<p className="font-semibold text-slate-500 mb-6">{t('cgv.version')}</p>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgv.art1_title')}</h2>
<p className="mb-4">
{t('cgv.art1_text1')}
</p>
<p>
{t('cgv.art1_text2')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgv.art2_title')}</h2>
<p className="mb-2">{t('cgv.art2_text')}</p>
<ul className="list-disc pl-5 space-y-2 mb-4">
<li>{t('cgv.art2_item1')}</li>
<li>{t('cgv.art2_item2')}</li>
</ul>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgv.art3_title')}</h2>
<p>
{t('cgv.art3_text')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgv.art4_title')}</h2>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgv.art4_1_title')}</h3>
<p className="mb-4">
{t('cgv.art4_1_text')}
</p>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgv.art4_2_title')}</h3>
<p className="mb-2">{t('cgv.art4_2_text')}</p>
<ul className="list-disc pl-5 space-y-2 mb-4">
<li>{t('cgv.art4_2_item1')}</li>
<li>{t('cgv.art4_2_item2')}</li>
</ul>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgv.art5_title')}</h2>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgv.art5_1_title')}</h3>
<p className="mb-4">
{t('cgv.art5_1_text')}
</p>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgv.art5_2_title')}</h3>
<p className="mb-2">{t('cgv.art5_2_text')}</p>
<ul className="list-disc pl-5 space-y-2 mb-4">
<li>{t('cgv.art5_2_item1')}</li>
<li>{t('cgv.art5_2_item2')}</li>
</ul>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgv.art6_title')}</h2>
<p className="mb-4">
{t('cgv.art6_text')}
</p>
<div className="bg-amber-50 text-amber-900 p-4 rounded-xl border border-amber-200 tex-sm my-4 font-medium">
{t('cgv.art6_warning')}
</div>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgv.art7_title')}</h2>
<p>
{t('cgv.art7_text')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgv.art8_title')}</h2>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgv.art8_1_title')}</h3>
<p className="mb-4">
{t('cgv.art8_1_text')}
</p>
<h3 className="font-bold text-slate-800 mt-4 mb-2">{t('cgv.art8_2_title')}</h3>
<p>
{t('cgv.art8_2_text')}
</p>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgv.art9_title')}</h2>
<p className="mb-2">{t('cgv.art9_text')}</p>
<ul className="list-disc pl-5 space-y-2 mb-4">
<li>{t('cgv.art9_item1')}</li>
<li>{t('cgv.art9_item2')}</li>
</ul>
</div>
<div>
<h2 className="text-xl font-bold text-slate-900 mb-4 uppercase tracking-wider">{t('cgv.art10_title')}</h2>
<p>
{t('cgv.art10_text')}
</p>
</div>
</div> </div>
</main> </main>
</div> </div>

View File

@@ -1,15 +0,0 @@
'use client';
import Checkout from '@/components/Checkout';
import { useRouter } from 'next/navigation';
export default function CheckoutPage() {
const router = useRouter();
return (
<Checkout
onComplete={() => router.push('/dashboard')}
onCancel={() => router.push('/pricing')}
/>
);
}

View File

@@ -3,6 +3,9 @@
@theme { @theme {
--font-sans: 'Inter', sans-serif; --font-sans: 'Inter', sans-serif;
--font-serif: 'Merriweather', serif; --font-serif: 'Merriweather', serif;
--font-lora: 'Lora', serif;
--font-garamond: 'EB Garamond', serif;
--font-playfair: 'Playfair Display', serif;
--color-paper: #fcfbf7; --color-paper: #fcfbf7;
/* Global Theme Colors */ /* Global Theme Colors */

View File

@@ -1,7 +1,10 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Inter, Merriweather } from "next/font/google"; import { Inter, Merriweather, Lora, EB_Garamond, Playfair_Display } from "next/font/google";
import Script from "next/script";
import { AuthProvider } from "@/providers/AuthProvider"; import { AuthProvider } from "@/providers/AuthProvider";
import { LanguageProvider } from "@/providers/LanguageProvider"; import { LanguageProvider } from "@/providers/LanguageProvider";
import { CookieBanner } from "@/components/CookieBanner";
import { Analytics } from "@/components/Analytics";
import "./globals.css"; import "./globals.css";
const inter = Inter({ const inter = Inter({
@@ -15,6 +18,21 @@ const merriweather = Merriweather({
variable: "--font-serif", variable: "--font-serif",
}); });
const lora = Lora({
subsets: ["latin"],
variable: "--font-lora",
});
const garamond = EB_Garamond({
subsets: ["latin"],
variable: "--font-garamond",
});
const playfair = Playfair_Display({
subsets: ["latin"],
variable: "--font-playfair",
});
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Pluume - Éditeur Intelligent", title: "Pluume - Éditeur Intelligent",
description: "Votre assistant éditorial intelligent propulsé par l'IA pour écrire votre prochain roman.", description: "Votre assistant éditorial intelligent propulsé par l'IA pour écrire votre prochain roman.",
@@ -27,10 +45,14 @@ export default function RootLayout({
}) { }) {
return ( return (
<html lang="en"> <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`}> <head>
</head>
<body className={`${inter.variable} ${merriweather.variable} ${lora.variable} ${garamond.variable} ${playfair.variable} font-sans h-screen overflow-x-hidden overflow-y-auto antialiased bg-theme-bg text-theme-text transition-colors duration-300`}>
<AuthProvider> <AuthProvider>
<LanguageProvider> <LanguageProvider>
<Analytics />
{children} {children}
<CookieBanner />
</LanguageProvider> </LanguageProvider>
</AuthProvider> </AuthProvider>
</body> </body>

View File

@@ -29,9 +29,29 @@ export default function PricingPage() {
<Pricing <Pricing
plans={plans} plans={plans}
isLoading={isLoading} isLoading={isLoading}
currentPlan={user?.subscription.plan || 'free'} currentPlan={user?.planId || 'free'}
onBack={() => router.push(user ? '/dashboard' : '/')} onBack={() => router.push(user ? '/dashboard' : '/')}
onSelectPlan={() => router.push(user ? '/checkout' : '/login')} onSelectPlan={async (id) => {
if (!user) {
router.push('/login');
return;
}
try {
const response = await fetch('/api/checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ planId: id })
});
const data = await response.json();
if (data.url) {
window.location.href = data.url;
}
} catch (err) {
console.error('Checkout error:', err);
}
}}
/> />
); );
} }

View File

@@ -4,12 +4,15 @@ import IdeaBoard from '@/components/IdeaBoard';
import { useProjectContext } from '@/providers/ProjectProvider'; import { useProjectContext } from '@/providers/ProjectProvider';
export default function IdeasPage() { export default function IdeasPage() {
const { project, updateProject } = useProjectContext(); const { project, projectId, createIdea, updateIdea, deleteIdea } = useProjectContext();
return ( return (
<IdeaBoard <IdeaBoard
projectId={projectId}
ideas={project.ideas || []} ideas={project.ideas || []}
onUpdate={(ideas) => updateProject({ ideas })} onCreate={(data) => createIdea(projectId, data)}
onUpdateIdea={(id, data) => updateIdea(projectId, id, data)}
onDelete={(id) => deleteIdea(projectId, id)}
/> />
); );
} }

View File

@@ -26,12 +26,15 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
const pathname = usePathname(); const pathname = usePathname();
const projectId = params.id as string; const projectId = params.id as string;
const { user, logout, incrementUsage, loading: authLoading } = useAuthContext(); const { user, logout, incrementUsage, refreshProfile, loading: authLoading } = useAuthContext();
const hasEverLoaded = useRef(false); const hasEverLoaded = useRef(false);
const { const {
projects, setCurrentProjectId, projects, setCurrentProjectId,
updateProject, updateChapter, addChapter, updateProject, updateChapter, addChapter,
createEntity, updateEntity, deleteEntity, deleteProject createEntity, updateEntity, deleteEntity,
createIdea, updateIdea, deleteIdea,
deleteProject,
reorderChapters
} = useProjects(user); } = useProjects(user);
const { chatHistory, isGenerating, sendMessage } = useChat(); const { chatHistory, isGenerating, sendMessage } = useChat();
@@ -77,6 +80,7 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
); );
} }
// Handle early returns for non-existent projects
if (!project) { if (!project) {
return ( return (
<div className="h-screen w-full flex flex-col items-center justify-center bg-slate-900 text-white"> <div className="h-screen w-full flex flex-col items-center justify-center bg-slate-900 text-white">
@@ -111,8 +115,12 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
createEntity: (type, data) => createEntity(projectId, type, data), createEntity: (type, data) => createEntity(projectId, type, data),
updateEntity: (entityId, data) => updateEntity(projectId, entityId, data), updateEntity: (entityId, data) => updateEntity(projectId, entityId, data),
deleteEntity: (entityId) => deleteEntity(projectId, entityId), deleteEntity: (entityId) => deleteEntity(projectId, entityId),
createIdea: (id, data) => createIdea(id, data),
updateIdea: (id, ideaId, data) => updateIdea(id, ideaId, data),
deleteIdea: (id, ideaId) => deleteIdea(id, ideaId),
deleteProject: () => deleteProject(projectId), deleteProject: () => deleteProject(projectId),
incrementUsage, incrementUsage,
refreshProfile,
}}> }}>
<EditorShell <EditorShell
project={project} project={project}
@@ -124,6 +132,14 @@ export default function ProjectLayout({ children }: { children: React.ReactNode
onViewModeChange={handleViewModeChange} onViewModeChange={handleViewModeChange}
onChapterSelect={(id) => { setCurrentChapterId(id); router.push(`/project/${projectId}`); }} onChapterSelect={(id) => { setCurrentChapterId(id); router.push(`/project/${projectId}`); }}
onUpdateProject={(updates) => updateProject(projectId, updates)} onUpdateProject={(updates) => updateProject(projectId, updates)}
onUpdateChapter={async (chapterId, data) => {
await updateChapter(projectId, chapterId, data);
// If content was updated, refresh profile to sync "mots du jour"
if (data.content !== undefined) {
refreshProfile();
}
}}
onReorderChapters={(chapters) => reorderChapters(projectId, chapters)}
onAddChapter={async () => { onAddChapter={async () => {
const id = await addChapter(projectId, {}); const id = await addChapter(projectId, {});
if (id) { if (id) {

View File

@@ -16,6 +16,7 @@ export default function WritePage() {
return ( return (
<RichTextEditor <RichTextEditor
ref={editorRef} ref={editorRef}
editorId={currentChapterId}
initialContent={currentChapter?.content || ''} initialContent={currentChapter?.content || ''}
onSave={(html) => updateChapter(currentChapterId, { content: html })} onSave={(html) => updateChapter(currentChapterId, { content: html })}
onAiTransform={async (text, mode) => { onAiTransform={async (text, mode) => {

View File

@@ -0,0 +1,40 @@
export async function GET() {
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'https://pluume.fr';
// Dates can be dynamically fetched in a real scenario, here we use current date for static
const currentDate = new Date().toISOString();
const staticRoutes = [
'',
'/features',
'/pricing',
'/login',
'/signup',
'/cgu',
'/cgv',
'/sitemap',
];
const sitemapEntries = staticRoutes.map((route) => {
const priority = route === '' ? '1.0' : '0.8';
const changeFrequency = route === '' ? 'weekly' : 'monthly';
return `
<url>
<loc>${baseUrl}${route}</loc>
<lastmod>${currentDate}</lastmod>
<changefreq>${changeFrequency}</changefreq>
<priority>${priority}</priority>
</url>`;
}).join('');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${sitemapEntries}
</urlset>`;
return new Response(xml, {
headers: {
'Content-Type': 'application/xml',
},
});
}

View File

@@ -1,17 +1,110 @@
'use client'; 'use client';
import React from 'react'; import React, { useState } from 'react';
import { useLanguage } from '@/providers/LanguageProvider'; import { useLanguage } from '@/providers/LanguageProvider';
import { ArrowLeft, Book, Link as LinkIcon } from 'lucide-react'; import { ArrowLeft, Book, Link as LinkIcon, ChevronDown, Shield, Home, Component, LogIn, Mail } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { LanguageSwitcher } from '@/components/LanguageSwitcher'; import { LanguageSwitcher } from '@/components/LanguageSwitcher';
interface SitemapLink {
title: string;
href: string;
icon: React.ElementType;
}
interface SitemapCategory {
id: string;
title: string;
color: string;
icon: React.ElementType;
links: SitemapLink[];
}
const SitemapCard = ({ category, defaultOpen = false }: { category: SitemapCategory, defaultOpen?: boolean }) => {
const [isOpen, setIsOpen] = useState(defaultOpen);
const Icon = category.icon;
return (
<div className="flex flex-col rounded-xl shadow-sm border border-slate-200 bg-white transition-all hover:shadow-md overflow-hidden">
{/* Colored Top Bar */}
<div className="h-2 w-full" style={{ backgroundColor: category.color }} />
{/* Header (Toggle) */}
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center justify-between p-4 w-full text-left bg-white hover:bg-slate-50 transition-colors focus:outline-none"
>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg" style={{ backgroundColor: `${category.color}15`, color: category.color }}>
<Icon size={20} />
</div>
<span className="font-bold text-slate-800 text-lg">{category.title}</span>
</div>
<div className={`p-1 rounded-full text-slate-400 hover:text-slate-600 transition-transform duration-300 ${isOpen ? 'rotate-180' : ''}`}>
<ChevronDown size={20} />
</div>
</button>
{/* Foldable Content */}
<div
className={`grid transition-all duration-300 ease-in-out ${isOpen ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'}`}
>
<div className="overflow-hidden">
<div className="p-4 pt-0 border-t border-slate-100 bg-slate-50/50">
<ul className="space-y-2 mt-3">
{category.links.map((link, idx) => {
const LinkIconComponent = link.icon || LinkIcon;
return (
<li key={idx}>
<Link
href={link.href}
className="flex items-center gap-3 px-3 py-2 rounded-lg text-slate-600 hover:text-indigo-600 hover:bg-indigo-50/50 transition-all font-medium group"
>
<LinkIconComponent size={16} className="text-slate-400 group-hover:text-indigo-500 transition-colors" />
{link.title}
</Link>
</li>
);
})}
</ul>
</div>
</div>
</div>
</div>
);
};
export default function SitemapPage() { export default function SitemapPage() {
const { t } = useLanguage(); const { t } = useLanguage();
const categories: SitemapCategory[] = [
{
id: 'general',
title: 'Navigation Principale',
color: '#3b82f6', // blue-500
icon: Home,
links: [
{ title: 'Accueil', href: '/', icon: Home },
{ title: 'Fonctionnalités', href: '/#features', icon: Component },
{ title: 'Authentification', href: '/auth', icon: LogIn },
{ title: 'Contact', href: 'mailto:contact@pluu.me', icon: Mail },
]
},
{
id: 'legal',
title: 'Informations Légales',
color: '#f59e0b', // amber-500
icon: Shield,
links: [
{ title: t('legal.cgu_title') || 'Conditions Générales d\'Utilisation', href: '/cgu', icon: Book },
{ title: t('legal.cgv_title') || 'Conditions Générales de Vente', href: '/cgv', icon: Book },
]
}
];
return ( return (
<div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200"> <div className="min-h-screen bg-[#eef2ff] font-sans selection:bg-blue-200">
<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"> <nav className="bg-white/80 backdrop-blur-md z-50 border-b border-indigo-100 px-4 md:px-8 h-16 flex items-center justify-between sticky top-0">
<Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity"> <Link href="/" className="flex items-center gap-2 hover:opacity-80 transition-opacity">
<div className="bg-blue-600 p-1.5 rounded-lg"> <div className="bg-blue-600 p-1.5 rounded-lg">
<Book className="text-white" size={24} /> <Book className="text-white" size={24} />
@@ -26,36 +119,16 @@ export default function SitemapPage() {
</div> </div>
</nav> </nav>
<main className="max-w-4xl mx-auto py-20 px-8"> <main className="max-w-4xl mx-auto py-12 md:py-20 px-4 md:px-8">
<h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-8 tracking-tight">{t('legal.sitemap_title')}</h1> <div className="text-center mb-12">
<div className="bg-white p-8 sm:p-12 rounded-3xl shadow-xl border border-indigo-50"> <h1 className="text-4xl md:text-5xl font-black text-slate-900 mb-4 tracking-tight">{t('legal.sitemap_title') || 'Plan du site'}</h1>
<ul className="space-y-4"> <p className="text-slate-500 text-lg">Retrouvez facilement toutes les pages de l'application Pluume.</p>
<li> </div>
<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 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
</Link> {categories.map((cat, idx) => (
</li> <SitemapCard key={cat.id} category={cat} defaultOpen={true} />
<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> </div>
</main> </main>
</div> </div>

View File

@@ -0,0 +1,36 @@
'use client';
import React, { useEffect, useState } from 'react';
import Script from 'next/script';
export const Analytics = () => {
const [hasConsent, setHasConsent] = useState(false);
const checkConsent = () => {
const consent = localStorage.getItem('cookie-consent');
setHasConsent(consent === 'accepted');
};
useEffect(() => {
// Check initial state
checkConsent();
// Listen for updates from CookieBanner
window.addEventListener('cookie-consent-updated', checkConsent);
return () => {
window.removeEventListener('cookie-consent-updated', checkConsent);
};
}, []);
if (!hasConsent) return null;
return (
<Script
defer
src="https://stats.kaelstudio.tech/script.js"
data-website-id="bce265f0-c9d4-4542-813f-f3bd9bf151bc"
strategy="afterInteractive"
/>
);
};

View File

@@ -43,8 +43,8 @@ interface AppRouterProps {
onUpdateProfile: (updates: Partial<UserProfile>) => void; onUpdateProfile: (updates: Partial<UserProfile>) => void;
onUpgradePlan: (plan: any) => void; onUpgradePlan: (plan: any) => void;
onSendMessage: (msg: string) => void; onSendMessage: (msg: string) => void;
onIncrementUsage: () => void;
onDeleteProject: (projectId: string) => void; onDeleteProject: (projectId: string) => void;
onReorderChapters: (projectId: string, chapters: { id: string, orderIndex: number }[]) => void;
} }
const AppRouter: React.FC<AppRouterProps> = (props) => { const AppRouter: React.FC<AppRouterProps> = (props) => {
@@ -139,6 +139,7 @@ const AppRouter: React.FC<AppRouterProps> = (props) => {
onViewModeChange={props.onViewModeChange} onViewModeChange={props.onViewModeChange}
onChapterSelect={(id) => { setCurrentChapterId(id); props.onViewModeChange('write'); }} onChapterSelect={(id) => { setCurrentChapterId(id); props.onViewModeChange('write'); }}
onUpdateProject={props.onUpdateProject} onUpdateProject={props.onUpdateProject}
onReorderChapters={(chapters) => props.onReorderChapters(project.id, chapters)}
onAddChapter={async () => { onAddChapter={async () => {
console.log("[AppRouter] onAddChapter triggered"); console.log("[AppRouter] onAddChapter triggered");
const id = await props.onAddChapter(); const id = await props.onAddChapter();

View File

@@ -16,6 +16,7 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [formData, setFormData] = useState({ name: '', email: '', password: '' }); const [formData, setFormData] = useState({ name: '', email: '', password: '' });
const [error, setError] = useState(''); const [error, setError] = useState('');
const [cguAccepted, setCguAccepted] = useState(false);
// On récupère les fonctions de connexion directement du hook // On récupère les fonctions de connexion directement du hook
const { user, login, signup } = useAuthContext(); const { user, login, signup } = useAuthContext();
@@ -156,8 +157,41 @@ const AuthPage: React.FC<AuthPageProps> = ({ onBack, onSuccess, initialMode = 's
</div> </div>
)} )}
{mode === 'signup' && (
<div className="pt-2 pb-1">
<label className="flex items-start gap-3 cursor-pointer group">
<div className="relative flex items-center mt-1 text-slate-900 font-bold mb-3">
<input
type="checkbox"
required
checked={cguAccepted}
onChange={(e) => setCguAccepted(e.target.checked)}
className="peer shrink-0 appearance-none w-5 h-5 border-2 border-slate-300 rounded-md bg-white checked:bg-blue-600 checked:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition-all cursor-pointer"
/>
<svg
className="absolute w-5 h-5 pointer-events-none opacity-0 peer-checked:opacity-100 peer-checked:text-white transition-opacity text-white stroke-white fill-none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
<span className="text-sm text-slate-600 font-medium leading-relaxed group-hover:text-slate-800 transition-colors">
{t('auth.accept_cgu')}
<a href="/cgu" target="_blank" rel="noopener noreferrer" className="text-blue-600 font-bold hover:underline hover:text-blue-700">
{t('auth.cgu_link')}
</a>
</span>
</label>
</div>
)}
<button <button
type="submit" type="submit"
data-umami-event={mode === 'signin' ? "Login" : mode === 'signup' ? "Signup" : "Send Reset"}
disabled={loading} disabled={loading}
className="w-full bg-slate-900 text-white py-4 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-blue-600 transition-all shadow-xl disabled:opacity-50 mt-4" className="w-full bg-slate-900 text-white py-4 rounded-xl font-bold flex items-center justify-center gap-2 hover:bg-blue-600 transition-all shadow-xl disabled:opacity-50 mt-4"
> >

View File

@@ -3,7 +3,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { BookProject, BookSettings } from '@/lib/types'; import { BookProject, BookSettings } from '@/lib/types';
import { GENRES, TONES, POV_OPTIONS, TENSE_OPTIONS } from '@/lib/constants'; import { GENRES, TONES, POV_OPTIONS, TENSE_OPTIONS } from '@/lib/constants';
import { Settings, Book, Feather, Users, Clock, Target, Hash } from 'lucide-react'; import { Settings, Book, Feather, Users, Clock, Target, Hash, Save, Check } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider'; import { useLanguage } from '@/providers/LanguageProvider';
import { TranslationKey } from '@/lib/i18n/translations'; import { TranslationKey } from '@/lib/i18n/translations';
@@ -24,39 +24,75 @@ const DEFAULT_SETTINGS: BookSettings = {
themes: '' themes: ''
}; };
const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate, onDeleteProject }) => { const BookSettingsComponent: React.FC<BookSettingsProps> = React.memo(({ project, onUpdate, onDeleteProject }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const [settings, setSettings] = useState<BookSettings>(project.settings || DEFAULT_SETTINGS);
// Local state for all editable fields to prevent excessive API calls
const [localTitle, setLocalTitle] = useState(project.title);
const [localAuthor, setLocalAuthor] = useState(project.author);
const [localStyleGuide, setLocalStyleGuide] = useState(project.styleGuide || '');
const [localSettings, setLocalSettings] = useState<BookSettings>(project.settings || DEFAULT_SETTINGS);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [showSavedFeedback, setShowSavedFeedback] = useState(false);
useEffect(() => { useEffect(() => {
setLocalTitle(project.title);
setLocalAuthor(project.author);
setLocalStyleGuide(project.styleGuide || '');
if (project.settings) { if (project.settings) {
setSettings(project.settings); setLocalSettings(project.settings);
} }
}, [project.settings]); }, [project.title, project.author, project.styleGuide, project.settings]);
const handleChange = (key: keyof BookSettings, value: string) => { const handleChange = (key: keyof BookSettings, value: string) => {
const newSettings = { ...settings, [key]: value }; setLocalSettings(prev => ({ ...prev, [key]: value }));
setSettings(newSettings);
onUpdate({ ...project, settings: newSettings });
}; };
const handleStyleGuideChange = (value: string) => { const handleSave = () => {
onUpdate({ ...project, styleGuide: value }); setIsSaving(true);
onUpdate({
...project,
title: localTitle,
author: localAuthor,
styleGuide: localStyleGuide,
settings: localSettings
});
// Simulate save delay for UI feedback
setTimeout(() => {
setIsSaving(false);
setShowSavedFeedback(true);
setTimeout(() => setShowSavedFeedback(false), 2000);
}, 500);
}; };
return ( return (
<div className="h-full bg-theme-bg p-8 overflow-y-auto transition-colors duration-300"> <div className="h-full bg-theme-bg p-8 overflow-y-auto transition-colors duration-300">
<div className="max-w-4xl mx-auto bg-theme-panel rounded-xl shadow-lg border border-theme-border overflow-hidden transition-colors duration-300"> <div className="max-w-4xl mx-auto bg-theme-panel rounded-xl shadow-lg border border-theme-border overflow-hidden transition-colors duration-300 pb-8">
<div className="bg-slate-900 text-white p-6 border-b border-slate-800 flex items-center gap-4"> <div className="bg-slate-900 text-white p-6 border-b border-slate-800 flex items-center justify-between">
<div className="bg-blue-600 p-3 rounded-lg"> <div className="flex items-center gap-4">
<Settings size={24} /> <div className="bg-blue-600 p-3 rounded-lg">
</div> <Settings size={24} />
<div> </div>
<h2 className="text-2xl font-bold">{t('book_settings.title')}</h2> <div>
<p className="text-slate-400 text-sm">{t('book_settings.subtitle')}</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>
<button
onClick={handleSave}
disabled={isSaving}
className={`flex items-center gap-2 px-6 py-2.5 rounded-lg font-bold transition-all ${showSavedFeedback
? 'bg-green-600 hover:bg-green-700 text-white'
: 'bg-blue-600 hover:bg-blue-700 text-white'
}`}
>
{showSavedFeedback ? <Check size={18} /> : <Save size={18} />}
{showSavedFeedback ? t('book_settings.saved' as TranslationKey) || 'Sauvegardé' : t('book_settings.save' as TranslationKey) || 'Sauvegarder'}
</button>
</div> </div>
<div className="p-8 space-y-8"> <div className="p-8 space-y-8">
@@ -69,8 +105,8 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.novel_title')}</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.novel_title')}</label>
<input <input
type="text" type="text"
value={project.title} value={localTitle}
onChange={(e) => onUpdate({ ...project, title: e.target.value })} onChange={(e) => setLocalTitle(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 font-serif font-bold text-lg transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none font-serif font-bold text-lg transition-colors duration-300"
/> />
</div> </div>
@@ -78,8 +114,8 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.author_name')}</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.author_name')}</label>
<input <input
type="text" type="text"
value={project.author} value={localAuthor}
onChange={(e) => onUpdate({ ...project, author: e.target.value })} onChange={(e) => setLocalAuthor(e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
/> />
</div> </div>
@@ -87,7 +123,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<div> <div>
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.global_synopsis')}</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.global_synopsis')}</label>
<textarea <textarea
value={settings.synopsis} value={localSettings.synopsis}
onChange={(e) => handleChange('synopsis', e.target.value)} onChange={(e) => handleChange('synopsis', e.target.value)}
className="w-full p-3 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none h-24 text-sm transition-colors duration-300" className="w-full p-3 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none h-24 text-sm transition-colors duration-300"
placeholder={t('book_settings.synopsis_placeholder')} placeholder={t('book_settings.synopsis_placeholder')}
@@ -105,7 +141,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<input <input
type="text" type="text"
list="genre-suggestions" list="genre-suggestions"
value={settings.genre} value={localSettings.genre}
onChange={(e) => handleChange('genre', e.target.value)} onChange={(e) => handleChange('genre', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder={t('book_settings.genre_placeholder')} placeholder={t('book_settings.genre_placeholder')}
@@ -118,7 +154,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.sub_genre')}</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.sub_genre')}</label>
<input <input
type="text" type="text"
value={settings.subGenre || ''} value={localSettings.subGenre || ''}
onChange={(e) => handleChange('subGenre', e.target.value)} onChange={(e) => handleChange('subGenre', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder={t('book_settings.subgenre_placeholder')} placeholder={t('book_settings.subgenre_placeholder')}
@@ -128,7 +164,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.target_audience')}</label> <label className="block text-sm font-semibold text-theme-muted mb-1">{t('book_settings.target_audience')}</label>
<input <input
type="text" type="text"
value={settings.targetAudience} value={localSettings.targetAudience}
onChange={(e) => handleChange('targetAudience', e.target.value)} onChange={(e) => handleChange('targetAudience', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder={t('book_settings.audience_placeholder')} placeholder={t('book_settings.audience_placeholder')}
@@ -141,7 +177,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<Hash size={14} className="absolute left-3 top-3 text-theme-muted" /> <Hash size={14} className="absolute left-3 top-3 text-theme-muted" />
<input <input
type="text" type="text"
value={settings.themes} value={localSettings.themes}
onChange={(e) => handleChange('themes', e.target.value)} onChange={(e) => handleChange('themes', e.target.value)}
className="w-full pl-9 p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full pl-9 p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder={t('book_settings.themes_placeholder')} placeholder={t('book_settings.themes_placeholder')}
@@ -160,7 +196,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<Users size={14} /> {t('book_settings.pov')} <Users size={14} /> {t('book_settings.pov')}
</label> </label>
<select <select
value={settings.pov} value={localSettings.pov}
onChange={(e) => handleChange('pov', e.target.value)} onChange={(e) => handleChange('pov', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
> >
@@ -173,7 +209,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<Clock size={14} /> {t('book_settings.tense')} <Clock size={14} /> {t('book_settings.tense')}
</label> </label>
<select <select
value={settings.tense} value={localSettings.tense}
onChange={(e) => handleChange('tense', e.target.value)} onChange={(e) => handleChange('tense', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
> >
@@ -186,7 +222,7 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
<input <input
type="text" type="text"
list="tone-suggestions" list="tone-suggestions"
value={settings.tone} value={localSettings.tone}
onChange={(e) => handleChange('tone', e.target.value)} onChange={(e) => handleChange('tone', e.target.value)}
className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300" className="w-full p-2.5 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-blue-500 outline-none transition-colors duration-300"
placeholder={t('book_settings.tone_placeholder')} placeholder={t('book_settings.tone_placeholder')}
@@ -205,8 +241,8 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
{t('book_settings.style_guide_help')} {t('book_settings.style_guide_help')}
</p> </p>
<textarea <textarea
value={project.styleGuide || ''} value={localStyleGuide}
onChange={(e) => handleStyleGuideChange(e.target.value)} onChange={(e) => setLocalStyleGuide(e.target.value)}
className="w-full p-3 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none h-32 text-sm font-mono transition-colors duration-300" className="w-full p-3 bg-theme-bg text-theme-text border border-theme-border rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none h-32 text-sm font-mono transition-colors duration-300"
placeholder={t('book_settings.style_guide_placeholder')} placeholder={t('book_settings.style_guide_placeholder')}
/> />
@@ -252,6 +288,6 @@ const BookSettingsComponent: React.FC<BookSettingsProps> = ({ project, onUpdate,
</div> </div>
</div> </div>
); );
}; });
export default BookSettingsComponent; export default BookSettingsComponent;

View File

@@ -1,67 +0,0 @@
'use client';
import React, { useState } from 'react';
import { CreditCard, Shield, Lock, ArrowRight, Loader2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider';
interface CheckoutProps {
onComplete: () => void;
onCancel: () => void;
}
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();
}, 2000);
};
return (
<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" /> {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">{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>
);
};
export default Checkout;

View File

@@ -0,0 +1,72 @@
'use client';
import React, { useState, useEffect } from 'react';
import { X, ShieldCheck } from 'lucide-react';
export const CookieBanner = () => {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
const consent = localStorage.getItem('cookie-consent');
if (!consent) {
setIsVisible(true);
}
}, []);
const handleAccept = () => {
localStorage.setItem('cookie-consent', 'accepted');
setIsVisible(false);
// Dispatch an event so Analytics component can react immediately without refresh
window.dispatchEvent(new Event('cookie-consent-updated'));
};
const handleDecline = () => {
localStorage.setItem('cookie-consent', 'declined');
setIsVisible(false);
window.dispatchEvent(new Event('cookie-consent-updated'));
};
if (!isVisible) return null;
return (
<div className="fixed bottom-0 left-0 right-0 z-[100] p-4 md:p-6 bg-slate-900/95 backdrop-blur shadow-2xl border-t border-slate-700/50 transform transition-transform duration-500 ease-out translate-y-0">
<div className="max-w-7xl mx-auto flex flex-col md:flex-row items-start md:items-center justify-between gap-4">
<div className="flex items-start gap-4">
<div className="p-3 bg-blue-500/20 text-blue-400 rounded-full shrink-0">
<ShieldCheck size={24} />
</div>
<div>
<h3 className="text-white font-bold text-lg mb-1">
Nous respectons votre vie privée
</h3>
<p className="text-slate-300 text-sm leading-relaxed max-w-3xl">
Nous utilisons des cookies (via Umami Analytics) exclusivement pour analyser le trafic de manière anonymisée et améliorer votre expérience sur l'application. Aucun parcours n'est lié à votre identité.
</p>
</div>
</div>
<div className="flex flex-col sm:flex-row w-full md:w-auto items-center gap-3 shrink-0">
<button
onClick={handleDecline}
className="w-full sm:w-auto px-6 py-2.5 rounded-xl border border-slate-600 text-slate-300 font-semibold hover:bg-slate-800 transition-colors text-sm"
>
Continuer sans accepter
</button>
<button
onClick={handleAccept}
className="w-full sm:w-auto px-6 py-2.5 rounded-xl bg-blue-600 text-white font-bold shadow-lg shadow-blue-600/20 hover:bg-blue-500 hover:-translate-y-0.5 transition-all text-sm"
>
Accepter
</button>
<button
onClick={() => setIsVisible(false)}
className="absolute top-4 right-4 md:hidden text-slate-400 p-2"
aria-label="Fermer"
>
<X size={20} />
</button>
</div>
</div>
</div>
);
};

View File

@@ -47,10 +47,10 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<LanguageSwitcher /> <LanguageSwitcher />
<button onClick={onProfile} className="bg-theme-bg text-theme-text px-4 md:px-5 py-2 md:py-2.5 rounded-xl text-xs md:text-sm font-bold hover:opacity-80 transition-all flex items-center gap-2 border border-theme-border"> <button onClick={onProfile} data-umami-event="Go To Profile" className="bg-theme-bg text-theme-text px-4 md:px-5 py-2 md:py-2.5 rounded-xl text-xs md:text-sm font-bold hover:opacity-80 transition-all flex items-center gap-2 border border-theme-border">
<User size={18} /> {t('dashboard.my_profile')} <User size={18} /> {t('dashboard.my_profile')}
</button> </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> <button onClick={onLogout} data-umami-event="Logout" title={t('sidebar.logout')} className="p-3 text-theme-muted hover:text-red-500 rounded-full hover:bg-red-500/10 transition-colors"><LogOut size={20} /></button>
</div> </div>
</div> </div>
@@ -74,7 +74,7 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<div className="bg-indigo-100 p-3 rounded-2xl text-indigo-600"><Target size={24} /></div> <div className="bg-indigo-100 p-3 rounded-2xl text-indigo-600"><Target size={24} /></div>
<div> <div>
<p className="text-xs font-bold text-theme-muted uppercase tracking-wider">{t('dashboard.daily_goal')}</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> <p className="text-2xl font-black text-theme-text">{user.stats.dailyWordCount} / {user.preferences.dailyWordGoal} {t('dashboard.words')}</p>
</div> </div>
</div> </div>
</div> </div>
@@ -86,6 +86,7 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<h3 className="text-2xl font-black text-theme-text">{t('dashboard.my_novels')}</h3> <h3 className="text-2xl font-black text-theme-text">{t('dashboard.my_novels')}</h3>
<button <button
onClick={onCreate} onClick={onCreate}
data-umami-event="Create Project"
className="flex items-center gap-2 bg-blue-600 text-white px-6 py-3 rounded-2xl font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-200" className="flex items-center gap-2 bg-blue-600 text-white px-6 py-3 rounded-2xl font-bold hover:bg-blue-700 transition-all shadow-xl shadow-blue-200"
> >
<Plus size={20} /> {t('dashboard.write_new')} <Plus size={20} /> {t('dashboard.write_new')}
@@ -97,6 +98,7 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
<div <div
key={p.id} key={p.id}
onClick={() => onSelect(p.id)} onClick={() => onSelect(p.id)}
data-umami-event="Open Project"
className="bg-theme-panel p-6 md:p-8 rounded-[2.5rem] border border-theme-border shadow-sm hover:shadow-2xl hover:scale-[1.02] transition-all cursor-pointer group flex flex-col justify-between h-64" className="bg-theme-panel p-6 md:p-8 rounded-[2.5rem] border border-theme-border shadow-sm hover:shadow-2xl hover:scale-[1.02] transition-all cursor-pointer group flex flex-col justify-between h-64"
> >
<div> <div>
@@ -153,7 +155,7 @@ const Dashboard: React.FC<DashboardProps> = ({ user, projects, onSelect, onCreat
</div> </div>
</div> </div>
</div> </div>
<button onClick={onPricing} className="w-full mt-10 bg-white/10 hover:bg-white/20 py-4 rounded-2xl text-sm font-bold transition-all"> <button onClick={onPricing} data-umami-event="Pricing Click" className="w-full mt-10 bg-white/10 hover:bg-white/20 py-4 rounded-2xl text-sm font-bold transition-all">
{t('dashboard.upgrade_plan')} {t('dashboard.upgrade_plan')}
</button> </button>
</div> </div>

View File

@@ -20,6 +20,8 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
const [pageSize, setPageSize] = useState<PageSize>('A4'); const [pageSize, setPageSize] = useState<PageSize>('A4');
const [includeCover, setIncludeCover] = useState(true); const [includeCover, setIncludeCover] = useState(true);
const [includeTOC, setIncludeTOC] = useState(true); const [includeTOC, setIncludeTOC] = useState(true);
const [includeBible, setIncludeBible] = useState(true);
const [includeOutils, setIncludeOutils] = useState(true);
const { t } = useLanguage(); const { t } = useLanguage();
if (!isOpen) return null; if (!isOpen) return null;
@@ -41,7 +43,14 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
a { color: #000; text-decoration: none; } a { color: #000; text-decoration: none; }
ul { list-style-type: none; padding: 0; } ul { list-style-type: none; padding: 0; }
li { margin-bottom: 0.5em; } li { margin-bottom: 0.5em; }
.mermaid-container { margin: 2em 0; text-align: center; }
.node-info { margin-bottom: 1.5em; padding-left: 1em; border-left: 3px solid #e2e8f0; }
.node-type { font-size: 0.8em; font-weight: bold; text-transform: uppercase; color: #64748b; }
</style> </style>
<script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
<script>
mermaid.initialize({ startOnLoad: true, theme: 'neutral' });
</script>
</head> </head>
<body> <body>
`; `;
@@ -72,6 +81,80 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
`; `;
}); });
if (includeBible && project.entities && project.entities.length > 0) {
html += `
<div class="chapter">
<h1>La Bible de l'Univers</h1>
`;
const types = ['Personnage', 'Lieu', 'Objet', 'Note'];
types.forEach(type => {
const entitiesOfType = project.entities.filter(e => e.type === type);
if (entitiesOfType.length > 0) {
html += `<h3>${type}s</h3><ul>`;
entitiesOfType.forEach(e => {
html += `<li><strong>${e.name}</strong>: ${e.description}</li>`;
});
html += `</ul>`;
}
});
html += `</div>`;
}
if (includeOutils) {
if (project.workflow && project.workflow.nodes && project.workflow.nodes.length > 0) {
// Generate Mermaid syntax
let mermaidText = "graph TD\n";
project.workflow.nodes.forEach(node => {
const safeTitle = node.title.replace(/[()]/g, '');
mermaidText += ` ${node.id}["${safeTitle}"]\n`;
});
if (project.workflow.connections) {
project.workflow.connections.forEach(conn => {
mermaidText += ` ${conn.source} --> ${conn.target}\n`;
});
}
html += `
<div class="chapter">
<h1>Structure du Récit</h1>
<div class="mermaid-container">
<pre class="mermaid">
${mermaidText}
</pre>
</div>
<h2>Détails des Étapes</h2>
`;
project.workflow.nodes.forEach(node => {
html += `
<div class="node-info">
<div class="node-type">${node.type || 'Étape'}</div>
<strong>${node.title}</strong>
<p>${node.description}</p>
</div>
`;
});
html += `</div>`;
}
if (project.ideas && project.ideas.length > 0) {
html += `
<div class="chapter">
<h1>Idées & Notes</h1>
`;
project.ideas.forEach(idea => {
html += `
<div class="node-info">
<div class="node-type">${idea.category}${idea.status}</div>
<strong>${idea.title}</strong>
<p>${idea.description}</p>
</div>
`;
});
html += `</div>`;
}
}
html += `</body></html>`; html += `</body></html>`;
return html; return html;
}; };
@@ -116,6 +199,58 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
const text = c.content.replace(/<[^>]+>/g, '\n'); const text = c.content.replace(/<[^>]+>/g, '\n');
md += `## ${c.title}\n\n${text}\n\n---\n\n`; md += `## ${c.title}\n\n${text}\n\n---\n\n`;
}); });
if (includeBible && project.entities && project.entities.length > 0) {
md += `# La Bible de l'Univers\n\n`;
const types = ['Personnage', 'Lieu', 'Objet', 'Note'];
types.forEach(type => {
const entitiesOfType = project.entities.filter(e => e.type === type);
if (entitiesOfType.length > 0) {
md += `## ${type}s\n\n`;
entitiesOfType.forEach(e => {
md += `### ${e.name}\n${e.description}\n\n`;
});
}
});
md += `---\n\n`;
}
if (includeOutils) {
if (project.workflow && project.workflow.nodes && project.workflow.nodes.length > 0) {
md += `# Structure du Récit\n\n`;
// Mermaid block
md += `\`\`\`mermaid\ngraph TD\n`;
project.workflow.nodes.forEach(node => {
const safeTitle = node.title.replace(/[()]/g, '');
md += ` ${node.id}["${safeTitle}"]\n`;
});
if (project.workflow.connections) {
project.workflow.connections.forEach(conn => {
md += ` ${conn.source} --> ${conn.target}\n`;
});
}
md += `\`\`\`\n\n`;
md += `## Détails des Étapes\n\n`;
project.workflow.nodes.forEach(node => {
md += `### ${node.title}\n`;
md += `**Type**: ${node.type || 'Étape'}\n\n`;
md += `${node.description}\n\n`;
});
md += `\n---\n\n`;
}
if (project.ideas && project.ideas.length > 0) {
md += `# Idées & Notes\n\n`;
project.ideas.forEach(idea => {
md += `## ${idea.title}\n`;
md += `**Catégorie**: ${idea.category} | **Statut**: ${idea.status}\n\n`;
md += `${idea.description}\n\n`;
});
md += `\n---\n\n`;
}
}
const blob = new Blob([md], { type: 'text/markdown' }); const blob = new Blob([md], { type: 'text/markdown' });
downloadBlob(blob, `${filename}.md`); downloadBlob(blob, `${filename}.md`);
} }
@@ -227,6 +362,28 @@ const ExportModal: React.FC<ExportModalProps> = ({ isOpen, onClose, project, onP
/> />
</div> </div>
<div className="flex items-center justify-between border-t border-slate-200 pt-4 mt-4">
<label className="text-slate-700 font-medium cursor-pointer" htmlFor="bible">Inclure la Bible (Personnages, Lieux...)</label>
<input
id="bible"
type="checkbox"
checked={includeBible}
onChange={(e) => setIncludeBible(e.target.checked)}
className="w-5 h-5 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
/>
</div>
<div className="flex items-center justify-between">
<label className="text-slate-700 font-medium cursor-pointer" htmlFor="outils">Inclure les Outils (Nodes, Idées)</label>
<input
id="outils"
type="checkbox"
checked={includeOutils}
onChange={(e) => setIncludeOutils(e.target.checked)}
className="w-5 h-5 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
/>
</div>
{format === 'epub' && ( {format === 'epub' && (
<p className="text-xs text-amber-600 bg-amber-50 p-2 rounded mt-2"> <p className="text-xs text-amber-600 bg-amber-50 p-2 rounded mt-2">
{t('export.epub_note')} {t('export.epub_note')}

View File

@@ -1,14 +1,17 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { Idea, IdeaStatus, IdeaCategory } from '@/lib/types'; import { Idea, IdeaStatus, IdeaCategory } from '@/lib/types';
import { Plus, X, GripVertical, CheckCircle, Circle, Clock, Lightbulb, Search, Trash2, Edit3, Save } from 'lucide-react'; import { Plus, X, GripVertical, CheckCircle, Circle, Clock, Lightbulb, Search, Trash2, Edit3, Save, Check, CheckCheck, Loader2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider'; import { useLanguage } from '@/providers/LanguageProvider';
import { TranslationKey } from '@/lib/i18n/translations'; import { TranslationKey } from '@/lib/i18n/translations';
interface IdeaBoardProps { interface IdeaBoardProps {
projectId: string;
ideas: Idea[]; ideas: Idea[];
onUpdate: (ideas: Idea[]) => void; onCreate: (data: Partial<Idea>) => Promise<string>;
onUpdateIdea: (id: string, data: Partial<Idea>) => Promise<void>;
onDelete: (id: string) => Promise<void>;
} }
const CATEGORIES: Record<IdeaCategory, { labelKey: string, color: string, icon: any }> = { const CATEGORIES: Record<IdeaCategory, { labelKey: string, color: string, icon: any }> = {
@@ -26,10 +29,26 @@ const STATUS_LABELS: Record<IdeaStatus, string> = {
const MAX_DESCRIPTION_LENGTH = 500; const MAX_DESCRIPTION_LENGTH = 500;
const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => { const IdeaBoard: React.FC<IdeaBoardProps> = React.memo(({ projectId, ideas, onCreate, onUpdateIdea, onDelete }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const [newIdeaTitle, setNewIdeaTitle] = useState(''); const [newIdeaTitle, setNewIdeaTitle] = useState('');
const [newIdeaCategory, setNewIdeaCategory] = useState<IdeaCategory>('plot'); const [newIdeaCategory, setNewIdeaCategory] = useState<IdeaCategory>('plot');
const [localIdeas, setLocalIdeas] = useState<Idea[]>(ideas);
// Auto-Save State
const [saveStatus, setSaveStatus] = useState<'saved_local' | 'saved_db' | 'saving' | 'unsaved'>('saved_db');
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Sync from parent
useEffect(() => {
setLocalIdeas(ideas);
}, [ideas]);
const saveLocally = (updated: Idea[]) => {
setLocalIdeas(updated);
localStorage.setItem(`ideas_${projectId}`, JSON.stringify(updated));
setSaveStatus('saved_local');
};
// Drag and Drop State // Drag and Drop State
const [draggedIdeaId, setDraggedIdeaId] = useState<string | null>(null); const [draggedIdeaId, setDraggedIdeaId] = useState<string | null>(null);
@@ -39,12 +58,12 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
// --- ACTIONS --- // --- ACTIONS ---
const handleAddIdea = (e: React.FormEvent) => { const handleAddIdea = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!newIdeaTitle.trim()) return; if (!newIdeaTitle.trim()) return;
const newIdea: Idea = { const newIdea: Idea = {
id: `idea-${Date.now()}`, id: `temp-${Date.now()}`,
title: newIdeaTitle, title: newIdeaTitle,
description: '', description: '',
category: newIdeaCategory, category: newIdeaCategory,
@@ -52,36 +71,86 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
createdAt: Date.now() createdAt: Date.now()
}; };
onUpdate([...ideas, newIdea]); saveLocally([...localIdeas, newIdea]);
setNewIdeaTitle(''); setNewIdeaTitle('');
}; setSaveStatus('saving');
const handleDelete = (id: string) => { try {
if (confirm(t('ideaboard.delete') + " ?")) { await onCreate({
onUpdate(ideas.filter(i => i.id !== id)); title: newIdea.title,
if (editingItem?.id === id) setEditingItem(null); category: newIdea.category,
status: newIdea.status,
});
setSaveStatus('saved_db');
} catch (err) {
setSaveStatus('saved_local');
} }
}; };
const handleSaveEdit = () => { const handleDelete = async (id: string) => {
if (confirm(t('ideaboard.delete') + " ?")) {
saveLocally(localIdeas.filter(i => i.id !== id));
if (editingItem?.id === id) setEditingItem(null);
setSaveStatus('saving');
try {
if (!id.startsWith('temp-')) {
await onDelete(id);
}
setSaveStatus('saved_db');
} catch (err) {
setSaveStatus('saved_local');
}
}
};
const handleSaveEdit = async () => {
if (!editingItem || !editingItem.title?.trim()) return; if (!editingItem || !editingItem.title?.trim()) return;
if (editingItem.id) { let isNew = !editingItem.id || editingItem.id.startsWith('temp-');
if (!isNew) {
// Update existing // Update existing
onUpdate(ideas.map(i => i.id === editingItem.id ? { ...i, ...editingItem } as Idea : i)); saveLocally(localIdeas.map(i => i.id === editingItem.id ? { ...i, ...editingItem } as Idea : i));
setEditingItem(null);
setSaveStatus('saving');
try {
await onUpdateIdea(editingItem.id!, {
title: editingItem.title,
description: editingItem.description,
category: editingItem.category,
status: editingItem.status
});
setSaveStatus('saved_db');
} catch (err) {
setSaveStatus('saved_local');
}
} else { } else {
// Create new from modal // Create new from modal
const newIdea: Idea = { const newIdea: Idea = {
id: `idea-${Date.now()}`, id: `temp-${Date.now()}`,
title: editingItem.title || '', title: editingItem.title || '',
description: editingItem.description || '', description: editingItem.description || '',
category: editingItem.category || 'plot', category: editingItem.category || 'plot',
status: editingItem.status || 'todo', status: editingItem.status || 'todo',
createdAt: Date.now() createdAt: Date.now()
}; };
onUpdate([...ideas, newIdea]); saveLocally([...localIdeas, newIdea]);
setEditingItem(null);
setSaveStatus('saving');
try {
await onCreate({
title: newIdea.title,
description: newIdea.description,
category: newIdea.category,
status: newIdea.status,
});
setSaveStatus('saved_db');
} catch (err) {
setSaveStatus('saved_local');
}
} }
setEditingItem(null);
}; };
const openQuickAdd = (status: IdeaStatus) => { const openQuickAdd = (status: IdeaStatus) => {
@@ -104,14 +173,24 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.effectAllowed = 'move';
}; };
const handleDrop = (e: React.DragEvent, status: IdeaStatus) => { const handleDrop = async (e: React.DragEvent, status: IdeaStatus) => {
e.preventDefault(); e.preventDefault();
if (draggedIdeaId) { if (draggedIdeaId) {
const updatedIdeas = ideas.map(idea => saveLocally(localIdeas.map(idea =>
idea.id === draggedIdeaId ? { ...idea, status } : idea idea.id === draggedIdeaId ? { ...idea, status } : idea
); ));
onUpdate(updatedIdeas); const idToUpdate = draggedIdeaId;
setDraggedIdeaId(null); setDraggedIdeaId(null);
if (!idToUpdate.startsWith('temp-')) {
setSaveStatus('saving');
try {
await onUpdateIdea(idToUpdate, { status });
setSaveStatus('saved_db');
} catch (err) {
setSaveStatus('saved_local');
}
}
} }
}; };
@@ -123,7 +202,7 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
// --- RENDERERS --- // --- RENDERERS ---
const Column = ({ title, status, icon: Icon }: { title: string, status: IdeaStatus, icon: any }) => { const Column = ({ title, status, icon: Icon }: { title: string, status: IdeaStatus, icon: any }) => {
const columnIdeas = ideas.filter(i => i.status === status); const columnIdeas = localIdeas.filter(i => i.status === status);
return ( return (
<div <div
@@ -227,38 +306,56 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
{/* Header & Add Form (Top Bar) */} {/* Header & Add Form (Top Bar) */}
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-theme-panel p-4 rounded-xl border border-theme-border shadow-sm shrink-0 transition-colors duration-300"> <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4 bg-theme-panel p-4 rounded-xl border border-theme-border shadow-sm shrink-0 transition-colors duration-300">
<div> <div className="flex justify-between items-center w-full md:w-auto">
<h2 className="text-2xl font-bold text-theme-text flex items-center gap-2"> <div>
<Lightbulb className="text-yellow-500" /> {t('ideaboard.title')} <h2 className="text-xl font-bold text-theme-text flex items-center gap-2">
</h2> <Lightbulb className="text-yellow-500" /> {t('ideaboard.title')}
<p className="text-theme-muted text-sm">{t('ideaboard.desc')}</p> </h2>
<p className="text-theme-muted text-xs">{t('ideaboard.desc')}</p>
</div>
{/* Status Indicator for Mobile */}
<div className="md:hidden flex items-center gap-2 text-xs font-medium text-slate-400">
{saveStatus === 'saving' && <><Loader2 size={12} className="animate-spin text-blue-500" /></>}
{saveStatus === 'saved_local' && <><Check size={14} className="text-green-500" /></>}
{saveStatus === 'saved_db' && <><CheckCheck size={14} className="text-emerald-600" /></>}
</div>
</div> </div>
<form onSubmit={handleAddIdea} className="flex-1 w-full md:w-auto max-w-2xl flex gap-2"> <div className="flex items-center gap-4 w-full md:w-auto flex-1 justify-end">
<select {/* Status Indicator for Desktop */}
value={newIdeaCategory} <div className="hidden md:flex items-center gap-2 text-xs font-medium text-slate-400">
onChange={(e) => setNewIdeaCategory(e.target.value as IdeaCategory)} {saveStatus === 'saving' && <><Loader2 size={12} className="animate-spin text-blue-500" /> <span className="text-blue-500">Sauvegarde...</span></>}
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" {saveStatus === 'saved_local' && <><Check size={14} className="text-green-500" /> <span className="text-green-500">Brouillon local</span></>}
> {saveStatus === 'saved_db' && <><CheckCheck size={14} className="text-emerald-600" /> <span className="text-emerald-600">Sauvegardé</span></>}
{Object.entries(CATEGORIES).map(([key, val]) => ( {saveStatus === 'unsaved' && <span className="text-amber-500">Non sauvegardé...</span>}
<option key={key} value={key}>{t(val.labelKey as TranslationKey)}</option> </div>
))}
</select> <form onSubmit={handleAddIdea} className="flex-1 w-full md:w-auto max-w-lg flex gap-2">
<input <select
type="text" value={newIdeaCategory}
value={newIdeaTitle} onChange={(e) => setNewIdeaCategory(e.target.value as IdeaCategory)}
onChange={(e) => setNewIdeaTitle(e.target.value)} 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"
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" {Object.entries(CATEGORIES).map(([key, val]) => (
/> <option key={key} value={key}>{t(val.labelKey as TranslationKey)}</option>
<button ))}
type="submit" </select>
disabled={!newIdeaTitle.trim()} <input
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 disabled:opacity-50 transition-colors flex items-center gap-2" type="text"
> value={newIdeaTitle}
<Plus size={18} /> onChange={(e) => setNewIdeaTitle(e.target.value)}
</button> placeholder={t('ideaboard.add_idea')}
</form> 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
type="submit"
disabled={!newIdeaTitle.trim()}
className="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 disabled:opacity-50 transition-colors flex items-center gap-2"
>
<Plus size={18} />
</button>
</form>
</div>
</div> </div>
{/* Kanban Board */} {/* Kanban Board */}
@@ -368,6 +465,6 @@ const IdeaBoard: React.FC<IdeaBoardProps> = ({ ideas, onUpdate }) => {
</div> </div>
); );
}; });
export default IdeaBoard; export default IdeaBoard;

View File

@@ -47,6 +47,7 @@ export const LanguageSwitcher: React.FC = () => {
{languages.map((lang) => ( {languages.map((lang) => (
<button <button
key={lang.code} key={lang.code}
data-umami-event="Change Language"
onClick={() => { onClick={() => {
setLanguage(lang.code); setLanguage(lang.code);
setIsOpen(false); setIsOpen(false);

View File

@@ -3,7 +3,7 @@
import React, { useRef, useEffect, useState, useImperativeHandle, forwardRef, useMemo } from 'react'; import React, { useRef, useEffect, useState, useImperativeHandle, forwardRef, useMemo } from 'react';
import { import {
Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, List, Heading1, Heading2, Bold, Italic, Underline, AlignLeft, AlignCenter, AlignRight, List, Heading1, Heading2,
Copy, Wand2, Check, RefreshCw, Maximize2, Loader2, MousePointerClick, History, RotateCcw, Copy, Wand2, Check, CheckCheck, RefreshCw, Maximize2, Loader2, MousePointerClick, History, RotateCcw,
ChevronDown, ChevronUp, Layers ChevronDown, ChevronUp, Layers
} from 'lucide-react'; } from 'lucide-react';
@@ -12,6 +12,7 @@ export interface RichTextEditorHandle {
} }
interface RichTextEditorProps { interface RichTextEditorProps {
editorId?: string; // Used to uniquely identify the draft in localStorage
initialContent: string; initialContent: string;
onChange?: (html: string) => void; onChange?: (html: string) => void;
onSave?: (html: string) => void; onSave?: (html: string) => void;
@@ -34,13 +35,17 @@ interface VersionGroup {
versions: Version[]; versions: Version[];
} }
const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({ initialContent, onChange, onSave, onSelectionChange, onAiTransform }, ref) => { const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({ editorId, initialContent, onChange, onSave, onSelectionChange, onAiTransform }, ref) => {
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null); const scrollContainerRef = useRef<HTMLDivElement>(null);
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
// Appearance State
const [editorFont, setEditorFont] = useState('font-serif');
const [showFontMenu, setShowFontMenu] = useState(false);
// Auto-Save State // Auto-Save State
const [saveStatus, setSaveStatus] = useState<'saved' | 'saving' | 'unsaved'>('saved'); const [saveStatus, setSaveStatus] = useState<'saved_local' | 'saved_db' | 'saving' | 'unsaved'>('saved_db');
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null); const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Track sync state to avoid autosave loopbacks wiping current edits // Track sync state to avoid autosave loopbacks wiping current edits
@@ -168,8 +173,21 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
useEffect(() => { useEffect(() => {
if (!contentRef.current || initialContent === undefined) return; if (!contentRef.current || initialContent === undefined) return;
let contentToLoad = initialContent;
let hasLocalDraft = false;
// Check localStorage for a newer draft
if (editorId) {
const localDraft = localStorage.getItem(`draft_${editorId}`);
if (localDraft && localDraft !== initialContent) {
contentToLoad = localDraft;
hasLocalDraft = true;
setSaveStatus('saved_local');
}
}
// 1. Si le contenu entrant est identique à ce qu'on a déjà, on ne touche à rien // 1. Si le contenu entrant est identique à ce qu'on a déjà, on ne touche à rien
if (initialContent === contentRef.current.innerHTML) return; if (contentToLoad === contentRef.current.innerHTML) return;
// 2. LOGIQUE CRUCIALE : On ne met à jour le DOM que si : // 2. LOGIQUE CRUCIALE : On ne met à jour le DOM que si :
// - L'éditeur est vide (premier chargement) // - L'éditeur est vide (premier chargement)
@@ -177,12 +195,26 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
// - OU si l'utilisateur n'est PAS en train de focus l'éditeur // - OU si l'utilisateur n'est PAS en train de focus l'éditeur
const isUserEditing = document.activeElement === contentRef.current; const isUserEditing = document.activeElement === contentRef.current;
if (!isUserEditing || (contentRef.current.innerHTML === "" && initialContent !== "")) { if (!isUserEditing || (contentRef.current.innerHTML === "" && contentToLoad !== "")) {
contentRef.current.innerHTML = initialContent; contentRef.current.innerHTML = contentToLoad;
syncRef.current = initialContent; syncRef.current = contentToLoad;
latestContentRef.current = initialContent; latestContentRef.current = contentToLoad;
} }
}, [initialContent]); }, [initialContent, editorId]);
// Load saved font preference
useEffect(() => {
const savedFont = localStorage.getItem('rte_font_preference');
if (savedFont) {
setEditorFont(savedFont);
}
}, []);
const handleFontChange = (fontClass: string) => {
setEditorFont(fontClass);
localStorage.setItem('rte_font_preference', fontClass);
setShowFontMenu(false);
};
// Flush pending save on unmount // Flush pending save on unmount
useEffect(() => { useEffect(() => {
@@ -213,9 +245,16 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
if (onChange) onChange(currentHtml); if (onChange) onChange(currentHtml);
// Auto-Save Debounce // 1. Save locally immediately
if (onSave) { if (editorId) {
localStorage.setItem(`draft_${editorId}`, currentHtml);
setSaveStatus('saved_local');
} else {
setSaveStatus('unsaved'); setSaveStatus('unsaved');
}
// 2. Auto-Save Debounce for DB
if (onSave) {
if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current); if (saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current);
saveTimeoutRef.current = setTimeout(async () => { saveTimeoutRef.current = setTimeout(async () => {
@@ -227,10 +266,15 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
syncRef.current = htmlToSave; syncRef.current = htmlToSave;
try { try {
await onSave(htmlToSave); await onSave(htmlToSave);
setSaveStatus('saved_db');
if (editorId) {
// Once saved to DB, we can consider the local draft synced if we want,
// or just keep it there. It will be overwritten on next load.
}
} catch (err) { } catch (err) {
console.error('Auto-save failed:', err); console.error('Auto-save failed:', err);
setSaveStatus('saved_local'); // Revert to local save status on error
} }
setSaveStatus('saved');
}, 2000); // 2 seconds }, 2000); // 2 seconds
} }
} }
@@ -363,6 +407,32 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
font-style: italic; font-style: italic;
cursor: text; cursor: text;
} }
.editor-content h1 {
font-size: 2.25rem;
font-weight: 800;
margin-top: 1.5rem;
margin-bottom: 1rem;
line-height: 1.2;
display: block;
}
.editor-content h2 {
font-size: 1.875rem;
font-weight: 700;
margin-top: 1.25rem;
margin-bottom: 0.75rem;
line-height: 1.3;
display: block;
}
.editor-content ul {
list-style-type: disc;
margin-left: 1.5rem;
margin-bottom: 1rem;
}
.editor-content ol {
list-style-type: decimal;
margin-left: 1.5rem;
margin-bottom: 1rem;
}
`}</style> `}</style>
{/* Toolbar */} {/* Toolbar */}
@@ -371,22 +441,63 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<ToolbarButton icon={Italic} cmd="italic" label="Italique" /> <ToolbarButton icon={Italic} cmd="italic" label="Italique" />
<ToolbarButton icon={Underline} cmd="underline" label="Souligné" /> <ToolbarButton icon={Underline} cmd="underline" label="Souligné" />
<div className="w-px h-6 bg-slate-300 mx-1" /> <div className="w-px h-6 bg-slate-300 mx-1" />
<ToolbarButton icon={Heading1} cmd="formatBlock" arg="H1" label="Titre 1" /> <ToolbarButton icon={Heading1} cmd="formatBlock" arg="<h1>" label="Titre 1" />
<ToolbarButton icon={Heading2} cmd="formatBlock" arg="H2" label="Titre 2" /> <ToolbarButton icon={Heading2} cmd="formatBlock" arg="<h2>" label="Titre 2" />
<button
onMouseDown={(e) => {
e.preventDefault();
execCommand('formatBlock', '<p>');
}}
className="p-1.5 rounded transition-colors text-slate-500 hover:text-slate-800 hover:bg-slate-200 font-bold text-sm px-2"
title="Paragraphe"
>
P
</button>
<div className="w-px h-6 bg-slate-300 mx-1" /> <div className="w-px h-6 bg-slate-300 mx-1" />
<ToolbarButton icon={AlignLeft} cmd="justifyLeft" label="Aligner à gauche" /> <ToolbarButton icon={AlignLeft} cmd="justifyLeft" label="Aligner à gauche" />
<ToolbarButton icon={AlignCenter} cmd="justifyCenter" label="Centrer" /> <ToolbarButton icon={AlignCenter} cmd="justifyCenter" label="Centrer" />
<ToolbarButton icon={AlignRight} cmd="justifyRight" label="Aligner à droite" /> <ToolbarButton icon={AlignRight} cmd="justifyRight" label="Aligner à droite" />
<div className="w-px h-6 bg-slate-300 mx-1" /> <div className="w-px h-6 bg-slate-300 mx-1" />
<ToolbarButton icon={List} cmd="insertUnorderedList" label="Liste" /> <ToolbarButton icon={List} cmd="insertUnorderedList" label="Liste" />
<div className="w-px h-6 bg-slate-300 mx-1" />
{/* Font Selector */}
<div className="relative">
<button
onClick={() => setShowFontMenu(!showFontMenu)}
className="flex items-center gap-2 px-2 py-1.5 text-xs font-semibold text-slate-600 hover:bg-slate-200 rounded transition-colors"
title="Choisir la police d'écriture"
>
<div className="flex items-center gap-1">
<span className="opacity-70">A</span>
<span className="font-bold">Aa</span>
</div>
<ChevronDown size={14} className="opacity-50" />
</button>
{showFontMenu && (
<>
<div className="fixed inset-0 z-40 bg-transparent" onClick={() => setShowFontMenu(false)} />
<div className="absolute top-full mt-1 left-0 w-48 bg-white shadow-xl border border-slate-200 rounded-lg p-1.5 z-50 animate-in fade-in zoom-in-95 duration-100 flex flex-col gap-1">
<div className="px-2 py-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">Polices</div>
<button onClick={() => handleFontChange('font-sans')} className={`text-left px-3 py-2 text-sm rounded transition-colors font-sans ${editorFont === 'font-sans' ? 'bg-indigo-50 text-indigo-700' : 'hover:bg-slate-50 text-slate-700'}`}>Classique Sans (Inter)</button>
<button onClick={() => handleFontChange('font-serif')} className={`text-left px-3 py-2 text-sm rounded transition-colors font-serif ${editorFont === 'font-serif' ? 'bg-indigo-50 text-indigo-700' : 'hover:bg-slate-50 text-slate-700'}`}>Classique Serif (Merriweather)</button>
<button onClick={() => handleFontChange('font-lora')} className={`text-left px-3 py-2 text-sm rounded transition-colors font-lora ${editorFont === 'font-lora' ? 'bg-indigo-50 text-indigo-700' : 'hover:bg-slate-50 text-slate-700'}`} style={{ fontFamily: 'var(--font-lora)' }}>Littéraire (Lora)</button>
<button onClick={() => handleFontChange('font-garamond')} className={`text-left px-3 py-2 text-sm rounded transition-colors font-garamond ${editorFont === 'font-garamond' ? 'bg-indigo-50 text-indigo-700' : 'hover:bg-slate-50 text-slate-700'}`} style={{ fontFamily: 'var(--font-garamond)' }}>Ancienne (EB Garamond)</button>
<button onClick={() => handleFontChange('font-playfair')} className={`text-left px-3 py-2 text-sm rounded transition-colors font-playfair ${editorFont === 'font-playfair' ? 'bg-indigo-50 text-indigo-700' : 'hover:bg-slate-50 text-slate-700'}`} style={{ fontFamily: 'var(--font-playfair)' }}>Élégante (Playfair)</button>
</div>
</>
)}
</div>
<div className="flex-1" /> <div className="flex-1" />
{/* Save Status Indicator */} {/* Save Status Indicator */}
<div className="flex items-center gap-2 mr-4 text-xs font-medium text-slate-400"> <div className="flex items-center gap-2 mr-4 text-xs font-medium text-slate-400">
{saveStatus === 'saving' && <><Loader2 size={12} className="animate-spin" /> Sauvegarde...</>} {saveStatus === 'saving' && <><Loader2 size={12} className="animate-spin text-blue-500" /> <span className="text-blue-500 hidden sm:inline">Sauvegarde en cours...</span></>}
{saveStatus === 'saved' && <><Check size={12} className="text-green-500" /> Sauvegardé</>} {saveStatus === 'saved_local' && <><Check size={14} className="text-green-500" /> <span className="text-green-500 hidden sm:inline">Brouillon local</span></>}
{saveStatus === 'unsaved' && <span className="text-amber-500">Modifications non enregistrées...</span>} {saveStatus === 'saved_db' && <><CheckCheck size={14} className="text-emerald-600" /> <span className="text-emerald-600 hidden sm:inline">Sauvegardé</span></>}
{saveStatus === 'unsaved' && <span className="text-amber-500">Non sauvegardé...</span>}
</div> </div>
<div className="w-px h-6 bg-slate-300 mx-1" /> <div className="w-px h-6 bg-slate-300 mx-1" />
@@ -412,7 +523,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
suppressContentEditableWarning suppressContentEditableWarning
spellCheck={true} spellCheck={true}
lang="fr-FR" lang="fr-FR"
className="bg-theme-editor-bg shadow-sm w-[800px] min-h-[1000px] p-12 outline-none font-serif text-lg leading-relaxed text-theme-editor-text editor-content transition-colors duration-300" className={`bg-theme-editor-bg shadow-sm w-[800px] min-h-[1000px] p-12 outline-none text-lg leading-relaxed text-theme-editor-text editor-content transition-all duration-300 ${editorFont}`}
onInput={handleInput} onInput={handleInput}
onBlur={() => { setIsFocused(false); saveSelection(); }} onBlur={() => { setIsFocused(false); saveSelection(); }}
onFocus={() => setIsFocused(true)} onFocus={() => setIsFocused(true)}
@@ -555,6 +666,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<button <button
onClick={() => handleAiAction('correct')} onClick={() => handleAiAction('correct')}
data-umami-event="AI Correct"
disabled={!hasSelection} disabled={!hasSelection}
className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-indigo-50 hover:text-indigo-700'}`} className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-indigo-50 hover:text-indigo-700'}`}
> >
@@ -563,6 +675,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<button <button
onClick={() => handleAiAction('rewrite')} onClick={() => handleAiAction('rewrite')}
data-umami-event="AI Rewrite"
disabled={!hasSelection} disabled={!hasSelection}
className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-indigo-50 hover:text-indigo-700'}`} className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-indigo-50 hover:text-indigo-700'}`}
> >
@@ -571,6 +684,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<button <button
onClick={() => handleAiAction('expand')} onClick={() => handleAiAction('expand')}
data-umami-event="AI Expand"
disabled={!hasSelection} disabled={!hasSelection}
className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-indigo-50 hover:text-indigo-700'}`} className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-indigo-50 hover:text-indigo-700'}`}
> >
@@ -579,6 +693,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<button <button
onClick={() => handleAiAction('continue')} onClick={() => handleAiAction('continue')}
data-umami-event="AI Continue"
className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-700 text-left transition-colors" className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-indigo-50 hover:text-indigo-700 text-left transition-colors"
> >
<Wand2 size={14} /> Continuer l'écriture <Wand2 size={14} /> Continuer l'écriture
@@ -586,12 +701,127 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<div className="h-px bg-slate-100 my-1" /> <div className="h-px bg-slate-100 my-1" />
<div className="px-3 py-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider">
Mise en forme
</div>
<div className="flex px-1 gap-1 border-b border-slate-50 pb-1 mb-1">
<button
onClick={() => {
if (savedRange.current) {
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(savedRange.current);
}
execCommand('bold');
setContextMenu(null);
}}
className="flex-1 p-2 flex items-center justify-center hover:bg-slate-100 rounded text-slate-700"
title="Gras"
>
<Bold size={16} />
</button>
<button
onClick={() => {
if (savedRange.current) {
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(savedRange.current);
}
execCommand('italic');
setContextMenu(null);
}}
className="flex-1 p-2 flex items-center justify-center hover:bg-slate-100 rounded text-slate-700"
title="Italique"
>
<Italic size={16} />
</button>
<button
onClick={() => {
if (savedRange.current) {
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(savedRange.current);
}
execCommand('underline');
setContextMenu(null);
}}
className="flex-1 p-2 flex items-center justify-center hover:bg-slate-100 rounded text-slate-700"
title="Souligné"
>
<Underline size={16} />
</button>
</div>
<button
onClick={() => {
if (savedRange.current) {
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(savedRange.current);
}
execCommand('formatBlock', '<h1>');
setContextMenu(null);
}}
className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 text-left transition-colors"
>
<Heading1 size={14} /> Titre 1
</button>
<button
onClick={() => {
if (savedRange.current) {
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(savedRange.current);
}
execCommand('formatBlock', '<h2>');
setContextMenu(null);
}}
className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 text-left transition-colors"
>
<Heading2 size={14} /> Titre 2
</button>
<button
onClick={() => {
if (savedRange.current) {
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(savedRange.current);
}
execCommand('formatBlock', '<p>');
setContextMenu(null);
}}
className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 text-left transition-colors"
>
<span className="font-bold text-xs w-[14px] text-center">P</span> Paragraphe (Normal)
</button>
<button
onClick={() => {
if (savedRange.current) {
const sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(savedRange.current);
}
execCommand('insertUnorderedList');
setContextMenu(null);
}}
className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 text-left transition-colors"
>
<List size={14} /> Liste à puces
</button>
<div className="h-px bg-slate-100 my-1" />
<div className="px-3 py-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider"> <div className="px-3 py-1 text-[10px] font-bold text-slate-400 uppercase tracking-wider">
Édition Édition
</div> </div>
<button <button
onClick={handleCopy} onClick={handleCopy}
data-umami-event="Editor Copy"
disabled={!hasSelection} disabled={!hasSelection}
className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-slate-50'}`} className={`flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${!hasSelection ? 'text-slate-300 cursor-not-allowed' : 'text-slate-700 hover:bg-slate-50'}`}
> >
@@ -600,6 +830,7 @@ const RichTextEditor = forwardRef<RichTextEditorHandle, RichTextEditorProps>(({
<button <button
onClick={handleSelectAll} onClick={handleSelectAll}
data-umami-event="Editor Select All"
className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 text-left transition-colors" className="flex items-center gap-2 px-3 py-2 text-sm text-slate-700 hover:bg-slate-50 text-left transition-colors"
> >
<MousePointerClick size={14} /> Tout sélectionner <MousePointerClick size={14} /> Tout sélectionner

View File

@@ -278,7 +278,7 @@ interface SuggestionState {
filteredEntities: Entity[]; filteredEntities: Entity[];
} }
const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities, onNavigateToEntity }) => { const StoryWorkflow: React.FC<StoryWorkflowProps> = React.memo(({ data, onUpdate, entities, onNavigateToEntity }) => {
const { t } = useLanguage(); const { t } = useLanguage();
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);
const rafRef = useRef<number | null>(null); const rafRef = useRef<number | null>(null);
@@ -746,6 +746,6 @@ const StoryWorkflow: React.FC<StoryWorkflowProps> = ({ data, onUpdate, entities,
)} )}
</div> </div>
); );
}; });
export default StoryWorkflow; export default StoryWorkflow;

View File

@@ -1,11 +1,12 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import { UserProfile, UserPreferences } from '@/lib/types'; import { UserProfile, UserPreferences } from '@/lib/types';
import { User, Settings, Globe, Shield, Bell, Save, Camera, Target, Flame, Layout } from 'lucide-react'; import { User, Settings, Globe, Shield, Bell, Save, Camera, Target, Flame, Layout } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider'; import { useLanguage } from '@/providers/LanguageProvider';
import { LanguageSwitcher } from '@/components/LanguageSwitcher'; import { LanguageSwitcher } from '@/components/LanguageSwitcher';
import { useRouter } from 'next/navigation';
interface UserProfileSettingsProps { interface UserProfileSettingsProps {
user: UserProfile; user: UserProfile;
@@ -30,9 +31,42 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
bio: user.bio || '', bio: user.bio || '',
email: user.email, email: user.email,
theme: user.preferences.theme, theme: user.preferences.theme,
dailyWordGoal: user.preferences.dailyWordGoal dailyWordGoal: user.preferences.dailyWordGoal,
customColors: user.preferences.customColors || {
bg: '#1a1b26',
panel: '#24283b',
text: '#c0caf5',
accent: '#7aa2f7'
},
customApiProvider: user.customApiProvider || 'openai',
customApiKey: user.customApiKey || ''
}); });
const router = useRouter();
// Handle Live Preview of Theme Changes
useEffect(() => {
const root = document.documentElement;
root.classList.remove('theme-light', 'theme-dark', 'theme-sepia', 'theme-custom');
root.classList.add(`theme-${formData.theme}`);
if (formData.theme === 'custom') {
root.style.setProperty('--theme-bg', formData.customColors.bg);
root.style.setProperty('--theme-panel', formData.customColors.panel);
root.style.setProperty('--theme-text', formData.customColors.text);
root.style.setProperty('--theme-border', formData.customColors.text + '20');
root.style.setProperty('--theme-muted', formData.customColors.text + '99');
root.style.setProperty('--theme-accent', formData.customColors.accent);
} else {
root.style.removeProperty('--theme-bg');
root.style.removeProperty('--theme-panel');
root.style.removeProperty('--theme-text');
root.style.removeProperty('--theme-border');
root.style.removeProperty('--theme-muted');
root.style.removeProperty('--theme-accent');
}
}, [formData.theme, formData.customColors]);
const fileInputRef = React.useRef<HTMLInputElement>(null); const fileInputRef = React.useRef<HTMLInputElement>(null);
const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => { const handleImageUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
@@ -83,22 +117,31 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
preferences: { preferences: {
...user.preferences, ...user.preferences,
theme: formData.theme, theme: formData.theme,
dailyWordGoal: formData.dailyWordGoal dailyWordGoal: formData.dailyWordGoal,
} customColors: formData.customColors
},
customApiProvider: formData.customApiProvider as any,
customApiKey: formData.customApiKey
}); });
localStorage.setItem('plumeia_theme', formData.theme);
if (formData.theme === 'custom') {
localStorage.setItem('plumeia_custom_colors', JSON.stringify(formData.customColors));
}
alert(t('profile.saved_success') || "Profil mis à jour !"); alert(t('profile.saved_success') || "Profil mis à jour !");
}; };
const isDark = formData.theme === 'dark'; const isDark = formData.theme === 'dark';
const isSepia = formData.theme === 'sepia'; const isSepia = formData.theme === 'sepia';
const isCustom = formData.theme === 'custom';
const themeOuterClass = isDark ? 'bg-slate-900 text-white' : isSepia ? 'bg-[#eaddc4] text-[#433422]' : 'bg-slate-50 text-slate-900'; const themeOuterClass = isCustom ? 'bg-theme-bg text-theme-text' : isDark ? 'bg-slate-900 text-white' : isSepia ? 'bg-[#eaddc4] text-[#433422]' : 'bg-slate-50 text-slate-900';
const themeInnerClass = isDark ? 'bg-slate-800 border-slate-700' : isSepia ? 'bg-[#f4ecd8] border-[#dfcdae]' : 'bg-white border-slate-200'; const themeInnerClass = isCustom ? 'bg-theme-panel border-theme-border' : isDark ? 'bg-slate-800 border-slate-700' : isSepia ? 'bg-[#f4ecd8] border-[#dfcdae]' : 'bg-white border-slate-200';
const themeTextHeading = isDark ? 'text-white' : isSepia ? 'text-[#332616]' : 'text-slate-900'; const themeTextHeading = isCustom ? 'text-theme-text' : isDark ? 'text-white' : isSepia ? 'text-[#332616]' : 'text-slate-900';
const themeTextMuted = isDark ? 'text-slate-400' : isSepia ? 'text-[#735e44]' : 'text-slate-500'; const themeTextMuted = isCustom ? 'text-theme-muted' : isDark ? 'text-slate-400' : isSepia ? 'text-[#735e44]' : 'text-slate-500';
const themeInputBg = isDark ? 'bg-slate-900 border-slate-700 text-white' : isSepia ? 'bg-[#fbf8f1] border-[#eaddc4] text-[#433422]' : 'bg-slate-50 border-slate-200 text-slate-900'; const themeInputBg = isCustom ? 'bg-theme-bg border-theme-border text-theme-text' : isDark ? 'bg-slate-900 border-slate-700 text-white' : isSepia ? 'bg-[#fbf8f1] border-[#eaddc4] text-[#433422]' : 'bg-slate-50 border-slate-200 text-slate-900';
const themeTabActive = isDark ? 'bg-white text-slate-900 shadow-lg' : isSepia ? 'bg-[#5c4731] text-white shadow-lg' : 'bg-slate-900 text-white shadow-lg'; const themeTabActive = isCustom ? 'bg-theme-text text-theme-bg shadow-lg' : isDark ? 'bg-white text-slate-900 shadow-lg' : isSepia ? 'bg-[#5c4731] text-white shadow-lg' : 'bg-slate-900 text-white shadow-lg';
const themeTabInactive = isDark ? 'text-slate-400 hover:bg-slate-800 hover:text-white' : isSepia ? 'text-[#735e44] hover:bg-[#eaddc4] hover:text-[#332616]' : 'text-slate-500 hover:bg-white hover:text-slate-900'; const themeTabInactive = isCustom ? 'text-theme-muted hover:bg-theme-panel hover:text-theme-text' : isDark ? 'text-slate-400 hover:bg-slate-800 hover:text-white' : isSepia ? 'text-[#735e44] hover:bg-[#eaddc4] hover:text-[#332616]' : 'text-slate-500 hover:bg-white hover:text-slate-900';
return ( return (
<div className={`h-screen overflow-y-auto p-8 font-sans ${themeOuterClass}`}> <div className={`h-screen overflow-y-auto p-8 font-sans ${themeOuterClass}`}>
@@ -141,7 +184,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<div className={`flex-1 rounded-2xl shadow-sm border p-8 ${themeInnerClass}`}> <div className={`flex-1 rounded-2xl shadow-sm border p-8 ${themeInnerClass}`}>
{activeTab === 'profile' && ( {activeTab === 'profile' && (
<div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-300"> <div className="space-y-8 animate-in fade-in slide-in-from-bottom-4 duration-300">
<div className={`flex items-center gap-6 pb-8 border-b ${isDark ? 'border-slate-700' : isSepia ? 'border-[#dfcdae]' : 'border-slate-100'}`}> <div className={`flex items-center gap-6 pb-8 border-b ${isCustom ? 'border-theme-border' : isDark ? 'border-slate-700' : isSepia ? 'border-[#dfcdae]' : 'border-slate-100'}`}>
<div className="relative group cursor-pointer" onClick={() => fileInputRef.current?.click()}> <div className="relative group cursor-pointer" onClick={() => fileInputRef.current?.click()}>
<input <input
type="file" type="file"
@@ -150,7 +193,7 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
accept="image/*" accept="image/*"
className="hidden" className="hidden"
/> />
<img src={formData.avatar || 'https://via.placeholder.com/150'} className={`w-24 h-24 rounded-full object-cover border-4 shadow-md ${isDark ? 'border-slate-800' : isSepia ? 'border-[#f4ecd8]' : 'border-slate-50'}`} alt="Avatar" /> <img src={formData.avatar || 'https://via.placeholder.com/150'} className={`w-24 h-24 rounded-full object-cover border-4 shadow-md ${isCustom ? 'border-theme-panel' : 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={t('profile.change_avatar')}> <div className="absolute inset-0 bg-black/40 text-white rounded-full opacity-0 group-hover:opacity-100 flex items-center justify-center transition-opacity" title={t('profile.change_avatar')}>
<Camera size={20} /> <Camera size={20} />
</div> </div>
@@ -212,18 +255,42 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<label className={`text-xs font-black uppercase tracking-widest flex items-center gap-2 ${themeTextMuted}`}> <label className={`text-xs font-black uppercase tracking-widest flex items-center gap-2 ${themeTextMuted}`}>
{t('profile.editor_theme')} {t('profile.editor_theme')}
</label> </label>
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-4 gap-3">
{['light', 'sepia', 'dark'].map((t) => ( {['light', 'sepia', 'dark', 'custom'].map((t) => (
<button <button
key={t} key={t}
onClick={() => setFormData({ ...formData, theme: t as any })} onClick={() => setFormData({ ...formData, theme: t as any })}
className={`p-4 rounded-xl border-2 transition-all flex flex-col items-center gap-2 ${formData.theme === t ? 'border-blue-500 bg-blue-50 text-blue-700' : isDark ? 'border-slate-700 hover:border-slate-600' : isSepia ? 'border-[#dfcdae] hover:border-[#cfbd9e]' : 'border-slate-100 hover:border-slate-200'}`} className={`p-4 rounded-xl border-2 transition-all flex flex-col items-center gap-2 ${formData.theme === t ? isCustom ? 'border-theme-text text-theme-text' : 'border-blue-500 bg-blue-50 text-blue-700' : isCustom ? 'border-theme-border text-theme-muted hover:border-theme-text' : isDark ? 'border-slate-700 hover:border-slate-600' : isSepia ? 'border-[#dfcdae] hover:border-[#cfbd9e]' : 'border-slate-100 hover:border-slate-200'}`}
> >
<div className={`w-8 h-8 rounded-full border border-slate-200 ${t === 'light' ? 'bg-white' : t === 'sepia' ? 'bg-[#f4ecd8]' : 'bg-slate-900'}`} /> <div className={`w-8 h-8 rounded-full border border-slate-200 ${t === 'light' ? 'bg-white' : t === 'sepia' ? 'bg-[#f4ecd8]' : t === 'custom' ? 'bg-gradient-to-tr from-pink-500 via-purple-500 to-indigo-500 border-none' : 'bg-slate-900'}`} />
<span className={`text-[10px] font-bold uppercase ${formData.theme !== t ? themeTextMuted : ''}`}>{t}</span> <span className={`text-[10px] font-bold uppercase ${formData.theme !== t ? themeTextMuted : ''}`}>{t}</span>
</button> </button>
))} ))}
</div> </div>
{formData.theme === 'custom' && (
<div className={`mt-6 space-y-4 pt-4 border-t ${isCustom ? 'border-theme-border' : 'border-slate-100'}`}>
<h4 className={`text-sm font-bold ${themeTextHeading}`}>Couleurs Personnalisées</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className={`flex items-center justify-between p-3 rounded-xl border ${themeInputBg}`}>
<label className={`text-xs font-bold uppercase ${themeTextMuted}`}>Arrière-plan</label>
<input type="color" value={formData.customColors.bg} onChange={(e) => setFormData({ ...formData, customColors: { ...formData.customColors, bg: e.target.value } })} className="w-8 h-8 rounded shrink-0 cursor-pointer" />
</div>
<div className={`flex items-center justify-between p-3 rounded-xl border ${themeInputBg}`}>
<label className={`text-xs font-bold uppercase ${themeTextMuted}`}>Panneaux</label>
<input type="color" value={formData.customColors.panel} onChange={(e) => setFormData({ ...formData, customColors: { ...formData.customColors, panel: e.target.value } })} className="w-8 h-8 rounded shrink-0 cursor-pointer" />
</div>
<div className={`flex items-center justify-between p-3 rounded-xl border ${themeInputBg}`}>
<label className={`text-xs font-bold uppercase ${themeTextMuted}`}>Texte Principal</label>
<input type="color" value={formData.customColors.text} onChange={(e) => setFormData({ ...formData, customColors: { ...formData.customColors, text: e.target.value } })} className="w-8 h-8 rounded shrink-0 cursor-pointer" />
</div>
<div className={`flex items-center justify-between p-3 rounded-xl border ${themeInputBg}`}>
<label className={`text-xs font-bold uppercase ${themeTextMuted}`}>Détails / Accent</label>
<input type="color" value={formData.customColors.accent} onChange={(e) => setFormData({ ...formData, customColors: { ...formData.customColors, accent: e.target.value } })} className="w-8 h-8 rounded shrink-0 cursor-pointer" />
</div>
</div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -236,7 +303,12 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
<h4 className="font-bold text-blue-900">{t('profile.plan_title')} {(user.subscription.planDetails?.displayName || user.subscription.plan).toUpperCase()}</h4> <h4 className="font-bold text-blue-900">{t('profile.plan_title')} {(user.subscription.planDetails?.displayName || user.subscription.plan).toUpperCase()}</h4>
<p className="text-xs text-blue-700">{t('profile.active_sub')}</p> <p className="text-xs text-blue-700">{t('profile.active_sub')}</p>
</div> </div>
<button className="bg-blue-600 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-blue-700 shadow-md shadow-blue-200">{t('profile.manage')}</button> <button
onClick={() => router.push('/pricing')}
className="bg-blue-600 text-white px-4 py-2 rounded-lg text-xs font-bold hover:bg-blue-700 shadow-md shadow-blue-200"
>
{t('profile.manage')}
</button>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
@@ -249,16 +321,67 @@ const UserProfileSettings: React.FC<UserProfileSettingsProps> = ({ user, onUpdat
/> />
</div> </div>
<div className="pt-4"> {/* BYOK Settings */}
{user.subscription.plan === 'byok' && (
<div className={`p-6 rounded-xl border mt-8 ${isCustom ? 'border-theme-border bg-theme-panel' : isDark ? 'border-slate-700 bg-slate-800/50' : isSepia ? 'border-[#dfcdae] bg-[#f4ecd8]/50' : 'border-slate-200 bg-slate-50'}`}>
<h4 className={`font-bold mb-4 flex items-center gap-2 ${themeTextHeading}`}>
<Globe size={18} className="text-blue-500" /> Vos clés API (BYOK)
</h4>
<p className={`text-xs mb-6 ${themeTextMuted}`}>
Vous avez l'abonnement "Clé Perso". Vous pouvez utiliser l'IA en illimité en renseignant votre propre clé API.
</p>
<div className="space-y-4">
<div className="space-y-1">
<label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>Fournisseur IA</label>
<select
value={formData.customApiProvider}
onChange={(e) => setFormData({ ...formData, customApiProvider: e.target.value as any })}
className={`w-full p-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 ${themeInputBg}`}
>
<option value="openai">OpenAI (ChatGPT)</option>
<option value="anthropic">Anthropic (Claude)</option>
<option value="gemini">Google (Gemini)</option>
</select>
</div>
<div className="space-y-1">
<label className={`text-xs font-black uppercase tracking-widest ${themeTextMuted}`}>Clé API ({formData.customApiProvider === 'openai' ? 'sk-proj-...' : formData.customApiProvider === 'anthropic' ? 'sk-ant-...' : 'AIza...'})</label>
<input
type="password"
value={formData.customApiKey}
onChange={(e) => setFormData({ ...formData, customApiKey: e.target.value })}
placeholder="Collez votre clé secrète ici..."
className={`w-full p-3 rounded-xl outline-none focus:ring-2 focus:ring-blue-500 font-mono text-sm ${themeInputBg}`}
/>
</div>
</div>
</div>
)}
{user.subscription.plan !== 'byok' && user.subscription.plan !== 'master' && (
<div className="p-4 bg-indigo-50 border border-indigo-100 rounded-xl mt-8">
<h4 className="font-bold text-indigo-900 text-sm mb-1">Passer à l'abonnement "Clé Perso (BYOK)" ?</h4>
<p className="text-xs text-indigo-700 mb-3">Pour 4,99€/mois, utilisez vos propres clés API ChatGPT, Claude ou Gemini sans limite de tokens.</p>
<button
onClick={() => router.push('/pricing')}
className="bg-white text-indigo-600 px-4 py-2 rounded-lg text-xs font-bold border border-indigo-200 hover:bg-indigo-50 transition w-full"
>
Découvrir le plan BYOK
</button>
</div>
)}
<div className="pt-4 mt-8 pt-8 border-t border-red-100/20">
<button className="text-red-500 text-sm font-bold hover:underline">{t('profile.delete_account')}</button> <button className="text-red-500 text-sm font-bold hover:underline">{t('profile.delete_account')}</button>
</div> </div>
</div> </div>
)} )}
<div className={`mt-12 pt-8 border-t flex justify-end ${isDark ? 'border-slate-700' : isSepia ? 'border-[#dfcdae]' : 'border-slate-100'}`}> <div className={`mt-12 pt-8 border-t flex justify-end ${isCustom ? 'border-theme-border' : isDark ? 'border-slate-700' : isSepia ? 'border-[#dfcdae]' : 'border-slate-100'}`}>
<button <button
onClick={handleSave} onClick={handleSave}
className={`px-8 py-3 rounded-xl font-bold flex items-center gap-2 transition-all shadow-xl hover:shadow-blue-200 ${isDark ? 'bg-white text-slate-900 hover:bg-blue-500 hover:text-white' : isSepia ? 'bg-[#5c4731] text-white hover:bg-blue-600' : 'bg-slate-900 text-white hover:bg-blue-600'}`} className={`px-8 py-3 rounded-xl font-bold flex items-center gap-2 transition-all shadow-xl hover:-translate-y-1 ${isCustom ? 'bg-theme-text text-theme-bg shadow-theme-border' : isDark ? 'bg-white text-slate-900 shadow-slate-900 hover:bg-slate-100' : isSepia ? 'bg-[#5c4731] text-white shadow-[#dfcdae] hover:bg-[#433422]' : 'bg-slate-900 text-white shadow-slate-200 hover:bg-slate-800'}`}
> >
<Save size={18} /> {t('profile.save_changes')} <Save size={18} /> {t('profile.save_changes')}
</button> </button>

View File

@@ -3,7 +3,7 @@
import React, { useState, useMemo, useEffect } from 'react'; import React, { useState, useMemo, useEffect } from 'react';
import { useLanguage } from '@/providers/LanguageProvider'; import { useLanguage } from '@/providers/LanguageProvider';
import { Entity, EntityType, CharacterAttributes, EntityTemplate, CustomFieldDefinition, CustomFieldType } from '@/lib/types'; import { Entity, EntityType, CharacterAttributes, EntityTemplate, CustomFieldDefinition, CustomFieldType } from '@/lib/types';
import { Plus, Trash2, Save, X, Sparkles, User, Activity, Brain, Ruler, Settings, Layout, List, ToggleLeft } from 'lucide-react'; import { Plus, Trash2, Save, X, Sparkles, User, Activity, Brain, Ruler, Settings, Layout, List, ToggleLeft, Camera } from 'lucide-react';
import { ENTITY_ICONS, ENTITY_COLORS, HAIR_COLORS, EYE_COLORS, ARCHETYPES } from '@/lib/constants'; import { ENTITY_ICONS, ENTITY_COLORS, HAIR_COLORS, EYE_COLORS, ARCHETYPES } from '@/lib/constants';
interface WorldBuilderProps { interface WorldBuilderProps {
@@ -32,7 +32,7 @@ const DEFAULT_CHAR_ATTRIBUTES: CharacterAttributes = {
behavioralQuirk: '' behavioralQuirk: ''
}; };
const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdate, onDelete, templates, onUpdateTemplates, initialSelectedId }) => { const WorldBuilder = React.memo(({ entities, onCreate, onUpdate, onDelete, templates, onUpdateTemplates, initialSelectedId }: WorldBuilderProps) => {
const { t } = useLanguage(); const { t } = useLanguage();
const [editingId, setEditingId] = useState<string | null>(null); const [editingId, setEditingId] = useState<string | null>(null);
const [tempEntity, setTempEntity] = useState<Entity | null>(null); const [tempEntity, setTempEntity] = useState<Entity | null>(null);
@@ -146,6 +146,18 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
} }
}; };
const handleAvatarUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file || !tempEntity) return;
const reader = new FileReader();
reader.onload = (event) => {
const base64 = event.target?.result as string;
setTempEntity({ ...tempEntity, avatar: base64 });
};
reader.readAsDataURL(file);
};
// --- TEMPLATE ACTIONS --- // --- TEMPLATE ACTIONS ---
const addCustomField = (type: EntityType) => { const addCustomField = (type: EntityType) => {
@@ -614,8 +626,19 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
className={`p-3 cursor-pointer hover:bg-blue-500/10 transition-colors flex justify-between group ${editingId === entity.id ? 'bg-blue-500/10 border-l-4 border-blue-500' : ''}`} className={`p-3 cursor-pointer hover:bg-blue-500/10 transition-colors flex justify-between group ${editingId === entity.id ? 'bg-blue-500/10 border-l-4 border-blue-500' : ''}`}
> >
<div> <div>
<div className="font-medium text-theme-text">{entity.name}</div> <div className="flex items-center gap-3">
<div className="text-xs text-theme-muted truncate">{entity.description}</div> {entity.avatar ? (
<img src={entity.avatar} alt={entity.name} className="w-8 h-8 rounded-full object-cover border border-theme-border shadow-sm" />
) : (
<div className={`w-8 h-8 rounded-full flex items-center justify-center font-bold text-xs ${ENTITY_COLORS[entity.type]} shadow-sm`}>
{entity.name.substring(0, 2).toUpperCase()}
</div>
)}
<div>
<div className="font-medium text-theme-text">{entity.name}</div>
<div className="text-xs text-theme-muted truncate">{entity.description}</div>
</div>
</div>
</div> </div>
<button <button
onClick={(e) => { e.stopPropagation(); handleDelete(entity.id); }} onClick={(e) => { e.stopPropagation(); handleDelete(entity.id); }}
@@ -651,25 +674,46 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<div> <div className="flex flex-col md:flex-row gap-6">
<label className="block text-sm font-medium text-theme-text mb-1">{t('wb.name')}</label> <div className="flex flex-col items-center gap-2">
<input <label className="relative group cursor-pointer">
type="text" <input type="file" accept="image/*" onChange={handleAvatarUpload} className="hidden" />
value={tempEntity.name} {tempEntity.avatar ? (
onChange={e => setTempEntity({ ...tempEntity, name: e.target.value })} <img src={tempEntity.avatar} alt="Avatar" className="w-24 h-24 rounded-2xl object-cover shadow-md border-2 border-theme-border group-hover:border-indigo-400 transition-all" />
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={t('wb.name_ph')} <div className="w-24 h-24 rounded-2xl bg-slate-100 flex items-center justify-center border-2 border-dashed border-slate-300 group-hover:border-indigo-400 group-hover:bg-indigo-50 transition-all text-slate-400">
/> <Camera size={32} />
</div> </div>
)}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 rounded-2xl flex items-center justify-center transition-opacity">
<Camera size={24} className="text-white" />
</div>
</label>
<span className="text-xs font-semibold text-theme-muted text-center">Avatar / Plan</span>
</div>
<div> <div className="flex-1 space-y-4">
<label className="block text-sm font-medium text-theme-text mb-1">{t('wb.short_desc')}</label> <div>
<textarea <label className="block text-sm font-medium text-theme-text mb-1">{t('wb.name')}</label>
value={tempEntity.description} <input
onChange={e => setTempEntity({ ...tempEntity, description: e.target.value })} type="text"
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" value={tempEntity.name}
placeholder={t('wb.short_desc_ph')} 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={t('wb.name_ph')}
/>
</div>
<div>
<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={t('wb.short_desc_ph')}
/>
</div>
</div>
</div> </div>
{tempEntity.type === EntityType.CHARACTER && renderCharacterEditor()} {tempEntity.type === EntityType.CHARACTER && renderCharacterEditor()}
@@ -721,6 +765,6 @@ const WorldBuilder: React.FC<WorldBuilderProps> = ({ entities, onCreate, onUpdat
</div> </div>
</div> </div>
); );
}; });
export default WorldBuilder; export default WorldBuilder;

View File

@@ -1,8 +1,8 @@
'use client'; 'use client';
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import { BookProject, UserProfile, ViewMode, ChatMessage } from '@/lib/types'; import { BookProject, UserProfile, ViewMode, ChatMessage, Chapter } from '@/lib/types';
import AIPanel from '@/components/AIPanel'; import AIPanel from '@/components/AIPanel';
import { Book, FileText, Globe, GitGraph, Lightbulb, Settings, Menu, ChevronRight, ChevronLeft, Share2, HelpCircle, LogOut, LayoutDashboard, User, Plus, Trash2, Wand2 } from 'lucide-react'; import { Book, FileText, Globe, GitGraph, Lightbulb, Settings, Menu, ChevronRight, ChevronLeft, Share2, HelpCircle, LogOut, LayoutDashboard, User, Plus, Trash2, Wand2 } from 'lucide-react';
import { useLanguage } from '@/providers/LanguageProvider'; import { useLanguage } from '@/providers/LanguageProvider';
@@ -18,6 +18,8 @@ interface EditorShellProps {
onViewModeChange: (mode: ViewMode) => void; onViewModeChange: (mode: ViewMode) => void;
onChapterSelect: (id: string) => void; onChapterSelect: (id: string) => void;
onUpdateProject: (updates: Partial<BookProject>) => void; onUpdateProject: (updates: Partial<BookProject>) => void;
onUpdateChapter?: (chapterId: string, updates: Partial<Chapter>) => void;
onReorderChapters?: (chapters: { id: string, orderIndex: number }[]) => void;
onAddChapter: () => Promise<void>; onAddChapter: () => Promise<void>;
onDeleteChapter: (id: string) => void; onDeleteChapter: (id: string) => void;
onLogout: () => void; onLogout: () => void;
@@ -32,10 +34,28 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
const { project, user, viewMode, currentChapterId, children } = props; const { project, user, viewMode, currentChapterId, children } = props;
const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [isSidebarOpen, setIsSidebarOpen] = useState(true);
const [isAiPanelOpen, setIsAiPanelOpen] = useState(true); const [isAiPanelOpen, setIsAiPanelOpen] = useState(true);
const [draggedChapterIdx, setDraggedChapterIdx] = useState<number | null>(null);
const [dragOverChapterIdx, setDragOverChapterIdx] = useState<number | null>(null);
const { t } = useLanguage(); const { t } = useLanguage();
const currentChapter = project.chapters.find(c => c.id === currentChapterId); const currentChapter = project.chapters.find(c => c.id === currentChapterId);
// Local state for debounced title input
const [localTitle, setLocalTitle] = useState(currentChapter?.title || "");
useEffect(() => {
// Only update local title from props if it differs from what's currently in state,
// to prevent overwriting while the user is typing, but still catch external updates (e.g. from DB load)
if (currentChapter?.title && currentChapter.title !== localTitle) {
setLocalTitle(currentChapter.title);
} else if (!currentChapter) {
setLocalTitle("");
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentChapter?.title, currentChapterId]);
const titleDebounceRef = useRef<NodeJS.Timeout | null>(null);
// Auto-close sidebars on mobile when navigating // Auto-close sidebars on mobile when navigating
useEffect(() => { useEffect(() => {
if (typeof window !== 'undefined' && window.innerWidth < 1024) { if (typeof window !== 'undefined' && window.innerWidth < 1024) {
@@ -81,23 +101,76 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
{t('nav.chapters')} <button onClick={props.onAddChapter} className="hover:text-blue-400"><Plus size={14} /></button> {t('nav.chapters')} <button onClick={props.onAddChapter} className="hover:text-blue-400"><Plus size={14} /></button>
</div> </div>
{project.chapters.map((chap, idx) => ( {project.chapters.map((chap, idx) => (
<div key={chap.id} className="group relative"> <div
key={chap.id}
className={`group relative transition-all duration-200 border-y-2 ${draggedChapterIdx === idx ? 'opacity-20 scale-95 border-transparent' :
dragOverChapterIdx === idx ? (
draggedChapterIdx !== null && draggedChapterIdx > idx
? 'border-t-blue-500 border-b-transparent bg-blue-900/20'
: 'border-b-blue-500 border-t-transparent bg-blue-900/20'
) : 'border-transparent'
}`}
draggable
onDragStart={(e) => {
setDraggedChapterIdx(idx);
e.dataTransfer.effectAllowed = 'move';
}}
onDragOver={(e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
}}
onDragEnter={() => {
if (draggedChapterIdx !== null && draggedChapterIdx !== idx) {
setDragOverChapterIdx(idx);
}
}}
onDragLeave={() => {
// optional edge smoothing here
}}
onDragEnd={() => {
setDraggedChapterIdx(null);
setDragOverChapterIdx(null);
}}
onDrop={(e) => {
e.preventDefault();
if (draggedChapterIdx === null || draggedChapterIdx === idx) return;
const newChapters = [...project.chapters];
const [removed] = newChapters.splice(draggedChapterIdx, 1);
newChapters.splice(idx, 0, removed);
setDraggedChapterIdx(null);
setDragOverChapterIdx(null);
// Re-index chapters to match new visual order
const updatedChapters = newChapters.map((c, i) => ({ id: c.id, orderIndex: i }));
if (props.onReorderChapters) {
props.onReorderChapters(updatedChapters);
} else {
props.onUpdateProject({ chapters: newChapters.map((c, i) => ({ ...c, orderIndex: i })) });
}
}}
>
<button <button
onClick={() => props.onChapterSelect(chap.id)} onClick={() => props.onChapterSelect(chap.id)}
className={`w-full text-left px-4 py-2 text-sm truncate transition-colors ${currentChapterId === chap.id && viewMode === 'write' ? 'bg-blue-900 text-white border-r-2 border-blue-400' : 'hover:bg-slate-800'}`} className={`w-full text-left px-4 py-2 text-sm truncate transition-colors cursor-grab active:cursor-grabbing ${currentChapterId === chap.id && viewMode === 'write' ? 'bg-blue-900 text-white border-r-2 border-blue-400' : 'hover:bg-slate-800'} ${draggedChapterIdx !== null ? 'pointer-events-none' : ''}`}
> >
{idx + 1}. {chap.title} {idx + 1}. {chap.title}
</button> </button>
<button onClick={() => props.onDeleteChapter(chap.id)} className="absolute right-2 top-2 text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100"><Trash2 size={14} /></button> <button onClick={() => props.onDeleteChapter(chap.id)} className="absolute right-2 top-2 text-slate-600 hover:text-red-400 opacity-0 group-hover:opacity-100 bg-slate-900/50 backdrop-blur rounded p-1"><Trash2 size={12} /></button>
</div> </div>
))} ))}
<div className="mt-6 px-4 py-2 text-xs font-semibold text-slate-500 uppercase">{t('nav.tools')}</div> <div className="mt-6 px-4 py-2 text-xs font-semibold text-slate-500 uppercase">{t('nav.tools')}</div>
<button onClick={() => props.onViewModeChange('write')} className={`w-full text-left px-4 py-2 text-sm flex items-center gap-2 ${viewMode === 'write' ? 'bg-blue-900 text-white' : 'hover:bg-slate-800'}`}><FileText size={16} /> {t('sidebar.write')}</button> {React.useMemo(() => (
<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('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('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('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('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> <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>
</>
), [viewMode, t, props])}
</div> </div>
<div className="p-4 border-t border-slate-800"> <div className="p-4 border-t border-slate-800">
@@ -123,8 +196,28 @@ const EditorShell: React.FC<EditorShellProps> = (props) => {
{viewMode === 'write' ? ( {viewMode === 'write' ? (
<input <input
type="text" type="text"
value={currentChapter?.title || ""} value={localTitle}
onChange={(e) => props.onUpdateProject({ chapters: project.chapters.map(c => c.id === currentChapterId ? { ...c, title: e.target.value } : c) })} onChange={(e) => {
const newTitle = e.target.value;
setLocalTitle(newTitle);
if (currentChapterId.startsWith('placeholder-')) return;
// Local optimistic update via project for UI consistency
props.onUpdateProject({
chapters: project.chapters.map(c =>
c.id === currentChapterId ? { ...c, title: newTitle } : c
)
});
// Debounce the actual backend save
if (titleDebounceRef.current) clearTimeout(titleDebounceRef.current);
titleDebounceRef.current = setTimeout(() => {
if (props.onUpdateChapter) {
props.onUpdateChapter(currentChapterId, { title: newTitle });
}
}, 1000);
}}
className="font-serif font-bold text-lg bg-transparent border-b border-transparent focus:border-blue-500 focus:outline-none" className="font-serif font-bold text-lg bg-transparent border-b border-transparent focus:border-blue-500 focus:outline-none"
/> />
) : ( ) : (

View File

@@ -44,6 +44,7 @@ export const useAuth = () => {
preferences: { theme: 'light', dailyWordGoal: dbUser.dailyWordGoal || 500, language: 'fr' }, preferences: { theme: 'light', dailyWordGoal: dbUser.dailyWordGoal || 500, language: 'fr' },
stats: { stats: {
totalWordsWritten: dbUser.totalWords || 0, totalWordsWritten: dbUser.totalWords || 0,
dailyWordCount: dbUser.dailyWordCount || 0,
writingStreak: dbUser.writingStreak || 0, writingStreak: dbUser.writingStreak || 0,
lastWriteDate: dbUser.lastWriteDate ? new Date(dbUser.lastWriteDate).getTime() : 0, lastWriteDate: dbUser.lastWriteDate ? new Date(dbUser.lastWriteDate).getTime() : 0,
}, },
@@ -59,7 +60,7 @@ export const useAuth = () => {
subscription: { plan: 'free', startDate: Date.now(), status: 'active' }, subscription: { plan: 'free', startDate: Date.now(), status: 'active' },
usage: { aiActionsCurrent: 0, aiActionsLimit: 100, projectsLimit: 3 }, usage: { aiActionsCurrent: 0, aiActionsLimit: 100, projectsLimit: 3 },
preferences: { theme: 'light', dailyWordGoal: 500, language: 'fr' }, preferences: { theme: 'light', dailyWordGoal: 500, language: 'fr' },
stats: { totalWordsWritten: 0, writingStreak: 0, lastWriteDate: 0 }, stats: { totalWordsWritten: 0, dailyWordCount: 0, writingStreak: 0, lastWriteDate: 0 },
}); });
}); });
} else if (status === 'unauthenticated') { } else if (status === 'unauthenticated') {
@@ -133,5 +134,50 @@ export const useAuth = () => {
} }
}, [user]); }, [user]);
return { user, login, signup, logout, incrementUsage, updateProfile, loading }; const refreshProfile = useCallback(async () => {
if (!session?.user?.id) return;
try {
const res = await fetch('/api/user/profile', { cache: 'no-store' });
const dbUser = await res.json();
const planId = dbUser.plan || 'free';
const planDetails = dbUser.planDetails || {
id: 'free',
displayName: 'Gratuit',
maxAiActions: 100,
maxProjects: 3
};
setUser({
id: dbUser.id,
email: dbUser.email,
name: dbUser.name || 'User',
avatar: dbUser.avatar,
bio: dbUser.bio,
subscription: {
plan: planId,
planDetails: planDetails,
startDate: new Date(dbUser.createdAt).getTime(),
status: 'active'
},
usage: {
aiActionsCurrent: dbUser.aiActionsUsed || 0,
aiActionsLimit: planDetails.maxAiActions,
projectsLimit: planDetails.maxProjects,
},
preferences: { theme: 'light', dailyWordGoal: dbUser.dailyWordGoal || 500, language: 'fr' },
stats: {
totalWordsWritten: dbUser.totalWords || 0,
dailyWordCount: dbUser.dailyWordCount || 0,
writingStreak: dbUser.writingStreak || 0,
lastWriteDate: dbUser.lastWriteDate ? new Date(dbUser.lastWriteDate).getTime() : 0,
},
});
} catch (err) {
console.error('Failed to refresh user profile:', err);
}
}, [session]);
return { user, login, signup, logout, incrementUsage, updateProfile, refreshProfile, loading };
}; };

View File

@@ -7,6 +7,7 @@ import {
Entity, Entity,
EntityType, EntityType,
UserProfile, UserProfile,
Idea
} from '@/lib/types'; } from '@/lib/types';
import api from '@/lib/api'; import api from '@/lib/api';
import { import {
@@ -88,6 +89,7 @@ export const useProjects = (user: UserProfile | null) => {
id: e.id, id: e.id,
type: e.type, type: e.type,
name: e.name, name: e.name,
avatar: e.avatar,
description: e.description, description: e.description,
details: e.details, details: e.details,
storyContext: e.storyContext, storyContext: e.storyContext,
@@ -212,6 +214,8 @@ export const useProjects = (user: UserProfile | null) => {
}; };
const updateChapter = async (projectId: string, chapterId: string, data: Partial<Chapter>) => { const updateChapter = async (projectId: string, chapterId: string, data: Partial<Chapter>) => {
if (chapterId.startsWith('placeholder-')) return;
setProjects(prev => prev.map(p => { setProjects(prev => prev.map(p => {
if (p.id !== projectId) return p; if (p.id !== projectId) return p;
return { return {
@@ -227,12 +231,41 @@ export const useProjects = (user: UserProfile | null) => {
} }
}; };
const reorderChapters = async (projectId: string, chaptersToReorder: { id: string, orderIndex: number }[]) => {
// Optimistic UI update
setProjects(prev => prev.map(p => {
if (p.id !== projectId) return p;
// Map the new order indexes to the existing chapters
const orderMap = new Map(chaptersToReorder.map(c => [c.id, c.orderIndex]));
const updatedChapters = p.chapters.map(c => {
if (orderMap.has(c.id)) {
return { ...c, orderIndex: orderMap.get(c.id)! };
}
return c;
});
// Sort them immediately so the UI doesn't jump on next render
updatedChapters.sort((a, b) => (a.orderIndex || 0) - (b.orderIndex || 0));
return { ...p, chapters: updatedChapters };
}));
try {
await api.chapters.reorder(projectId, chaptersToReorder);
} catch (err) {
console.error("Failed to reorder chapters", err);
// In a real app we might revert the state here on failure
}
};
const createEntity = async (projectId: string, type: EntityType, initialData?: Partial<Entity>) => { const createEntity = async (projectId: string, type: EntityType, initialData?: Partial<Entity>) => {
try { try {
const newEntity = await api.entities.create({ const newEntity = await api.entities.create({
projectId, projectId,
type, type,
name: initialData?.name || `Nouveau ${type}`, name: initialData?.name || `Nouveau ${type}`,
avatar: initialData?.avatar || undefined,
description: initialData?.description || '', description: initialData?.description || '',
details: initialData?.details || '', details: initialData?.details || '',
attributes: initialData?.attributes || undefined, attributes: initialData?.attributes || undefined,
@@ -247,6 +280,7 @@ export const useProjects = (user: UserProfile | null) => {
id: newEntity.id, id: newEntity.id,
type: newEntity.type, type: newEntity.type,
name: newEntity.name, name: newEntity.name,
avatar: newEntity.avatar,
description: newEntity.description, description: newEntity.description,
details: newEntity.details, details: newEntity.details,
attributes: newEntity.attributes, attributes: newEntity.attributes,
@@ -293,6 +327,67 @@ export const useProjects = (user: UserProfile | null) => {
} }
}; };
const createIdea = async (projectId: string, data: Partial<Idea>) => {
try {
const newIdea = await api.ideas.create({
projectId,
title: data.title || 'Nouveau',
description: data.description || '',
status: data.status || 'todo',
category: data.category || 'plot',
});
setProjects(prev => prev.map(p => {
if (p.id !== projectId) return p;
return {
...p,
ideas: [...(p.ideas || []), {
...newIdea,
createdAt: new Date(newIdea.createdAt).getTime()
}]
};
}));
return newIdea.id;
} catch (err) {
console.error("Failed to create idea", err);
throw err;
}
};
const updateIdea = async (projectId: string, ideaId: string, data: Partial<Idea>) => {
setProjects(prev => prev.map(p => {
if (p.id !== projectId) return p;
return {
...p,
ideas: (p.ideas || []).map(i => i.id === ideaId ? { ...i, ...data } : i)
};
}));
try {
await api.ideas.update(ideaId, data);
} catch (err) {
console.error("Failed to update idea", err);
throw err;
}
};
const deleteIdea = async (projectId: string, ideaId: string) => {
setProjects(prev => prev.map(p => {
if (p.id !== projectId) return p;
return {
...p,
ideas: (p.ideas || []).filter(i => i.id !== ideaId)
};
}));
try {
await api.ideas.delete(ideaId);
} catch (err) {
console.error("Failed to delete idea", err);
throw err;
}
};
return { return {
projects, projects,
currentProjectId, currentProjectId,
@@ -301,9 +396,13 @@ export const useProjects = (user: UserProfile | null) => {
updateProject, updateProject,
addChapter, addChapter,
updateChapter, updateChapter,
reorderChapters,
createEntity, createEntity,
updateEntity, updateEntity,
deleteEntity, deleteEntity,
createIdea,
updateIdea,
deleteIdea,
deleteProject: async (projectId: string) => { deleteProject: async (projectId: string) => {
try { try {
// Cascade delete is handled by Prisma, just delete the project // Cascade delete is handled by Prisma, just delete the project

View File

@@ -113,11 +113,18 @@ const api = {
method: 'DELETE', method: 'DELETE',
}); });
}, },
async reorder(projectId: string, chapters: { id: string, orderIndex: number }[]) {
return api.request(`/projects/${projectId}/chapters/reorder`, {
method: 'PUT',
body: JSON.stringify({ chapters }),
});
},
}, },
// --- ENTITIES --- // --- ENTITIES ---
entities: { entities: {
async create(data: { projectId: string; type: string; name?: string; description?: string; details?: string; attributes?: any; customValues?: any }) { async create(data: { projectId: string; type: string; name?: string; avatar?: string; description?: string; details?: string; attributes?: any; customValues?: any }) {
return api.request<any>('/entities', { return api.request<any>('/entities', {
method: 'POST', method: 'POST',
body: JSON.stringify(data), body: JSON.stringify(data),

View File

@@ -2,6 +2,8 @@
// This file is only imported by API routes, never by client code // This file is only imported by API routes, never by client code
import { GoogleGenAI, Type } from "@google/genai"; import { GoogleGenAI, Type } from "@google/genai";
import OpenAI from "openai";
import Anthropic from "@anthropic-ai/sdk";
import { BookProject, UserProfile } from "./types"; import { BookProject, UserProfile } from "./types";
const truncate = (str: string, length: number) => { const truncate = (str: string, length: number) => {
@@ -10,7 +12,8 @@ const truncate = (str: string, length: number) => {
}; };
const checkUsage = (user: UserProfile) => { const checkUsage = (user: UserProfile) => {
if (user.subscription.plan === 'master') return true; // BYOK and Master plans have unlimited AI actions
if (user.subscription.plan === 'master' || user.subscription.plan === 'byok') return true;
return user.usage.aiActionsCurrent < user.usage.aiActionsLimit; return user.usage.aiActionsCurrent < user.usage.aiActionsLimit;
}; };
@@ -86,37 +89,77 @@ export const generateStoryContent = async (
} }
try { try {
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const finalPrompt = buildContextPrompt(project, currentChapterId, userPrompt); const finalPrompt = buildContextPrompt(project, currentChapterId, userPrompt);
const modelName = user.subscription.plan === 'master' ? 'gemini-3-pro-preview' : 'gemini-3-flash-preview'; // --- BYOK: Bring Your Own Key Logic ---
const isBYOK = user.subscription.plan === 'byok' && user.customApiKey;
const provider = isBYOK ? (user.customApiProvider || 'openai') : 'gemini';
const response = await ai.models.generateContent({ if (provider === 'openai' && isBYOK) {
model: modelName, const openai = new OpenAI({ apiKey: user.customApiKey });
contents: finalPrompt, const completion = await openai.chat.completions.create({
config: { model: "gpt-4o-mini",
messages: [{ role: "user", content: finalPrompt }],
response_format: { type: "json_object" },
temperature: 0.7, temperature: 0.7,
responseMimeType: "application/json", });
responseSchema: { const result = JSON.parse(completion.choices[0].message.content || "{}");
type: Type.OBJECT, return {
properties: { text: result.content || "Erreur de génération.",
responseType: { type: result.responseType || "reflection"
type: Type.STRING, };
enum: ["draft", "reflection"] }
}, else if (provider === 'anthropic' && isBYOK) {
content: { const anthropic = new Anthropic({ apiKey: user.customApiKey });
type: Type.STRING // Anthropic doesn't have strict JSON mode matching OpenAI's structure natively at this tier,
// so we prompt it to return JSON.
const systemPrompt = "You must respond in valid JSON format with exact keys: `responseType` (either 'draft' or 'reflection') and `content` (string).";
const message = await anthropic.messages.create({
model: "claude-3-haiku-20240320",
max_tokens: 4000,
system: systemPrompt,
messages: [{ role: "user", content: finalPrompt }],
});
const textContent = message.content[0].type === 'text' ? message.content[0].text : '{}';
const result = JSON.parse(textContent);
return {
text: result.content || "Erreur de génération.",
type: result.responseType || "reflection"
};
}
else {
// Default: Pluume Gemini implementation
const ai = new GoogleGenAI({ apiKey: isBYOK && user.customApiProvider === 'gemini' ? user.customApiKey : process.env.GEMINI_API_KEY });
const modelName = user.subscription.plan === 'master' ? 'gemini-3-pro-preview' : 'gemini-3-flash-preview';
const response = await ai.models.generateContent({
model: modelName,
contents: finalPrompt,
config: {
temperature: 0.7,
responseMimeType: "application/json",
responseSchema: {
type: Type.OBJECT,
properties: {
responseType: {
type: Type.STRING,
enum: ["draft", "reflection"]
},
content: {
type: Type.STRING
}
} }
} }
} }
} });
});
const result = JSON.parse(response.text || "{}");
return {
text: result.content || "Erreur de génération.",
type: result.responseType || "reflection"
};
}
const result = JSON.parse(response.text || "{}");
return {
text: result.content || "Erreur de génération.",
type: result.responseType || "reflection"
};
} catch (error) { } catch (error) {
console.error("AI Generation Error:", error); console.error("AI Generation Error:", error);
return { text: "Erreur lors de la communication avec l'IA.", type: 'reflection' }; return { text: "Erreur lors de la communication avec l'IA.", type: 'reflection' };
@@ -130,11 +173,35 @@ export const transformTextServer = async (
user: UserProfile, user: UserProfile,
): Promise<string> => { ): Promise<string> => {
if (!checkUsage(user)) return "Limite d'actions IA atteinte."; if (!checkUsage(user)) return "Limite d'actions IA atteinte.";
try { try {
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }); const prompt = `Action: ${mode}. Texte: ${text}. Contexte: ${truncate(context, 1000)}. Renvoie juste le texte transformé sans commentaires.`;
const prompt = `Action: ${mode}. Texte: ${text}. Contexte: ${truncate(context, 1000)}. Renvoie juste le texte transformé.`;
const response = await ai.models.generateContent({ model: 'gemini-3-flash-preview', contents: prompt }); const isBYOK = user.subscription.plan === 'byok' && user.customApiKey;
return response.text?.trim() || text; const provider = isBYOK ? (user.customApiProvider || 'openai') : 'gemini';
if (provider === 'openai' && isBYOK) {
const openai = new OpenAI({ apiKey: user.customApiKey });
const completion = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
});
return completion.choices[0].message.content || text;
}
else if (provider === 'anthropic' && isBYOK) {
const anthropic = new Anthropic({ apiKey: user.customApiKey });
const message = await anthropic.messages.create({
model: "claude-3-haiku-20240320",
max_tokens: 4000,
messages: [{ role: "user", content: prompt }],
});
return message.content[0].type === 'text' ? message.content[0].text : text;
}
else {
const ai = new GoogleGenAI({ apiKey: isBYOK && user.customApiProvider === 'gemini' ? user.customApiKey : process.env.GEMINI_API_KEY });
const response = await ai.models.generateContent({ model: 'gemini-3-flash-preview', contents: prompt });
return response.text?.trim() || text;
}
} catch { } catch {
return text; return text;
} }

View File

@@ -94,6 +94,8 @@ export const translations = {
'book_settings.confirm_delete': 'Oui, supprimer définitivement', 'book_settings.confirm_delete': 'Oui, supprimer définitivement',
'book_settings.cancel': 'Annuler', 'book_settings.cancel': 'Annuler',
'book_settings.delete_button': 'Supprimer ce projet', 'book_settings.delete_button': 'Supprimer ce projet',
'book_settings.save': 'Sauvegarder',
'book_settings.saved': 'Sauvegardé',
// Landing Page // Landing Page
'landing.nav_features': 'Fonctionnalités', 'landing.nav_features': 'Fonctionnalités',
@@ -162,6 +164,8 @@ export const translations = {
'auth.signup_link': "S'inscrire", 'auth.signup_link': "S'inscrire",
'auth.signin_link': 'Se connecter', 'auth.signin_link': 'Se connecter',
'auth.back_to_site': '← Revenir au site', 'auth.back_to_site': '← Revenir au site',
'auth.accept_cgu': "J'ai lu et j'accepte les ",
'auth.cgu_link': "Conditions Générales d'Utilisation",
'auth.hero_title_part1': "L'endroit où vos", 'auth.hero_title_part1': "L'endroit où vos",
'auth.hero_title_part2': "histoires", 'auth.hero_title_part2': "histoires",
'auth.hero_title_part3': "prennent vie.", 'auth.hero_title_part3': "prennent vie.",
@@ -323,11 +327,112 @@ export const translations = {
'sw.save_color': '+ SAUVER', 'sw.save_color': '+ SAUVER',
// Genre, Tense Constants Translation Setup // Genre, Tense Constants Translation Setup
'pov_options.première_personne': 'Première Personne', 'pov_options.1ère_personne_(je)': '1ère personne (Je)',
'pov_options.troisième_personne_limitée': 'Troisième Personne Limitée', 'pov_options.3ème_personne_(limitée_au_protagoniste)': '3ème personne (Limitée au protagoniste)',
'pov_options.troisième_personne_omnisciente': 'Troisième Personne Omnisciente', 'pov_options.3ème_personne_(omnisciente)': '3ème personne (Omnisciente)',
'tense_options.présent': 'Présent', 'pov_options.multi-points_de_vue_(alterné)': 'Multi-points de vue (Alterné)',
'tense_options.passé': 'Passé', 'tense_options.passé_(passé_simple_/_imparfait)': 'Passé (Passé simple / Imparfait)',
'tense_options.présent_de_narration': 'Présent de narration',
// CGU Page
'cgu.title': "Conditions Générales d'Utilisation (CGU)",
'cgu.version': "Version en vigueur au : 06/03/2026",
'cgu.preamble_title': "Préambule",
'cgu.preamble_text1': "La présente plateforme (ci-après \"la Plateforme\"), accessible à l'adresse https://pluu.me, est éditée par Pluume. La Plateforme propose un service de génération de contenus textuels assisté par l'intelligence artificielle (technologie Google Gemini) destiné à la création d'ebooks.",
'cgu.preamble_text2': "Les présentes Conditions Générales d'Utilisation (CGU) ont pour objet de définir les règles d'accès et d'utilisation du service. Tout accès ou utilisation de la Plateforme suppose l'acceptation sans réserve de l'intégralité des présentes conditions.",
'cgu.art1_title': "Article 1 : Définitions",
'cgu.art1_user': "Utilisateur : Toute personne physique ou morale accédant à la Plateforme.",
'cgu.art1_service': "Service : Ensemble des outils de génération de texte, de structuration et d'exportation d'ebooks mis à disposition.",
'cgu.art1_content': "Contenu Généré : Textes, plans ou documents produits par l'IA suite aux instructions de l'Utilisateur.",
'cgu.art1_prompt': "Prompt : Instructions textuelles saisies par l'Utilisateur pour diriger l'IA.",
'cgu.art2_title': "Article 2 : Accès et Inscription",
'cgu.art2_text': "L'accès à la Plateforme est réservé aux personnes majeures. Pour utiliser les services de génération, l'Utilisateur doit créer un compte. Il est responsable de la confidentialité de ses identifiants.",
'cgu.art3_title': "Article 3 : Conditions Financières (Abonnements et Crédits)",
'cgu.art3_1_title': "3.1 Tarifs",
'cgu.art3_1_text': "L'accès aux fonctionnalités de génération est soumis à une tarification consultable sur la page Tarifs. Les prix sont exprimés en Euros TTC.",
'cgu.art3_1_sub1': "Abonnements : Prélèvement périodique automatique.",
'cgu.art3_1_sub2': "Packs de crédits : Achat ponctuel pour un nombre défini de générations.",
'cgu.art3_2_title': "3.2 Paiement et Sécurité",
'cgu.art3_2_text': "Les paiements sont traités par un prestataire de paiement sécurisé. La Plateforme ne stocke aucune coordonnée bancaire.",
'cgu.art4_title': "Article 4 : Obligations de l'Utilisateur et Sécurité (Clause Stricte)",
'cgu.art4_text': "L'Utilisateur s'engage à utiliser le Service de manière licite.",
'cgu.art4_1_title': "4.1 Contenus Interdits",
'cgu.art4_1_text': "Il est formellement interdit d'utiliser la Plateforme pour générer des contenus :",
'cgu.art4_1_item1': "Pédopornographiques (CSAM) : Toute tentative de génération, requête ou diffusion de contenu lié à l'exploitation sexuelle des mineurs fera l'objet d'un bannissement immédiat sans préavis ni remboursement. Conformément à la loi, la Plateforme procédera à un signalement systématique auprès des autorités compétentes.",
'cgu.art4_1_item2': "Haineux et Discriminatoires : Incitation à la violence, au racisme, à l'antisémitisme, à l'homophobie ou toute forme de discrimination.",
'cgu.art4_1_item3': "Illégaux : Apologie de crimes, terrorisme, ou violation de droits de propriété intellectuelle tiers.",
'cgu.art4_2_title': "4.2 Surveillance et Modération",
'cgu.art4_2_text': "La Plateforme utilise des algorithmes de filtrage en temps réel. En cas de violation répétée ou grave de cet article, la Plateforme se réserve le droit de suspendre le compte de l'Utilisateur de plein droit.",
'cgu.art5_title': "Article 5 : Propriété Intellectuelle",
'cgu.art5_1_title': "5.1 Droits de l'Utilisateur",
'cgu.art5_1_text': "La Plateforme concède à l'Utilisateur, sous réserve du paiement des frais éventuels, la pleine propriété des droits d'exploitation sur les ebooks générés.",
'cgu.art5_1_note': "Note sur l'IA : L'Utilisateur est informé que la protection par le droit d'auteur des œuvres générées par IA peut varier selon les législations nationales et nécessite souvent une intervention créative humaine significative de la part de l'Utilisateur.",
'cgu.art5_2_title': "5.2 Droits de la Plateforme",
'cgu.art5_2_text': "L'interface, les algorithmes de connexion à l'API Gemini et l'identité visuelle du site restent la propriété exclusive de l'Éditeur.",
'cgu.art6_title': "Article 6 : Politique de \"Zéro Stockage\" et Confidentialité",
'cgu.art6_1_title': "6.1 Traitement Éphémère",
'cgu.art6_1_text1': "La Plateforme applique une politique stricte de confidentialité. Aucune donnée de création (prompts, textes générés, ebooks en cours) n'est sauvegardée sur nos serveurs de manière permanente.",
'cgu.art6_1_text2': "Les données ne sont conservées que le temps de la session de travail pour permettre l'affichage et l'exportation.",
'cgu.art6_1_text3': "Quoi qu'il arrive, si l'Utilisateur décide de supprimer ses informations, son projet ou ferme sa session, toutes les données associées sont immédiatement et irréversiblement supprimées de nos bases de données.",
'cgu.art6_2_title': "6.2 Responsabilité de Sauvegarde",
'cgu.art6_2_text': "En raison de cette politique de non-conservation, il appartient exclusivement à l'Utilisateur de télécharger et de sauvegarder ses travaux (format PDF, EPUB, etc.) avant la clôture de sa session. La Plateforme ne pourra être tenue responsable d'une perte de données consécutive à une déconnexion ou une suppression volontaire.",
'cgu.art7_title': "Article 7 : Limitation de Responsabilité",
'cgu.art7_1_title': "7.1 Qualité du Contenu",
'cgu.art7_1_text': "Le Service utilise la technologie Google Gemini. L'Utilisateur accepte que l'IA puisse produire des informations inexactes, incomplètes ou biaisées (\"hallucinations\"). La Plateforme ne saurait être tenue responsable du contenu des ebooks ou de l'utilisation qui en est faite.",
'cgu.art7_2_title': "7.2 Disponibilité Technique",
'cgu.art7_2_text': "La Plateforme ne peut garantir une disponibilité ininterrompue du service, celle-ci dépendant de prestataires tiers (hébergement et API Google).",
'cgu.art8_title': "Article 8 : Protection des Données Personnelles (RGPD)",
'cgu.art8_text': "Les seules données conservées sont celles strictement nécessaires à la gestion du compte (email, facturation). L'Utilisateur dispose d'un droit d'accès, de rectification et de suppression totale de ses données personnelles.",
'cgu.art9_title': "Article 9 : Modification et Résiliation",
'cgu.art9_1_title': "9.1 Modification des CGU",
'cgu.art9_1_text': "La Plateforme se réserve le droit de modifier les présentes CGU à tout moment. L'Utilisateur sera informé de toute modification substantielle.",
'cgu.art9_2_title': "9.2 Résiliation",
'cgu.art9_2_text': "L'Utilisateur peut supprimer son compte à tout moment. Cette action entraîne la suppression immédiate de toutes ses données, conformément à l'Article 6.",
'cgu.art10_title': "Article 10 : Loi Applicable et Juridiction",
'cgu.art10_text': "Les présentes CGU sont soumises au droit français. Tout litige relatif à leur interprétation ou exécution relève de la compétence exclusive des tribunaux compétents.",
// CGV Page
'cgv.title': "Conditions Générales de Vente (CGV)",
'cgv.version': "Version en vigueur au : 06/03/2026",
'cgv.art1_title': "Article 1 : Objet et Champ d'Application",
'cgv.art1_text1': "Les présentes Conditions Générales de Vente (CGV) s'appliquent, sans restriction ni réserve, à tout achat des services de génération d'ebooks (ci-après \"les Services\") proposés par Pluume sur la plateforme https://pluu.me.",
'cgv.art1_text2': "Ces CGV encadrent les modalités de commande, de paiement, de livraison des crédits/abonnements et de gestion des éventuels litiges entre le Vendeur et l'Utilisateur (ci-après \"le Client\").",
'cgv.art2_title': "Article 2 : Caractéristiques des Services",
'cgv.art2_text': "Le Vendeur propose des solutions de création d'ebooks assistées par l'intelligence artificielle Google Gemini. Les offres sont divisées en deux catégories :",
'cgv.art2_item1': "Packs de Crédits : Achat ponctuel d'un volume défini de générations de contenu.",
'cgv.art2_item2': "Abonnements : Accès illimité ou plafonné aux services pour une durée déterminée (mensuelle ou annuelle) avec renouvellement automatique.",
'cgv.art3_title': "Article 3 : Commande et Validation",
'cgv.art3_text': "Le Client sélectionne l'offre de son choix sur la Plateforme. La commande est considérée comme définitive dès la validation du paiement par le prestataire tiers. Un e-mail de confirmation est envoyé au Client à l'adresse renseignée lors de la création du compte.",
'cgv.art4_title': "Article 4 : Conditions Financières",
'cgv.art4_1_title': "4.1 Tarifs",
'cgv.art4_1_text': "Les prix sont indiqués en Euros et s'entendent toutes taxes comprises (TTC). Le Vendeur se réserve le droit de modifier ses tarifs à tout moment, mais les services seront facturés sur la base des tarifs en vigueur au moment de l'enregistrement de la commande.",
'cgv.art4_2_title': "4.2 Modalités de paiement (Stripe)",
'cgv.art4_2_text': "Le paiement s'effectue exclusivement par carte bancaire via le système de paiement sécurisé Stripe.",
'cgv.art4_2_item1': "Le Vendeur n'a jamais accès aux coordonnées bancaires du Client.",
'cgv.art4_2_item2': "Stripe garantit la confidentialité et la sécurité des transactions grâce au protocole SSL et à la conformité PCI-DSS.",
'cgv.art5_title': "Article 5 : Gestion des Abonnements",
'cgv.art5_1_title': "5.1 Renouvellement automatique",
'cgv.art5_1_text': "Tout abonnement souscrit est à tacite reconduction. Le Client autorise Stripe à prélever le montant de l'abonnement à chaque échéance (mois ou année) sur la carte bancaire utilisée lors de l'achat initial.",
'cgv.art5_2_title': "5.2 Résiliation",
'cgv.art5_2_text': "Le Client peut résilier son abonnement à tout moment et sans frais depuis son espace personnel.",
'cgv.art5_2_item1': "La résiliation interrompt le renouvellement automatique mais laisse l'accès aux services actif jusqu'à la fin de la période en cours.",
'cgv.art5_2_item2': "Aucun remboursement pro rata n'est effectué pour la période restant à courir entre la date de résiliation et la fin de l'échéance.",
'cgv.art6_title': "Article 6 : Droit de Rétractation (Produits Numériques)",
'cgv.art6_text': "Conformément à l'article L221-28 du Code de la consommation, le droit de rétractation de 14 jours ne s'applique pas aux contrats de fourniture de contenu numérique non fourni sur un support matériel dont l'exécution a commencé après accord préalable exprès du consommateur.",
'cgv.art6_warning': "En procédant au paiement et en utilisant son premier crédit de génération, le Client accepte expressément l'exécution immédiate du service et renonce à son droit de rétractation. Aucun remboursement ne sera accordé une fois le service consommé.",
'cgv.art7_title': "Article 7 : Livraison et Accès",
'cgv.art7_text': "Les Services sont réputés \"livrés\" dès que les crédits sont ajoutés au compte du Client ou que l'accès premium est débloqué. En cas de problème technique empêchant l'accès immédiat, le Client doit contacter le support.",
'cgv.art8_title': "Article 8 : Garanties et Responsabilité Commerciale",
'cgv.art8_1_title': "8.1 Qualité de l'IA",
'cgv.art8_1_text': "Le Vendeur vend un accès à un outil de génération. Il ne garantit pas la qualité littéraire, l'exactitude des faits ou l'originalité absolue du texte produit par l'IA. Par conséquent, aucune demande de remboursement ne pourra être motivée par une \"déception\" quant au style de l'IA.",
'cgv.art8_2_title': "8.2 Continuité de service",
'cgv.art8_2_text': "Le Vendeur s'engage à faire ses meilleurs efforts pour maintenir l'accès au service, mais ne pourra être tenu responsable des pannes dues aux prestataires tiers (Stripe, API Google Gemini).",
'cgv.art9_title': "Article 9 : Politique de Suppression des Données",
'cgv.art9_text': "Comme stipulé dans les CGU, le Vendeur applique une politique de zéro stockage.",
'cgv.art9_item1': "Si le Client supprime ses informations ou son compte, toutes les données de facturation sont archivées uniquement pour la durée légale fiscale, mais les contenus créés sont supprimés irréversiblement.",
'cgv.art9_item2': "Le Client ne pourra prétendre à un dédommagement en cas de perte de données suite à une suppression volontaire.",
'cgv.art10_title': "Article 10 : Litiges et Loi Applicable",
'cgv.art10_text': "Les présentes CGV sont soumises au droit français. En cas de litige, une solution amiable sera recherchée avant toute action judiciaire. À défaut d'accord, compétence exclusive est attribuée aux tribunaux compétents.",
}, },
en: { en: {
// General Navigation // General Navigation
@@ -422,6 +527,8 @@ export const translations = {
'book_settings.confirm_delete': 'Yes, delete permanently', 'book_settings.confirm_delete': 'Yes, delete permanently',
'book_settings.cancel': 'Cancel', 'book_settings.cancel': 'Cancel',
'book_settings.delete_button': 'Delete this project', 'book_settings.delete_button': 'Delete this project',
'book_settings.save': 'Save',
'book_settings.saved': 'Saved',
// Landing Page // Landing Page
'landing.nav_features': 'Features', 'landing.nav_features': 'Features',
@@ -490,6 +597,8 @@ export const translations = {
'auth.signup_link': 'Sign up', 'auth.signup_link': 'Sign up',
'auth.signin_link': 'Log in', 'auth.signin_link': 'Log in',
'auth.back_to_site': '← Back to site', 'auth.back_to_site': '← Back to site',
'auth.accept_cgu': "I have read and agree to the ",
'auth.cgu_link': "Terms of Service",
'auth.hero_title_part1': "The place where your", 'auth.hero_title_part1': "The place where your",
'auth.hero_title_part2': "stories", 'auth.hero_title_part2': "stories",
'auth.hero_title_part3': "come to life.", 'auth.hero_title_part3': "come to life.",
@@ -651,11 +760,110 @@ export const translations = {
'sw.save_color': '+ SAVE', 'sw.save_color': '+ SAVE',
// Genre, Tense Constants Translation Setup // Genre, Tense Constants Translation Setup
'pov_options.première_personne': 'First Person', 'pov_options.1ère_personne_(je)': '1st Person (I)',
'pov_options.troisième_personne_limitée': 'Third Person Limited', 'pov_options.3ème_personne_(limitée_au_protagoniste)': '3rd Person (Limited)',
'pov_options.troisième_personne_omnisciente': 'Third Person Omniscient', 'pov_options.3ème_personne_(omnisciente)': '3rd Person (Omniscient)',
'tense_options.présent': 'Present', 'pov_options.multi-points_de_vue_(alterné)': 'Multi-POV (Alternating)',
'tense_options.passé': 'Past', 'tense_options.passé_(passé_simple_/_imparfait)': 'Past Tense',
'tense_options.présent_de_narration': 'Present Tense',
// CGU Page
'cgu.title': "Terms of Service (ToS)",
'cgu.version': "Effective as of: 2026-03-06",
'cgu.preamble_title': "Preamble",
'cgu.preamble_text1': "This platform (hereinafter \"the Platform\"), accessible at https://pluu.me, is published by Pluume. The Platform offers a text generation service assisted by artificial intelligence (Google Gemini technology) intended for creating ebooks.",
'cgu.preamble_text2': "These Terms of Service (ToS) aim to define the rules for access and use of the service. Any access or use of the Platform implies unreserved acceptance of all these conditions.",
'cgu.art1_title': "Article 1: Definitions",
'cgu.art1_user': "User: Any natural or legal person accessing the Platform.",
'cgu.art1_service': "Service: All tools for text generation, structuring, and ebook exportation provided.",
'cgu.art1_content': "Generated Content: Texts, plans, or documents produced by the AI following User instructions.",
'cgu.art1_prompt': "Prompt: Textual instructions entered by the User to direct the AI.",
'cgu.art2_title': "Article 2: Access and Registration",
'cgu.art2_text': "Access to the Platform is reserved for adults. To use the generation services, the User must create an account. They are responsible for the confidentiality of their credentials.",
'cgu.art3_title': "Article 3: Financial Conditions (Subscriptions and Credits)",
'cgu.art3_1_title': "3.1 Rates",
'cgu.art3_1_text': "Access to generation features is subject to pricing available on the Pricing page. Prices are expressed in Euros including all taxes.",
'cgu.art3_1_sub1': "Subscriptions: Automatic periodic withdrawal.",
'cgu.art3_1_sub2': "Credit Packs: One-time purchase for a defined number of generations.",
'cgu.art3_2_title': "3.2 Payment and Security",
'cgu.art3_2_text': "Payments are processed by a secure payment provider. The Platform does not store any bank details.",
'cgu.art4_title': "Article 4: User Obligations and Security (Strict Clause)",
'cgu.art4_text': "The User agrees to use the Service lawfully.",
'cgu.art4_1_title': "4.1 Prohibited Content",
'cgu.art4_1_text': "It is strictly forbidden to use the Platform to generate content that is:",
'cgu.art4_1_item1': "Child Sexual Abuse Material (CSAM): Any attempt to generate, request, or disseminate content related to the sexual exploitation of minors will result in an immediate ban without notice or refund. In accordance with the law, the Platform will proceed with a systematic report to the competent authorities.",
'cgu.art4_1_item2': "Hateful and Discriminatory: Incitement to violence, racism, anti-Semitism, homophobia, or any form of discrimination.",
'cgu.art4_1_item3': "Illegal: Apology for crimes, terrorism, or violation of third-party intellectual property rights.",
'cgu.art4_2_title': "4.2 Monitoring and Moderation",
'cgu.art4_2_text': "The Platform uses real-time filtering algorithms. In case of repeated or serious violation of this article, the Platform reserves the right to suspend the User's account as of right.",
'cgu.art5_title': "Article 5: Intellectual Property",
'cgu.art5_1_title': "5.1 User Rights",
'cgu.art5_1_text': "The Platform grants the User, subject to payment of any fees, full ownership of the exploitation rights to the generated ebooks.",
'cgu.art5_1_note': "Note on AI: The User is informed that copyright protection for AI-generated works may vary by national legislation and often requires significant creative human intervention from the User.",
'cgu.art5_2_title': "5.2 Platform Rights",
'cgu.art5_2_text': "The interface, connection algorithms to the Gemini API, and the visual identity of the site remain the exclusive property of the Publisher.",
'cgu.art6_title': "Article 6: \"Zero Storage\" Policy and Confidentiality",
'cgu.art6_1_title': "6.1 Ephemeral Processing",
'cgu.art6_1_text1': "The Platform applies a strict privacy policy. No creation data (prompts, generated texts, ebooks in progress) is saved on our servers permanently.",
'cgu.art6_1_text2': "Data is only kept for the duration of the work session to allow for display and export.",
'cgu.art6_1_text3': "Whatever happens, if the User decides to delete their information, their project, or closes their session, all associated data is immediately and irreversibly deleted from our databases.",
'cgu.art6_2_title': "6.2 Backup Responsibility",
'cgu.art6_2_text': "Due to this non-retention policy, it is the User's exclusive responsibility to download and save their work (PDF, EPUB, etc.) before the end of their session. The Platform cannot be held responsible for data loss following a disconnection or voluntary deletion.",
'cgu.art7_title': "Article 7: Limitation of Liability",
'cgu.art7_1_title': "7.1 Content Quality",
'cgu.art7_1_text': "The Service uses Google Gemini technology. The User accepts that the AI may produce inaccurate, incomplete, or biased information (\"hallucinations\"). The Platform cannot be held responsible for the content of the ebooks or the use made of them.",
'cgu.art7_2_title': "7.2 Technical Availability",
'cgu.art7_2_text': "The Platform cannot guarantee uninterrupted availability of the service, as it depends on third-party providers (hosting and Google API).",
'cgu.art8_title': "Article 8: Personal Data Protection (GDPR)",
'cgu.art8_text': "The only data kept are those strictly necessary for account management (email, billing). The User has a right of access, rectification, and total deletion of their personal data.",
'cgu.art9_title': "Article 9: Modification and Termination",
'cgu.art9_1_title': "9.1 ToS Modification",
'cgu.art9_1_text': "The Platform reserves the right to modify these ToS at any time. The User will be informed of any substantial modification.",
'cgu.art9_2_title': "9.2 Termination",
'cgu.art9_2_text': "The User can delete their account at any time. This action results in the immediate deletion of all their data, in accordance with Article 6.",
'cgu.art10_title': "Article 10: Applicable Law and Jurisdiction",
'cgu.art10_text': "These ToS are subject to French law. Any dispute relating to their interpretation or execution falls under the exclusive competence of the competent courts.",
'cgv.title': "General Terms of Sale (ToS)",
'cgv.version': "Effective as of: 2026-03-06",
'cgv.art1_title': "Article 1: Purpose and Scope",
'cgv.art1_text1': "These General Terms of Sale (ToS) apply, without restriction or reservation, to any purchase of ebook generation services (hereinafter \"the Services\") offered by Pluume on the platform https://pluu.me.",
'cgv.art1_text2': "These ToS frame the terms of ordering, payment, delivery of credits/subscriptions, and management of possible disputes between the Seller and the User (hereinafter \"the Client\").",
'cgv.art2_title': "Article 2: Characteristics of the Services",
'cgv.art2_text': "The Seller offers ebook creation solutions assisted by Google Gemini artificial intelligence. The offers are divided into two categories:",
'cgv.art2_item1': "Credit Packs: One-time purchase of a defined volume of content generations.",
'cgv.art2_item2': "Subscriptions: Unlimited or capped access to services for a determined duration (monthly or annual) with automatic renewal.",
'cgv.art3_title': "Article 3: Order and Validation",
'cgv.art3_text': "The Client selects the offer of their choice on the Platform. The order is considered final upon validation of the payment by the third-party provider. A confirmation email is sent to the Client at the address provided during account creation.",
'cgv.art4_title': "Article 4: Financial Conditions",
'cgv.art4_1_title': "4.1 Rates",
'cgv.art4_1_text': "Prices are indicated in Euros and are inclusive of all taxes (TTC). The Seller reserves the right to modify its rates at any time, but the services will be invoiced based on the rates in effect at the time of order registration.",
'cgv.art4_2_title': "4.2 Payment Terms (Stripe)",
'cgv.art4_2_text': "Payment is made exclusively by credit card via the Stripe secure payment system.",
'cgv.art4_2_item1': "The Seller never has access to the Client's bank details.",
'cgv.art4_2_item2': "Stripe guarantees the confidentiality and security of transactions thanks to the SSL protocol and PCI-DSS compliance.",
'cgv.art5_title': "Article 5: Subscription Management",
'cgv.art5_1_title': "5.1 Automatic Renewal",
'cgv.art5_1_text': "Any subscription taken out is with tacit renewal. The Client authorizes Stripe to withdraw the subscription amount at each due date (month or year) from the credit card used during the initial purchase.",
'cgv.art5_2_title': "5.2 Termination",
'cgv.art5_2_text': "The Client can terminate their subscription at any time and free of charge from their personal space.",
'cgv.art5_2_item1': "Termination interrupts automatic renewal but leaves access to services active until the end of the current period.",
'cgv.art5_2_item2': "No pro rata refund is made for the period remaining between the termination date and the end of the period.",
'cgv.art6_title': "Article 6: Right of Withdrawal (Digital Products)",
'cgv.art6_text': "In accordance with article L221-28 of the Consumer Code, the 14-day right of withdrawal does not apply to contracts for the supply of digital content not provided on a tangible medium whose execution has begun after the consumer's express prior agreement.",
'cgv.art6_warning': "By proceeding with payment and using their first generation credit, the Client expressly accepts the immediate execution of the service and waives their right of withdrawal. No refund will be granted once the service is consumed.",
'cgv.art7_title': "Article 7: Delivery and Access",
'cgv.art7_text': "The Services are deemed \"delivered\" as soon as the credits are added to the Client's account or premium access is unlocked. In case of a technical problem preventing immediate access, the Client should contact support.",
'cgv.art8_title': "Article 8: Guarantees and Commercial Liability",
'cgv.art8_1_title': "8.1 AI Quality",
'cgv.art8_1_text': "The Seller sells access to a generation tool. It does not guarantee literary quality, factual accuracy, or absolute originality of the text produced by the AI. Therefore, no refund request can be motivated by a \"disappointment\" regarding the AI's style.",
'cgv.art8_2_title': "8.2 Continuity of Service",
'cgv.art8_2_text': "The Seller agrees to make its best efforts to maintain access to the service but cannot be held responsible for failures due to third-party providers (Stripe, Google Gemini API).",
'cgv.art9_title': "Article 9: Data Deletion Policy",
'cgv.art9_text': "As stipulated in the ToS, the Seller applies a zero storage policy.",
'cgv.art9_item1': "If the Client deletes their info or account, all billing data is archived only for the legal fiscal duration, but created content is irreversibly deleted.",
'cgv.art9_item2': "The Client cannot claim compensation for data loss following a voluntary deletion.",
'cgv.art10_title': "Article 10: Disputes and Applicable Law",
'cgv.art10_text': "These ToS are subject to French law. In case of dispute, an amicable solution will be sought before any legal action. Failing agreement, exclusive competence is attributed to the competent courts.",
}, },
es: { es: {
// General Navigation // General Navigation
@@ -750,6 +958,8 @@ export const translations = {
'book_settings.confirm_delete': 'Sí, eliminar permanentemente', 'book_settings.confirm_delete': 'Sí, eliminar permanentemente',
'book_settings.cancel': 'Cancelar', 'book_settings.cancel': 'Cancelar',
'book_settings.delete_button': 'Eliminar este proyecto', 'book_settings.delete_button': 'Eliminar este proyecto',
'book_settings.save': 'Guardar',
'book_settings.saved': 'Guardado',
// Landing Page // Landing Page
'landing.nav_features': 'Características', 'landing.nav_features': 'Características',
@@ -818,6 +1028,8 @@ export const translations = {
'auth.signup_link': 'Regístrate', 'auth.signup_link': 'Regístrate',
'auth.signin_link': 'Iniciar sesión', 'auth.signin_link': 'Iniciar sesión',
'auth.back_to_site': '← Volver al sitio', 'auth.back_to_site': '← Volver al sitio',
'auth.accept_cgu': "He leído y acepto los ",
'auth.cgu_link': "Términos de Servicio",
'auth.hero_title_part1': "El lugar donde tus", 'auth.hero_title_part1': "El lugar donde tus",
'auth.hero_title_part2': "historias", 'auth.hero_title_part2': "historias",
'auth.hero_title_part3': "cobran vida.", 'auth.hero_title_part3': "cobran vida.",
@@ -979,11 +1191,112 @@ export const translations = {
'sw.save_color': '+ GUARDAR', 'sw.save_color': '+ GUARDAR',
// Genre, Tense Constants Translation Setup // Genre, Tense Constants Translation Setup
'pov_options.première_personne': 'Primera Persona', 'pov_options.1ère_personne_(je)': ' Persona (Yo)',
'pov_options.troisième_personne_limitée': 'Tercera Persona Limitada', 'pov_options.3ème_personne_(limitée_au_protagoniste)': ' Persona (Limitada)',
'pov_options.troisième_personne_omnisciente': 'Tercera Persona Omnisciente', 'pov_options.3ème_personne_(omnisciente)': ' Persona (Omnisciente)',
'tense_options.présent': 'Presente', 'pov_options.multi-points_de_vue_(alterné)': 'Múltiples POV (Alternando)',
'tense_options.passé': 'Pasado', 'tense_options.passé_(passé_simple_/_imparfait)': 'Tiempo Pasado',
'tense_options.présent_de_narration': 'Tiempo Presente',
// CGU Page
'cgu.title': "Condiciones Generales de Uso (CGU)",
'cgu.version': "Versión vigente al: 06/03/2026",
'cgu.preamble_title': "Preámbulo",
'cgu.preamble_text1': "La presente plataforma (en adelante \"la Plataforma\"), accesible en https://pluu.me, es editada por Pluume. La Plataforma ofrece un servicio de generación de contenidos textuales asistido por inteligencia artificial (tecnología Google Gemini) destinado a la creación de ebooks.",
'cgu.preamble_text2': "Las presentes Condiciones Generales de Uso (CGU) tienen por objeto definir las reglas de acceso y uso del servicio. Todo acceso o uso de la Plataforma supone la aceptación sin reserva de la totalidad de las presentes condiciones.",
'cgu.art1_title': "Artículo 1: Definiciones",
'cgu.art1_user': "Usuario: Cualquier persona física o jurídica que acceda a la Plataforma.",
'cgu.art1_service': "Servicio: Conjunto de herramientas de generación de texto, estructuración y exportación de ebooks puestas a disposición.",
'cgu.art1_content': "Contenido Generado: Textos, esquemas o documentos producidos por la IA siguiendo las instrucciones del Usuario.",
'cgu.art1_prompt': "Prompt: Instrucciones textuales introducidas por el Usuario para dirigir a la IA.",
'cgu.art2_title': "Artículo 2: Acceso y Registro",
'cgu.art2_text': "El acceso a la Plataforma está reservado a personas mayores de edad. Para utilizar los servicios de generación, el Usuario debe crear una cuenta. Es responsable de la confidencialidad de sus credenciales.",
'cgu.art3_title': "Artículo 3: Condiciones Financieras (Suscripciones y Créditos)",
'cgu.art3_1_title': "3.1 Tarifas",
'cgu.art3_1_text': "El acceso a las funcionalidades de generación está sujeto a una tarificación consultable en la página de Tarifas. Los precios se expresan en Euros con todos los impuestos incluidos.",
'cgu.art3_1_sub1': "Suscripciones: Cargo periódico automático.",
'cgu.art3_1_sub2': "Packs de créditos: Compra puntual de un volumen definido de generaciones.",
'cgu.art3_2_title': "3.2 Pago y Seguridad",
'cgu.art3_2_text': "Los pagos son procesados por un proveedor de pagos seguro. La Plataforma no almacena ningún dato bancario.",
'cgu.art4_title': "Artículo 4: Obligaciones del Usuario y Seguridad (Cláusula Estricta)",
'cgu.art4_text': "El Usuario se compromete a utilizar el Servicio de manera lícita.",
'cgu.art4_1_title': "4.1 Contenidos Prohibidos",
'cgu.art4_1_text': "Está formalmente prohibido utilizar la Plataforma para generar contenidos:",
'cgu.art4_1_item1': "Pornografía Infantil (CSAM): Cualquier intento de generación, solicitud o difusión de contenido relacionado con la explotación sexual de menores resultará en una expulsión inmediata sin previo aviso ni reembolso. De acuerdo con la ley, la Plataforma procederá a informar sistemáticamente a las autoridades competentes.",
'cgu.art4_1_item2': "Odiosos y Discriminatorios: Incitación a la violencia, al racismo, al antisemitismo, al homophobia o cualquier forma de discriminación.",
'cgu.art4_1_item3': "Ilegales: Apología de crímenes, terrorismo o violación de derechos de propiedad intelectual de terceros.",
'cgu.art4_2_title': "4.2 Vigilancia y Moderación",
'cgu.art4_2_text': "La Plataforma utiliza algoritmos de filtrado en tiempo real. En caso de violación repetida o grave de este artículo, la Plataforma se reserva el derecho de suspender la cuenta del Usuario por pleno derecho.",
'cgu.art5_title': "Artículo 5: Propiedad Intelectual",
'cgu.art5_1_title': "5.1 Derechos del Usuario",
'cgu.art5_1_text': "La Plataforma concede al Usuario, sujeto al pago de las tasas correspondientes, la plena propiedad de los derechos de explotación sobre los ebooks generados.",
'cgu.art5_1_note': "Nota sobre la IA: Se informa al Usuario que la protección por derechos de autor de las obras generadas por IA puede variar según las legislaciones nacionales y a menudo requiere una intervención creativa humana significativa por parte del Usuario.",
'cgu.art5_2_title': "5.2 Derechos de la Plataforma",
'cgu.art5_2_text': "La interfaz, los algoritmos de conexión a la API Gemini y la identidad visual del sitio siguen siendo propiedad exclusiva del Editor.",
'cgu.art6_title': "Artículo 6: Política de \"Cero Almacenamiento\" y Confidencialidad",
'cgu.art6_1_title': "6.1 Tratamiento Efímero",
'cgu.art6_1_text1': "La Plataforma aplica una política estricta de confidencialidad. Ningún dato de creación (prompts, textos generados, ebooks en curso) se guarda en nuestros servidores de manera permanente.",
'cgu.art6_1_text2': "Los datos solo se conservan durante el tiempo de la sesión de trabajo para permitir la visualización y exportación.",
'cgu.art6_1_text3': "Pase lo que pase, si el Usuario decide eliminar su información, su proyecto o cierra su sesión, todos los datos asociados se eliminan inmediata e irreversiblemente de nuestras bases de datos.",
'cgu.art6_2_title': "6.2 Responsabilidad de Copia de Seguridad",
'cgu.art6_2_text': "Debido a esta política de no retención, es responsabilidad exclusiva del Usuario descargar y guardar sus trabajos (formato PDF, EPUB, etc.) antes del cierre de su sesión. La Plataforma no podrá ser considerada responsable de una pérdida de datos tras una desconexión o eliminación voluntaria.",
'cgu.art7_title': "Artículo 7: Limitación de Responsabilidad",
'cgu.art7_1_title': "7.1 Calidad del Contenido",
'cgu.art7_1_text': "El Servicio utiliza la tecnología Google Gemini. El Usuario acepta que la IA pueda producir informaciones inexactas, incompletas o sesgadas (\"alucinaciones\"). La Plataforma no será responsable del contenido de los ebooks ni del uso que se haga de ellos.",
'cgu.art7_2_title': "7.2 Disponibilidad Técnica",
'cgu.art7_2_text': "La Plataforma no puede garantizar la disponibilidad ininterrumpida del servicio, ya que depende de terceros proveedores (alojamiento y API Google).",
'cgu.art8_title': "Artículo 8: Protección de Datos Personales (RGPD)",
'cgu.art8_text': "Los únicos datos conservados son los estrictamente necesarios para la gestión de la cuenta (email, facturación). El Usuario tiene derecho de acceso, rectificación y eliminación total de sus datos personales.",
'cgu.art9_title': "Artículo 9: Modificación y Rescisión",
'cgu.art9_1_title': "9.1 Modificación de las CGU",
'cgu.art9_1_text': "La Plataforma se reserva el derecho de modificar las presentes CGU en cualquier momento. El Usuario será informado de cualquier modificación sustancial.",
'cgu.art9_2_title': "9.2 Rescisión",
'cgu.art9_2_text': "El Usuario puede eliminar su cuenta en cualquier momento. Esta acción conlleva la eliminación inmediata de todos sus datos, de conformidad con el Artículo 6.",
'cgu.art10_title': "Artículo 10: Ley Aplicable y Jurisdicción",
'cgu.art10_text': "Las presentes CGU están sujetas a la ley francesa. Cualquier disputa relativa a su interpretación o ejecución corresponde a la competencia exclusiva de los tribunales competentes.",
// CGV Page
'cgv.title': "Condiciones Generales de Venta (CGV)",
'cgv.version': "Versión vigente al: 06/03/2026",
'cgv.art1_title': "Artículo 1: Objeto y Ámbito de Aplicación",
'cgv.art1_text1': "Las presentes Condiciones Generales de Venta (CGV) se aplican, sin restricción ni reserva, a cualquier compra de servicios de generación de ebooks (en adelante \"los Servicios\") ofrecidos por Pluume en la plataforma https://pluu.me.",
'cgv.art1_text2': "Estas CGV regulan las modalidades de pedido, pago, entrega de créditos/suscripciones y gestión de posibles disputas entre el Vendedor y el Usuario (en adelante \"el Cliente\").",
'cgv.art2_title': "Artículo 2: Características de los Servicios",
'cgv.art2_text': "El Vendeur ofrece soluciones de creación de ebooks asistidas por inteligencia artificial Google Gemini. Las ofertas se dividen en dos categorías:",
'cgv.art2_item1': "Packs de Créditos: Compra puntual de un volumen definido de generaciones de contenido.",
'cgv.art2_item2': "Suscripciones: Acceso ilimitado o limitado a los servicios por una duración determinada (mensual o anual) con renovación automática.",
'cgv.art3_title': "Artículo 3: Pedido y Validación",
'cgv.art3_text': "El Cliente selecciona la oferta de su elección en la Plataforma. El pedido se considera definitivo tras la validación del pago por el proveedor tercero. Se envía un correo de confirmación al Cliente a la dirección proporcionada durante la creación de la cuenta.",
'cgv.art4_title': "Artículo 4: Condiciones Financieras",
'cgv.art4_1_title': "4.1 Tarifas",
'cgv.art4_1_text': "Los precios se indican en Euros y se entienden con todos los impuestos incluidos (TTC). El Vendedor se reserva el derecho de modificar sus tarifas en cualquier momento, pero los servicios se facturarán según las tarifas vigentes en el momento del registro del pedido.",
'cgv.art4_2_title': "4.2 Modalidades de pago (Stripe)",
'cgv.art4_2_text': "El pago se realiza exclusivamente por tarjeta bancaria a través del sistema de pago seguro Stripe.",
'cgv.art4_2_item1': "El Vendedor nunca tiene acceso a los datos bancarios del Cliente.",
'cgv.art4_2_item2': "Stripe garantiza la confidencialidad y la seguridad de las transacciones gracias al protocolo SSL y al cumplimiento de PCI-DSS.",
'cgv.art5_title': "Artículo 5: Gestión de Suscripciones",
'cgv.art5_1_title': "5.1 Renovación Automática",
'cgv.art5_1_text': "Cualquier suscripción contratada es con renovación automática. El Cliente autoriza a Stripe a retirar el importe de la suscripción en cada vencimiento (mes o año) de la tarjeta bancaria utilizada durante la compra inicial.",
'cgv.art5_2_title': "5.2 Rescisión",
'cgv.art5_2_text': "El Cliente puede rescindir su suscripción en cualquier momento y sin cargo desde su espacio personal.",
'cgv.art5_2_item1': "La rescisión interrumpe la renovación automática pero deja el acceso a los servicios activo hasta el final del periodo en curso.",
'cgv.art5_2_item2': "No se realiza ningún reembolso prorrata por el periodo restante entre la fecha de rescisión y el final del periodo.",
'cgv.art6_title': "Artículo 6: Derecho de Desistimiento (Productos Digitales)",
'cgv.art6_text': "De conformidad con el artículo L221-28 del Código del Consumidor, el derecho de desistimiento de 14 días no se aplica a los contratos de suministro de contenido digital no proporcionado en un soporte material cuya ejecución haya comenzado tras el acuerdo previo expreso del consumidor.",
'cgv.art6_warning': "Al proceder al pago y utilizar su primer crédito de generación, el Cliente acepta expresamente la ejecución inmediata del servicio y renuncia a su derecho de desistimiento. No se concederá ningún reembolso una vez consumido el servicio.",
'cgv.art7_title': "Artículo 7: Entrega y Acceso",
'cgv.art7_text': "Los Servicios se consideran \"entregados\" tan pronto como los créditos se añaden a la cuenta del Cliente o se desbloquea el acceso premium. En caso de problema técnico que impida el acceso inmediato, el Cliente debe contactar con soporte.",
'cgv.art8_title': "Artículo 8: Garantías y Responsabilidad Comercial",
'cgv.art8_1_title': "8.1 Calidad de la IA",
'cgv.art8_1_text': "El Vendedor vende acceso a una herramienta de generación. No garantiza la calidad literaria, la exactitud de los hechos ni la originalidad absoluta del texto producido por la IA. Por lo tanto, ninguna solicitud de reembolso podrá motivarse por una \"decepción\" respecto al estilo de la IA.",
'cgv.art8_2_title': "8.2 Continuidad del Servicio",
'cgv.art8_2_text': "El Vendedor se compromete a hacer sus mejores esfuerzos para mantener el acceso al servicio, pero no será responsable de los fallos debidos a proveedores terceros (Stripe, API Google Gemini).",
'cgv.art9_title': "Artículo 9: Política de Eliminación de Datos",
'cgv.art9_text': "Como se estipula en las CGU, el Vendedor aplica una política de cero almacenamiento.",
'cgv.art9_item1': "Si el Cliente elimina su información o su cuenta, todos los datos de facturación se archivan únicamente durante la duración legal fiscal, pero los contenidos creados se eliminan irreversiblemente.",
'cgv.art9_item2': "El Cliente no podrá reclamar una indemnización por pérdida de datos tras una eliminación voluntaria.",
'cgv.art10_title': "Artículo 10: Litigios y Ley Aplicable",
'cgv.art10_text': "Las presentes CGV están sujetas a la ley francesa. En caso de disputa, se buscará una solución amistosa antes de cualquier acción legal. A falta de acuerdo, la competencia exclusiva se atribuye a los tribunales competentes.",
}, },
de: { de: {
// General Navigation // General Navigation
@@ -1078,6 +1391,8 @@ export const translations = {
'book_settings.confirm_delete': 'Ja, dauerhaft löschen', 'book_settings.confirm_delete': 'Ja, dauerhaft löschen',
'book_settings.cancel': 'Abbrechen', 'book_settings.cancel': 'Abbrechen',
'book_settings.delete_button': 'Dieses Projekt löschen', 'book_settings.delete_button': 'Dieses Projekt löschen',
'book_settings.save': 'Speichern',
'book_settings.saved': 'Gespeichert',
// Landing Page // Landing Page
'landing.nav_features': 'Funktionen', 'landing.nav_features': 'Funktionen',
@@ -1146,6 +1461,8 @@ export const translations = {
'auth.signup_link': 'Registrieren', 'auth.signup_link': 'Registrieren',
'auth.signin_link': 'Anmelden', 'auth.signin_link': 'Anmelden',
'auth.back_to_site': '← Zurück zur Website', 'auth.back_to_site': '← Zurück zur Website',
'auth.accept_cgu': "Ich akzeptiere die ",
'auth.cgu_link': "Nutzungsbedingungen",
'auth.hero_title_part1': "Der Ort, an dem deine", 'auth.hero_title_part1': "Der Ort, an dem deine",
'auth.hero_title_part2': "Geschichten", 'auth.hero_title_part2': "Geschichten",
'auth.hero_title_part3': "zum Leben erwachen.", 'auth.hero_title_part3': "zum Leben erwachen.",
@@ -1307,12 +1624,113 @@ export const translations = {
'sw.save_color': '+ SPEICHERN', 'sw.save_color': '+ SPEICHERN',
// Genre, Tense Constants Translation Setup // Genre, Tense Constants Translation Setup
'pov_options.première_personne': 'Ich-Perspektive', 'pov_options.1ère_personne_(je)': 'Ich-Perspektive',
'pov_options.troisième_personne_limitée': 'Personale Erzählsituation', 'pov_options.3ème_personne_(limitée_au_protagoniste)': 'Er/Sie-Perspektive (personal)',
'pov_options.troisième_personne_omnisciente': 'Auktorialer Erzähler', 'pov_options.3ème_personne_(omnisciente)': 'Er/Sie-Perspektive (auktorial)',
'tense_options.présent': 'Präsens', 'pov_options.multi-points_de_vue_(alterné)': 'Mehrere Perspektiven',
'tense_options.passé': 'Präteritum', 'tense_options.passé_(passé_simple_/_imparfait)': 'Vergangenheit (Präteritum)',
} 'tense_options.présent_de_narration': 'Gegenwart (Präsens)',
// CGU Page
'cgu.title': "Allgemeine Nutzungsbedingungen (ANB)",
'cgu.version': "Gültig ab: 06.03.2026",
'cgu.preamble_title': "Präambel",
'cgu.preamble_text1': "Diese Plattform (nachfolgend \"die Plattform\"), erreichbar unter https://pluu.me, wird von Pluume herausgegeben. Die Plattform bietet einen durch künstliche Intelligenz (Google Gemini Technologie) unterstützten Textgenerierungsdienst zur Erstellung von E-Books an.",
'cgu.preamble_text2': "Diese Allgemeinen Nutzungsbedingungen (ANB) dienen der Festlegung der Zugangs- und Nutzungsregeln für den Dienst. Jeder Zugang oder jede Nutzung der Plattform setzt die vorbehaltlose Annahme der Gesamtheit dieser Bedingungen voraus.",
'cgu.art1_title': "Artikel 1: Definitionen",
'cgu.art1_user': "Nutzer: Jede natürliche oder juristische Person, die auf die Plattform zugreift.",
'cgu.art1_service': "Dienst: Gesamtheit der zur Verfügung gestellten Werkzeuge zur Textgenerierung, Strukturierung und zum Export von E-Books.",
'cgu.art1_content': "Generierter Inhalt: Texte, Pläne oder Dokumente, die von der KI aufgrund der Anweisungen des Nutzers erstellt wurden.",
'cgu.art1_prompt': "Prompt: Textanweisungen, die der Nutzer eingibt, um die KI zu steuern.",
'cgu.art2_title': "Artikel 2: Zugang und Anmeldung",
'cgu.art2_text': "Der Zugang zur Plattform ist volljährigen Personen vorbehalten. Um die Generierungsdienste zu nutzen, muss der Nutzer ein Konto erstellen. Er ist für die Vertraulichkeit seiner Zugangsdaten verantwortlich.",
'cgu.art3_title': "Artikel 3: Finanzielle Bedingungen (Abonnements und Credits)",
'cgu.art3_1_title': "3.1 Tarife",
'cgu.art3_1_text': "Der Zugang zu den Generierungsfunktionen unterliegt einer Preisgestaltung, die auf der Seite Preise einsehbar ist. Die Preise verstehen sich in Euro inkl. MwSt.",
'cgu.art3_1_sub1': "Abonnements: Automatischer periodischer Einzug.",
'cgu.art3_1_sub2': "Credit-Packs: Einmaliger Kauf eines definierten Volumens an Generierungen.",
'cgu.art3_2_title': "3.2 Zahlung und Sicherheit",
'cgu.art3_2_text': "Zahlungen werden über einen sicheren Zahlungsanbieter abgewickelt. Die Plattform speichert keine Bankdaten.",
'cgu.art4_title': "Artikel 4: Pflichten des Nutzers und Sicherheit (Strenge Klausel)",
'cgu.art4_text': "Der Nutzer verpflichtet sich, den Dienst rechtmäßig zu nutzen.",
'cgu.art4_1_title': "4.1 Verbotene Inhalte",
'cgu.art4_1_text': "Es ist ausdrücklich untersagt, die Plattform zur Generierung folgender Inhalte zu nutzen:",
'cgu.art4_1_item1': "Kinderpornographie (CSAM): Jeder Versuch der Generierung, Abfrage oder Verbreitung von Inhalten im Zusammenhang mit der sexuellen Ausbeutung Minderjähriger führt zum sofortigen Ausschluss ohne Vorankündigung oder Rückerstattung. Gesetzgemäß erfolgt eine systematische Meldung an die zuständigen Behörden.",
'cgu.art4_1_item2': "Hasserfüllt und diskriminierend: Aufstachelung zu Gewalt, Rassismus, Antisemitismus, Homophobie oder jeder Form von Diskriminierung.",
'cgu.art4_1_item3': "Illegal: Verherrlichung von Verbrechen, Terrorismus oder Verletzung von geistigem Eigentum Dritter.",
'cgu.art4_2_title': "4.2 Überwachung und Moderation",
'cgu.art4_2_text': "Die Plattform verwendet Echtzeit-Filteralgorithmen. Bei wiederholtem oder schwerem Verstoß gegen diesen Artikel behält sich die Plattform das Recht vor, das Konto des Nutzers von Rechts wegen zu sperren.",
'cgu.art5_title': "Artikel 5: Geistiges Eigentum",
'cgu.art5_1_title': "5.1 Rechte des Nutzers",
'cgu.art5_1_text': "Die Plattform gewährt dem Nutzer, unter Vorbehalt der Zahlung etwaiger Gebühren, das volle Eigentum an den Verwertungsrechten an den generierten E-Books.",
'cgu.art5_1_note': "Hinweis zur KI: Der Nutzer wird hiermit informiert, dass der urheberrechtliche Schutz von KI-generierten Werken je nach nationaler Gesetzgebung variieren kann und oft eine erhebliche menschliche kreative Leistung des Nutzers erfordert.",
'cgu.art5_2_title': "5.2 Rechte der Plattform",
'cgu.art5_2_text': "Die Benutzeroberfläche, die Verbindungsalgorithmen zur Gemini-API und die visuelle Identität der Website bleiben das ausschließliche Eigentum des Herausgebers.",
'cgu.art6_title': "Artikel 6: Politik der \"Null-Speicherung\" und Vertraulichkeit",
'cgu.art6_1_title': "6.1 Flüchtige Verarbeitung",
'cgu.art6_1_text1': "Die Plattform wendet eine strenge Vertraulichkeitspolitik an. Es werden keine Erstellungsdaten (Prompts, generierte Texte, laufende E-Books) dauerhaft auf unseren Servern gespeichert.",
'cgu.art6_1_text2': "Die Daten werden nur für die Dauer der Arbeitssitzung aufbewahrt, um die Anzeige und den Export zu ermöglichen.",
'cgu.art6_1_text3': "In jedem Fall werden bei Löschung der Informationen, des Projekts oder beim Schließen der Sitzung durch den Nutzer alle damit verbundenen Daten sofort und unwiderruflich aus unseren Datenbanken gelöscht.",
'cgu.art6_2_title': "6.2 Verantwortung für die Sicherung",
'cgu.art6_2_text': "Aufgrund dieser Nicht-Aufbewahrungspolitik liegt es in der ausschließlichen Verantwortung des Nutzers, seine Arbeiten vor Beendigung der Sitzung herunterzuladen und zu sichern (Format PDF, EPUB usw.). Die Plattform kann nicht für Datenverlust infolge einer Trennung oder freiwilligen Löschung haftbar gemacht werden.",
'cgu.art7_title': "Artikel 7: Haftungsbeschränkung",
'cgu.art7_1_title': "7.1 Qualität des Inhalts",
'cgu.art7_1_text': "Der Dienst nutzt die Google Gemini Technologie. Der Nutzer akzeptiert, dass die KI ungenaue, unvollständige oder voreingenommene Informationen (\"Halluzinationen\") produzieren kann. Die Plattform haftet nicht für den Inhalt der E-Books oder deren Verwendung.",
'cgu.art7_2_title': "7.2 Technische Verfügbarkeit",
'cgu.art7_2_text': "Die Plattform kann keine ununterbrochene Verfügbarkeit des Dienstes garantieren, da diese von Drittanbietern (Hosting und Google-API) abhängt.",
'cgu.art8_title': "Artikel 8: Schutz personenbezogener Daten (DSGVO)",
'cgu.art8_text': "Es werden nur die Daten aufbewahrt, die für die Kontoverwaltung (E-Mail, Abrechnung) unbedingt erforderlich sind. Der Nutzer hat ein Recht auf Auskunft, Berichtigung und vollständige Löschung seiner personenbezogenen Daten.",
'cgu.art9_title': "Artikel 9: Änderung und Beendigung",
'cgu.art9_1_title': "9.1 Änderung der ANB",
'cgu.art9_1_text': "Die Plattform behält sich das Recht vor, diese ANB jederzeit zu ändern. Der Nutzer wird über jede wesentliche Änderung informiert.",
'cgu.art9_2_title': "9.2 Beendigung",
'cgu.art9_2_text': "Der Nutzer kann sein Konto jederzeit löschen. Diese Aktion führt zur sofortigen Löschung all seiner Daten gemäß Artikel 6.",
'cgu.art10_title': "Artikel 10: Anwendbares Recht und Gerichtsstand",
'cgu.art10_text': "Diese ANB unterliegen französischem Recht. Jeder Streitfall im Zusammenhang mit ihrer Auslegung oder Ausführung fällt in die ausschließliche Zuständigkeit der zuständigen Gerichte.",
// CGV Page
'cgv.title': "Allgemeine Verkaufsbedingungen (AVB)",
'cgv.version': "Gültig ab: 06.03.2026",
'cgv.art1_title': "Artikel 1: Gegenstand und Anwendungsbereich",
'cgv.art1_text1': "Diese Allgemeinen Verkaufsbedingungen (AVB) gelten ohne Einschränkung oder Vorbehalt für jeden Kauf von E-Book-Generierungsdiensten (nachfolgend \"die Dienste\"), die von Pluume auf der Plattform https://pluu.me angeboten werden.",
'cgv.art1_text2': "Diese AVB regeln die Modalitäten von Bestellung, Zahlung, Lieferung von Credits/Abonnements und die Abwicklung etwaiger Streitigkeiten zwischen dem Verkäufer und dem Nutzer (nachfolgend \"der Kunde\").",
'cgv.art2_title': "Artikel 2: Merkmale der Dienste",
'cgv.art2_text': "Der Verkäufer bietet Lösungen zur Erstellung von E-Books an, die durch die künstliche Intelligenz Google Gemini unterstützt werden. Die Angebote sind in zwei Kategorien unterteilt:",
'cgv.art2_item1': "Credit-Packs: Einmaliger Kauf eines definierten Volumens an Inhaltsgenerierungen.",
'cgv.art2_item2': "Abonnements: Unbegrenzter oder gedrosselter Zugang zu den Diensten für eine bestimmte Dauer (monatlich oder jährlich) mit automatischer Verlängerung.",
'cgv.art3_title': "Artikel 3: Bestellung und Validierung",
'cgv.art3_text': "Der Kunde wählt das Angebot seiner Wahl auf der Plattform aus. Die Bestellung gilt als endgültig nach Bestätigung der Zahlung durch den Drittanbieter. Eine Bestätigungs-E-Mail wird an den Kunden an die bei der Kontoerstellung angegebene Adresse gesendet.",
'cgv.art4_title': "Artikel 4: Finanzielle Bedingungen",
'cgv.art4_1_title': "4.1 Tarife",
'cgv.art4_1_text': "Die Preise sind in Euro angegeben und verstehen sich inklusive aller Steuern (MwSt.). Der Verkäufer behält sich das Recht vor, seine Preise jederzeit zu ändern, aber die Dienste werden auf der Grundlage der zum Zeitpunkt der Registrierung der Bestellung gültigen Tarife berechnet.",
'cgv.art4_2_title': "4.2 Zahlungsmodalitäten (Stripe)",
'cgv.art4_2_text': "Die Zahlung erfolgt ausschließlich per Kreditkarte über das sichere Zahlungssystem Stripe.",
'cgv.art4_2_item1': "Der Verkäufer hat niemals Zugang zu den Bankdaten des Kunden.",
'cgv.art4_2_item2': "Stripe garantiert die Vertraulichkeit und Sicherheit der Transaktionen dank des SSL-Protokolls und der PCI-DSS-Konformität.",
'cgv.art5_title': "Artikel 5: Verwaltung von Abonnements",
'cgv.art5_1_title': "5.1 Automatische Verlängerung",
'cgv.art5_1_text': "Jedes abgeschlossene Abonnement verlängert sich stillschweigend. Der Kunde ermächtigt Stripe, den Abonnementbetrag bei jeder Fälligkeit (Monat oder Jahr) von der beim ursprünglichen Kauf verwendeten Kreditkarte einzuziehen.",
'cgv.art5_2_title': "5.2 Kündigung",
'cgv.art5_2_text': "Der Kunde kann sein Abonnement jederzeit und kostenlos über seinen persönlichen Bereich kündigen.",
'cgv.art5_2_item1': "Die Kündigung unterbricht die automatische Verlängerung, lässt den Zugang zu den Diensten jedoch bis zum Ende des laufenden Zeitraums aktiv.",
'cgv.art5_2_item2': "Es erfolgt keine anteilige Rückerstattung für den Zeitraum zwischen dem Kündigungsdatum und dem Ende der Laufzeit.",
'cgv.art6_title': "Artikel 6: Widerrufsrecht (Digitale Produkte)",
'cgv.art6_text': "Gemäß Artikel L221-28 des Verbrauchergesetzbuches gilt das 14-tägige Widerrufsrecht nicht für Verträge über die Lieferung von digitalen Inhalten, die nicht auf einem körperlichen Datenträger geliefert werden und deren Ausführung nach ausdrücklicher vorheriger Zustimmung des Verbrauchers begonnen hat.",
'cgv.art6_warning': "Mit der Durchführung der Zahlung und der Nutzung seines ersten Generierungs-Credits akzeptiert der Kunde ausdrücklich die sofortige Ausführung des Dienstes und verzichtet auf sein Widerrufsrecht. Nach Inanspruchnahme des Dienstes wird keine Rückerstattung gewährt.",
'cgv.art7_title': "Artikel 7: Lieferung und Zugang",
'cgv.art7_text': "Die Dienste gelten als \"geliefert\", sobald die Credits dem Konto des Kunden gutgeschrieben wurden oder der Premium-Zugang freigeschaltet wurde. Bei technischen Problemen sollte der Kunde den Support kontaktieren.",
'cgv.art8_title': "Artikel 8: Garantien und kommerzielle Haftung",
'cgv.art8_1_title': "8.1 Qualität der KI",
'cgv.art8_1_text': "Der Verkäufer verkauft den Zugang zu einem Generierungswerkzeug. Er garantiert nicht die literarische Qualität, die sachliche Richtigkeit oder die absolute Originalität des von der KI erzeugten Textes. Daher kann kein Rückerstattungsantrag mit einer \"Enttäuschung\" über den Stil der KI begründet werden.",
'cgv.art8_2_title': "8.2 Kontinuität des Dienstes",
'cgv.art8_2_text': "Der Verkäufer verpflichtet sich, sein Bestes zu tun, um den Zugang zum Dienst aufrechtzuerhalten, kann jedoch nicht für Ausfälle aufgrund von Drittanbietern (Stripe, Google Gemini API) verantwortlich gemacht werden.",
'cgv.art9_title': "Artikel 9: Politik zur Datenlöschung",
'cgv.art9_text': "Wie in den ANB festgelegt, wendet der Verkäufer eine Null-Speicherungspolitik an.",
'cgv.art9_item1': "Wenn der Kunde seine Informationen oder sein Konto löscht, werden alle Abrechnungsdaten nur für die gesetzliche steuerliche Dauer archiviert, die erstellten Inhalte jedoch unwiderruflich gelöscht.",
'cgv.art9_item2': "Der Kunde kann keinen Anspruch auf Entschädigung für Datenverlust infolge einer freiwilligen Löschung geltend machen.",
'cgv.art10_title': "Artikel 10: Streitigkeiten und anwendbares Recht",
'cgv.art10_text': "Diese AVB unterliegen französischem Recht. Im Streitfall wird vor gerichtlichen Schritten eine gütliche Einigung angestrebt. Kommt keine Einigung zustande, wird die ausschließliche Zuständigkeit den zuständigen Gerichten zugewiesen.",
},
}; };
export type TranslationKey = keyof typeof translations.fr; export type TranslationKey = keyof typeof translations.fr;

68
src/lib/streak.ts Normal file
View File

@@ -0,0 +1,68 @@
import { prisma } from './prisma';
/**
* Updates the user's writing streak based on their last write date.
* Should be called whenever a user performs a writing action (e.g., saving a chapter).
*/
export async function updateWritingStreak(userId: string, wordDelta: number = 0) {
try {
const user = await prisma.user.findUnique({
where: { id: userId },
select: { writingStreak: true, lastWriteDate: true, dailyWordCount: true }
}) as any;
if (!user) return;
const now = new Date();
const today = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()));
let newStreak = user.writingStreak;
let lastWrite = user.lastWriteDate ? new Date(user.lastWriteDate) : null;
let newDailyCount = user.dailyWordCount || 0;
if (lastWrite) {
const lastWriteDay = new Date(Date.UTC(lastWrite.getFullYear(), lastWrite.getMonth(), lastWrite.getDate()));
// Calculate difference in days
const diffTime = today.getTime() - lastWriteDay.getTime();
const diffDays = Math.round(diffTime / (1000 * 60 * 60 * 24));
if (diffDays === 0) {
// Already wrote today, increment daily count
newDailyCount += wordDelta;
} else if (diffDays === 1) {
// Wrote yesterday, increment streak, reset daily count to the new delta
newStreak += 1;
newDailyCount = wordDelta;
} else {
// Missed a day (or more), reset streak to 1, reset daily count to the new delta
newStreak = 1;
newDailyCount = wordDelta;
}
} else {
// First time writing
newStreak = 1;
newDailyCount = wordDelta;
}
// Update database
const updatedUser = await prisma.user.update({
where: { id: userId },
data: {
writingStreak: newStreak,
lastWriteDate: now,
dailyWordCount: newDailyCount
},
select: {
writingStreak: true,
dailyWordCount: true,
lastWriteDate: true
}
});
return updatedUser;
} catch (error) {
console.error('Failed to update writing streak:', error);
return null;
}
}

10
src/lib/stripe.ts Normal file
View File

@@ -0,0 +1,10 @@
import Stripe from 'stripe';
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error('STRIPE_SECRET_KEY is not defined');
}
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2026-02-25.clover', // Update to match types
typescript: true,
});

View File

@@ -41,6 +41,7 @@ export interface Entity {
id: string; id: string;
type: EntityType; type: EntityType;
name: string; name: string;
avatar?: string;
description: string; description: string;
details: string; details: string;
storyContext?: string; storyContext?: string;
@@ -53,6 +54,7 @@ export interface Chapter {
title: string; title: string;
content: string; content: string;
summary?: string; summary?: string;
orderIndex?: number;
} }
export type PlotNodeType = 'story' | 'dialogue' | 'action'; export type PlotNodeType = 'story' | 'dialogue' | 'action';
@@ -155,13 +157,23 @@ export interface UserUsage {
} }
export interface UserPreferences { export interface UserPreferences {
theme: 'light' | 'dark' | 'sepia'; theme: 'light' | 'dark' | 'sepia' | 'custom';
customColors?: {
bg: string;
panel: string;
text: string;
accent: string;
};
dailyWordGoal: number; dailyWordGoal: number;
language: 'fr' | 'en'; language: 'fr' | 'en';
customApiProvider?: 'openai' | 'anthropic' | 'gemini';
// We can store the masked key or omit it from the client
customApiKey?: string;
} }
export interface UserStats { export interface UserStats {
totalWordsWritten: number; totalWordsWritten: number;
dailyWordCount: number;
writingStreak: number; writingStreak: number;
lastWriteDate: number; lastWriteDate: number;
} }
@@ -176,6 +188,8 @@ export interface UserProfile {
usage: UserUsage; usage: UserUsage;
preferences: UserPreferences; preferences: UserPreferences;
stats: UserStats; stats: UserStats;
customApiProvider?: 'openai' | 'anthropic' | 'gemini';
customApiKey?: string;
} }
export type ViewMode = 'write' | 'world_building' | 'workflow' | 'settings' | 'preview' | 'ideas' | 'landing' | 'features' | 'pricing' | 'checkout' | 'dashboard' | 'auth' | 'signup' | 'profile'; export type ViewMode = 'write' | 'world_building' | 'workflow' | 'settings' | 'preview' | 'ideas' | 'landing' | 'features' | 'pricing' | 'checkout' | 'dashboard' | 'auth' | 'signup' | 'profile';

View File

@@ -28,13 +28,19 @@ export const LanguageProvider: React.FC<{ children: React.ReactNode }> = ({ chil
localStorage.setItem('pluume_language', lang); localStorage.setItem('pluume_language', lang);
}; };
const t = (key: TranslationKey): string => { const t = React.useCallback((key: TranslationKey): string => {
if (!isMounted) return translations.fr[key] || key; // SSR fallback if (!isMounted) return translations.fr[key] || key; // SSR fallback
return translations[language][key] || key; return translations[language][key] || key;
}; }, [isMounted, language]);
const contextValue = React.useMemo(() => ({
language,
setLanguage,
t
}), [language, t]);
return ( return (
<LanguageContext.Provider value={{ language, setLanguage, t }}> <LanguageContext.Provider value={contextValue}>
{children} {children}
</LanguageContext.Provider> </LanguageContext.Provider>
); );

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { BookProject, UserProfile, Entity, EntityType } from '@/lib/types'; import { BookProject, UserProfile, Entity, EntityType, Idea } from '@/lib/types';
interface ProjectContextType { interface ProjectContextType {
project: BookProject; project: BookProject;
@@ -14,8 +14,12 @@ interface ProjectContextType {
createEntity: (type: EntityType, initialData?: Partial<Entity>) => Promise<string>; createEntity: (type: EntityType, initialData?: Partial<Entity>) => Promise<string>;
updateEntity: (entityId: string, data: Partial<Entity>) => Promise<void>; updateEntity: (entityId: string, data: Partial<Entity>) => Promise<void>;
deleteEntity: (entityId: string) => Promise<void>; deleteEntity: (entityId: string) => Promise<void>;
createIdea: (projectId: string, data: Partial<Idea>) => Promise<string>;
updateIdea: (projectId: string, ideaId: string, data: Partial<Idea>) => Promise<void>;
deleteIdea: (projectId: string, ideaId: string) => Promise<void>;
deleteProject: () => Promise<void>; deleteProject: () => Promise<void>;
incrementUsage: () => void; incrementUsage: () => void;
refreshProfile: () => Promise<void>;
} }
const ProjectContext = createContext<ProjectContextType | null>(null); const ProjectContext = createContext<ProjectContextType | null>(null);

View File

@@ -7,14 +7,50 @@ export function ThemeProvider({ children }: { children: React.ReactNode }) {
const { user } = useAuthContext(); const { user } = useAuthContext();
useEffect(() => { useEffect(() => {
if (!user) return; // Read from localStorage to apply theme instantly across reloads
const savedTheme = localStorage.getItem('plumeia_theme');
const savedColorsStr = localStorage.getItem('plumeia_custom_colors');
const theme = user.preferences?.theme || 'light'; const theme = savedTheme || user?.preferences?.theme || 'light';
const root = document.documentElement; const root = document.documentElement;
root.classList.remove('theme-light', 'theme-dark', 'theme-sepia'); root.classList.remove('theme-light', 'theme-dark', 'theme-sepia', 'theme-custom');
root.classList.add(`theme-${theme}`); root.classList.add(`theme-${theme}`);
}, [user?.preferences?.theme]);
if (theme === 'custom') {
let colors = user?.preferences?.customColors || {
bg: '#ffffff',
panel: '#f8fafc',
text: '#0f172a',
accent: '#3b82f6'
};
if (savedColorsStr) {
try {
colors = JSON.parse(savedColorsStr);
} catch (e) {
console.error("Failed to parse custom colors", e);
}
}
root.style.setProperty('--theme-bg', colors.bg);
root.style.setProperty('--theme-panel', colors.panel);
root.style.setProperty('--theme-text', colors.text);
// To ensure UI remains legible, we compute a translucent border by default if not strictly provided
root.style.setProperty('--theme-border', colors.text + '20'); // 20% opacity of text color
root.style.setProperty('--theme-muted', colors.text + '99'); // 60% opacity of text color
root.style.setProperty('--theme-accent', colors.accent);
} else {
root.style.removeProperty('--theme-bg');
root.style.removeProperty('--theme-panel');
root.style.removeProperty('--theme-text');
root.style.removeProperty('--theme-border');
root.style.removeProperty('--theme-muted');
root.style.removeProperty('--theme-accent');
}
}, [user?.preferences?.theme, user?.preferences?.customColors]);
return <>{children}</>; return <>{children}</>;
} }

41
test-streak.ts Normal file
View File

@@ -0,0 +1,41 @@
import { updateWritingStreak } from './src/lib/streak';
import { prisma } from './src/lib/prisma';
async function main() {
// 1. Get the first user
const user = await prisma.user.findFirst();
if (!user) {
console.log("No user found.");
return;
}
console.log(`Original streak for ${user.email}:`, user.writingStreak, "Last write date:", user.lastWriteDate);
// 2. Simulate setting lastWriteDate to yesterday
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
await prisma.user.update({
where: { id: user.id },
data: {
lastWriteDate: yesterday,
writingStreak: 5 // let's pretend it was 5
}
});
console.log("Updated lastWriteDate to yesterday, streak is 5.");
// 3. Call our function to update the streak (as if they wrote today)
console.log("Calling updateWritingStreak...");
await updateWritingStreak(user.id);
// 4. Verify the new values
const updatedUser = await prisma.user.findUnique({
where: { id: user.id },
select: { writingStreak: true, lastWriteDate: true }
});
console.log(`New streak:`, updatedUser?.writingStreak, "Last write date:", updatedUser?.lastWriteDate);
}
main().catch(console.error);