create-snapify-app 0.0.0-stage → 1.1.0

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 +33 -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 +100 -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,137 @@
1
+ /**
2
+ * Snapify — module Espace client
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 } from 'react';
8
+ import { Heart, UserRound } from 'lucide-react';
9
+ import { definePublicModule, type PublicModuleProps, type PublicPageProps } from '@snapify/sdk';
10
+ import { COMMERCE_PATHS, getShared, useWishlist } from '@snapify/sdk/commerce';
11
+ import { Button, Container, Link, Spinner } from '@snapify/sdk/ui/commerce';
12
+ import { AddressesPage, OrderPage, OrdersPage, OverviewPage, ProfilePage, WishlistPage } from './AccountPages';
13
+ import { ForgotPage, LoginPage, RegisterPage, ResetPage, safeNext } from './AuthPages';
14
+ import { CustomerAvatar } from './Avatar';
15
+ import { api, loadMe, useMe } from './session';
16
+ import type { Settings } from '../types';
17
+
18
+ /* ------------------------------------------------------------------ */
19
+ /* Favoris : enregistrés dans le compte quand le client est connecté */
20
+ /* ------------------------------------------------------------------ */
21
+
22
+ let syncInstalled = false;
23
+ function installWishlistSync() {
24
+ if (syncInstalled) return;
25
+ syncInstalled = true;
26
+ let timer: ReturnType<typeof setTimeout> | undefined;
27
+ let last = '';
28
+ window.addEventListener('snapify:wishlist-changed', (e) => {
29
+ const ids = ((e as CustomEvent<string[]>).detail ?? []).slice();
30
+ clearTimeout(timer);
31
+ timer = setTimeout(() => {
32
+ const key = ids.join(',');
33
+ if (!getShared('customer') || key === last) return;
34
+ last = key;
35
+ void api.post('/wishlist', { ids }).catch(() => {
36
+ last = '';
37
+ });
38
+ }, 600);
39
+ });
40
+ }
41
+
42
+ /* ------------------------------------------------------------------ */
43
+ /* Icônes de l'en-tête */
44
+ /* ------------------------------------------------------------------ */
45
+
46
+ const ICON_BTN = 'relative inline-flex h-10 w-10 items-center justify-center rounded-full text-current transition hover:bg-black/5';
47
+
48
+ function AccountSlot({ navigate }: PublicModuleProps<Settings>) {
49
+ const me = useMe();
50
+ const ids = useWishlist();
51
+ useEffect(() => {
52
+ installWishlistSync();
53
+ void loadMe();
54
+ }, []);
55
+ return (
56
+ <div className="flex items-center">
57
+ <Link to={COMMERCE_PATHS.accountSection('favoris')} navigate={navigate} aria-label={`Favoris${ids.length ? ` (${ids.length})` : ''}`} className={`${ICON_BTN} hidden sm:inline-flex`}>
58
+ <Heart className="h-[21px] w-[21px]" strokeWidth={1.8} />
59
+ {ids.length ? <span className="absolute right-1 top-1 h-2.5 w-2.5 rounded-full bg-px-primary ring-2 ring-white" /> : null}
60
+ </Link>
61
+ <Link to={me ? COMMERCE_PATHS.account : COMMERCE_PATHS.login} navigate={navigate} aria-label={me ? `Mon compte (${me.firstName})` : 'Se connecter'} title={me ? `${me.firstName} ${me.lastName}` : 'Se connecter'} className={ICON_BTN}>
62
+ {me ? (
63
+ <CustomerAvatar person={me} className="h-8 w-8 text-[12px] tracking-wide" />
64
+ ) : (
65
+ <UserRound className="h-[22px] w-[22px]" strokeWidth={1.8} />
66
+ )}
67
+ </Link>
68
+ </div>
69
+ );
70
+ }
71
+
72
+ /* ------------------------------------------------------------------ */
73
+ /* Pages /compte/… */
74
+ /* ------------------------------------------------------------------ */
75
+
76
+ const PUBLIC_PAGES = new Set(['connexion', 'inscription', 'mot-de-passe-oublie', 'reinitialiser']);
77
+
78
+ function Redirect({ to, navigate }: { to: string; navigate: (p: string) => void }) {
79
+ useEffect(() => navigate(to), [to, navigate]);
80
+ return <Loading />;
81
+ }
82
+
83
+ function Loading() {
84
+ return (
85
+ <div className="flex min-h-[50vh] items-center justify-center">
86
+ <Spinner className="h-7 w-7" />
87
+ </div>
88
+ );
89
+ }
90
+
91
+ function NotFound({ navigate }: { navigate: (p: string) => void }) {
92
+ return (
93
+ <Container className="py-24 text-center">
94
+ <p className="font-heading text-2xl font-semibold text-px-navy">Page introuvable</p>
95
+ <Button className="mt-6" onClick={() => navigate(COMMERCE_PATHS.account)}>
96
+ Mon compte
97
+ </Button>
98
+ </Container>
99
+ );
100
+ }
101
+
102
+ function AccountRouter(props: PublicPageProps<Settings>) {
103
+ const me = useMe();
104
+ useEffect(() => {
105
+ installWishlistSync();
106
+ void loadMe();
107
+ }, []);
108
+
109
+ const parts = props.path.replace(/^\/compte\/?/, '').split('/').filter(Boolean);
110
+ const section = parts[0] ?? '';
111
+
112
+ if (me === undefined) return <Loading />;
113
+
114
+ if (PUBLIC_PAGES.has(section)) {
115
+ if (me && section !== 'reinitialiser') return <Redirect to={safeNext(props.search)} navigate={props.navigate} />;
116
+ if (section === 'connexion') return <LoginPage {...props} />;
117
+ if (section === 'inscription') return <RegisterPage {...props} />;
118
+ if (section === 'mot-de-passe-oublie') return <ForgotPage {...props} />;
119
+ return <ResetPage {...props} />;
120
+ }
121
+
122
+ if (section === 'favoris' && parts.length === 1) return <WishlistPage {...props} me={me} />;
123
+
124
+ if (!me) return <Redirect to={`${COMMERCE_PATHS.login}?suite=${encodeURIComponent(props.path + props.search)}`} navigate={props.navigate} />;
125
+
126
+ if (!section) return <OverviewPage {...props} me={me} />;
127
+ if (section === 'commandes' && parts.length === 1) return <OrdersPage {...props} me={me} />;
128
+ if (section === 'commandes' && parts.length === 2) return <OrderPage {...props} me={me} number={decodeURIComponent(parts[1])} />;
129
+ if (section === 'adresses' && parts.length === 1) return <AddressesPage {...props} me={me} />;
130
+ if (section === 'profil' && parts.length === 1) return <ProfilePage {...props} me={me} />;
131
+ return <NotFound navigate={props.navigate} />;
132
+ }
133
+
134
+ export default definePublicModule<Settings>({
135
+ slots: { 'header-actions': AccountSlot },
136
+ page: AccountRouter,
137
+ });
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Snapify — module Espace client
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 { createModuleApi } from '@snapify/sdk';
8
+ import { getShared, MODULE_IDS, setCustomer, setShared, useShared, wishlist } from '@snapify/sdk/commerce';
9
+ import type { Me } from '../types';
10
+
11
+ export const api = createModuleApi(MODULE_IDS.customers);
12
+ const ME_KEY = 'customers:me';
13
+
14
+ /** Client connecté avec ses adresses (`undefined` : pas encore chargé). */
15
+ export const useMe = () => useShared<Me | null | undefined>(ME_KEY, undefined);
16
+
17
+ /** Enregistre le client connecté et fusionne les favoris du navigateur avec ceux du compte. */
18
+ export function applyMe(me: Me | null) {
19
+ setShared(ME_KEY, me);
20
+ setCustomer(me ? { id: me.id, firstName: me.firstName, lastName: me.lastName, email: me.email, phone: me.phone, avatarUrl: me.avatarUrl || undefined } : null);
21
+ if (me) {
22
+ const local = wishlist.ids();
23
+ const merged = [...new Set([...me.wishlist, ...local])];
24
+ if (merged.length !== local.length || merged.some((id, i) => id !== local[i])) wishlist.set(merged);
25
+ }
26
+ }
27
+
28
+ export const patchMe = (patch: Partial<Me>) => {
29
+ const me = getShared<Me | null>(ME_KEY);
30
+ if (me) applyMe({ ...me, ...patch });
31
+ };
32
+
33
+ let loading: Promise<void> | null = null;
34
+
35
+ export function loadMe(force = false): Promise<void> {
36
+ if (!force && (loading || getShared(ME_KEY) !== undefined)) return loading ?? Promise.resolve();
37
+ loading = api
38
+ .get<{ customer: Me | null }>('/me')
39
+ .then((r) => applyMe(r.customer))
40
+ .catch(() => applyMe(null))
41
+ .finally(() => {
42
+ loading = null;
43
+ });
44
+ return loading;
45
+ }
46
+
47
+ export async function logout() {
48
+ await api.post('/logout').catch(() => undefined);
49
+ applyMe(null);
50
+ wishlist.set([]);
51
+ }
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Snapify — module Espace client
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 { createHash, randomBytes, scrypt as scryptCb, timingSafeEqual } from 'node:crypto';
8
+ import { promisify } from 'node:util';
9
+ import { defineServerModule, type ModuleCookie, type ModuleRequest, type ServerContext, type StoredRecord } from '@snapify/sdk';
10
+ import { isEmail, isPhone, MODULE_IDS, validateAddress, type CheckoutServices, type CustomerIdentity, type CustomerServices, type SavedAddress } from '@snapify/sdk/commerce';
11
+ import type { Customer, Me, Session, Settings } from './types';
12
+
13
+ const scrypt = promisify(scryptCb) as (pw: string, salt: Buffer, len: number) => Promise<Buffer>;
14
+ const COOKIE = 'sm_customer';
15
+ const MIN_PASSWORD = 8;
16
+ const RESET_TTL_MS = 60 * 60 * 1000;
17
+ const MAX_ADDRESSES = 10;
18
+ const AVATAR_MAX_BYTES = 512 * 1024;
19
+ const LOGIN_WINDOW_MS = 15 * 60 * 1000;
20
+ const LOGIN_MAX_FAILS = 8;
21
+
22
+ const sha256 = (v: string) => createHash('sha256').update(v).digest('hex');
23
+ const str = (v: unknown, max: number) => (typeof v === 'string' ? v.trim().slice(0, max) : '');
24
+ const body = (req: ModuleRequest) => (req.body && typeof req.body === 'object' ? (req.body as Record<string, unknown>) : {});
25
+ const normEmail = (v: unknown) => str(v, 254).toLowerCase();
26
+
27
+ async function hashPassword(pw: string): Promise<string> {
28
+ const salt = randomBytes(16);
29
+ const key = await scrypt(pw, salt, 64);
30
+ return `scrypt$${salt.toString('base64')}$${key.toString('base64')}`;
31
+ }
32
+
33
+ async function verifyPassword(pw: string, stored: string): Promise<boolean> {
34
+ const [algo, salt, hash] = stored.split('$');
35
+ if (algo !== 'scrypt' || !salt || !hash) return false;
36
+ const expected = Buffer.from(hash, 'base64');
37
+ const key = await scrypt(pw, Buffer.from(salt, 'base64'), expected.length);
38
+ return timingSafeEqual(key, expected);
39
+ }
40
+
41
+ /** Mot de passe factice : une connexion sur un e-mail inconnu prend autant de temps qu'une vraie. */
42
+ let dummyHash: Promise<string> | null = null;
43
+
44
+ export default defineServerModule<Settings>({
45
+ activate(ctx: ServerContext<Settings>) {
46
+ const customers = ctx.db.collection<Customer>('customers');
47
+ const sessions = ctx.db.collection<Session>('sessions');
48
+ const s = () => ctx.settings();
49
+ const checkout = () => ctx.use<CheckoutServices>(MODULE_IDS.checkout);
50
+ const failures = new Map<string, { n: number; until: number }>();
51
+ dummyHash ??= hashPassword(randomBytes(12).toString('hex'));
52
+
53
+ const toMe = (c: StoredRecord<Customer>): Me => ({
54
+ id: c.id,
55
+ email: c.email,
56
+ firstName: c.firstName,
57
+ lastName: c.lastName,
58
+ phone: c.phone,
59
+ avatarUrl: c.avatarUrl ?? '',
60
+ addresses: c.addresses,
61
+ wishlist: c.wishlist,
62
+ createdAt: c.createdAt,
63
+ });
64
+
65
+ async function current(cookies: Record<string, string>): Promise<StoredRecord<Customer> | null> {
66
+ const token = cookies[COOKIE];
67
+ if (!token || token.length > 100) return null;
68
+ const session = await sessions.get(sha256(token));
69
+ if (!session || session.expiresAt < new Date().toISOString()) return null;
70
+ const c = await customers.get(session.customerId);
71
+ return c && c.status === 'active' ? c : null;
72
+ }
73
+
74
+ const requireCustomer = async (req: ModuleRequest) => {
75
+ const c = await current(req.cookies);
76
+ if (!c) throw ctx.error(401, 'Votre session a expiré : reconnectez-vous.');
77
+ return c;
78
+ };
79
+
80
+ async function openSession(c: StoredRecord<Customer>, req: ModuleRequest, remember: boolean): Promise<ModuleCookie> {
81
+ const token = randomBytes(32).toString('base64url');
82
+ const days = remember ? s().sessionDays : 1;
83
+ await sessions.insert({ customerId: c.id, expiresAt: new Date(Date.now() + days * 86400_000).toISOString(), ip: req.ip, userAgent: req.userAgent.slice(0, 200) }, { id: sha256(token) });
84
+ await customers.update(c.id, { lastLoginAt: new Date().toISOString() });
85
+ return { name: COOKIE, value: token, ...(remember ? { maxAgeSec: days * 86400 } : {}) };
86
+ }
87
+
88
+ async function closeSessions(customerId: string) {
89
+ for (;;) {
90
+ const { items } = await sessions.find({ where: { customerId }, limit: 200 });
91
+ if (!items.length) return;
92
+ for (const x of items) await sessions.delete(x.id);
93
+ }
94
+ }
95
+
96
+ const mergeWishlist = (a: string[], b: unknown) => [...new Set([...a, ...(Array.isArray(b) ? b.filter((x): x is string => typeof x === 'string' && x.length <= 64) : [])])].slice(0, 200);
97
+
98
+ function checkPassword(pw: string) {
99
+ if (pw.length < MIN_PASSWORD) throw ctx.error(422, `Le mot de passe doit contenir au moins ${MIN_PASSWORD} caractères.`);
100
+ if (pw.length > 200) throw ctx.error(422, 'Mot de passe trop long.');
101
+ }
102
+
103
+ const siteUrl = () => (ctx.site().publicUrl || '').replace(/\/+$/, '');
104
+
105
+ async function sendReset(c: StoredRecord<Customer>) {
106
+ const token = randomBytes(32).toString('base64url');
107
+ await customers.update(c.id, { resetHash: sha256(token), resetExpires: new Date(Date.now() + RESET_TTL_MS).toISOString() });
108
+ const url = `${siteUrl()}/compte/reinitialiser/?t=${token}`;
109
+ const mail = ctx.mail.render({
110
+ title: 'Réinitialisation de votre mot de passe',
111
+ intro: `Bonjour ${c.firstName}, vous avez demandé à choisir un nouveau mot de passe pour votre compte client.`,
112
+ button: { label: 'Choisir un nouveau mot de passe', url },
113
+ outro: 'Ce lien est valable une heure. Si vous n’êtes pas à l’origine de cette demande, ignorez simplement cet e-mail.',
114
+ });
115
+ return ctx.mail.send({ to: c.email, subject: `${ctx.site().name} — nouveau mot de passe`, ...mail });
116
+ }
117
+
118
+ const services: CustomerServices = {
119
+ current: async (cookies): Promise<CustomerIdentity | null> => {
120
+ const c = await current(cookies);
121
+ return c ? { id: c.id, email: c.email, firstName: c.firstName, lastName: c.lastName, phone: c.phone, addresses: c.addresses } : null;
122
+ },
123
+ };
124
+ ctx.provide(services);
125
+
126
+ const timer = setInterval(() => {
127
+ void sessions.find({ filters: [{ field: 'expiresAt', op: 'lt', value: new Date().toISOString() }], limit: 500 }).then(({ items }) => Promise.all(items.map((x) => sessions.delete(x.id)))).catch(() => undefined);
128
+ const now = Date.now();
129
+ for (const [k, v] of failures) if (v.until < now) failures.delete(k);
130
+ }, 60 * 60 * 1000);
131
+ timer.unref?.();
132
+
133
+ /* ---------------------------------------------------------------- Session */
134
+
135
+ ctx.http.get('/me', async (req) => {
136
+ const c = await current(req.cookies);
137
+ return { customer: c ? toMe(c) : null };
138
+ });
139
+
140
+ ctx.http.post('/register', async (req) => {
141
+ if (!s().allowRegistration) throw ctx.error(403, 'Les inscriptions sont fermées pour le moment.');
142
+ const b = body(req);
143
+ const email = normEmail(b.email);
144
+ const firstName = str(b.firstName, 60);
145
+ const lastName = str(b.lastName, 60);
146
+ const phone = str(b.phone, 24);
147
+ const password = typeof b.password === 'string' ? b.password : '';
148
+ if (!firstName || !lastName) throw ctx.error(422, 'Indiquez votre prénom et votre nom.');
149
+ if (!isEmail(email)) throw ctx.error(422, 'Adresse e-mail invalide.');
150
+ if ((s().requirePhone || phone) && !isPhone(phone)) throw ctx.error(422, 'Numéro de téléphone invalide.');
151
+ checkPassword(password);
152
+ const c = await ctx.lock(`register:${email}`, async () => {
153
+ if (await customers.findOne({ email })) throw ctx.error(409, 'Un compte existe déjà avec cette adresse e-mail : connectez-vous ou réinitialisez votre mot de passe.');
154
+ return customers.insert({ email, firstName, lastName, phone, passwordHash: await hashPassword(password), addresses: [], wishlist: mergeWishlist([], b.wishlist), status: 'active', lastLoginAt: '', notes: '' });
155
+ });
156
+ const cookie = await openSession(c, req, b.remember !== false);
157
+ if (s().welcomeEmail) {
158
+ const mail = ctx.mail.render({
159
+ title: `Bienvenue, ${firstName} !`,
160
+ intro: s().welcomeText,
161
+ button: { label: 'Accéder à mon espace client', url: `${siteUrl()}/compte/` },
162
+ });
163
+ void ctx.mail.send({ to: email, subject: `Bienvenue sur ${ctx.site().name}`, ...mail });
164
+ }
165
+ return ctx.http.reply({ customer: toMe((await customers.get(c.id))!) }, { status: 201, cookies: [cookie] });
166
+ });
167
+
168
+ ctx.http.post('/login', async (req) => {
169
+ const b = body(req);
170
+ const email = normEmail(b.email);
171
+ const password = typeof b.password === 'string' ? b.password.slice(0, 200) : '';
172
+ const key = `${email}|${req.ip}`;
173
+ const f = failures.get(key);
174
+ if (f && f.n >= LOGIN_MAX_FAILS && f.until > Date.now()) throw ctx.error(429, 'Trop de tentatives : réessayez dans quelques minutes ou réinitialisez votre mot de passe.');
175
+ const c = email ? await customers.findOne({ email }) : null;
176
+ const ok = c ? await verifyPassword(password, c.passwordHash) : (await verifyPassword(password, await dummyHash!), false);
177
+ if (!c || !ok) {
178
+ failures.set(key, { n: (f && f.until > Date.now() ? f.n : 0) + 1, until: Date.now() + LOGIN_WINDOW_MS });
179
+ throw ctx.error(401, 'E-mail ou mot de passe incorrect.');
180
+ }
181
+ if (c.status !== 'active') throw ctx.error(403, 'Ce compte est désactivé. Contactez-nous pour plus d’informations.');
182
+ failures.delete(key);
183
+ const wishlist = mergeWishlist(c.wishlist, b.wishlist);
184
+ if (wishlist.length !== c.wishlist.length) await customers.update(c.id, { wishlist });
185
+ const cookie = await openSession(c, req, b.remember !== false);
186
+ return ctx.http.reply({ customer: toMe((await customers.get(c.id))!) }, { cookies: [cookie] });
187
+ });
188
+
189
+ ctx.http.post('/logout', async (req) => {
190
+ const token = req.cookies[COOKIE];
191
+ if (token) await sessions.delete(sha256(token));
192
+ return ctx.http.reply({ ok: true }, { cookies: [{ name: COOKIE, value: '', maxAgeSec: 0 }] });
193
+ });
194
+
195
+ ctx.http.post('/password/forgot', async (req) => {
196
+ const email = normEmail(body(req).email);
197
+ if (!isEmail(email)) throw ctx.error(422, 'Adresse e-mail invalide.');
198
+ const c = await customers.findOne({ email });
199
+ if (c && c.status === 'active') {
200
+ const last = c.resetExpires ? new Date(c.resetExpires).getTime() - RESET_TTL_MS : 0;
201
+ if (Date.now() - last > 60_000) await sendReset(c);
202
+ }
203
+ // Réponse identique que le compte existe ou non.
204
+ return { ok: true };
205
+ });
206
+
207
+ ctx.http.post('/password/reset', async (req) => {
208
+ const b = body(req);
209
+ const token = str(b.token, 100);
210
+ const password = typeof b.password === 'string' ? b.password : '';
211
+ checkPassword(password);
212
+ const c = token ? await customers.findOne({ resetHash: sha256(token) }) : null;
213
+ if (!c || !c.resetExpires || c.resetExpires < new Date().toISOString()) throw ctx.error(400, 'Ce lien n’est plus valable : demandez-en un nouveau.');
214
+ await customers.update(c.id, { passwordHash: await hashPassword(password), resetHash: '', resetExpires: '' });
215
+ await closeSessions(c.id);
216
+ const cookie = await openSession(c, req, true);
217
+ return ctx.http.reply({ customer: toMe((await customers.get(c.id))!) }, { cookies: [cookie] });
218
+ });
219
+
220
+ /* ---------------------------------------------------------------- Compte */
221
+
222
+ ctx.http.post('/profile', async (req) => {
223
+ const c = await requireCustomer(req);
224
+ const b = body(req);
225
+ const firstName = str(b.firstName, 60);
226
+ const lastName = str(b.lastName, 60);
227
+ const phone = str(b.phone, 24);
228
+ const email = normEmail(b.email ?? c.email);
229
+ if (!firstName || !lastName) throw ctx.error(422, 'Indiquez votre prénom et votre nom.');
230
+ if (phone && !isPhone(phone)) throw ctx.error(422, 'Numéro de téléphone invalide.');
231
+ if (!isEmail(email)) throw ctx.error(422, 'Adresse e-mail invalide.');
232
+ if (email !== c.email) {
233
+ if (!(await verifyPassword(typeof b.password === 'string' ? b.password : '', c.passwordHash))) throw ctx.error(403, 'Mot de passe actuel incorrect : il est demandé pour changer d’adresse e-mail.');
234
+ const other = await customers.findOne({ email });
235
+ if (other && other.id !== c.id) throw ctx.error(409, 'Cette adresse e-mail est déjà utilisée par un autre compte.');
236
+ }
237
+ const updated = await customers.update(c.id, { firstName, lastName, phone, email });
238
+ return { customer: toMe(updated!) };
239
+ });
240
+
241
+ ctx.http.post('/avatar', async (req) => {
242
+ const c = await requireCustomer(req);
243
+ if (!s().avatars) throw ctx.error(403, 'Les photos de profil sont désactivées sur ce site.');
244
+ const b = body(req);
245
+ const url = await ctx.files.saveImage({ dataBase64: str(b.dataBase64, 3_000_000), mimeType: str(b.mimeType, 30) }, { prefix: 'avatar', maxBytes: AVATAR_MAX_BYTES });
246
+ const updated = await customers.update(c.id, { avatarUrl: url });
247
+ await ctx.files.delete(c.avatarUrl);
248
+ return { customer: toMe(updated!) };
249
+ });
250
+
251
+ ctx.http.post('/avatar/delete', async (req) => {
252
+ const c = await requireCustomer(req);
253
+ const updated = await customers.update(c.id, { avatarUrl: '' });
254
+ await ctx.files.delete(c.avatarUrl);
255
+ return { customer: toMe(updated!) };
256
+ });
257
+
258
+ ctx.http.post('/password', async (req) => {
259
+ const c = await requireCustomer(req);
260
+ const b = body(req);
261
+ if (!(await verifyPassword(typeof b.current === 'string' ? b.current : '', c.passwordHash))) throw ctx.error(403, 'Mot de passe actuel incorrect.');
262
+ const next = typeof b.next === 'string' ? b.next : '';
263
+ checkPassword(next);
264
+ await customers.update(c.id, { passwordHash: await hashPassword(next) });
265
+ await closeSessions(c.id);
266
+ const cookie = await openSession(c, req, true);
267
+ return ctx.http.reply({ ok: true }, { cookies: [cookie] });
268
+ });
269
+
270
+ ctx.http.post('/addresses', async (req) => {
271
+ const c = await requireCustomer(req);
272
+ const b = body(req);
273
+ const { value, errors } = validateAddress(b.address);
274
+ const first = Object.values(errors)[0];
275
+ if (first) throw ctx.error(422, first);
276
+ const id = typeof b.id === 'string' && c.addresses.some((a) => a.id === b.id) ? b.id : randomBytes(6).toString('base64url');
277
+ const exists = c.addresses.some((a) => a.id === id);
278
+ if (!exists && c.addresses.length >= MAX_ADDRESSES) throw ctx.error(422, `${MAX_ADDRESSES} adresses au maximum.`);
279
+ const makeDefault = b.isDefault === true || c.addresses.length === 0;
280
+ const entry: SavedAddress = { ...value, id, label: str(b.label, 40) || 'Adresse', isDefault: makeDefault || (exists && !!c.addresses.find((a) => a.id === id)?.isDefault) };
281
+ let addresses = exists ? c.addresses.map((a) => (a.id === id ? entry : a)) : [...c.addresses, entry];
282
+ if (entry.isDefault) addresses = addresses.map((a) => ({ ...a, isDefault: a.id === id }));
283
+ const updated = await customers.update(c.id, { addresses });
284
+ return { addresses: updated!.addresses, id };
285
+ });
286
+
287
+ ctx.http.post('/addresses/:id/delete', async (req) => {
288
+ const c = await requireCustomer(req);
289
+ let addresses = c.addresses.filter((a) => a.id !== req.params.id);
290
+ if (addresses.length && !addresses.some((a) => a.isDefault)) addresses = addresses.map((a, i) => ({ ...a, isDefault: i === 0 }));
291
+ const updated = await customers.update(c.id, { addresses });
292
+ return { addresses: updated!.addresses };
293
+ });
294
+
295
+ ctx.http.post('/addresses/:id/default', async (req) => {
296
+ const c = await requireCustomer(req);
297
+ if (!c.addresses.some((a) => a.id === req.params.id)) throw ctx.error(404, 'Adresse introuvable');
298
+ const updated = await customers.update(c.id, { addresses: c.addresses.map((a) => ({ ...a, isDefault: a.id === req.params.id })) });
299
+ return { addresses: updated!.addresses };
300
+ });
301
+
302
+ ctx.http.post('/wishlist', async (req) => {
303
+ const c = await requireCustomer(req);
304
+ const wishlist = mergeWishlist([], body(req).ids);
305
+ await customers.update(c.id, { wishlist });
306
+ return { wishlist };
307
+ });
308
+
309
+ ctx.http.get('/overview', async (req) => {
310
+ const c = await requireCustomer(req);
311
+ const co = checkout();
312
+ const [stats, recent] = co ? await Promise.all([co.customerStats(c.id), co.ordersForCustomer(c.id, { limit: 3 })]) : [null, null];
313
+ return { stats, recent: recent?.items ?? [], ordersEnabled: !!co };
314
+ });
315
+
316
+ ctx.http.get('/orders', async (req) => {
317
+ const c = await requireCustomer(req);
318
+ const co = checkout();
319
+ if (!co) return { items: [], total: 0, ordersEnabled: false };
320
+ const page = Math.max(1, Math.floor(Number(req.query.page)) || 1);
321
+ const r = await co.ordersForCustomer(c.id, { limit: 10, offset: (page - 1) * 10 });
322
+ return { ...r, ordersEnabled: true };
323
+ });
324
+
325
+ ctx.http.get('/orders/:number', async (req) => {
326
+ const c = await requireCustomer(req);
327
+ const order = await checkout()?.orderForCustomer(c.id, str(req.params.number, 40));
328
+ if (!order) throw ctx.error(404, 'Commande introuvable');
329
+ return { order };
330
+ });
331
+
332
+ ctx.http.post('/delete-account', async (req) => {
333
+ const c = await requireCustomer(req);
334
+ if (!(await verifyPassword(typeof body(req).password === 'string' ? (body(req).password as string) : '', c.passwordHash))) throw ctx.error(403, 'Mot de passe incorrect.');
335
+ await closeSessions(c.id);
336
+ await customers.delete(c.id);
337
+ await ctx.files.delete(c.avatarUrl);
338
+ return ctx.http.reply({ ok: true }, { cookies: [{ name: COOKIE, value: '', maxAgeSec: 0 }] });
339
+ });
340
+
341
+ /* ---------------------------------------------------------------- Administration */
342
+
343
+ ctx.admin.get('/customers', async (req) => {
344
+ const q = req.query;
345
+ const size = Math.min(500, Math.max(1, Math.floor(Number(q.size)) || 25));
346
+ const page = Math.max(1, Math.floor(Number(q.page)) || 1);
347
+ const text = str(q.q, 100);
348
+ const { items, total } = await customers.find({
349
+ where: q.status === 'active' || q.status === 'disabled' ? { status: q.status } : undefined,
350
+ search: text ? { fields: ['email', 'firstName', 'lastName', 'phone'], text } : undefined,
351
+ orderBy: [{ field: q.sort === 'name' ? 'lastName' : 'createdAt', dir: q.sort === 'name' ? 'asc' : 'desc' }],
352
+ limit: size,
353
+ offset: (page - 1) * size,
354
+ });
355
+ const co = checkout();
356
+ const stats = co ? await Promise.all(items.map((c) => co.customerStats(c.id))) : [];
357
+ return {
358
+ items: items.map((c, i) => ({ id: c.id, email: c.email, firstName: c.firstName, lastName: c.lastName, phone: c.phone, avatarUrl: c.avatarUrl ?? '', status: c.status, createdAt: c.createdAt, lastLoginAt: c.lastLoginAt, city: c.addresses.find((a) => a.isDefault)?.city ?? '', orders: stats[i]?.orders ?? 0, spent: stats[i]?.spent ?? 0 })),
359
+ total,
360
+ ordersEnabled: !!co,
361
+ };
362
+ });
363
+
364
+ ctx.admin.get('/customers/:id', async (req) => {
365
+ const c = await customers.get(req.params.id);
366
+ if (!c) throw ctx.error(404, 'Client introuvable');
367
+ const co = checkout();
368
+ const [stats, orders] = co ? await Promise.all([co.customerStats(c.id), co.ordersForCustomer(c.id, { limit: 50 })]) : [null, null];
369
+ const { passwordHash: _p, resetHash: _r, resetExpires: _e, ...safe } = c;
370
+ return { customer: { ...safe, wishlistCount: c.wishlist.length }, stats, orders: orders?.items ?? [], ordersEnabled: !!co };
371
+ });
372
+
373
+ ctx.admin.put('/customers/:id', async (req) => {
374
+ const c = await customers.get(req.params.id);
375
+ if (!c) throw ctx.error(404, 'Client introuvable');
376
+ const b = body(req);
377
+ const firstName = str(b.firstName, 60) || c.firstName;
378
+ const lastName = str(b.lastName, 60) || c.lastName;
379
+ const phone = str(b.phone, 24);
380
+ if (phone && !isPhone(phone)) throw ctx.error(422, 'Numéro de téléphone invalide.');
381
+ const status = b.status === 'disabled' ? 'disabled' : 'active';
382
+ await customers.update(c.id, { firstName, lastName, phone, status, notes: str(b.notes, 4000) });
383
+ if (status === 'disabled') await closeSessions(c.id);
384
+ return { ok: true };
385
+ });
386
+
387
+ ctx.admin.post('/customers/:id/reset-link', async (req) => {
388
+ const c = await customers.get(req.params.id);
389
+ if (!c) throw ctx.error(404, 'Client introuvable');
390
+ const r = await sendReset(c);
391
+ if (!r.sent) throw ctx.error(502, `E-mail non envoyé : ${r.reason || 'vérifiez la configuration SMTP dans Paramètres > E-mails.'}`);
392
+ return { ok: true };
393
+ });
394
+
395
+ ctx.admin.post('/customers/:id/avatar/delete', async (req) => {
396
+ const c = await customers.get(req.params.id);
397
+ if (!c) throw ctx.error(404, 'Client introuvable');
398
+ await customers.update(c.id, { avatarUrl: '' });
399
+ await ctx.files.delete(c.avatarUrl);
400
+ return { ok: true };
401
+ });
402
+
403
+ ctx.admin.delete('/customers/:id', async (req) => {
404
+ const c = await customers.get(req.params.id);
405
+ await closeSessions(req.params.id);
406
+ if (!c || !(await customers.delete(c.id))) throw ctx.error(404, 'Client introuvable');
407
+ await ctx.files.delete(c.avatarUrl);
408
+ return { ok: true };
409
+ });
410
+
411
+ ctx.admin.status(async () => {
412
+ const monthStart = new Date();
413
+ monthStart.setDate(1);
414
+ monthStart.setHours(0, 0, 0, 0);
415
+ const [total, month] = await Promise.all([customers.count(), customers.count({ filters: [{ field: 'createdAt', op: 'gte', value: monthStart.toISOString() }] })]);
416
+ return { stats: [{ label: 'Clients inscrits', value: total }, { label: 'Nouveaux ce mois-ci', value: month }] };
417
+ });
418
+
419
+ return () => clearInterval(timer);
420
+ },
421
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Snapify — module Espace client
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 type { SavedAddress } from '@snapify/sdk/commerce';
8
+
9
+ export interface Settings {
10
+ allowRegistration: boolean;
11
+ requirePhone: boolean;
12
+ avatars: boolean;
13
+ sessionDays: number;
14
+ welcomeEmail: boolean;
15
+ welcomeText: string;
16
+ privacyPath: string;
17
+ }
18
+
19
+ export interface Customer {
20
+ email: string;
21
+ firstName: string;
22
+ lastName: string;
23
+ phone: string;
24
+ avatarUrl?: string;
25
+ passwordHash: string;
26
+ addresses: SavedAddress[];
27
+ wishlist: string[];
28
+ status: 'active' | 'disabled';
29
+ lastLoginAt: string;
30
+ notes: string;
31
+ resetHash?: string;
32
+ resetExpires?: string;
33
+ }
34
+
35
+ export interface Session {
36
+ customerId: string;
37
+ expiresAt: string;
38
+ ip: string;
39
+ userAgent: string;
40
+ }
41
+
42
+ /** Données renvoyées au navigateur du client connecté. */
43
+ export interface Me {
44
+ id: string;
45
+ email: string;
46
+ firstName: string;
47
+ lastName: string;
48
+ phone: string;
49
+ avatarUrl: string;
50
+ addresses: SavedAddress[];
51
+ wishlist: string[];
52
+ createdAt: string;
53
+ }