create-snapify-app 0.0.0-stage → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (614) hide show
  1. package/LICENSE.md +24 -0
  2. package/README.md +55 -2
  3. package/bin/create-snapify-app.mjs +174 -0
  4. package/lib/args.mjs +59 -0
  5. package/lib/copy.mjs +44 -0
  6. package/lib/env.mjs +84 -0
  7. package/lib/prompt.mjs +63 -0
  8. package/package.json +35 -4
  9. package/template/.env.example +79 -0
  10. package/template/.github/workflows/ci.yml +23 -0
  11. package/template/.github/workflows/deploy.yml +26 -0
  12. package/template/.github/workflows/ping.yml +8 -0
  13. package/template/.github/workflows/release.yml +67 -0
  14. package/template/.snapify-install.json +5 -0
  15. package/template/CHANGELOG.md +28 -0
  16. package/template/LICENSE +14 -0
  17. package/template/README.md +287 -0
  18. package/template/administrator.html +23 -0
  19. package/template/apps/client/src/App.tsx +765 -0
  20. package/template/apps/client/src/auth.css +400 -0
  21. package/template/apps/client/src/lib/api.ts +22 -0
  22. package/template/apps/client/src/main.tsx +20 -0
  23. package/template/apps/community/src/App.tsx +592 -0
  24. package/template/apps/community/src/main.tsx +20 -0
  25. package/template/apps/community/src/styles.css +24 -0
  26. package/template/apps/developers/src/App.tsx +643 -0
  27. package/template/apps/developers/src/main.tsx +17 -0
  28. package/template/apps/developers/src/pages.tsx +1587 -0
  29. package/template/apps/developers/src/styles.css +98 -0
  30. package/template/apps/developers/src/ui.tsx +179 -0
  31. package/template/apps/landing/src/App.tsx +598 -0
  32. package/template/apps/landing/src/LegalPages.tsx +158 -0
  33. package/template/apps/landing/src/OrderPage.tsx +381 -0
  34. package/template/apps/landing/src/SeoPages.tsx +923 -0
  35. package/template/apps/landing/src/copy.ts +195 -0
  36. package/template/apps/landing/src/main.tsx +20 -0
  37. package/template/apps/landing/src/styles.css +803 -0
  38. package/template/apps/landing/src/types.ts +19 -0
  39. package/template/apps/marketplace/src/App.tsx +1229 -0
  40. package/template/apps/marketplace/src/developer/DeveloperApp.tsx +1266 -0
  41. package/template/apps/marketplace/src/main.tsx +20 -0
  42. package/template/apps/marketplace/src/styles.css +1001 -0
  43. package/template/apps/shared/AuthContext.tsx +65 -0
  44. package/template/apps/shared/Logo.tsx +33 -0
  45. package/template/apps/shared/api.ts +47 -0
  46. package/template/apps/shared/ui.tsx +128 -0
  47. package/template/apps/superadmin/src/App.tsx +1264 -0
  48. package/template/apps/superadmin/src/lib/api.ts +20 -0
  49. package/template/apps/superadmin/src/lib/hooks.ts +20 -0
  50. package/template/apps/superadmin/src/main.tsx +16 -0
  51. package/template/apps/superadmin/src/pages/ClientsPage.tsx +185 -0
  52. package/template/apps/superadmin/src/pages/InvoicesPage.tsx +157 -0
  53. package/template/apps/superadmin/src/pages/NewsletterPage.tsx +212 -0
  54. package/template/apps/superadmin/src/pages/OrdersPage.tsx +195 -0
  55. package/template/apps/superadmin/src/pages/PaymentsPage.tsx +150 -0
  56. package/template/apps/superadmin/src/pages/PayoutsPage.tsx +133 -0
  57. package/template/apps/superadmin/src/pages/PublishersPage.tsx +151 -0
  58. package/template/apps/superadmin/src/pages/SeoControlPage.tsx +692 -0
  59. package/template/apps/superadmin/src/pages/SettingsPage.tsx +199 -0
  60. package/template/apps/superadmin/src/pages/SystemPage.tsx +109 -0
  61. package/template/apps/superadmin/src/pages/TicketsPage.tsx +201 -0
  62. package/template/apps/superadmin/src/pages/UsersPage.tsx +167 -0
  63. package/template/client.html +21 -0
  64. package/template/community/hamdiayari/akhbar/README.md +23 -0
  65. package/template/community/hamdiayari/akhbar/screenshot.jpg +0 -0
  66. package/template/community/hamdiayari/akhbar/snapify.theme.json +76 -0
  67. package/template/community/hamdiayari/akhbar/src/Brand.tsx +26 -0
  68. package/template/community/hamdiayari/akhbar/src/Footer.tsx +182 -0
  69. package/template/community/hamdiayari/akhbar/src/Header.tsx +191 -0
  70. package/template/community/hamdiayari/akhbar/src/Layout.tsx +49 -0
  71. package/template/community/hamdiayari/akhbar/src/Social.tsx +33 -0
  72. package/template/community/hamdiayari/akhbar/src/Ticker.tsx +86 -0
  73. package/template/community/hamdiayari/akhbar/src/Views.tsx +285 -0
  74. package/template/community/hamdiayari/akhbar/src/blocks/CollectionBlocks.tsx +672 -0
  75. package/template/community/hamdiayari/akhbar/src/blocks/ContentBlocks.tsx +305 -0
  76. package/template/community/hamdiayari/akhbar/src/blocks/ConversionBlocks.tsx +569 -0
  77. package/template/community/hamdiayari/akhbar/src/blocks/HeaderBlocks.tsx +287 -0
  78. package/template/community/hamdiayari/akhbar/src/index.tsx +51 -0
  79. package/template/community/hamdiayari/akhbar/src/kit.tsx +357 -0
  80. package/template/community/hamdiayari/akhbar/src/site.ts +96 -0
  81. package/template/community/hamdiayari/akhbar/src/styles.css +566 -0
  82. package/template/community/hamdiayari/akhbar-actualites/README.md +28 -0
  83. package/template/community/hamdiayari/akhbar-actualites/screenshot.jpg +0 -0
  84. package/template/community/hamdiayari/akhbar-actualites/snapify.module.json +107 -0
  85. package/template/community/hamdiayari/akhbar-actualites/src/admin/AudiencePage.tsx +185 -0
  86. package/template/community/hamdiayari/akhbar-actualites/src/admin/index.tsx +11 -0
  87. package/template/community/hamdiayari/akhbar-actualites/src/public/Archive.tsx +142 -0
  88. package/template/community/hamdiayari/akhbar-actualites/src/public/cards.tsx +152 -0
  89. package/template/community/hamdiayari/akhbar-actualites/src/public/index.tsx +71 -0
  90. package/template/community/hamdiayari/akhbar-actualites/src/public/posts.ts +103 -0
  91. package/template/community/hamdiayari/akhbar-actualites/src/public/styles.css +568 -0
  92. package/template/community/hamdiayari/akhbar-actualites/src/public/widgets.tsx +226 -0
  93. package/template/community/hamdiayari/akhbar-actualites/src/server.ts +119 -0
  94. package/template/community/hamdiayari/akhbar-actualites/src/types.ts +25 -0
  95. package/template/community/hamdiayari/akhbar-urgent/README.md +36 -0
  96. package/template/community/hamdiayari/akhbar-urgent/screenshot.jpg +0 -0
  97. package/template/community/hamdiayari/akhbar-urgent/snapify.module.json +64 -0
  98. package/template/community/hamdiayari/akhbar-urgent/src/admin/FlashesPage.tsx +229 -0
  99. package/template/community/hamdiayari/akhbar-urgent/src/admin/index.tsx +11 -0
  100. package/template/community/hamdiayari/akhbar-urgent/src/public/Ticker.tsx +81 -0
  101. package/template/community/hamdiayari/akhbar-urgent/src/public/index.tsx +63 -0
  102. package/template/community/hamdiayari/akhbar-urgent/src/public/styles.css +142 -0
  103. package/template/community/hamdiayari/akhbar-urgent/src/server.ts +101 -0
  104. package/template/community/hamdiayari/akhbar-urgent/src/types.ts +35 -0
  105. package/template/community.html +21 -0
  106. package/template/developers.html +24 -0
  107. package/template/docker-compose.db.yml +24 -0
  108. package/template/docs/BACKOFFICE_SNAPIFY.md +130 -0
  109. package/template/docs/CI_CD.md +66 -0
  110. package/template/docs/MODULES.md +346 -0
  111. package/template/docs/PLATFORM_DEPLOY.md +54 -0
  112. package/template/docs/logo.svg +10 -0
  113. package/template/drizzle/0000_init.sql +147 -0
  114. package/template/drizzle/0001_page_project.sql +5 -0
  115. package/template/drizzle/0002_modules.sql +23 -0
  116. package/template/drizzle/0003_themes.sql +4 -0
  117. package/template/drizzle/0004_security.sql +82 -0
  118. package/template/drizzle/0005_module_records.sql +16 -0
  119. package/template/drizzle/0006_avatars.sql +4 -0
  120. package/template/drizzle/0007_platform.sql +264 -0
  121. package/template/drizzle/0008_mp_validation.sql +3 -0
  122. package/template/drizzle/0009_saas_ops.sql +77 -0
  123. package/template/drizzle/0010_cms_chrome.sql +13 -0
  124. package/template/drizzle/0011_client_portal.sql +26 -0
  125. package/template/drizzle/meta/0000_snapshot.json +998 -0
  126. package/template/drizzle/meta/0001_snapshot.json +1004 -0
  127. package/template/drizzle/meta/_journal.json +90 -0
  128. package/template/drizzle.config.ts +16 -0
  129. package/template/ecosystem.snapify.cjs +24 -0
  130. package/template/for.html +1 -0
  131. package/template/index.html +29 -0
  132. package/template/landing.html +37 -0
  133. package/template/marketplace/catalog.json +373 -0
  134. package/template/marketplace/gallery/hamdiayari.preveris/01-home.jpg +0 -0
  135. package/template/marketplace/gallery/hamdiayari.recaptcha/01-reglages.jpg +0 -0
  136. package/template/marketplace/gallery/hamdiayari.recaptcha/02-logo.jpg +0 -0
  137. package/template/marketplace/gallery/novalabs.ai-articles/01-generer.jpg +0 -0
  138. package/template/marketplace/gallery/novalabs.ai-articles/02-fournisseur-ia.jpg +0 -0
  139. package/template/marketplace/gallery/novalabs.calendar/01-agenda-site.jpg +0 -0
  140. package/template/marketplace/gallery/novalabs.calendar/02-evenements-admin.jpg +0 -0
  141. package/template/marketplace/gallery/novalabs.calendar/03-reglages.jpg +0 -0
  142. package/template/marketplace/screenshots/hamdiayari.preveris-1.0.6.jpg +0 -0
  143. package/template/marketplace/screenshots/hamdiayari.recaptcha-1.0.0.jpg +0 -0
  144. package/template/marketplace/screenshots/hamdiayari.wp-seo-migrator-1.1.1.jpg +0 -0
  145. package/template/marketplace/screenshots/novalabs.ai-articles-1.0.1.jpg +0 -0
  146. package/template/marketplace/screenshots/novalabs.ai-chat-1.0.1.jpg +0 -0
  147. package/template/marketplace/screenshots/novalabs.calendar-1.0.1.jpg +0 -0
  148. package/template/marketplace/screenshots/novalabs.cart-1.0.0.jpg +0 -0
  149. package/template/marketplace/screenshots/novalabs.checkout-1.0.0.jpg +0 -0
  150. package/template/marketplace/screenshots/novalabs.customers-1.1.0.jpg +0 -0
  151. package/template/marketplace/screenshots/novalabs.dogga-1.0.0.jpg +0 -0
  152. package/template/marketplace/screenshots/novalabs.mahdia-1.0.0.jpg +0 -0
  153. package/template/marketplace/screenshots/novalabs.shop-1.0.0.jpg +0 -0
  154. package/template/marketplace/screenshots/novalabs.sidibou-1.0.0.jpg +0 -0
  155. package/template/marketplace/screenshots/novalabs.whatsapp-1.0.1.jpg +0 -0
  156. package/template/marketplace.html +21 -0
  157. package/template/metadata.json +7 -0
  158. package/template/modules/ai-articles/screenshot.jpg +0 -0
  159. package/template/modules/ai-articles/snapify.module.json +74 -0
  160. package/template/modules/ai-articles/src/admin/ArticlesPage.tsx +162 -0
  161. package/template/modules/ai-articles/src/admin/GeneratePage.tsx +118 -0
  162. package/template/modules/ai-articles/src/admin/index.tsx +12 -0
  163. package/template/modules/ai-articles/src/public/index.tsx +119 -0
  164. package/template/modules/ai-articles/src/server.ts +287 -0
  165. package/template/modules/ai-articles/src/types.ts +30 -0
  166. package/template/modules/ai-chat/screenshot.jpg +0 -0
  167. package/template/modules/ai-chat/snapify.module.json +217 -0
  168. package/template/modules/ai-chat/src/public.tsx +198 -0
  169. package/template/modules/ai-chat/src/server.ts +142 -0
  170. package/template/modules/ai-theme-studio/README.md +15 -0
  171. package/template/modules/ai-theme-studio/screenshot.jpg +0 -0
  172. package/template/modules/ai-theme-studio/snapify.module.json +175 -0
  173. package/template/modules/ai-theme-studio/src/admin/HistoryPage.tsx +91 -0
  174. package/template/modules/ai-theme-studio/src/admin/StudioPage.tsx +414 -0
  175. package/template/modules/ai-theme-studio/src/admin/index.tsx +11 -0
  176. package/template/modules/ai-theme-studio/src/lib/images.ts +240 -0
  177. package/template/modules/ai-theme-studio/src/lib/llm.ts +123 -0
  178. package/template/modules/ai-theme-studio/src/lib/normalize.ts +714 -0
  179. package/template/modules/ai-theme-studio/src/lib/prompts.ts +198 -0
  180. package/template/modules/ai-theme-studio/src/server.ts +137 -0
  181. package/template/modules/ai-theme-studio/src/types.ts +112 -0
  182. package/template/modules/calendar/screenshot.jpg +0 -0
  183. package/template/modules/calendar/snapify.module.json +45 -0
  184. package/template/modules/calendar/src/admin/EventsPage.tsx +217 -0
  185. package/template/modules/calendar/src/admin/index.tsx +11 -0
  186. package/template/modules/calendar/src/public/index.tsx +231 -0
  187. package/template/modules/calendar/src/server.ts +143 -0
  188. package/template/modules/calendar/src/types.ts +25 -0
  189. package/template/modules/cart/screenshot.jpg +0 -0
  190. package/template/modules/cart/snapify.module.json +90 -0
  191. package/template/modules/cart/src/admin/index.tsx +265 -0
  192. package/template/modules/cart/src/public/CartPage.tsx +198 -0
  193. package/template/modules/cart/src/public/index.tsx +261 -0
  194. package/template/modules/cart/src/public/useQuote.ts +67 -0
  195. package/template/modules/cart/src/server.ts +148 -0
  196. package/template/modules/cart/src/types.ts +31 -0
  197. package/template/modules/checkout/screenshot.jpg +0 -0
  198. package/template/modules/checkout/snapify.module.json +264 -0
  199. package/template/modules/checkout/src/admin/OrderDetailPage.tsx +304 -0
  200. package/template/modules/checkout/src/admin/badges.tsx +28 -0
  201. package/template/modules/checkout/src/admin/index.tsx +236 -0
  202. package/template/modules/checkout/src/public/CheckoutPage.tsx +461 -0
  203. package/template/modules/checkout/src/public/OrderPages.tsx +153 -0
  204. package/template/modules/checkout/src/public/index.tsx +23 -0
  205. package/template/modules/checkout/src/server.ts +383 -0
  206. package/template/modules/checkout/src/types.ts +70 -0
  207. package/template/modules/customers/screenshot.jpg +0 -0
  208. package/template/modules/customers/snapify.module.json +115 -0
  209. package/template/modules/customers/src/admin/CustomerDetailPage.tsx +274 -0
  210. package/template/modules/customers/src/admin/index.tsx +194 -0
  211. package/template/modules/customers/src/public/AccountPages.tsx +470 -0
  212. package/template/modules/customers/src/public/AddressForm.tsx +115 -0
  213. package/template/modules/customers/src/public/AuthPages.tsx +349 -0
  214. package/template/modules/customers/src/public/Avatar.tsx +29 -0
  215. package/template/modules/customers/src/public/ProfileSection.tsx +291 -0
  216. package/template/modules/customers/src/public/index.tsx +137 -0
  217. package/template/modules/customers/src/public/session.ts +51 -0
  218. package/template/modules/customers/src/server.ts +421 -0
  219. package/template/modules/customers/src/types.ts +53 -0
  220. package/template/modules/recaptcha/logo.jpg +0 -0
  221. package/template/modules/recaptcha/screenshot.jpg +0 -0
  222. package/template/modules/recaptcha/snapify.module.json +99 -0
  223. package/template/modules/recaptcha/src/public/index.tsx +94 -0
  224. package/template/modules/recaptcha/src/server.ts +65 -0
  225. package/template/modules/recaptcha/src/types.ts +18 -0
  226. package/template/modules/shop/screenshot.jpg +0 -0
  227. package/template/modules/shop/snapify.module.json +340 -0
  228. package/template/modules/shop/src/admin/CategoriesPage.tsx +224 -0
  229. package/template/modules/shop/src/admin/ProductEditor.tsx +313 -0
  230. package/template/modules/shop/src/admin/ProductsPage.tsx +307 -0
  231. package/template/modules/shop/src/admin/index.tsx +14 -0
  232. package/template/modules/shop/src/public/Catalog.tsx +244 -0
  233. package/template/modules/shop/src/public/NotFound.tsx +22 -0
  234. package/template/modules/shop/src/public/ProductPage.tsx +195 -0
  235. package/template/modules/shop/src/public/index.tsx +134 -0
  236. package/template/modules/shop/src/public/useFetch.ts +26 -0
  237. package/template/modules/shop/src/server.ts +493 -0
  238. package/template/modules/shop/src/shared.ts +41 -0
  239. package/template/modules/shop/src/types.ts +60 -0
  240. package/template/modules/whatsapp/screenshot.jpg +0 -0
  241. package/template/modules/whatsapp/snapify.module.json +121 -0
  242. package/template/modules/whatsapp/src/public.tsx +119 -0
  243. package/template/modules/wp-seo-migrator/ARCHITECTURE.md +113 -0
  244. package/template/modules/wp-seo-migrator/README.md +21 -0
  245. package/template/modules/wp-seo-migrator/fixtures/aioseo-sample.csv +3 -0
  246. package/template/modules/wp-seo-migrator/fixtures/sample-exporter.json +39 -0
  247. package/template/modules/wp-seo-migrator/fixtures/sample-page.html +12 -0
  248. package/template/modules/wp-seo-migrator/fixtures/sample.htaccess +3 -0
  249. package/template/modules/wp-seo-migrator/screenshot.jpg +0 -0
  250. package/template/modules/wp-seo-migrator/snapify.module.json +87 -0
  251. package/template/modules/wp-seo-migrator/src/admin/HistoryPage.tsx +64 -0
  252. package/template/modules/wp-seo-migrator/src/admin/WizardPage.tsx +788 -0
  253. package/template/modules/wp-seo-migrator/src/admin/i18n.ts +156 -0
  254. package/template/modules/wp-seo-migrator/src/admin/index.tsx +11 -0
  255. package/template/modules/wp-seo-migrator/src/connectors/CUSTOM_CONNECTOR.md +31 -0
  256. package/template/modules/wp-seo-migrator/src/connectors/csvSeoFactory.ts +106 -0
  257. package/template/modules/wp-seo-migrator/src/connectors/exporter.ts +96 -0
  258. package/template/modules/wp-seo-migrator/src/connectors/gscCsv.ts +53 -0
  259. package/template/modules/wp-seo-migrator/src/connectors/htaccess.ts +65 -0
  260. package/template/modules/wp-seo-migrator/src/connectors/htmlCrawler.ts +90 -0
  261. package/template/modules/wp-seo-migrator/src/connectors/index.ts +113 -0
  262. package/template/modules/wp-seo-migrator/src/connectors/pluginCsv.ts +77 -0
  263. package/template/modules/wp-seo-migrator/src/connectors/rankMathCsv.ts +77 -0
  264. package/template/modules/wp-seo-migrator/src/connectors/redirection.ts +62 -0
  265. package/template/modules/wp-seo-migrator/src/connectors/sitemap.ts +44 -0
  266. package/template/modules/wp-seo-migrator/src/connectors/utils.ts +111 -0
  267. package/template/modules/wp-seo-migrator/src/connectors/wxr.ts +103 -0
  268. package/template/modules/wp-seo-migrator/src/connectors/yoastCsv.ts +56 -0
  269. package/template/modules/wp-seo-migrator/src/pipeline/analyze.ts +158 -0
  270. package/template/modules/wp-seo-migrator/src/pipeline/htmlParse.ts +58 -0
  271. package/template/modules/wp-seo-migrator/src/pipeline/remoteAuto.ts +328 -0
  272. package/template/modules/wp-seo-migrator/src/server.ts +492 -0
  273. package/template/modules/wp-seo-migrator/src/types.ts +151 -0
  274. package/template/modules/wp-seo-migrator/tests/connectors-extra.test.ts +39 -0
  275. package/template/modules/wp-seo-migrator/tests/merge.test.ts +25 -0
  276. package/template/package-lock.json +6016 -0
  277. package/template/package.json +74 -0
  278. package/template/public/images/photos/caravane-chameaux-desert.webp +0 -0
  279. package/template/public/images/photos/hammamet-medina-plage.jpg +0 -0
  280. package/template/public/images/photos/maison-campagne-collines.webp +0 -0
  281. package/template/public/images/photos/maisons-troglodytes-desert.jpg +0 -0
  282. package/template/public/images/photos/panorama-ville.jpg +0 -0
  283. package/template/public/images/photos/preveris-accessibilite-erp.jpg +0 -0
  284. package/template/public/images/photos/preveris-collectivites.jpg +0 -0
  285. package/template/public/images/photos/preveris-etudes-plans.jpg +0 -0
  286. package/template/public/images/photos/preveris-hero-erp.jpg +0 -0
  287. package/template/public/images/photos/preveris-ssi-incendie.jpg +0 -0
  288. package/template/public/images/photos/preveris-suivi-hotel.jpg +0 -0
  289. package/template/public/images/photos/ruelle-bleue-medina.jpg +0 -0
  290. package/template/public/images/photos/sidi-bou-said-port.webp +0 -0
  291. package/template/public/images/photos/terrasse-blanche-mer-voilier.jpg +0 -0
  292. package/template/public/landing/hero-ai-cms.jpg +0 -0
  293. package/template/public/landing/shot-dogga.jpg +0 -0
  294. package/template/public/landing/shot-mahdia.jpg +0 -0
  295. package/template/public/landing/shot-sidibou.jpg +0 -0
  296. package/template/public/llms.txt +35 -0
  297. package/template/public/logo.svg +10 -0
  298. package/template/scripts/app-pack.ts +155 -0
  299. package/template/scripts/app-publish.ts +123 -0
  300. package/template/scripts/apply-ecommerce-home.ts +196 -0
  301. package/template/scripts/auto-deploy-poll.sh +13 -0
  302. package/template/scripts/db-backup.sh +16 -0
  303. package/template/scripts/db-restore.sh +11 -0
  304. package/template/scripts/deploy.sh +34 -0
  305. package/template/scripts/github-webhook-deploy.sh +7 -0
  306. package/template/scripts/install-new-modules.ts +96 -0
  307. package/template/scripts/refresh-demo-baseline.ts +9 -0
  308. package/template/scripts/reinstall-marketplace-packs.ts +51 -0
  309. package/template/scripts/restore-classic-home.ts +13 -0
  310. package/template/scripts/sdk-pack.ts +289 -0
  311. package/template/scripts/sdk-publish.ts +75 -0
  312. package/template/scripts/seed-demo-products.ts +120 -0
  313. package/template/scripts/seed-theme-content-packs.ts +42 -0
  314. package/template/scripts/snapify-module.ts +207 -0
  315. package/template/scripts/test-theme-switch.ts +24 -0
  316. package/template/sdk/admin.css +18 -0
  317. package/template/sdk/admin.ts +95 -0
  318. package/template/sdk/build.ts +249 -0
  319. package/template/sdk/commerce.ts +512 -0
  320. package/template/sdk/content.ts +76 -0
  321. package/template/sdk/icons.ts +101 -0
  322. package/template/sdk/image.ts +60 -0
  323. package/template/sdk/index.ts +500 -0
  324. package/template/sdk/package.ts +298 -0
  325. package/template/sdk/recaptcha.tsx +122 -0
  326. package/template/sdk/settings.ts +84 -0
  327. package/template/sdk/theme.css +29 -0
  328. package/template/sdk/theme.ts +100 -0
  329. package/template/sdk/ui/commerce.tsx +385 -0
  330. package/template/sdk/ui/orders.tsx +136 -0
  331. package/template/sdk-kit/CHANGELOG.md +17 -0
  332. package/template/sdk-kit/LICENSE.md +47 -0
  333. package/template/sdk-kit/README.md +81 -0
  334. package/template/sdk-kit/cli.ts +339 -0
  335. package/template/sdk-kit/create-snapify/README.md +28 -0
  336. package/template/sdk-kit/create-snapify/create-snapify.mjs +11 -0
  337. package/template/sdk-kit/create-snapify/package.template.json +16 -0
  338. package/template/sdk-kit/package.template.json +95 -0
  339. package/template/sdk-kit/templates/common/README.md.tpl +27 -0
  340. package/template/sdk-kit/templates/common/_gitignore +3 -0
  341. package/template/sdk-kit/templates/common/package.json.tpl +19 -0
  342. package/template/sdk-kit/templates/common/src/snapify-env.d.ts +2 -0
  343. package/template/sdk-kit/templates/common/tsconfig.json +15 -0
  344. package/template/sdk-kit/templates/module/snapify.module.json.tpl +33 -0
  345. package/template/sdk-kit/templates/module/src/admin.tsx +53 -0
  346. package/template/sdk-kit/templates/module/src/public.tsx +49 -0
  347. package/template/sdk-kit/templates/module/src/server.ts +30 -0
  348. package/template/sdk-kit/templates/module/src/types.ts +19 -0
  349. package/template/sdk-kit/templates/theme/snapify.theme.json.tpl +22 -0
  350. package/template/sdk-kit/templates/theme/src/Footer.tsx +40 -0
  351. package/template/sdk-kit/templates/theme/src/Header.tsx +47 -0
  352. package/template/sdk-kit/templates/theme/src/Views.tsx +36 -0
  353. package/template/sdk-kit/templates/theme/src/blocks.tsx +93 -0
  354. package/template/sdk-kit/templates/theme/src/index.tsx +30 -0
  355. package/template/sdk-kit/templates/theme/src/site.tsx +59 -0
  356. package/template/sdk-kit/templates/theme/src/styles.css +71 -0
  357. package/template/server/client/routes.ts +252 -0
  358. package/template/server/cms/analyticsStore.ts +180 -0
  359. package/template/server/cms/auth.ts +155 -0
  360. package/template/server/cms/blockCatalog.ts +342 -0
  361. package/template/server/cms/blocks.ts +521 -0
  362. package/template/server/cms/contentMigration.ts +1119 -0
  363. package/template/server/cms/defaults.ts +303 -0
  364. package/template/server/cms/permissions.ts +154 -0
  365. package/template/server/cms/restoreClassicHome.ts +183 -0
  366. package/template/server/cms/revisions.ts +99 -0
  367. package/template/server/cms/scheduler.ts +78 -0
  368. package/template/server/cms/seed.ts +72 -0
  369. package/template/server/cms/seoAnalyzer.ts +213 -0
  370. package/template/server/cms/store.ts +122 -0
  371. package/template/server/cms/themeContent.ts +1386 -0
  372. package/template/server/cms/themePacks/sidibou-branding.json +1 -0
  373. package/template/server/cms/themePacks/sidibou-home.json +1 -0
  374. package/template/server/cms/themePacks/sidibou-menus.json +1 -0
  375. package/template/server/cms/types.ts +432 -0
  376. package/template/server/community/routes.ts +410 -0
  377. package/template/server/db/client.ts +43 -0
  378. package/template/server/db/repository.ts +389 -0
  379. package/template/server/db/schema.ts +726 -0
  380. package/template/server/demo/cmsReset.ts +196 -0
  381. package/template/server/developers/sdk.ts +85 -0
  382. package/template/server/geoip.ts +15 -0
  383. package/template/server/http.ts +53 -0
  384. package/template/server/mail.ts +216 -0
  385. package/template/server/marketplace/catalog.ts +74 -0
  386. package/template/server/marketplace/packageValidate.ts +475 -0
  387. package/template/server/marketplace/payments.ts +193 -0
  388. package/template/server/marketplace/routes.ts +850 -0
  389. package/template/server/marketplace/signing.ts +116 -0
  390. package/template/server/marketplace/syncOfficial.ts +282 -0
  391. package/template/server/media/images.ts +63 -0
  392. package/template/server/middleware/adminAuth.ts +134 -0
  393. package/template/server/modules/legacy.ts +44 -0
  394. package/template/server/modules/manager.ts +1122 -0
  395. package/template/server/modules/marketplace.ts +178 -0
  396. package/template/server/modules/outbound.ts +178 -0
  397. package/template/server/modules/purchases.ts +149 -0
  398. package/template/server/modules/records.ts +168 -0
  399. package/template/server/modules/themeHtml.ts +32 -0
  400. package/template/server/modules/trust.ts +16 -0
  401. package/template/server/modules/types.ts +147 -0
  402. package/template/server/platform/auth.ts +223 -0
  403. package/template/server/platform/clientWelcome.ts +168 -0
  404. package/template/server/platform/clients.ts +139 -0
  405. package/template/server/platform/dailyImprove.ts +356 -0
  406. package/template/server/platform/flouci.ts +126 -0
  407. package/template/server/platform/host.ts +69 -0
  408. package/template/server/platform/hostingProviders.ts +143 -0
  409. package/template/server/platform/licenseOrders.ts +291 -0
  410. package/template/server/platform/money.ts +42 -0
  411. package/template/server/platform/passwords.ts +41 -0
  412. package/template/server/platform/region.ts +86 -0
  413. package/template/server/platform/routes.ts +225 -0
  414. package/template/server/platform/seed.ts +82 -0
  415. package/template/server/platform/settings.ts +72 -0
  416. package/template/server/routes/adminAccount.ts +403 -0
  417. package/template/server/routes/adminCms.ts +808 -0
  418. package/template/server/routes/adminSystem.ts +243 -0
  419. package/template/server/routes/adminUsers.ts +449 -0
  420. package/template/server/routes/googleOAuth.ts +327 -0
  421. package/template/server/routes/modules.ts +323 -0
  422. package/template/server/routes/publicCms.ts +313 -0
  423. package/template/server/routes/seoPublic.ts +158 -0
  424. package/template/server/security/accounts.ts +121 -0
  425. package/template/server/security/audit.ts +143 -0
  426. package/template/server/security/authTokens.ts +74 -0
  427. package/template/server/security/ip.ts +68 -0
  428. package/template/server/security/rateLimit.ts +85 -0
  429. package/template/server/security/retention.ts +46 -0
  430. package/template/server/security/secrets.ts +44 -0
  431. package/template/server/security/sessions.ts +140 -0
  432. package/template/server/security/totp.ts +101 -0
  433. package/template/server/superadmin/analytics.ts +109 -0
  434. package/template/server/superadmin/auth.ts +92 -0
  435. package/template/server/superadmin/backlinksCatalog.ts +210 -0
  436. package/template/server/superadmin/frSeoContent.ts +165 -0
  437. package/template/server/superadmin/landingSeo.ts +675 -0
  438. package/template/server/superadmin/ops.ts +238 -0
  439. package/template/server/superadmin/routes.ts +1095 -0
  440. package/template/server/superadmin/tnSeoContent.ts +130 -0
  441. package/template/server.ts +522 -0
  442. package/template/src/App.tsx +263 -0
  443. package/template/src/admin/AdminApp.tsx +162 -0
  444. package/template/src/admin/account/AuthLayout.tsx +57 -0
  445. package/template/src/admin/account/SecurityForms.tsx +276 -0
  446. package/template/src/admin/admin.css +127 -0
  447. package/template/src/admin/context/AuthContext.tsx +147 -0
  448. package/template/src/admin/context/SiteContext.tsx +50 -0
  449. package/template/src/admin/editor/BlockLibrary.tsx +173 -0
  450. package/template/src/admin/editor/BlocksPanel.tsx +396 -0
  451. package/template/src/admin/editor/EditorBits.tsx +106 -0
  452. package/template/src/admin/editor/PreviewFrame.tsx +145 -0
  453. package/template/src/admin/editor/RichTextEditor.tsx +276 -0
  454. package/template/src/admin/editor/SeoPanel.tsx +349 -0
  455. package/template/src/admin/editor/StyleEditor.tsx +114 -0
  456. package/template/src/admin/editor/blockSchemas.ts +699 -0
  457. package/template/src/admin/editor/fields.tsx +486 -0
  458. package/template/src/admin/layout/AdminShell.tsx +418 -0
  459. package/template/src/admin/layout/nav.ts +95 -0
  460. package/template/src/admin/lib/api.ts +408 -0
  461. package/template/src/admin/lib/audit.ts +141 -0
  462. package/template/src/admin/lib/format.ts +84 -0
  463. package/template/src/admin/lib/hooks.ts +125 -0
  464. package/template/src/admin/main.tsx +16 -0
  465. package/template/src/admin/modules/InstallDialog.tsx +160 -0
  466. package/template/src/admin/modules/SettingsForm.tsx +145 -0
  467. package/template/src/admin/modules/kit.tsx +127 -0
  468. package/template/src/admin/modules/labels.tsx +226 -0
  469. package/template/src/admin/pages/AccountSetupPage.tsx +62 -0
  470. package/template/src/admin/pages/AnalyticsPage.tsx +206 -0
  471. package/template/src/admin/pages/BrandingPage.tsx +154 -0
  472. package/template/src/admin/pages/DashboardPage.tsx +239 -0
  473. package/template/src/admin/pages/EngagementPage.tsx +168 -0
  474. package/template/src/admin/pages/FormsPage.tsx +435 -0
  475. package/template/src/admin/pages/GeneralPage.tsx +120 -0
  476. package/template/src/admin/pages/LoginPage.tsx +249 -0
  477. package/template/src/admin/pages/MaintenancePage.tsx +125 -0
  478. package/template/src/admin/pages/MarketplacePage.tsx +426 -0
  479. package/template/src/admin/pages/MediaPage.tsx +251 -0
  480. package/template/src/admin/pages/MenusPage.tsx +446 -0
  481. package/template/src/admin/pages/ModuleAdminPage.tsx +113 -0
  482. package/template/src/admin/pages/ModuleDetailPage.tsx +293 -0
  483. package/template/src/admin/pages/ModulesPage.tsx +135 -0
  484. package/template/src/admin/pages/NewsletterPage.tsx +193 -0
  485. package/template/src/admin/pages/PageEditorPage.tsx +322 -0
  486. package/template/src/admin/pages/PagesListPage.tsx +277 -0
  487. package/template/src/admin/pages/PostEditorPage.tsx +292 -0
  488. package/template/src/admin/pages/PostsListPage.tsx +187 -0
  489. package/template/src/admin/pages/ProfilePage.tsx +489 -0
  490. package/template/src/admin/pages/ResetPasswordPage.tsx +110 -0
  491. package/template/src/admin/pages/SeoPage.tsx +670 -0
  492. package/template/src/admin/pages/SettingsPage.tsx +189 -0
  493. package/template/src/admin/pages/ThemeCustomizePage.tsx +170 -0
  494. package/template/src/admin/pages/ThemesPage.tsx +292 -0
  495. package/template/src/admin/pages/UsersPage.tsx +57 -0
  496. package/template/src/admin/pages/WidgetsPage.tsx +200 -0
  497. package/template/src/admin/seo/GoogleSetup.tsx +529 -0
  498. package/template/src/admin/settings/EmailTab.tsx +293 -0
  499. package/template/src/admin/settings/OtherTabs.tsx +270 -0
  500. package/template/src/admin/settings/SecurityTab.tsx +172 -0
  501. package/template/src/admin/settings/controls.tsx +133 -0
  502. package/template/src/admin/ui/Pagination.tsx +170 -0
  503. package/template/src/admin/ui/SaveBar.tsx +77 -0
  504. package/template/src/admin/ui/TagInput.tsx +54 -0
  505. package/template/src/admin/ui/charts.tsx +108 -0
  506. package/template/src/admin/ui/media.tsx +265 -0
  507. package/template/src/admin/ui/overlays.tsx +239 -0
  508. package/template/src/admin/ui/primitives.tsx +544 -0
  509. package/template/src/admin/users/AuditTab.tsx +104 -0
  510. package/template/src/admin/users/RolesTab.tsx +368 -0
  511. package/template/src/admin/users/UsersTab.tsx +723 -0
  512. package/template/src/admin/users/shared.tsx +278 -0
  513. package/template/src/cms/icons.ts +7 -0
  514. package/template/src/components/CookieBanner.tsx +166 -0
  515. package/template/src/components/MaintenanceGate.tsx +66 -0
  516. package/template/src/components/SiteAnalytics.tsx +64 -0
  517. package/template/src/components/SnapifyLogo.tsx +44 -0
  518. package/template/src/components/cms/BlockRenderer.tsx +154 -0
  519. package/template/src/components/cms/WidgetArea.tsx +193 -0
  520. package/template/src/context/SiteConfigContext.tsx +130 -0
  521. package/template/src/demoBase.ts +23 -0
  522. package/template/src/index.css +90 -0
  523. package/template/src/main.tsx +16 -0
  524. package/template/src/modules/ModuleHost.tsx +197 -0
  525. package/template/src/preview.ts +63 -0
  526. package/template/src/runtime.ts +38 -0
  527. package/template/src/themes/ThemeHost.tsx +168 -0
  528. package/template/src/themes/fallback.tsx +71 -0
  529. package/template/src/types.ts +94 -0
  530. package/template/src/views/BlogPostView.tsx +68 -0
  531. package/template/src/views/CmsPageView.tsx +34 -0
  532. package/template/start-pm2.sh +3 -0
  533. package/template/superadmin.html +20 -0
  534. package/template/themes/ai-canvas/screenshot.jpg +0 -0
  535. package/template/themes/ai-canvas/snapify.theme.json +96 -0
  536. package/template/themes/ai-canvas/src/CeeSimulator.tsx +247 -0
  537. package/template/themes/ai-canvas/src/Footer.tsx +190 -0
  538. package/template/themes/ai-canvas/src/Header.tsx +324 -0
  539. package/template/themes/ai-canvas/src/Layout.tsx +56 -0
  540. package/template/themes/ai-canvas/src/NotFound.tsx +58 -0
  541. package/template/themes/ai-canvas/src/Post.tsx +26 -0
  542. package/template/themes/ai-canvas/src/SnapifyLogo.tsx +44 -0
  543. package/template/themes/ai-canvas/src/blocks/CollectionBlocks.tsx +711 -0
  544. package/template/themes/ai-canvas/src/blocks/ContentBlocks.tsx +318 -0
  545. package/template/themes/ai-canvas/src/blocks/ConversionBlocks.tsx +559 -0
  546. package/template/themes/ai-canvas/src/blocks/HeaderBlocks.tsx +341 -0
  547. package/template/themes/ai-canvas/src/index.tsx +54 -0
  548. package/template/themes/ai-canvas/src/kit.tsx +398 -0
  549. package/template/themes/ai-canvas/src/site.ts +29 -0
  550. package/template/themes/ai-canvas/src/styles.css +317 -0
  551. package/template/themes/dogga/screenshot.jpg +0 -0
  552. package/template/themes/dogga/snapify.theme.json +55 -0
  553. package/template/themes/dogga/src/Brand.tsx +41 -0
  554. package/template/themes/dogga/src/Footer.tsx +202 -0
  555. package/template/themes/dogga/src/Header.tsx +239 -0
  556. package/template/themes/dogga/src/Layout.tsx +43 -0
  557. package/template/themes/dogga/src/Views.tsx +71 -0
  558. package/template/themes/dogga/src/blocks/CollectionBlocks.tsx +674 -0
  559. package/template/themes/dogga/src/blocks/ContentBlocks.tsx +307 -0
  560. package/template/themes/dogga/src/blocks/ConversionBlocks.tsx +571 -0
  561. package/template/themes/dogga/src/blocks/HeaderBlocks.tsx +289 -0
  562. package/template/themes/dogga/src/index.tsx +53 -0
  563. package/template/themes/dogga/src/kit.tsx +362 -0
  564. package/template/themes/dogga/src/site.ts +56 -0
  565. package/template/themes/dogga/src/styles.css +396 -0
  566. package/template/themes/mahdia/screenshot.jpg +0 -0
  567. package/template/themes/mahdia/snapify.theme.json +65 -0
  568. package/template/themes/mahdia/src/Brand.tsx +39 -0
  569. package/template/themes/mahdia/src/Footer.tsx +210 -0
  570. package/template/themes/mahdia/src/Header.tsx +291 -0
  571. package/template/themes/mahdia/src/Layout.tsx +44 -0
  572. package/template/themes/mahdia/src/Views.tsx +79 -0
  573. package/template/themes/mahdia/src/blocks/CollectionBlocks.tsx +674 -0
  574. package/template/themes/mahdia/src/blocks/ContentBlocks.tsx +307 -0
  575. package/template/themes/mahdia/src/blocks/ConversionBlocks.tsx +571 -0
  576. package/template/themes/mahdia/src/blocks/HeaderBlocks.tsx +289 -0
  577. package/template/themes/mahdia/src/index.tsx +53 -0
  578. package/template/themes/mahdia/src/kit.tsx +362 -0
  579. package/template/themes/mahdia/src/site.ts +75 -0
  580. package/template/themes/mahdia/src/styles.css +446 -0
  581. package/template/themes/preveris/screenshot.jpg +0 -0
  582. package/template/themes/preveris/snapify.theme.json +126 -0
  583. package/template/themes/preveris/src/Brand.tsx +44 -0
  584. package/template/themes/preveris/src/Footer.tsx +202 -0
  585. package/template/themes/preveris/src/Header.tsx +239 -0
  586. package/template/themes/preveris/src/Layout.tsx +42 -0
  587. package/template/themes/preveris/src/Views.tsx +71 -0
  588. package/template/themes/preveris/src/blocks/CollectionBlocks.tsx +678 -0
  589. package/template/themes/preveris/src/blocks/ContentBlocks.tsx +307 -0
  590. package/template/themes/preveris/src/blocks/ConversionBlocks.tsx +571 -0
  591. package/template/themes/preveris/src/blocks/HeaderBlocks.tsx +299 -0
  592. package/template/themes/preveris/src/index.tsx +53 -0
  593. package/template/themes/preveris/src/kit.tsx +365 -0
  594. package/template/themes/preveris/src/site.ts +79 -0
  595. package/template/themes/preveris/src/styles.css +446 -0
  596. package/template/themes/sidibou/screenshot.jpg +0 -0
  597. package/template/themes/sidibou/snapify.theme.json +21 -0
  598. package/template/themes/sidibou/src/CeeSimulator.tsx +247 -0
  599. package/template/themes/sidibou/src/Footer.tsx +203 -0
  600. package/template/themes/sidibou/src/Header.tsx +324 -0
  601. package/template/themes/sidibou/src/Layout.tsx +53 -0
  602. package/template/themes/sidibou/src/NotFound.tsx +58 -0
  603. package/template/themes/sidibou/src/Post.tsx +26 -0
  604. package/template/themes/sidibou/src/SnapifyLogo.tsx +44 -0
  605. package/template/themes/sidibou/src/blocks/CollectionBlocks.tsx +711 -0
  606. package/template/themes/sidibou/src/blocks/ContentBlocks.tsx +318 -0
  607. package/template/themes/sidibou/src/blocks/ConversionBlocks.tsx +559 -0
  608. package/template/themes/sidibou/src/blocks/HeaderBlocks.tsx +341 -0
  609. package/template/themes/sidibou/src/index.tsx +54 -0
  610. package/template/themes/sidibou/src/kit.tsx +398 -0
  611. package/template/themes/sidibou/src/site.ts +28 -0
  612. package/template/themes/sidibou/src/styles.css +317 -0
  613. package/template/tsconfig.json +33 -0
  614. package/template/vite.config.ts +42 -0
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Snapify
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ * Contact : contact@novalabs.tn · +216 24 734 647
5
+ * Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
6
+ */
7
+ import React, { useState } from 'react';
8
+ import { CheckCircle2, Info, RefreshCw, Send, XCircle } from 'lucide-react';
9
+ import { toast } from 'sonner';
10
+ import { PasswordInput } from '../account/SecurityForms';
11
+ import { useAuth } from '../context/AuthContext';
12
+ import { api, type SystemMeta } from '../lib/api';
13
+ import { fmtDateTime } from '../lib/format';
14
+ import { errorMessage, useAsync } from '../lib/hooks';
15
+ import { TagInput } from '../ui/TagInput';
16
+ import { Pagination, usePaging } from '../ui/Pagination';
17
+ import { Alert, Badge, Button, Field, Input, Section, Segmented, Switch, Table, Td, Th } from '../ui/primitives';
18
+ import { NumberField, type TabProps } from './controls';
19
+
20
+ const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
21
+
22
+ const PRESETS: { label: string; host: string; port: number; security: 'starttls' | 'ssl'; username?: string }[] = [
23
+ { label: 'Gmail / Google Workspace', host: 'smtp.gmail.com', port: 587, security: 'starttls' },
24
+ { label: 'Microsoft 365', host: 'smtp.office365.com', port: 587, security: 'starttls' },
25
+ { label: 'OVHcloud', host: 'ssl0.ovh.net', port: 465, security: 'ssl' },
26
+ { label: 'Brevo', host: 'smtp-relay.brevo.com', port: 587, security: 'starttls' },
27
+ { label: 'Mailgun', host: 'smtp.eu.mailgun.org', port: 587, security: 'starttls' },
28
+ { label: 'SendGrid', host: 'smtp.sendgrid.net', port: 587, security: 'starttls', username: 'apikey' },
29
+ { label: 'Amazon SES', host: 'email-smtp.eu-west-3.amazonaws.com', port: 587, security: 'starttls' },
30
+ ];
31
+
32
+ const DEFAULT_PORT = { starttls: 587, ssl: 465, none: 25 } as const;
33
+
34
+ const SOURCE: Record<SystemMeta['smtpSource'], [React.ComponentProps<typeof Badge>['tone'], string]> = {
35
+ settings: ['success', 'Serveur configuré ici'],
36
+ env: ['info', 'Serveur du fichier .env'],
37
+ none: ['warning', 'Aucun serveur — e-mails non envoyés'],
38
+ };
39
+
40
+ const KIND_LABELS: Record<string, string> = {
41
+ test: 'Test',
42
+ security: 'Sécurité',
43
+ invite: 'Invitation',
44
+ reset: 'Mot de passe',
45
+ notification: 'Notification',
46
+ form: 'Formulaire',
47
+ newsletter: 'Newsletter',
48
+ system: 'Système',
49
+ };
50
+
51
+ function MailLog({ reloadKey }: { reloadKey: number }) {
52
+ const { page, setPage, pageSize, setPageSize } = usePaging('mail-log');
53
+ const log = useAsync(() => api.system.mailLog(page, pageSize), [page, pageSize, reloadKey]);
54
+ return (
55
+ <Section
56
+ title="Journal des e-mails"
57
+ description="Chaque envoi (réussi, échoué ou ignoré faute de serveur) est tracé ici."
58
+ actions={<Button size="sm" variant="ghost" icon={RefreshCw} loading={log.loading} onClick={() => void log.reload()} aria-label="Actualiser" />}
59
+ >
60
+ {log.error ? <Alert tone="danger">{log.error}</Alert> : null}
61
+ {log.data && log.data.items.length === 0 ? (
62
+ <p className="py-6 text-center text-sm text-slate-500">Aucun e-mail envoyé pour le moment.</p>
63
+ ) : (
64
+ <div className="-mx-5 -mb-5">
65
+ <Table>
66
+ <thead>
67
+ <tr>
68
+ <Th>Date</Th>
69
+ <Th>Type</Th>
70
+ <Th>Destinataire</Th>
71
+ <Th>Sujet</Th>
72
+ <Th>Statut</Th>
73
+ </tr>
74
+ </thead>
75
+ <tbody>
76
+ {(log.data?.items ?? []).map((m) => (
77
+ <tr key={m.id}>
78
+ <Td className="whitespace-nowrap text-slate-500">{fmtDateTime(m.ts)}</Td>
79
+ <Td>
80
+ <Badge>{KIND_LABELS[m.kind] ?? m.kind}</Badge>
81
+ </Td>
82
+ <Td className="max-w-[200px] truncate">{m.toAddr}</Td>
83
+ <Td className="max-w-[260px] truncate text-slate-600">{m.subject}</Td>
84
+ <Td>
85
+ {m.status === 'sent' ? (
86
+ <Badge tone="success" dot>
87
+ Envoyé
88
+ </Badge>
89
+ ) : (
90
+ <span title={m.error ?? undefined}>
91
+ <Badge tone={m.status === 'failed' ? 'danger' : 'warning'} dot>
92
+ {m.status === 'failed' ? 'Échec' : 'Ignoré'}
93
+ </Badge>
94
+ </span>
95
+ )}
96
+ </Td>
97
+ </tr>
98
+ ))}
99
+ </tbody>
100
+ </Table>
101
+ {log.data ? <Pagination page={page} pageSize={log.data.pageSize} total={log.data.total} onPage={setPage} onPageSize={setPageSize} noun="e-mails" /> : null}
102
+ </div>
103
+ )}
104
+ </Section>
105
+ );
106
+ }
107
+
108
+ export function EmailTab({ v, set, meta }: TabProps) {
109
+ const { user } = useAuth();
110
+ const smtp = v.smtp;
111
+ const [to, setTo] = useState(user?.email ?? '');
112
+ const [testing, setTesting] = useState(false);
113
+ const [result, setResult] = useState<{ ok: boolean; error?: string } | null>(null);
114
+ const [logKey, setLogKey] = useState(0);
115
+ const [source, label] = SOURCE[meta.smtpSource];
116
+
117
+ const changeSecurity = (security: typeof smtp.security) => {
118
+ const isDefaultPort = Object.values(DEFAULT_PORT).includes(smtp.port as 587);
119
+ set('smtp', { security, ...(isDefaultPort ? { port: DEFAULT_PORT[security] } : {}) });
120
+ };
121
+
122
+ const test = async () => {
123
+ setTesting(true);
124
+ setResult(null);
125
+ try {
126
+ const { password, clearPassword, hasPassword: _h, ...draft } = smtp;
127
+ const r = await api.system.testSmtp(to.trim(), { ...draft, password: password || undefined, clearPassword });
128
+ setResult(r);
129
+ if (r.ok) toast.success(`E-mail de test envoyé à ${to.trim()}`);
130
+ } catch (e) {
131
+ setResult({ ok: false, error: errorMessage(e) });
132
+ } finally {
133
+ setTesting(false);
134
+ setLogKey((k) => k + 1);
135
+ }
136
+ };
137
+
138
+ return (
139
+ <div className="grid gap-6 xl:grid-cols-2">
140
+ <div className="space-y-6">
141
+ <Section title="Serveur d’envoi (SMTP)" description="Utilisé pour les invitations, la réinitialisation des mots de passe, les alertes de sécurité et les formulaires." actions={<Badge tone={source} dot>{label}</Badge>}>
142
+ <Switch
143
+ checked={smtp.enabled}
144
+ onChange={(enabled) => set('smtp', { enabled })}
145
+ label="Utiliser ce serveur SMTP"
146
+ description={
147
+ meta.envSmtp
148
+ ? 'Désactivé, le serveur défini dans le fichier .env (SMTP_HOST…) est utilisé.'
149
+ : 'Désactivé, aucun e-mail n’est envoyé : les liens d’invitation et de réinitialisation sont à copier manuellement.'
150
+ }
151
+ />
152
+ <fieldset disabled={!smtp.enabled} className="space-y-4 disabled:opacity-60">
153
+ <Field label="Préréglages" hint="Remplit le serveur, le port et le chiffrement. Vérifiez ensuite les identifiants.">
154
+ <div className="flex flex-wrap gap-1.5">
155
+ {PRESETS.map((p) => (
156
+ <button
157
+ key={p.label}
158
+ type="button"
159
+ onClick={() => set('smtp', { host: p.host, port: p.port, security: p.security, ...(p.username ? { username: p.username } : {}) })}
160
+ className="rounded-full border border-slate-200 bg-white px-2.5 py-1 text-xs font-medium text-slate-600 transition hover:border-brand-300 hover:text-brand-700"
161
+ >
162
+ {p.label}
163
+ </button>
164
+ ))}
165
+ </div>
166
+ </Field>
167
+ <div className="grid gap-4 sm:grid-cols-[1fr_auto]">
168
+ <Field label="Serveur">
169
+ <Input value={smtp.host} onChange={(e) => set('smtp', { host: e.target.value.trim() })} placeholder="smtp.exemple.com" autoComplete="off" />
170
+ </Field>
171
+ <NumberField path="smtp.port" label="Port" value={smtp.port} onChange={(port) => set('smtp', { port })} />
172
+ </div>
173
+ <Field label="Chiffrement" hint={smtp.security === 'none' ? 'Déconseillé : identifiants et contenus circulent en clair.' : undefined}>
174
+ <Segmented
175
+ value={smtp.security}
176
+ onChange={changeSecurity}
177
+ options={[
178
+ { value: 'starttls', label: 'STARTTLS (587)' },
179
+ { value: 'ssl', label: 'SSL / TLS (465)' },
180
+ { value: 'none', label: 'Aucun' },
181
+ ]}
182
+ />
183
+ </Field>
184
+ <div className="grid gap-4 sm:grid-cols-2">
185
+ <Field label="Identifiant">
186
+ <Input value={smtp.username} onChange={(e) => set('smtp', { username: e.target.value.trim() })} autoComplete="off" placeholder="contact@exemple.com" />
187
+ </Field>
188
+ <Field
189
+ label="Mot de passe"
190
+ hint={
191
+ smtp.clearPassword ? (
192
+ <span className="text-amber-700">Le mot de passe sera supprimé à l’enregistrement.</span>
193
+ ) : smtp.hasPassword ? (
194
+ <span>
195
+ Enregistré et chiffré. Laissez vide pour le conserver ·{' '}
196
+ <button type="button" className="font-medium text-rose-600 hover:underline" onClick={() => set('smtp', { clearPassword: true, password: '' })}>
197
+ Supprimer
198
+ </button>
199
+ </span>
200
+ ) : (
201
+ 'Stocké chiffré (AES-256), jamais réaffiché.'
202
+ )
203
+ }
204
+ >
205
+ <PasswordInput
206
+ value={smtp.password}
207
+ onChange={(e) => set('smtp', { password: e.target.value, clearPassword: false })}
208
+ placeholder={smtp.hasPassword && !smtp.clearPassword ? '••••••••••' : ''}
209
+ autoComplete="new-password"
210
+ />
211
+ </Field>
212
+ </div>
213
+ <div className="grid gap-4 sm:grid-cols-2">
214
+ <NumberField path="smtp.timeoutSec" label="Délai de connexion" suffix="secondes" value={smtp.timeoutSec} onChange={(timeoutSec) => set('smtp', { timeoutSec })} />
215
+ </div>
216
+ <Switch
217
+ checked={smtp.allowSelfSigned}
218
+ onChange={(allowSelfSigned) => set('smtp', { allowSelfSigned })}
219
+ label="Accepter les certificats auto-signés"
220
+ description="À n’activer que pour un serveur interne de confiance."
221
+ />
222
+ </fieldset>
223
+ </Section>
224
+
225
+ <Section title="Expéditeur" description="Nom et adresse affichés dans la boîte de réception des destinataires.">
226
+ <div className="grid gap-4 sm:grid-cols-2">
227
+ <Field label="Nom de l’expéditeur" hint="Par défaut : le nom du site.">
228
+ <Input value={smtp.fromName} onChange={(e) => set('smtp', { fromName: e.target.value })} placeholder="Snapify" />
229
+ </Field>
230
+ <Field label="Adresse d’expédition" error={smtp.fromEmail && !EMAIL.test(smtp.fromEmail) ? 'Adresse invalide' : undefined} hint="Par défaut : l’identifiant SMTP.">
231
+ <Input type="email" value={smtp.fromEmail} onChange={(e) => set('smtp', { fromEmail: e.target.value.trim() })} placeholder="no-reply@exemple.com" />
232
+ </Field>
233
+ </div>
234
+ <Field label="Adresse de réponse (facultatif)" error={smtp.replyTo && !EMAIL.test(smtp.replyTo) ? 'Adresse invalide' : undefined} hint="Les réponses des destinataires arriveront à cette adresse.">
235
+ <Input type="email" value={smtp.replyTo} onChange={(e) => set('smtp', { replyTo: e.target.value.trim() })} placeholder="contact@exemple.com" />
236
+ </Field>
237
+ </Section>
238
+ </div>
239
+
240
+ <div className="space-y-6">
241
+ <Section title="Tester la configuration" description="Vérifie la connexion au serveur puis envoie un e-mail réel. Les valeurs du formulaire sont utilisées, même non enregistrées.">
242
+ <div className="flex flex-col gap-2 sm:flex-row">
243
+ <Input type="email" value={to} onChange={(e) => setTo(e.target.value)} placeholder="vous@exemple.com" className="flex-1" />
244
+ <Button variant="primary" icon={Send} loading={testing} disabled={!EMAIL.test(to.trim())} onClick={() => void test()}>
245
+ Envoyer un test
246
+ </Button>
247
+ </div>
248
+ {result ? (
249
+ result.ok ? (
250
+ <Alert tone="success" icon={CheckCircle2} title="Envoi réussi">
251
+ Vérifiez la boîte de réception (et les indésirables) de {to.trim()}.
252
+ </Alert>
253
+ ) : (
254
+ <Alert tone="danger" icon={XCircle} title="L’envoi a échoué">
255
+ <span className="break-words font-mono text-xs">{result.error}</span>
256
+ </Alert>
257
+ )
258
+ ) : null}
259
+ </Section>
260
+
261
+ <Section title="Notifications" description="E-mails envoyés automatiquement par l’administration.">
262
+ <Switch
263
+ checked={v.notifications.securityAlerts}
264
+ onChange={(securityAlerts) => set('notifications', { securityAlerts })}
265
+ label="Alertes de sécurité aux utilisateurs"
266
+ description="Connexion depuis une nouvelle adresse IP, mot de passe modifié, double authentification activée ou retirée."
267
+ />
268
+ <Switch
269
+ checked={v.notifications.accountChanges}
270
+ onChange={(accountChanges) => set('notifications', { accountChanges })}
271
+ label="Changements des accès"
272
+ description="Prévient les adresses ci-dessous lors d’une création, suppression, suspension ou d’un changement de rôle."
273
+ />
274
+ <Field label="Adresses à prévenir" hint="Appuyez sur Entrée après chaque adresse (20 maximum).">
275
+ <TagInput
276
+ value={v.notifications.adminEmails}
277
+ onChange={(adminEmails) => set('notifications', { adminEmails: adminEmails.map((e) => e.toLowerCase()) })}
278
+ validate={(e) => EMAIL.test(e)}
279
+ placeholder="securite@exemple.com"
280
+ />
281
+ </Field>
282
+ {v.notifications.accountChanges && v.notifications.adminEmails.length === 0 ? (
283
+ <Alert tone="warning" icon={Info}>
284
+ Ajoutez au moins une adresse pour recevoir les notifications de changement des accès.
285
+ </Alert>
286
+ ) : null}
287
+ </Section>
288
+
289
+ <MailLog reloadKey={logKey} />
290
+ </div>
291
+ </div>
292
+ );
293
+ }
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Snapify
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ * Contact : contact@novalabs.tn · +216 24 734 647
5
+ * Tous droits réservés. Toute reproduction, modification ou diffusion sans autorisation écrite est interdite.
6
+ */
7
+ import React, { useState } from 'react';
8
+ import { AlertTriangle, Database, HardDrive, RefreshCw, Server, Trash2 } from 'lucide-react';
9
+ import { toast } from 'sonner';
10
+ import { api, type SystemView } from '../lib/api';
11
+ import { fmtBytes, fmtNumber } from '../lib/format';
12
+ import { errorMessage, useAsync } from '../lib/hooks';
13
+ import { useConfirm } from '../ui/overlays';
14
+ import { Alert, Button, ErrorState, Section, Skeleton, Switch } from '../ui/primitives';
15
+ import { NumberField, type TabProps } from './controls';
16
+
17
+ type WindowKey = 'formSubmissions' | 'newsletter' | 'passwordReset';
18
+
19
+ function RateWindowRow({ k, label, description, v, set }: { k: WindowKey; label: string; description: string } & Pick<TabProps, 'v' | 'set'>) {
20
+ const w = v.limits[k];
21
+ return (
22
+ <div className="grid gap-3 border-b border-slate-100 pb-4 last:border-0 last:pb-0 sm:grid-cols-[1fr_auto_auto] sm:items-start">
23
+ <div>
24
+ <p className="text-sm font-medium text-slate-800">{label}</p>
25
+ <p className="mt-0.5 text-[13px] leading-relaxed text-slate-500">{description}</p>
26
+ </div>
27
+ <NumberField path={`limits.${k}.max`} label="Maximum" suffix="envois" value={w.max} onChange={(max) => set('limits', { [k]: { ...w, max } })} />
28
+ <NumberField
29
+ path={`limits.${k}.windowMinutes`}
30
+ label="Par période de"
31
+ suffix="min"
32
+ value={w.windowMinutes}
33
+ onChange={(windowMinutes) => set('limits', { [k]: { ...w, windowMinutes } })}
34
+ />
35
+ </div>
36
+ );
37
+ }
38
+
39
+ export function LimitsTab({ v, set }: TabProps) {
40
+ return (
41
+ <div className="grid gap-6 xl:grid-cols-2">
42
+ <Section title="Limites globales" description="Au-delà, le serveur répond « 429 – Trop de requêtes » avec le délai d’attente. 0 = illimité.">
43
+ <NumberField
44
+ path="limits.publicApiPerMinute"
45
+ label="Site public, par visiteur"
46
+ suffix="requêtes / minute"
47
+ value={v.limits.publicApiPerMinute}
48
+ onChange={(publicApiPerMinute) => set('limits', { publicApiPerMinute })}
49
+ hint="Contenu, statistiques et modules appelés par le site. Protège contre les robots trop agressifs."
50
+ />
51
+ <NumberField
52
+ path="limits.adminApiPerMinute"
53
+ label="Administration, par utilisateur"
54
+ suffix="requêtes / minute"
55
+ value={v.limits.adminApiPerMinute}
56
+ onChange={(adminApiPerMinute) => set('limits', { adminApiPerMinute })}
57
+ hint="Limite un compte compromis ou un script qui s’emballe."
58
+ />
59
+ </Section>
60
+ <Section title="Actions sensibles" description="Quotas appliqués par adresse IP sur une fenêtre glissante.">
61
+ <RateWindowRow k="formSubmissions" label="Envoi de formulaires" description="Messages de contact et demandes de devis." v={v} set={set} />
62
+ <RateWindowRow k="newsletter" label="Inscription à la newsletter" description="Limite les inscriptions en masse." v={v} set={set} />
63
+ <RateWindowRow k="passwordReset" label="Mot de passe oublié" description="Demandes de lien de réinitialisation (par IP et par adresse)." v={v} set={set} />
64
+ </Section>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ export function MediaTab({ v, set }: TabProps) {
70
+ const u = v.uploads;
71
+ const none = !u.allowImages && !u.allowSvg && !u.allowVideo && !u.allowPdf;
72
+ return (
73
+ <div className="grid gap-6 xl:grid-cols-2">
74
+ <Section title="Fichiers envoyés" description="Contrôlés par le serveur à chaque envoi dans la médiathèque.">
75
+ <NumberField path="uploads.maxSizeMb" label="Taille maximale par fichier" suffix="Mo" value={u.maxSizeMb} onChange={(maxSizeMb) => set('uploads', { maxSizeMb })} />
76
+ <Switch checked={u.allowImages} onChange={(allowImages) => set('uploads', { allowImages })} label="Images" description="JPEG, PNG, WebP, AVIF et GIF." />
77
+ <Switch checked={u.allowVideo} onChange={(allowVideo) => set('uploads', { allowVideo })} label="Vidéos" description="MP4 et WebM." />
78
+ <Switch checked={u.allowPdf} onChange={(allowPdf) => set('uploads', { allowPdf })} label="Documents PDF" />
79
+ <Switch
80
+ checked={u.allowSvg}
81
+ onChange={(allowSvg) => set('uploads', { allowSvg })}
82
+ label="Images vectorielles SVG"
83
+ description="Un SVG peut contenir du code : n’autorisez que si les personnes qui envoient des fichiers sont de confiance."
84
+ />
85
+ {none ? (
86
+ <Alert tone="warning" icon={AlertTriangle}>
87
+ Aucun type n’est autorisé : plus aucun fichier ne pourra être ajouté à la médiathèque.
88
+ </Alert>
89
+ ) : null}
90
+ </Section>
91
+ </div>
92
+ );
93
+ }
94
+
95
+ export function DataTab({ v, set, dirty }: TabProps & { dirty: boolean }) {
96
+ const confirm = useConfirm();
97
+ const [running, setRunning] = useState(false);
98
+ const p = v.privacy;
99
+
100
+ const purge = async () => {
101
+ const ok = await confirm({
102
+ title: 'Purger les données anciennes ?',
103
+ message: 'Les statistiques, messages, journaux et sessions plus anciens que les durées enregistrées seront supprimés définitivement.',
104
+ confirmLabel: 'Purger maintenant',
105
+ danger: true,
106
+ });
107
+ if (!ok) return;
108
+ setRunning(true);
109
+ try {
110
+ const r = await api.system.runRetention();
111
+ const total = Object.values(r).reduce((a, b) => a + b, 0);
112
+ toast.success(total ? `${fmtNumber(total)} élément(s) supprimé(s)` : 'Rien à supprimer');
113
+ } catch (e) {
114
+ toast.error(errorMessage(e));
115
+ } finally {
116
+ setRunning(false);
117
+ }
118
+ };
119
+
120
+ return (
121
+ <div className="grid gap-6 xl:grid-cols-2">
122
+ <Section title="Durées de conservation" description="Une purge automatique a lieu chaque jour. Pensez au RGPD : ne conservez pas les données plus que nécessaire.">
123
+ <NumberField
124
+ path="privacy.analyticsRetentionDays"
125
+ label="Statistiques de visite"
126
+ suffix="jours"
127
+ value={p.analyticsRetentionDays}
128
+ onChange={(analyticsRetentionDays) => set('privacy', { analyticsRetentionDays })}
129
+ />
130
+ <NumberField
131
+ path="privacy.submissionsRetentionDays"
132
+ label="Messages des formulaires"
133
+ suffix="jours"
134
+ value={p.submissionsRetentionDays}
135
+ onChange={(submissionsRetentionDays) => set('privacy', { submissionsRetentionDays })}
136
+ hint="0 = conservés jusqu’à suppression manuelle."
137
+ />
138
+ <NumberField
139
+ path="privacy.auditRetentionDays"
140
+ label="Journal d’activité"
141
+ suffix="jours"
142
+ value={p.auditRetentionDays}
143
+ onChange={(auditRetentionDays) => set('privacy', { auditRetentionDays })}
144
+ />
145
+ <NumberField
146
+ path="privacy.mailLogRetentionDays"
147
+ label="Journal des e-mails"
148
+ suffix="jours"
149
+ value={p.mailLogRetentionDays}
150
+ onChange={(mailLogRetentionDays) => set('privacy', { mailLogRetentionDays })}
151
+ />
152
+ </Section>
153
+ <div className="space-y-6">
154
+ <Section title="Données personnelles">
155
+ <Switch
156
+ checked={p.storeSubmitterIp}
157
+ onChange={(storeSubmitterIp) => set('privacy', { storeSubmitterIp })}
158
+ label="Enregistrer l’adresse IP des expéditeurs"
159
+ description="Utile contre les abus, mais c’est une donnée personnelle. Désactivé, les nouveaux messages sont enregistrés sans IP."
160
+ />
161
+ </Section>
162
+ <Section title="Purge manuelle" description="Applique immédiatement les durées enregistrées, sans attendre la purge quotidienne.">
163
+ {dirty ? (
164
+ <Alert tone="warning" icon={AlertTriangle}>
165
+ Enregistrez d’abord vos modifications : la purge utilise les durées enregistrées.
166
+ </Alert>
167
+ ) : null}
168
+ <Button variant="danger" icon={Trash2} loading={running} disabled={dirty} onClick={() => void purge()}>
169
+ Purger maintenant
170
+ </Button>
171
+ </Section>
172
+ </div>
173
+ </div>
174
+ );
175
+ }
176
+
177
+ function fmtUptime(sec: number): string {
178
+ const d = Math.floor(sec / 86400);
179
+ const h = Math.floor((sec % 86400) / 3600);
180
+ const m = Math.floor((sec % 3600) / 60);
181
+ if (d) return `${d} j ${h} h`;
182
+ if (h) return `${h} h ${m} min`;
183
+ return `${m} min`;
184
+ }
185
+
186
+ function Rows({ rows }: { rows: [string, React.ReactNode][] }) {
187
+ return (
188
+ <dl className="divide-y divide-slate-100 text-sm">
189
+ {rows.map(([k, val]) => (
190
+ <div key={k} className="flex items-center justify-between gap-4 py-2">
191
+ <dt className="text-slate-500">{k}</dt>
192
+ <dd className="text-right font-medium tabular-nums text-slate-800">{val}</dd>
193
+ </div>
194
+ ))}
195
+ </dl>
196
+ );
197
+ }
198
+
199
+ export function SystemTab({ v, set }: Pick<TabProps, 'v' | 'set'>) {
200
+ const info = useAsync(() => api.system.info(), []);
201
+ const d = info.data;
202
+ if (info.error) return <ErrorState message={info.error} onRetry={() => void info.reload()} />;
203
+ return (
204
+ <div className="space-y-6">
205
+ <div className="grid gap-6 lg:grid-cols-2 xl:grid-cols-3">
206
+ <Section title={<span className="flex items-center gap-2"><Server className="h-4 w-4 text-slate-400" /> Application</span>} actions={<Button size="sm" variant="ghost" icon={RefreshCw} loading={info.loading} onClick={() => void info.reload()} aria-label="Actualiser" />}>
207
+ {d ? (
208
+ <Rows
209
+ rows={[
210
+ ['Version', `${d.app.name} ${d.app.version}`],
211
+ ['Environnement', d.app.environment === 'production' ? 'Production' : d.app.environment],
212
+ ['Node.js', d.runtime.node],
213
+ ['Système', d.runtime.platform],
214
+ ['Processeurs', d.runtime.cpus],
215
+ ['Mémoire utilisée', `${fmtNumber(d.runtime.memoryMb)} Mo`],
216
+ ['Démarré depuis', fmtUptime(d.runtime.uptimeSec)],
217
+ ]}
218
+ />
219
+ ) : (
220
+ <Skeleton className="h-56" />
221
+ )}
222
+ </Section>
223
+ <Section title={<span className="flex items-center gap-2"><Database className="h-4 w-4 text-slate-400" /> Base de données</span>}>
224
+ {d ? (
225
+ <Rows
226
+ rows={[
227
+ ['PostgreSQL', d.database.version],
228
+ ['Taille', fmtBytes(d.database.sizeBytes)],
229
+ ['Événements de visite', fmtNumber(d.database.analyticsEvents)],
230
+ ['Révisions de contenu', fmtNumber(d.database.revisions)],
231
+ ['Journal d’activité', fmtNumber(d.database.auditEntries)],
232
+ ['Journal des e-mails', fmtNumber(d.database.mailLog)],
233
+ ['Sessions actives', fmtNumber(d.database.activeSessions)],
234
+ ]}
235
+ />
236
+ ) : (
237
+ <Skeleton className="h-56" />
238
+ )}
239
+ </Section>
240
+ <Section title={<span className="flex items-center gap-2"><HardDrive className="h-4 w-4 text-slate-400" /> Contenu et fichiers</span>}>
241
+ {d ? (
242
+ <Rows
243
+ rows={[
244
+ ['Pages', fmtNumber(d.content.pages)],
245
+ ['Articles', fmtNumber(d.content.posts)],
246
+ ['Médias', fmtNumber(d.content.media)],
247
+ ['Fichiers envoyés', `${fmtNumber(d.uploads.files)} · ${fmtBytes(d.uploads.bytes)}`],
248
+ ['Messages reçus', fmtNumber(d.content.submissions)],
249
+ ['Abonnés newsletter', fmtNumber(d.content.subscribers)],
250
+ ['Utilisateurs', fmtNumber(d.content.users)],
251
+ ]}
252
+ />
253
+ ) : (
254
+ <Skeleton className="h-56" />
255
+ )}
256
+ </Section>
257
+ </div>
258
+ <Section title="Performance" description="Durée pendant laquelle les navigateurs et le CDN peuvent réutiliser les réponses publiques (contenu des pages).">
259
+ <NumberField
260
+ path="performance.publicCacheSeconds"
261
+ label="Cache des réponses publiques"
262
+ suffix="secondes"
263
+ value={v.performance.publicCacheSeconds}
264
+ onChange={(publicCacheSeconds) => set('performance', { publicCacheSeconds } as Partial<SystemView['performance']>)}
265
+ hint="0 = pas de cache. Une valeur élevée allège le serveur mais retarde l’affichage des modifications publiées."
266
+ />
267
+ </Section>
268
+ </div>
269
+ );
270
+ }