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,559 @@
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, { useEffect, useMemo, useState } from 'react';
8
+ import { ArrowRight, ArrowUpRight, CalendarClock, CheckCircle2, ChevronRight, Clock, Phone, ShieldCheck } from 'lucide-react';
9
+ import type { BlockStyle, ContactFormData, CtaData, ProjectMeta, ProjectsData, SimulatorData } from '@snapify/sdk/content';
10
+ import type { ContactFormConfig } from '@snapify/sdk/content';
11
+ import { CeeSimulator } from '../CeeSimulator';
12
+ import { IS_PREVIEW } from '../site';
13
+ import { getRecaptchaToken, RecaptchaField, type RecaptchaPublicConfig } from '@snapify/sdk/recaptcha';
14
+ import { ButtonRow, cx, Highlighted, Placeholder, Reveal, Section, SectionHeading, useCms, useGo } from '../kit';
15
+
16
+ /* ------------------------------------------------------------------ Appel à l'action */
17
+
18
+ export function CtaBlock({ d, style }: { d: CtaData; style: BlockStyle }) {
19
+ const { t } = useCms();
20
+ const box = {
21
+ gradient: 'bg-gradient-to-r from-px-primary-dark via-px-accent-mid to-px-accent px-gradient-pan text-white',
22
+ dark: 'bg-px-navy text-white shadow-[0_40px_80px_-40px_rgba(11,21,40,0.6)]',
23
+ image: 'bg-px-navy text-white',
24
+ light: 'border border-px-border bg-white text-px-navy',
25
+ }[d.variant];
26
+ const onDark = d.variant !== 'light';
27
+ const centered = d.layout === 'centered';
28
+
29
+ return (
30
+ <Section style={style}>
31
+ <Reveal>
32
+ <div className={cx('relative overflow-hidden rounded-[1.75rem] p-8 sm:p-10 lg:px-14', centered && 'px-6 py-14 text-center sm:p-16 lg:p-20', box)}>
33
+ {d.variant === 'gradient' ? (
34
+ <>
35
+ <div className="absolute -right-10 -top-16 h-72 w-72 rounded-full border-[28px] border-white/10" />
36
+ <div className="absolute -bottom-24 right-40 h-56 w-56 rounded-full bg-white/10 blur-2xl" />
37
+ </>
38
+ ) : null}
39
+ {d.variant === 'dark' || d.variant === 'image' ? (
40
+ <>
41
+ {d.backgroundImage ? <img src={d.backgroundImage} alt="" aria-hidden="true" loading="lazy" className="absolute inset-0 h-full w-full object-cover opacity-20" /> : null}
42
+ {d.backgroundImage ? <div className="absolute inset-0 bg-gradient-to-r from-px-navy via-px-navy/85 to-px-navy/60" /> : null}
43
+ {centered ? <div className="absolute inset-0 px-grid-bg opacity-40 [filter:invert(1)]" /> : null}
44
+ <div className="absolute -bottom-24 -left-24 h-80 w-80 rounded-full bg-px-primary/25 blur-[90px]" />
45
+ <div className="absolute right-0 top-0 h-96 w-96 rounded-full bg-px-accent/20 blur-[100px]" />
46
+ </>
47
+ ) : null}
48
+ {d.variant === 'light' ? <div className="absolute -right-16 -top-16 h-64 w-64 rounded-full bg-px-primary/10 blur-3xl" /> : null}
49
+
50
+ {centered ? (
51
+ <div className="relative mx-auto max-w-3xl space-y-6">
52
+ {d.badge ? (
53
+ <div className={cx('inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-medium', onDark ? 'border-white/15 bg-white/10 text-white/80' : 'border-px-border bg-px-surface text-slate-600')}>
54
+ <Clock className="h-3.5 w-3.5 text-px-primary-light" />
55
+ {t(d.badge)}
56
+ </div>
57
+ ) : null}
58
+ <h2 className={cx('text-4xl font-semibold leading-[1.05] tracking-tight sm:text-5xl lg:text-6xl', onDark ? 'text-white' : 'text-px-navy')}>
59
+ {t(d.title)}
60
+ {d.highlight ? (
61
+ <>
62
+ {' '}
63
+ <span className="bg-gradient-to-r from-px-primary-light to-px-accent-light bg-clip-text text-transparent">{t(d.highlight)}</span>
64
+ </>
65
+ ) : null}
66
+ </h2>
67
+ {d.text ? <p className={cx('text-base leading-relaxed sm:text-lg', onDark ? 'text-slate-300' : 'text-slate-600')}>{t(d.text)}</p> : null}
68
+ <ButtonRow buttons={d.buttons} center size="lg" className="pt-2" />
69
+ </div>
70
+ ) : d.bigNumber ? (
71
+ <div className="relative grid grid-cols-1 items-center gap-8 lg:grid-cols-12">
72
+ <div className="flex items-end gap-4 lg:col-span-5">
73
+ <div className="bg-gradient-to-br from-px-primary-light to-px-accent bg-clip-text text-[5.5rem] font-bold leading-none text-transparent sm:text-[7rem]">{t(d.bigNumber)}</div>
74
+ <div className="pb-4 text-sm leading-snug text-white/70">
75
+ {t(d.bigNumberLabel)}
76
+ {d.bigNumberStrong ? (
77
+ <>
78
+ <br />
79
+ <span className="font-semibold text-white">{t(d.bigNumberStrong)}</span>
80
+ </>
81
+ ) : null}
82
+ </div>
83
+ </div>
84
+ <div className="space-y-3 text-white lg:col-span-4">
85
+ {d.badge ? (
86
+ <div className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/10 px-3 py-1 text-[11px] font-semibold text-px-primary-light">
87
+ <CalendarClock className="h-3.5 w-3.5" />
88
+ {t(d.badge)}
89
+ </div>
90
+ ) : null}
91
+ <h3 className="text-xl font-semibold leading-snug sm:text-2xl">
92
+ <Highlighted title={d.title} highlight={d.highlight} />
93
+ </h3>
94
+ {d.text ? <p className="text-sm leading-relaxed text-white/65">{t(d.text)}</p> : null}
95
+ </div>
96
+ <div className="flex lg:col-span-3 lg:justify-end">
97
+ <ButtonRow buttons={d.buttons} />
98
+ </div>
99
+ </div>
100
+ ) : (
101
+ <div className={cx('relative grid grid-cols-1 items-center gap-8', d.image && 'lg:grid-cols-[1fr_auto]')}>
102
+ <div className="max-w-2xl space-y-6">
103
+ <div>
104
+ {d.eyebrow ? <div className={cx('mb-2 text-[11px] font-semibold uppercase tracking-[0.18em]', onDark ? 'text-white/80' : 'text-px-accent')}>{t(d.eyebrow)}</div> : null}
105
+ <h3 className={cx('text-2xl font-semibold leading-tight sm:text-3xl', onDark ? 'text-white' : 'text-px-navy')}>
106
+ {t(d.title)}
107
+ {d.highlight ? (
108
+ <>
109
+ {' '}
110
+ <span className={d.variant === 'gradient' ? 'text-px-navy' : onDark ? 'text-px-primary-light' : 'text-px-primary-dark'}>{t(d.highlight)}</span>
111
+ </>
112
+ ) : null}
113
+ </h3>
114
+ {d.text ? <p className={cx('mt-3 text-sm leading-relaxed', onDark ? 'text-white/75' : 'text-slate-600')}>{t(d.text)}</p> : null}
115
+ </div>
116
+ <ButtonRow buttons={d.buttons} />
117
+ </div>
118
+ {d.image ? (
119
+ <div className="relative mx-auto w-52 shrink-0 rotate-2 rounded-md bg-white p-2 pb-0 shadow-2xl lg:mx-0 lg:w-60">
120
+ <span className="px-tape" aria-hidden="true" />
121
+ <img src={d.image} alt={d.imageAlt || ''} loading="lazy" className="aspect-[4/3] w-full rounded-sm object-cover object-right" />
122
+ <div className="py-2 text-center">
123
+ <span className="font-hand text-xl leading-tight text-px-navy">{t(d.imageCaption)}</span>
124
+ </div>
125
+ </div>
126
+ ) : null}
127
+ </div>
128
+ )}
129
+ </div>
130
+ </Reveal>
131
+ </Section>
132
+ );
133
+ }
134
+
135
+ /* ------------------------------------------------------------------ Réalisations */
136
+
137
+ export type ProjectCard = ProjectMeta & { id: string; path: string; title: string };
138
+
139
+ let projectsCache: Promise<ProjectCard[]> | null = null;
140
+ function loadProjects() {
141
+ projectsCache ??= fetch('/api/public/projects')
142
+ .then((r) => (r.ok ? r.json() : []))
143
+ .catch(() => []);
144
+ return projectsCache;
145
+ }
146
+
147
+ function useProjects() {
148
+ const [list, setList] = useState<ProjectCard[] | null>(null);
149
+ useEffect(() => {
150
+ let alive = true;
151
+ void loadProjects().then((p) => alive && setList(p));
152
+ return () => {
153
+ alive = false;
154
+ };
155
+ }, []);
156
+ return list;
157
+ }
158
+
159
+ export function ProjectsBlock({ d, style }: { d: ProjectsData; style: BlockStyle }) {
160
+ const all = useProjects();
161
+ const go = useGo();
162
+ const { t } = useCms();
163
+ const [filter, setFilter] = useState('all');
164
+
165
+ const projects = useMemo(() => {
166
+ let list = all || [];
167
+ if (d.filter === 'sector' && d.sector) list = list.filter((p) => p.sector.toLowerCase().includes(d.sector!.toLowerCase()));
168
+ if (filter !== 'all') list = list.filter((p) => p.sector === filter);
169
+ return d.limit > 0 ? list.slice(0, d.limit) : list;
170
+ }, [all, d.filter, d.sector, d.limit, filter]);
171
+ const sectors = useMemo(() => [...new Set((all || []).map((p) => p.sector))], [all]);
172
+
173
+ if (all && !projects.length && filter === 'all') {
174
+ return IS_PREVIEW ? (
175
+ <Section style={style}>
176
+ <Placeholder label="Aucune réalisation publiée pour ce filtre" className="h-40 rounded-2xl" />
177
+ </Section>
178
+ ) : null;
179
+ }
180
+
181
+ const body = !all ? (
182
+ <div className="grid grid-cols-1 gap-5 md:grid-cols-3">
183
+ {[0, 1, 2].map((i) => (
184
+ <div key={i} className="h-72 animate-pulse rounded-[1.25rem] bg-px-surface" />
185
+ ))}
186
+ </div>
187
+ ) : d.layout === 'featured' ? (
188
+ <FeaturedProjects projects={projects} go={go} t={t} />
189
+ ) : d.layout === 'compact' ? (
190
+ <div className="grid grid-cols-1 gap-6 md:grid-cols-3">
191
+ {projects.map((p, i) => (
192
+ <Reveal key={p.id} delay={i * 80}>
193
+ <button type="button" onClick={() => go(p.path)} className="card-snapify group w-full overflow-hidden text-left">
194
+ <div className="aspect-16/10 overflow-hidden bg-slate-100">
195
+ <img src={p.image} alt={p.solution} loading="lazy" className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" />
196
+ </div>
197
+ <div className="space-y-1 p-4">
198
+ <div className="text-xs font-semibold text-px-primary-dark">
199
+ {p.client} — {p.location}
200
+ </div>
201
+ <div className="line-clamp-1 text-sm font-semibold text-px-navy">{p.solution}</div>
202
+ </div>
203
+ </button>
204
+ </Reveal>
205
+ ))}
206
+ </div>
207
+ ) : (
208
+ <>
209
+ {d.showFilters && sectors.length > 1 ? (
210
+ <div className="mb-8 flex flex-wrap items-center gap-2 border-b border-slate-200 pb-4">
211
+ <span className="mr-2 text-xs font-semibold uppercase tracking-wider text-slate-400">Filtrer par secteur :</span>
212
+ {['all', ...sectors].map((s) => (
213
+ <button
214
+ key={s}
215
+ type="button"
216
+ onClick={() => setFilter(s)}
217
+ className={cx('rounded-lg px-3.5 py-1.5 text-xs font-medium transition-all', filter === s ? 'bg-px-navy text-white shadow-xs' : 'border border-slate-200 bg-white text-slate-700 hover:border-slate-300')}
218
+ >
219
+ {s === 'all' ? 'Tous les chantiers' : s}
220
+ </button>
221
+ ))}
222
+ </div>
223
+ ) : null}
224
+ <div className="grid grid-cols-1 gap-8 md:grid-cols-2">
225
+ {projects.map((p, i) => (
226
+ <Reveal key={p.id} delay={(i % 2) * 80}>
227
+ <button type="button" onClick={() => go(p.path)} className="card-snapify group flex h-full w-full flex-col justify-between overflow-hidden text-left">
228
+ <div>
229
+ <div className="relative aspect-16/10 overflow-hidden bg-px-navy">
230
+ <img src={p.image} alt={`${p.solution} chez ${p.client}`} loading="lazy" className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" />
231
+ <div className="absolute left-4 top-4 rounded-full border border-slate-200 bg-white/95 px-3 py-1 text-xs font-semibold text-px-navy">{p.sector}</div>
232
+ <div className="absolute right-4 top-4 rounded-full bg-px-navy/80 px-2.5 py-1 font-mono text-[11px] text-white">
233
+ {p.location}
234
+ {p.department ? ` (${p.department})` : ''}
235
+ </div>
236
+ </div>
237
+ <div className="space-y-3 p-6">
238
+ <div className="text-xs font-semibold text-px-primary-dark">{p.client}</div>
239
+ <h3 className="text-lg font-semibold text-px-navy transition-colors group-hover:text-px-primary-dark">{p.solution}</h3>
240
+ <p className="line-clamp-2 text-xs leading-relaxed text-slate-600 sm:text-sm">{p.summary}</p>
241
+ <div className="grid grid-cols-2 gap-2 border-t border-slate-100 pt-3 text-xs">
242
+ <div>
243
+ <span className="block text-[10px] uppercase text-slate-400">Fiche CEE :</span>
244
+ <span className="font-mono font-medium text-slate-800">{p.ceeSheet || 'Standard'}</span>
245
+ </div>
246
+ <div>
247
+ <span className="block text-[10px] uppercase text-slate-400">Contrôle :</span>
248
+ <span className="font-medium text-emerald-700">Organisme COFRAC</span>
249
+ </div>
250
+ </div>
251
+ </div>
252
+ </div>
253
+ <div className="flex items-center justify-between border-t border-slate-100 px-6 pb-6 pt-4 text-xs font-semibold text-px-navy group-hover:text-px-primary-dark">
254
+ <span>Lorem ipsum dolor</span>
255
+ <ChevronRight className="h-4 w-4 transition-transform group-hover:translate-x-1" />
256
+ </div>
257
+ </button>
258
+ </Reveal>
259
+ ))}
260
+ </div>
261
+ </>
262
+ );
263
+
264
+ return (
265
+ <Section style={style}>
266
+ <SectionHeading h={d} />
267
+ {body}
268
+ </Section>
269
+ );
270
+ }
271
+
272
+ function FeaturedProjects({ projects, go, t }: { projects: ProjectCard[]; go: (l?: string) => void; t: (s?: string) => string }) {
273
+ const [featured, ...others] = [...projects].sort((a, b) => Number(!!b.featured) - Number(!!a.featured));
274
+ if (!featured) return null;
275
+ return (
276
+ <div className="grid grid-cols-1 gap-5 lg:grid-cols-12">
277
+ <Reveal className="lg:col-span-7">
278
+ <button type="button" onClick={() => go(featured.path)} className="group relative h-full min-h-[420px] w-full overflow-hidden rounded-[1.75rem] text-left lg:min-h-[540px]">
279
+ <img src={featured.image} alt={`${featured.solution} — ${featured.client}`} className="absolute inset-0 h-full w-full object-cover transition-transform duration-[1200ms] group-hover:scale-105" />
280
+ <div className="absolute inset-0 bg-gradient-to-t from-px-navy via-px-navy/40 to-transparent" />
281
+ <div className="absolute left-5 top-5 flex gap-2">
282
+ <span className="rounded-full bg-white/95 px-3 py-1 text-[11px] font-semibold text-px-navy">{featured.sector}</span>
283
+ {featured.ceeSheet ? <span className="rounded-full bg-px-primary px-3 py-1 text-[11px] font-semibold text-px-navy">{featured.ceeSheet}</span> : null}
284
+ </div>
285
+ <div className="absolute inset-x-0 bottom-0 p-6 text-white sm:p-8">
286
+ <div className="mb-2 text-xs font-semibold text-px-primary-light">
287
+ {featured.client} — {featured.location}
288
+ {featured.department ? ` (${featured.department})` : ''}
289
+ </div>
290
+ <h3 className="max-w-lg text-2xl font-semibold leading-tight sm:text-3xl">{featured.solution}</h3>
291
+ <div className="grid grid-rows-[0fr] transition-all duration-500 group-hover:grid-rows-[1fr]">
292
+ <ul className="space-y-1.5 overflow-hidden text-sm text-white/80">
293
+ {featured.results.map((r) => (
294
+ <li key={r} className="flex items-start gap-2 pt-2 first:pt-4">
295
+ <CheckCircle2 className="mt-0.5 h-4 w-4 shrink-0 text-px-primary-light" />
296
+ {t(r)}
297
+ </li>
298
+ ))}
299
+ </ul>
300
+ </div>
301
+ <span className="mt-5 inline-flex items-center gap-2 text-sm font-semibold">
302
+ Voir le chantier
303
+ <span className="flex h-8 w-8 items-center justify-center rounded-full bg-white text-px-navy transition-colors group-hover:bg-px-primary">
304
+ <ArrowUpRight className="h-4 w-4" />
305
+ </span>
306
+ </span>
307
+ </div>
308
+ </button>
309
+ </Reveal>
310
+ <div className="grid grid-cols-1 gap-5 lg:col-span-5">
311
+ {others.slice(0, 3).map((item, idx) => (
312
+ <Reveal key={item.id} delay={100 + idx * 90} className="h-full">
313
+ <button
314
+ type="button"
315
+ onClick={() => go(item.path)}
316
+ className="group flex h-full w-full items-stretch gap-4 rounded-[1.25rem] border border-px-border bg-white p-3 text-left transition-all hover:border-px-primary/60 hover:shadow-[0_20px_40px_-20px_rgba(0,112,243,0.25)]"
317
+ >
318
+ <div className="relative w-28 shrink-0 overflow-hidden rounded-xl sm:w-36">
319
+ <img src={item.image} alt={`${item.solution} — ${item.client}`} className="absolute inset-0 h-full w-full object-cover transition-transform duration-700 group-hover:scale-110" />
320
+ </div>
321
+ <div className="flex min-w-0 flex-1 flex-col justify-between py-1.5 pr-2">
322
+ <div>
323
+ <div className="text-[11px] font-semibold text-px-primary-dark">
324
+ {item.client} · {item.sector}
325
+ </div>
326
+ <h3 className="mt-1 line-clamp-2 text-sm font-semibold leading-snug text-px-navy sm:text-base">{item.solution}</h3>
327
+ </div>
328
+ <div className="mt-3 flex items-center justify-between">
329
+ <span className="truncate font-mono text-[10px] text-slate-400">{item.ceeSheet}</span>
330
+ <ArrowUpRight className="h-4 w-4 shrink-0 text-slate-400 transition-all group-hover:-translate-y-0.5 group-hover:translate-x-0.5 group-hover:text-px-accent" />
331
+ </div>
332
+ </div>
333
+ </button>
334
+ </Reveal>
335
+ ))}
336
+ </div>
337
+ </div>
338
+ );
339
+ }
340
+
341
+ /* ------------------------------------------------------------------ Simulateur */
342
+
343
+ export function SimulatorBlock({ d, style }: { d: SimulatorData; style: BlockStyle }) {
344
+ const go = useGo();
345
+ return (
346
+ <Section style={style}>
347
+ <SectionHeading h={{ ...d, align: d.align ?? 'center' }} className="mb-10" />
348
+ <Reveal delay={100}>
349
+ <CeeSimulator onNavigateToContact={() => go('/contact/')} />
350
+ </Reveal>
351
+ </Section>
352
+ );
353
+ }
354
+
355
+ /* ------------------------------------------------------------------ Formulaire de contact */
356
+
357
+ type PublicForm = Omit<ContactFormConfig, 'recipientEmails'> & { recaptcha?: RecaptchaPublicConfig };
358
+
359
+ const inputCls = 'w-full rounded-xl border border-slate-200 bg-white px-3.5 py-2.5 text-xs text-slate-800 focus:border-px-primary focus:outline-hidden sm:text-sm';
360
+ const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
361
+
362
+ export function ContactFormBlock({ d, style }: { d: ContactFormData; style: BlockStyle }) {
363
+ const { t, tokens } = useCms();
364
+ const go = useGo();
365
+ const [form, setForm] = useState<PublicForm | null>(null);
366
+ const [failed, setFailed] = useState(false);
367
+ const [values, setValues] = useState<Record<string, string | boolean>>({});
368
+ const [honeypot, setHoneypot] = useState('');
369
+ const [errors, setErrors] = useState<Record<string, string>>({});
370
+ const [submitError, setSubmitError] = useState('');
371
+ const [submitting, setSubmitting] = useState(false);
372
+
373
+ useEffect(() => {
374
+ fetch(`/api/public/forms/${encodeURIComponent(d.formId || 'contact-main')}`)
375
+ .then((r) => (r.ok ? r.json() : Promise.reject()))
376
+ .then(setForm)
377
+ .catch(() => setFailed(true));
378
+ }, [d.formId]);
379
+
380
+ const validate = () => {
381
+ const errs: Record<string, string> = {};
382
+ for (const f of form?.fields || []) {
383
+ const v = values[f.name];
384
+ const empty = v === undefined || v === '' || v === false;
385
+ if (f.required && empty) errs[f.name] = f.type === 'checkbox' ? 'Cette case doit être cochée.' : `Merci de renseigner : ${f.label.toLowerCase()}.`;
386
+ else if (!empty && f.type === 'email' && !EMAIL_RE.test(String(v))) errs[f.name] = 'Veuillez saisir un e-mail valide.';
387
+ else if (!empty && f.type === 'tel' && String(v).replace(/\D/g, '').length < 8) errs[f.name] = 'Numéro de téléphone incomplet.';
388
+ }
389
+ return errs;
390
+ };
391
+
392
+ const submit = (e: React.FormEvent) => {
393
+ e.preventDefault();
394
+ if (!form || honeypot || IS_PREVIEW) return;
395
+ const errs = validate();
396
+ setErrors(errs);
397
+ if (Object.keys(errs).length) return;
398
+ setSubmitting(true);
399
+ setSubmitError('');
400
+ void (async () => {
401
+ try {
402
+ let recaptchaToken = '';
403
+ if (form.recaptcha?.siteKey) {
404
+ recaptchaToken = await getRecaptchaToken(form.recaptcha);
405
+ }
406
+ const r = await fetch(`/api/public/forms/${encodeURIComponent(form.id)}/submit`, {
407
+ method: 'POST',
408
+ headers: { 'Content-Type': 'application/json' },
409
+ body: JSON.stringify({
410
+ ...values,
411
+ ...(form.honeypotField ? { [form.honeypotField]: honeypot } : {}),
412
+ ...(recaptchaToken ? { 'g-recaptcha-response': recaptchaToken, recaptchaToken } : {}),
413
+ }),
414
+ });
415
+ const res = await r.json().catch(() => ({}));
416
+ setSubmitting(false);
417
+ if (r.ok) return go(res.redirect || form.successPath || '/contact/merci/');
418
+ if (res.fields) setErrors(res.fields);
419
+ setSubmitError(r.status === 429 ? 'Trop de demandes envoyées. Merci de patienter quelques minutes ou de nous appeler directement.' : res.error || "L'envoi a échoué. Merci de réessayer ou de nous appeler directement.");
420
+ } catch (err) {
421
+ setSubmitting(false);
422
+ setSubmitError(err instanceof Error ? err.message : 'Connexion impossible. Vérifiez votre réseau puis réessayez.');
423
+ }
424
+ })();
425
+ };
426
+
427
+ const set = (name: string, v: string | boolean) => setValues((s) => ({ ...s, [name]: v }));
428
+
429
+ return (
430
+ <Section style={style}>
431
+ <div className="grid grid-cols-1 gap-12 lg:grid-cols-12">
432
+ <Reveal className="space-y-6 lg:col-span-5">
433
+ <div className="space-y-3">
434
+ <h1 className="text-3xl font-semibold leading-tight tracking-tight text-px-navy sm:text-4xl lg:text-5xl">{t(d.title)}</h1>
435
+ {d.intro ? <p className="text-base leading-relaxed text-slate-600">{t(d.intro)}</p> : null}
436
+ </div>
437
+ <div className="space-y-4 rounded-2xl border border-px-border bg-white p-6 shadow-xs">
438
+ <div className="flex items-center gap-3">
439
+ <div className="flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 text-px-primary-dark">
440
+ <Phone className="h-5 w-5" />
441
+ </div>
442
+ <div>
443
+ <div className="text-xs font-medium text-slate-500">{t(d.phoneLabel) || 'Ligne directe'}</div>
444
+ <a href={`tel:${tokens.phoneClean}`} className="font-mono text-base font-bold text-px-navy transition-colors hover:text-px-primary-dark sm:text-lg">
445
+ {tokens.phone}
446
+ </a>
447
+ </div>
448
+ </div>
449
+ {d.phoneHint ? (
450
+ <div className="flex items-center gap-2 border-t border-slate-100 pt-2 text-xs text-slate-600">
451
+ <Clock className="h-4 w-4 shrink-0 text-px-primary-dark" />
452
+ <span>{t(d.phoneHint)}</span>
453
+ </div>
454
+ ) : null}
455
+ </div>
456
+ {d.listItems.length ? (
457
+ <div className="card-snapify space-y-3 bg-white p-6">
458
+ {d.listTitle ? <div className="text-xs font-semibold uppercase tracking-wider text-slate-400">{t(d.listTitle)}</div> : null}
459
+ <ul className="space-y-2 text-xs text-slate-600">
460
+ {d.listItems.map((it) => (
461
+ <li key={it} className="flex items-center gap-2">
462
+ <ShieldCheck className="h-4 w-4 text-px-primary-dark" />
463
+ <span>{t(it)}</span>
464
+ </li>
465
+ ))}
466
+ </ul>
467
+ </div>
468
+ ) : null}
469
+ </Reveal>
470
+
471
+ <Reveal delay={120} className="lg:col-span-7">
472
+ <form onSubmit={submit} noValidate className="card-snapify space-y-6 border border-px-border bg-white p-6 shadow-sm sm:p-8 lg:p-10">
473
+ <div className="hidden" aria-hidden="true">
474
+ <input type="text" name="user_extra_check" value={honeypot} onChange={(e) => setHoneypot(e.target.value)} tabIndex={-1} autoComplete="off" />
475
+ </div>
476
+ {failed ? (
477
+ <p className="text-sm text-slate-500">Le formulaire est momentanément indisponible. Appelez-nous au {tokens.phone}.</p>
478
+ ) : !form ? (
479
+ <div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
480
+ {[0, 1, 2, 3, 4, 5].map((i) => (
481
+ <div key={i} className="h-16 animate-pulse rounded-xl bg-px-surface" />
482
+ ))}
483
+ </div>
484
+ ) : (
485
+ <div className="grid grid-cols-1 gap-5 sm:grid-cols-2">
486
+ {form.fields.map((f) => {
487
+ const full = f.width === 'full' || f.type === 'textarea' || f.type === 'checkbox';
488
+ const err = errors[f.name];
489
+ if (f.type === 'checkbox') {
490
+ const parts = f.label.split(/(politique de confidentialité)/i);
491
+ return (
492
+ <div key={f.id} className="pt-2 sm:col-span-2">
493
+ <label className="flex cursor-pointer select-none items-start gap-3">
494
+ <input type="checkbox" checked={!!values[f.name]} onChange={(e) => set(f.name, e.target.checked)} className="mt-0.5 h-4 w-4 cursor-pointer rounded border-slate-300 text-px-primary focus:ring-px-primary" />
495
+ <span className="text-xs leading-relaxed text-slate-600">
496
+ {parts.map((p, i) =>
497
+ /politique de confidentialité/i.test(p) ? (
498
+ <button key={i} type="button" onClick={() => go('/politique-de-confidentialite/')} className="font-medium text-px-primary-dark underline hover:text-px-navy">
499
+ {p}
500
+ </button>
501
+ ) : (
502
+ <React.Fragment key={i}>{p}</React.Fragment>
503
+ )
504
+ )}
505
+ {f.required ? ' *' : ''}
506
+ </span>
507
+ </label>
508
+ {err ? <p className="mt-1 text-[11px] text-rose-600">{err}</p> : null}
509
+ </div>
510
+ );
511
+ }
512
+ return (
513
+ <div key={f.id} className={full ? 'sm:col-span-2' : ''}>
514
+ <label htmlFor={`f-${f.id}`} className="mb-1.5 block text-xs font-semibold uppercase tracking-wider text-slate-700">
515
+ {f.label}
516
+ {f.required ? ' *' : ''}
517
+ </label>
518
+ {f.type === 'select' ? (
519
+ <select id={`f-${f.id}`} value={String(values[f.name] ?? '')} onChange={(e) => set(f.name, e.target.value)} className={inputCls}>
520
+ <option value="">{f.placeholder || 'Sélectionnez…'}</option>
521
+ {(f.options || []).map((o) => (
522
+ <option key={o} value={o}>
523
+ {o}
524
+ </option>
525
+ ))}
526
+ </select>
527
+ ) : f.type === 'textarea' ? (
528
+ <textarea id={`f-${f.id}`} rows={4} placeholder={f.placeholder} value={String(values[f.name] ?? '')} onChange={(e) => set(f.name, e.target.value)} className={cx(inputCls, 'leading-relaxed')} />
529
+ ) : (
530
+ <input id={`f-${f.id}`} type={f.type} placeholder={f.placeholder} value={String(values[f.name] ?? '')} onChange={(e) => set(f.name, e.target.value)} className={inputCls} />
531
+ )}
532
+ {err ? <p className="mt-1 text-[11px] text-rose-600">{err}</p> : null}
533
+ </div>
534
+ );
535
+ })}
536
+ </div>
537
+ )}
538
+
539
+ {form?.recaptcha ? <RecaptchaField config={form.recaptcha} /> : null}
540
+
541
+ {submitError ? (
542
+ <div role="alert" className="rounded-xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
543
+ {submitError}
544
+ </div>
545
+ ) : null}
546
+
547
+ <div className="flex flex-col justify-between gap-4 border-t border-slate-100 pt-4 sm:flex-row sm:items-center">
548
+ <span className="order-2 text-[11px] text-slate-400 sm:order-1">* Champs obligatoires</span>
549
+ <button type="submit" disabled={submitting || !form} className="btn-primary-snapify order-1 w-full! justify-center sm:order-2 sm:w-auto!">
550
+ <span>{submitting ? 'Transmission en cours…' : t(d.submitLabel) || 'Envoyer ma demande'}</span>
551
+ <ArrowRight className="btn-arrow h-4 w-4" />
552
+ </button>
553
+ </div>
554
+ </form>
555
+ </Reveal>
556
+ </div>
557
+ </Section>
558
+ );
559
+ }