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,226 @@
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 { useEffect, useState } from 'react';
8
+ import {
9
+ BarChart3,
10
+ Bot,
11
+ Calendar,
12
+ Cookie,
13
+ CreditCard,
14
+ Database,
15
+ FolderTree,
16
+ Globe2,
17
+ Heart,
18
+ KeyRound,
19
+ LayoutDashboard,
20
+ Mail,
21
+ MessageCircle,
22
+ MessagesSquare,
23
+ Package,
24
+ Phone,
25
+ Puzzle,
26
+ Receipt,
27
+ Route,
28
+ Search,
29
+ ShieldCheck,
30
+ ShoppingBag,
31
+ ShoppingCart,
32
+ Sparkles,
33
+ Store,
34
+ Tag,
35
+ Ticket,
36
+ Truck,
37
+ UserRound,
38
+ Users,
39
+ Image as ImageIcon,
40
+ type LucideIcon,
41
+ } from 'lucide-react';
42
+ import type { InstalledModuleInfo, ModuleStatusCode } from '../../../server/modules/types';
43
+ import { api } from '../lib/api';
44
+ import { Badge, cx } from '../ui/primitives';
45
+
46
+ const ICONS: Record<string, LucideIcon> = {
47
+ bot: Bot,
48
+ calendar: Calendar,
49
+ 'chart-bar': BarChart3,
50
+ database: Database,
51
+ globe: Globe2,
52
+ mail: Mail,
53
+ 'message-circle': MessageCircle,
54
+ 'messages-square': MessagesSquare,
55
+ phone: Phone,
56
+ search: Search,
57
+ 'shopping-cart': ShoppingCart,
58
+ 'shopping-bag': ShoppingBag,
59
+ sparkles: Sparkles,
60
+ store: Store,
61
+ package: Package,
62
+ receipt: Receipt,
63
+ users: Users,
64
+ user: UserRound,
65
+ ticket: Ticket,
66
+ tag: Tag,
67
+ folder: FolderTree,
68
+ truck: Truck,
69
+ 'credit-card': CreditCard,
70
+ heart: Heart,
71
+ 'shield-check': ShieldCheck,
72
+ };
73
+
74
+ export const moduleIcon = (name?: string): LucideIcon => (name && ICONS[name]) || Puzzle;
75
+
76
+ export function ModuleIcon({ name, className }: { name?: string; className?: string }) {
77
+ const Icon = moduleIcon(name);
78
+ return (
79
+ <span className={cx('flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-brand-50 text-brand-700 ring-1 ring-brand-100', className)}>
80
+ <Icon className="h-5 w-5" />
81
+ </span>
82
+ );
83
+ }
84
+
85
+ /** Libellé lisible d'une permission, avec son icône. */
86
+ export function describePermission(p: string): { icon: LucideIcon; label: string } {
87
+ if (p === 'site:read') return { icon: Globe2, label: 'Lire les informations publiques du site (nom, coordonnées, couleurs)' };
88
+ if (p === 'storage') return { icon: Database, label: 'Enregistrer ses propres données dans la base du site' };
89
+ if (p === 'http:routes') return { icon: Route, label: 'Exposer des points d’API publics sous /api/modules/' };
90
+ if (p === 'database') return { icon: Database, label: 'Gérer ses propres collections de données (produits, commandes…)' };
91
+ if (p === 'files') return { icon: ImageIcon, label: 'Enregistrer des images publiques (photos de profil…) dans /uploads/modules/' };
92
+ if (p === 'http:cookies') return { icon: Cookie, label: 'Déposer des cookies de session pour les visiteurs (préfixe sm_)' };
93
+ if (p === 'admin:routes') return { icon: LayoutDashboard, label: 'Ajouter des pages et des actions à l’administration' };
94
+ if (p === 'mail:send') return { icon: Mail, label: 'Envoyer des e-mails via le serveur SMTP du site' };
95
+ if (p.startsWith('env:')) return { icon: KeyRound, label: `Lire la variable serveur ${p.slice(4)}` };
96
+ if (p.startsWith('net:')) return { icon: Globe2, label: `Envoyer des requêtes HTTPS vers ${p.slice(4)}` };
97
+ return { icon: Puzzle, label: p };
98
+ }
99
+
100
+ const STATUS_TONE: Record<ModuleStatusCode, 'success' | 'neutral' | 'warning' | 'danger'> = {
101
+ active: 'success',
102
+ disabled: 'neutral',
103
+ unsigned: 'danger',
104
+ incompatible: 'danger',
105
+ tampered: 'danger',
106
+ license_missing: 'warning',
107
+ license_invalid: 'danger',
108
+ license_expired: 'warning',
109
+ license_domain: 'warning',
110
+ dependency: 'warning',
111
+ error: 'danger',
112
+ };
113
+
114
+ const STATUS_LABEL: Record<ModuleStatusCode, string> = {
115
+ active: 'Actif',
116
+ disabled: 'Désactivé',
117
+ unsigned: 'Signature invalide',
118
+ incompatible: 'Incompatible',
119
+ tampered: 'Fichiers altérés',
120
+ license_missing: 'Licence requise',
121
+ license_invalid: 'Licence invalide',
122
+ license_expired: 'Licence expirée',
123
+ license_domain: 'Domaine non couvert',
124
+ dependency: 'Module requis',
125
+ error: 'Erreur',
126
+ };
127
+
128
+ export function ModuleStatusBadge({ code }: { code: ModuleStatusCode }) {
129
+ return (
130
+ <Badge tone={STATUS_TONE[code]} dot>
131
+ {STATUS_LABEL[code]}
132
+ </Badge>
133
+ );
134
+ }
135
+
136
+ export const PRICING_LABEL = { free: 'Gratuit', paid: 'Payant' } as const;
137
+
138
+ /* ------------------------------------------------------------------ */
139
+ /* Liste partagée des modules installés (menu latéral, pages) */
140
+ /* ------------------------------------------------------------------ */
141
+
142
+ const CHANGED = 'snapify:modules-changed';
143
+ let cache: InstalledModuleInfo[] | null = null;
144
+
145
+ export const notifyModulesChanged = () => window.dispatchEvent(new Event(CHANGED));
146
+
147
+ export function useInstalledModules(): InstalledModuleInfo[] {
148
+ const [list, setList] = useState<InstalledModuleInfo[]>(cache ?? []);
149
+ useEffect(() => {
150
+ let alive = true;
151
+ const load = () =>
152
+ api.modules
153
+ .list()
154
+ .then((r) => {
155
+ cache = r.modules;
156
+ if (alive) setList(r.modules);
157
+ })
158
+ .catch(() => undefined);
159
+ void load();
160
+ window.addEventListener(CHANGED, load);
161
+ return () => {
162
+ alive = false;
163
+ window.removeEventListener(CHANGED, load);
164
+ };
165
+ }, []);
166
+ return list;
167
+ }
168
+
169
+ export interface ModuleNavItem {
170
+ to: string;
171
+ label: string;
172
+ icon: LucideIcon;
173
+ moduleId: string;
174
+ pageId: string;
175
+ }
176
+
177
+ /** Libellés sidebar distincts des titres CMS (évite deux « Contenu » / « Contact »). */
178
+ const MODULE_NAV_TITLE: Record<string, string> = {
179
+ Contenu: 'Applications',
180
+ Contact: 'Messagerie',
181
+ Contacts: 'Messagerie',
182
+ };
183
+
184
+ /** Pages d'administration des modules actifs, regroupées par catégorie de module (ex. « E-commerce »). */
185
+ export function useModuleNav(enabled: boolean): { title: string; items: ModuleNavItem[] }[] {
186
+ const installed = useInstalledModules();
187
+ if (!enabled) return [];
188
+ const groups = new Map<string, ModuleNavItem[]>();
189
+ for (const m of installed) {
190
+ if (!m.admin || !m.adminPages.length) continue;
191
+ const raw = m.category || 'Modules';
192
+ const title = MODULE_NAV_TITLE[raw] || raw;
193
+ const list = groups.get(title) ?? [];
194
+ for (const p of m.adminPages) list.push({ to: `/m/${m.id}/${p.id}`, label: p.label, icon: moduleIcon(p.icon ?? m.icon), moduleId: m.id, pageId: p.id });
195
+ groups.set(title, list);
196
+ }
197
+ return [...groups].map(([title, items]) => ({ title, items }));
198
+ }
199
+
200
+ export function PermissionList({ permissions, hasServer }: { permissions: string[]; hasServer?: boolean }) {
201
+ if (!permissions.length) {
202
+ return <p className="text-sm text-slate-500">Aucune permission particulière : le module s’affiche uniquement sur le site{hasServer ? ' et exécute du code serveur sans accès externe' : ''}.</p>;
203
+ }
204
+ return (
205
+ <ul className="space-y-2">
206
+ {permissions.map((p) => {
207
+ const { icon: Icon, label } = describePermission(p);
208
+ return (
209
+ <li key={p} className="flex items-start gap-2.5 text-sm text-slate-700">
210
+ <Icon className="mt-0.5 h-4 w-4 shrink-0 text-slate-400" />
211
+ <span>{label}</span>
212
+ </li>
213
+ );
214
+ })}
215
+ </ul>
216
+ );
217
+ }
218
+
219
+ export function fileToBase64(file: File): Promise<string> {
220
+ return new Promise((resolve, reject) => {
221
+ const reader = new FileReader();
222
+ reader.onload = () => resolve(String(reader.result).split(',')[1] || '');
223
+ reader.onerror = () => reject(new Error('Lecture du fichier impossible'));
224
+ reader.readAsDataURL(file);
225
+ });
226
+ }
@@ -0,0 +1,62 @@
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
+ /** Écran obligatoire avant l'accès à l'administration : changement de mot de passe et/ou activation de la 2FA. */
8
+ import React from 'react';
9
+ import { KeyRound, LogOut, ShieldCheck } from 'lucide-react';
10
+ import { useAuth } from '../context/AuthContext';
11
+ import { Button, Card } from '../ui/primitives';
12
+ import { PasswordChangeForm, TwoFactorSetup } from '../account/SecurityForms';
13
+ import { SnapifyMark } from '../../components/SnapifyLogo';
14
+
15
+ export default function AccountSetupPage() {
16
+ const { user, logout } = useAuth();
17
+ if (!user) return null;
18
+ const passwordStep = user.mustChangePassword;
19
+
20
+ return (
21
+ <div className="min-h-screen bg-slate-50 px-4 py-10">
22
+ <div className="mx-auto max-w-xl">
23
+ <div className="mb-8 flex items-center justify-between">
24
+ <div className="flex items-center gap-3">
25
+ <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-brand-600">
26
+ <SnapifyMark className="h-7 w-7" accent="#FFFFFF" />
27
+ </div>
28
+ <div className="leading-tight">
29
+ <p className="text-sm font-semibold text-slate-900">{user.name}</p>
30
+ <p className="text-xs text-slate-500">{user.email}</p>
31
+ </div>
32
+ </div>
33
+ <Button size="sm" variant="ghost" icon={LogOut} onClick={() => void logout()}>
34
+ Se déconnecter
35
+ </Button>
36
+ </div>
37
+
38
+ <Card className="p-6 sm:p-8">
39
+ <div className="mb-6 flex items-start gap-3">
40
+ <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-brand-50 text-brand-700">
41
+ {passwordStep ? <KeyRound className="h-5 w-5" /> : <ShieldCheck className="h-5 w-5" />}
42
+ </span>
43
+ <div>
44
+ <h1 className="text-lg font-semibold text-slate-900">{passwordStep ? 'Choisissez un nouveau mot de passe' : 'Activez la double authentification'}</h1>
45
+ <p className="mt-1 text-sm leading-relaxed text-slate-500">
46
+ {passwordStep
47
+ ? user.passwordChangedAt
48
+ ? 'Votre mot de passe doit être renouvelé (politique de sécurité ou demande d’un administrateur).'
49
+ : 'Votre compte a été créé avec un mot de passe provisoire : remplacez-le pour continuer.'
50
+ : 'La politique de sécurité du site impose la double authentification pour votre compte.'}
51
+ </p>
52
+ </div>
53
+ </div>
54
+ {passwordStep ? <PasswordChangeForm submitLabel="Enregistrer et continuer" /> : <TwoFactorSetup />}
55
+ </Card>
56
+ {passwordStep && user.mustSetup2fa ? (
57
+ <p className="mt-4 text-center text-xs text-slate-500">Étape suivante : activation de la double authentification.</p>
58
+ ) : null}
59
+ </div>
60
+ </div>
61
+ );
62
+ }
@@ -0,0 +1,206 @@
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 { Clock, Download, Eye, Monitor, Radio, Smartphone, Tablet, TrendingDown, Users } from 'lucide-react';
9
+ import { api } from '../lib/api';
10
+ import { useAsync } from '../lib/hooks';
11
+ import { countryFlag, countryName, downloadCsv, fmtDateTime, fmtDuration, fmtNumber } from '../lib/format';
12
+ import { Badge, Button, Card, EmptyState, ErrorState, PageHeader, PageSkeleton, Segmented, StatCard, Table, Td, Th } from '../ui/primitives';
13
+ import { BarList, Legend, TrendChart } from '../ui/charts';
14
+ import { Pagination, usePagination } from '../ui/Pagination';
15
+
16
+ const SERIES = [
17
+ { key: 'views', label: 'Pages vues', color: '#00a89d' },
18
+ { key: 'sessions', label: 'Visiteurs', color: '#6366f1' },
19
+ ];
20
+ const DEVICE: Record<string, { label: string; icon: React.ElementType }> = {
21
+ desktop: { label: 'Ordinateur', icon: Monitor },
22
+ mobile: { label: 'Mobile', icon: Smartphone },
23
+ tablet: { label: 'Tablette', icon: Tablet },
24
+ unknown: { label: 'Inconnu', icon: Monitor },
25
+ };
26
+
27
+ export default function AnalyticsPage() {
28
+ const [days, setDays] = useState('30');
29
+ const { data: a, error, loading, reload } = useAsync(() => api.analytics(Number(days)), [days]);
30
+ const recent = usePagination(a?.recent ?? [], 'analytics-recent', { resetKey: days });
31
+
32
+ const exportCsv = () => {
33
+ if (!a) return;
34
+ downloadCsv(`statistiques-${days}j.csv`, [
35
+ ['Page', 'Vues', 'Temps moyen (s)'],
36
+ ...a.topPages.map((p) => [p.path, p.views, p.avgTimeSec]),
37
+ [],
38
+ ['Pays', 'Vues'],
39
+ ...a.countries.map((c) => [countryName(c.country), c.views]),
40
+ [],
41
+ ['Source', 'Vues'],
42
+ ...a.referrers.map((r) => [r.source, r.views]),
43
+ ]);
44
+ };
45
+
46
+ return (
47
+ <div className="space-y-6">
48
+ <PageHeader
49
+ title="Statistiques"
50
+ description="Mesure d’audience interne, sans cookie tiers. Les visites de l’administration et des robots sont exclues."
51
+ actions={
52
+ <>
53
+ <Segmented
54
+ value={days}
55
+ onChange={setDays}
56
+ options={[
57
+ { value: '7', label: '7 jours' },
58
+ { value: '30', label: '30 jours' },
59
+ { value: '90', label: '90 jours' },
60
+ { value: '365', label: '12 mois' },
61
+ ]}
62
+ />
63
+ <Button icon={Download} onClick={exportCsv} disabled={!a}>
64
+ Exporter
65
+ </Button>
66
+ </>
67
+ }
68
+ />
69
+
70
+ {loading && !a ? (
71
+ <PageSkeleton />
72
+ ) : error || !a ? (
73
+ <ErrorState message={error || ''} onRetry={reload} />
74
+ ) : (
75
+ <>
76
+ <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-5">
77
+ <StatCard label="Pages vues" value={fmtNumber(a.pageViews)} icon={Eye} />
78
+ <StatCard label="Visiteurs uniques" value={fmtNumber(a.uniqueSessions)} icon={Users} tone="violet" />
79
+ <StatCard label="Temps moyen par page" value={fmtDuration(a.avgTimeOnPageSec)} icon={Clock} tone="sky" />
80
+ <StatCard label="Taux de rebond" value={`${a.bounceRate} %`} icon={TrendingDown} tone="amber" hint="Visites d’une seule page" />
81
+ <StatCard label="En ce moment" value={fmtNumber(a.activeNow)} icon={Radio} tone="rose" hint="Actifs ces 5 dernières minutes" />
82
+ </div>
83
+
84
+ <Card>
85
+ <div className="mb-4 flex items-center justify-between">
86
+ <h2 className="text-[15px] font-semibold text-slate-900">Évolution du trafic</h2>
87
+ <Legend series={SERIES} />
88
+ </div>
89
+ <TrendChart data={a.timeseries} series={SERIES} height={300} />
90
+ </Card>
91
+
92
+ <div className="grid gap-6 lg:grid-cols-2">
93
+ <Card>
94
+ <h2 className="mb-3 text-[15px] font-semibold text-slate-900">Pages les plus consultées</h2>
95
+ <BarList valueLabel="Temps moyen · Vues" items={a.topPages.slice(0, 12).map((p) => ({ key: p.path, label: p.path, value: p.views, extra: fmtDuration(p.avgTimeSec) }))} />
96
+ </Card>
97
+ <Card>
98
+ <h2 className="mb-3 text-[15px] font-semibold text-slate-900">Pays</h2>
99
+ <BarList
100
+ valueLabel="Vues"
101
+ items={a.countries.slice(0, 12).map((c) => ({
102
+ key: c.country,
103
+ label: (
104
+ <span className="flex items-center gap-2">
105
+ <span className="text-base leading-none">{countryFlag(c.country)}</span>
106
+ {countryName(c.country)}
107
+ </span>
108
+ ),
109
+ value: c.views,
110
+ extra: a.pageViews ? `${Math.round((c.views / a.pageViews) * 100)} %` : undefined,
111
+ }))}
112
+ />
113
+ </Card>
114
+ <Card>
115
+ <h2 className="mb-3 text-[15px] font-semibold text-slate-900">Sources de trafic</h2>
116
+ <BarList valueLabel="Vues" items={a.referrers.slice(0, 10).map((r) => ({ key: r.source, label: r.source, value: r.views }))} />
117
+ </Card>
118
+ <Card>
119
+ <h2 className="mb-4 text-[15px] font-semibold text-slate-900">Appareils</h2>
120
+ {a.devices.length === 0 ? (
121
+ <p className="py-8 text-center text-sm text-slate-400">Pas encore de données</p>
122
+ ) : (
123
+ <div className="space-y-4">
124
+ {a.devices.map((d) => {
125
+ const meta = DEVICE[d.device] || DEVICE.unknown;
126
+ const pct = a.pageViews ? Math.round((d.views / a.pageViews) * 100) : 0;
127
+ return (
128
+ <div key={d.device}>
129
+ <div className="mb-1.5 flex items-center justify-between text-sm">
130
+ <span className="flex items-center gap-2 text-slate-700">
131
+ <meta.icon className="h-4 w-4 text-slate-400" /> {meta.label}
132
+ </span>
133
+ <span className="tabular-nums text-slate-900">
134
+ <span className="font-medium">{pct} %</span> <span className="text-xs text-slate-400">({fmtNumber(d.views)})</span>
135
+ </span>
136
+ </div>
137
+ <div className="h-2 overflow-hidden rounded-full bg-slate-100">
138
+ <div className="h-full rounded-full bg-brand-500" style={{ width: `${pct}%` }} />
139
+ </div>
140
+ </div>
141
+ );
142
+ })}
143
+ </div>
144
+ )}
145
+ </Card>
146
+ </div>
147
+
148
+ <Card padded={false}>
149
+ <div className="border-b border-slate-100 px-5 py-4">
150
+ <h2 className="text-[15px] font-semibold text-slate-900">Activité récente</h2>
151
+ <p className="text-xs text-slate-500">Dernières pages consultées, avec le temps passé lorsque le visiteur a quitté la page.</p>
152
+ </div>
153
+ {a.recent.length === 0 ? (
154
+ <EmptyState icon={Eye} title="Aucune visite enregistrée" description="Les visites apparaîtront ici dès que le site recevra du trafic." />
155
+ ) : (
156
+ <Table>
157
+ <thead>
158
+ <tr>
159
+ <Th>Date</Th>
160
+ <Th>Page</Th>
161
+ <Th>Événement</Th>
162
+ <Th>Localisation</Th>
163
+ <Th>Appareil</Th>
164
+ <Th>Provenance</Th>
165
+ </tr>
166
+ </thead>
167
+ <tbody>
168
+ {recent.pageItems.map((e) => (
169
+ <tr key={e.id} className="hover:bg-slate-50/60">
170
+ <Td className="whitespace-nowrap text-slate-500">{fmtDateTime(e.ts)}</Td>
171
+ <Td className="max-w-[260px] truncate font-medium text-slate-800">{e.path}</Td>
172
+ <Td>
173
+ {e.kind === 'leave' ? <Badge tone="info">Sortie · {fmtDuration(e.durationSec || 0)}</Badge> : <Badge tone="brand">Vue</Badge>}
174
+ </Td>
175
+ <Td className="whitespace-nowrap">
176
+ {e.country ? (
177
+ <span className="flex items-center gap-2">
178
+ {countryFlag(e.country)} {countryName(e.country)}
179
+ {e.city ? <span className="text-slate-400">· {e.city}</span> : null}
180
+ </span>
181
+ ) : (
182
+ <span className="text-slate-400">Inconnue</span>
183
+ )}
184
+ </Td>
185
+ <Td>{(DEVICE[e.device] || DEVICE.unknown).label}</Td>
186
+ <Td className="max-w-[200px] truncate text-slate-500">{e.referrer ? safeHost(e.referrer) : 'Accès direct'}</Td>
187
+ </tr>
188
+ ))}
189
+ </tbody>
190
+ </Table>
191
+ )}
192
+ <Pagination {...recent.props} noun="événements" />
193
+ </Card>
194
+ </>
195
+ )}
196
+ </div>
197
+ );
198
+ }
199
+
200
+ function safeHost(url: string) {
201
+ try {
202
+ return new URL(url).hostname.replace(/^www\./, '');
203
+ } catch {
204
+ return url;
205
+ }
206
+ }
@@ -0,0 +1,154 @@
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, { useCallback, useMemo, useState } from 'react';
8
+ import { RotateCcw } from 'lucide-react';
9
+ import type { ThemeBranding } from '../../../server/cms/types';
10
+ import { api } from '../lib/api';
11
+ import { useEditable } from '../lib/hooks';
12
+ import { useSite, useSiteData } from '../context/SiteContext';
13
+ import { Button, ColorField, Field, Input, PageHeader, Section, Select } from '../ui/primitives';
14
+ import { ImageField } from '../ui/media';
15
+ import { SaveBar } from '../ui/SaveBar';
16
+ import { DeviceSwitch, PreviewFrame, type Device } from '../editor/PreviewFrame';
17
+
18
+ const COLOR_LABELS: { key: keyof ThemeBranding['colors']; label: string; hint: string }[] = [
19
+ { key: 'primary', label: 'Couleur principale', hint: 'Début du dégradé des boutons, mots en couleur, icônes' },
20
+ { key: 'primaryHover', label: 'Principale foncée', hint: 'Liens, sur-titres, survols' },
21
+ { key: 'accent', label: 'Couleur secondaire / accent', hint: 'Fin du dégradé des boutons et des bandeaux' },
22
+ { key: 'navy', label: 'Bleu nuit / Sombre', hint: 'Titres, sections sombres, pied de page' },
23
+ { key: 'background', label: 'Fond des sections claires', hint: 'Sections « gris clair »' },
24
+ { key: 'text', label: 'Texte principal', hint: '' },
25
+ { key: 'muted', label: 'Texte secondaire', hint: 'Introductions, légendes' },
26
+ { key: 'border', label: 'Bordures', hint: 'Cartes, séparateurs' },
27
+ ];
28
+
29
+ const DEFAULT_COLORS: ThemeBranding['colors'] = {
30
+ primary: '#00C6BA',
31
+ primaryHover: '#00A89D',
32
+ accent: '#0066EE',
33
+ navy: '#0B1528',
34
+ background: '#F5F7FA',
35
+ text: '#0B1528',
36
+ muted: '#64748B',
37
+ border: '#E3E8EE',
38
+ };
39
+
40
+ const FONTS = ['Inter', 'Manrope', 'Plus Jakarta Sans', 'DM Sans', 'Poppins', 'Montserrat', 'Roboto', 'Open Sans', 'Lato', 'Source Sans 3', 'Space Grotesk', 'Outfit'].map((f) => ({
41
+ value: f,
42
+ label: f,
43
+ }));
44
+
45
+ /** Aperçu réel : le vrai site, avec l'identité visuelle en cours d'édition (non enregistrée). */
46
+ function LivePreview({ b, pages }: { b: ThemeBranding; pages: { value: string; label: string }[] }) {
47
+ const [path, setPath] = useState('/');
48
+ const [device, setDevice] = useState<Device>('desktop');
49
+ const payload = useMemo(() => ({ site: { branding: b } }), [b]);
50
+ return (
51
+ <div className="flex h-[calc(100vh-7rem)] min-h-[520px] flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
52
+ <div className="flex items-center gap-2 border-b border-slate-200 px-3 py-2">
53
+ <span className="flex items-center gap-1.5 text-xs font-semibold text-slate-600">
54
+ <span className="h-2 w-2 animate-pulse rounded-full bg-emerald-500" />
55
+ Aperçu en direct
56
+ </span>
57
+ <div className="ml-auto w-48">
58
+ <Select value={path} onChange={setPath} options={pages.length ? pages : [{ value: '/', label: 'Accueil' }]} />
59
+ </div>
60
+ <DeviceSwitch value={device} onChange={setDevice} />
61
+ </div>
62
+ <div className="min-h-0 flex-1">
63
+ <PreviewFrame path={path} payload={payload} device={device} />
64
+ </div>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ export default function BrandingPage() {
70
+ const site = useSiteData();
71
+ const { patchSite } = useSite();
72
+ const save = useCallback(
73
+ async (b: ThemeBranding) => {
74
+ await api.saveSection('branding', b);
75
+ patchSite('branding', b);
76
+ },
77
+ [patchSite]
78
+ );
79
+ const ed = useEditable<ThemeBranding>(site.branding, save, { successMessage: 'Identité visuelle enregistrée' });
80
+ if (!ed.value) return null;
81
+ const b = ed.value;
82
+ const setColor = (k: keyof ThemeBranding['colors'], v: string) => ed.patch({ colors: { ...b.colors, [k]: v } });
83
+ const pages = site.pages
84
+ .filter((p) => p.status === 'published')
85
+ .sort((x, y) => x.path.localeCompare(y.path))
86
+ .map((p) => ({ value: p.path, label: p.path === '/' ? 'Accueil' : p.title }));
87
+
88
+ return (
89
+ <div className="space-y-6">
90
+ <PageHeader title="Identité visuelle" description="Logo, couleurs et typographie du site." />
91
+ <div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_minmax(0,1.25fr)]">
92
+ <div className="min-w-0 space-y-6">
93
+ <Section title="Nom et logo">
94
+ <div className="grid gap-4 sm:grid-cols-2">
95
+ <Field label="Nom du site">
96
+ <Input value={b.siteName} onChange={(e) => ed.patch({ siteName: e.target.value })} />
97
+ </Field>
98
+ <Field label="Accroche">
99
+ <Input value={b.tagline} onChange={(e) => ed.patch({ tagline: e.target.value })} />
100
+ </Field>
101
+ </div>
102
+ <div className="grid gap-4 sm:grid-cols-3">
103
+ <Field label="Logo de l’en-tête" hint="SVG ou PNG transparent. Vide = logo Snapify + nom et accroche.">
104
+ <ImageField value={b.logoUrl} onChange={(logoUrl) => ed.patch({ logoUrl })} aspect="aspect-[3/2]" />
105
+ </Field>
106
+ <Field label="Logo du pied de page" hint="Version claire sur fond sombre. Vide = reprend le logo de l’en-tête.">
107
+ <div className="rounded-xl bg-navy-900 p-1">
108
+ <ImageField value={b.logoDarkUrl} onChange={(logoDarkUrl) => ed.patch({ logoDarkUrl })} aspect="aspect-[3/2]" />
109
+ </div>
110
+ </Field>
111
+ <Field label="Favicon" hint="Carré, 512 × 512 px.">
112
+ <ImageField value={b.faviconUrl} onChange={(faviconUrl) => ed.patch({ faviconUrl })} aspect="aspect-[3/2]" />
113
+ </Field>
114
+ </div>
115
+ </Section>
116
+
117
+ <Section
118
+ title="Couleurs"
119
+ actions={
120
+ <Button size="sm" variant="ghost" icon={RotateCcw} onClick={() => ed.patch({ colors: DEFAULT_COLORS })}>
121
+ Couleurs Snapify
122
+ </Button>
123
+ }
124
+ >
125
+ <div className="grid gap-4 sm:grid-cols-2">
126
+ {COLOR_LABELS.map((l) => (
127
+ <ColorField key={l.key} label={l.label} hint={l.hint || undefined} value={b.colors[l.key]} onChange={(v) => setColor(l.key, v)} />
128
+ ))}
129
+ </div>
130
+ </Section>
131
+
132
+ <Section title="Typographie">
133
+ <div className="grid gap-4 sm:grid-cols-2">
134
+ <Field label="Police des titres">
135
+ <Select value={b.fonts.heading} onChange={(heading) => ed.patch({ fonts: { ...b.fonts, heading } })} options={withCurrent(FONTS, b.fonts.heading)} />
136
+ </Field>
137
+ <Field label="Police du texte">
138
+ <Select value={b.fonts.body} onChange={(body) => ed.patch({ fonts: { ...b.fonts, body } })} options={withCurrent(FONTS, b.fonts.body)} />
139
+ </Field>
140
+ </div>
141
+ </Section>
142
+ </div>
143
+ <div className="min-w-0 xl:sticky xl:top-20 xl:self-start">
144
+ <LivePreview b={b} pages={pages} />
145
+ </div>
146
+ </div>
147
+ <SaveBar dirty={ed.dirty} saving={ed.saving} onSave={ed.commit} onReset={ed.reset} blocker={ed.blocker} />
148
+ </div>
149
+ );
150
+ }
151
+
152
+ function withCurrent(opts: { value: string; label: string }[], current: string) {
153
+ return opts.some((o) => o.value === current) || !current ? opts : [{ value: current, label: current }, ...opts];
154
+ }