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,349 @@
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, useState } from 'react';
8
+ import { Eye, EyeOff, Heart, MailCheck, Package, ShieldCheck, UserRound } from 'lucide-react';
9
+ import type { PublicPageProps } from '@snapify/sdk';
10
+ import { COMMERCE_PATHS, isEmail, isPhone, wishlist } from '@snapify/sdk/commerce';
11
+ import { Button, Checkbox, Container, FormField, Link, Notice, TextInput } from '@snapify/sdk/ui/commerce';
12
+ import { api, applyMe } from './session';
13
+ import type { Me, Settings } from '../types';
14
+
15
+ const MIN_PASSWORD = 8;
16
+
17
+ /** Retour après connexion : uniquement une adresse interne au site. */
18
+ export function safeNext(search: string): string {
19
+ const next = new URLSearchParams(search).get('suite') || '';
20
+ return next.startsWith('/') && !next.startsWith('//') && !next.startsWith('/\\') ? next : COMMERCE_PATHS.account;
21
+ }
22
+
23
+ const withNext = (path: string, search: string) => {
24
+ const next = new URLSearchParams(search).get('suite');
25
+ return next ? `${path}?suite=${encodeURIComponent(next)}` : path;
26
+ };
27
+
28
+ function AuthCard({ title, subtitle, children, aside }: { title: string; subtitle?: React.ReactNode; children: React.ReactNode; aside?: React.ReactNode }) {
29
+ return (
30
+ <Container className="py-12 sm:py-20">
31
+ <div className={aside ? 'mx-auto grid max-w-5xl gap-10 lg:grid-cols-[1fr_380px] lg:items-start' : 'mx-auto max-w-md'}>
32
+ <div className="rounded-3xl border border-px-border-soft bg-white p-6 shadow-sm sm:p-10">
33
+ <h1 className="font-heading text-2xl font-semibold text-px-navy sm:text-3xl">{title}</h1>
34
+ {subtitle ? <p className="mt-2 text-sm text-px-muted">{subtitle}</p> : null}
35
+ <div className="mt-8">{children}</div>
36
+ </div>
37
+ {aside}
38
+ </div>
39
+ </Container>
40
+ );
41
+ }
42
+
43
+ export function PasswordInput({ value, onChange, autoComplete, invalid }: { value: string; onChange: (v: string) => void; autoComplete: string; invalid?: boolean }) {
44
+ const [show, setShow] = useState(false);
45
+ return (
46
+ <div className="relative">
47
+ <TextInput type={show ? 'text' : 'password'} autoComplete={autoComplete} value={value} invalid={invalid} maxLength={200} onChange={(e) => onChange(e.target.value)} className="pr-11" />
48
+ <button type="button" onClick={() => setShow((s) => !s)} aria-label={show ? 'Masquer le mot de passe' : 'Afficher le mot de passe'} className="absolute inset-y-0 right-0 flex w-11 items-center justify-center text-px-muted hover:text-px-navy">
49
+ {show ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
50
+ </button>
51
+ </div>
52
+ );
53
+ }
54
+
55
+ function Benefits() {
56
+ const items = [
57
+ { icon: Package, title: 'Suivi de vos commandes', text: 'Lorem ipsum dolor sit amet, retrouvez chaque commande et son statut.' },
58
+ { icon: UserRound, title: 'Commande express', text: 'Vos adresses enregistrées, sans tout ressaisir à chaque achat.' },
59
+ { icon: Heart, title: 'Favoris synchronisés', text: 'Consectetur adipiscing elit : vos coups de cœur sur tous vos appareils.' },
60
+ ];
61
+ return (
62
+ <aside className="space-y-6 rounded-3xl bg-px-surface p-8">
63
+ <p className="font-heading text-lg font-semibold text-px-navy">Pourquoi créer un compte ?</p>
64
+ {items.map(({ icon: Icon, title, text }) => (
65
+ <div key={title} className="flex gap-4">
66
+ <span className="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-white text-px-primary shadow-sm">
67
+ <Icon className="h-5 w-5" />
68
+ </span>
69
+ <div>
70
+ <p className="text-sm font-semibold text-px-navy">{title}</p>
71
+ <p className="mt-0.5 text-sm text-px-text">{text}</p>
72
+ </div>
73
+ </div>
74
+ ))}
75
+ </aside>
76
+ );
77
+ }
78
+
79
+ export function LoginPage({ navigate, search, settings, setMeta }: PublicPageProps<Settings>) {
80
+ const [email, setEmail] = useState('');
81
+ const [password, setPassword] = useState('');
82
+ const [remember, setRemember] = useState(true);
83
+ const [error, setError] = useState('');
84
+ const [busy, setBusy] = useState(false);
85
+ useEffect(() => setMeta({ title: 'Connexion' }), [setMeta]);
86
+
87
+ const submit = async (e: React.FormEvent) => {
88
+ e.preventDefault();
89
+ if (!isEmail(email.trim()) || !password) return setError('Saisissez votre adresse e-mail et votre mot de passe.');
90
+ setBusy(true);
91
+ setError('');
92
+ try {
93
+ const r = await api.post<{ customer: Me }>('/login', { email: email.trim(), password, remember, wishlist: wishlist.ids() });
94
+ applyMe(r.customer);
95
+ navigate(safeNext(search));
96
+ } catch (err) {
97
+ setError((err as Error).message);
98
+ setBusy(false);
99
+ }
100
+ };
101
+
102
+ return (
103
+ <AuthCard
104
+ title="Connexion"
105
+ subtitle="Accédez à vos commandes, adresses et favoris."
106
+ aside={settings.allowRegistration ? (
107
+ <div className="space-y-4">
108
+ <Benefits />
109
+ <Button variant="outline" block size="lg" onClick={() => navigate(withNext(COMMERCE_PATHS.register, search))}>
110
+ Créer un compte
111
+ </Button>
112
+ </div>
113
+ ) : undefined}
114
+ >
115
+ <form onSubmit={submit} noValidate className="space-y-5">
116
+ {error ? <Notice tone="danger">{error}</Notice> : null}
117
+ <FormField label="Adresse e-mail" required>
118
+ <TextInput type="email" autoComplete="email" autoFocus value={email} maxLength={254} onChange={(e) => setEmail(e.target.value)} />
119
+ </FormField>
120
+ <FormField label="Mot de passe" required>
121
+ <PasswordInput autoComplete="current-password" value={password} onChange={setPassword} />
122
+ </FormField>
123
+ <div className="flex flex-wrap items-center justify-between gap-3">
124
+ <Checkbox checked={remember} onChange={setRemember}>
125
+ Rester connecté
126
+ </Checkbox>
127
+ <Link to="/compte/mot-de-passe-oublie/" navigate={navigate} className="text-sm font-medium text-px-primary hover:underline">
128
+ Mot de passe oublié ?
129
+ </Link>
130
+ </div>
131
+ <Button type="submit" size="lg" block loading={busy}>
132
+ Se connecter
133
+ </Button>
134
+ </form>
135
+ </AuthCard>
136
+ );
137
+ }
138
+
139
+ export function RegisterPage({ navigate, search, settings, setMeta }: PublicPageProps<Settings>) {
140
+ const params = new URLSearchParams(search);
141
+ const [f, setF] = useState({ firstName: '', lastName: '', email: params.get('email') || '', phone: '', password: '' });
142
+ const [errors, setErrors] = useState<Record<string, string>>({});
143
+ const [error, setError] = useState('');
144
+ const [busy, setBusy] = useState(false);
145
+ useEffect(() => setMeta({ title: 'Créer un compte' }), [setMeta]);
146
+
147
+ if (!settings.allowRegistration) {
148
+ return (
149
+ <AuthCard title="Créer un compte">
150
+ <Notice tone="info">Les inscriptions sont fermées pour le moment. Vous pouvez toujours commander sans compte.</Notice>
151
+ <Button className="mt-6" variant="outline" onClick={() => navigate(withNext(COMMERCE_PATHS.login, search))}>
152
+ J’ai déjà un compte
153
+ </Button>
154
+ </AuthCard>
155
+ );
156
+ }
157
+
158
+ const set = (k: keyof typeof f, v: string) => {
159
+ setF((x) => ({ ...x, [k]: v }));
160
+ setErrors((e) => ({ ...e, [k]: '' }));
161
+ };
162
+
163
+ const submit = async (e: React.FormEvent) => {
164
+ e.preventDefault();
165
+ const errs: Record<string, string> = {};
166
+ if (!f.firstName.trim()) errs.firstName = 'Indiquez votre prénom.';
167
+ if (!f.lastName.trim()) errs.lastName = 'Indiquez votre nom.';
168
+ if (!isEmail(f.email.trim())) errs.email = 'Adresse e-mail invalide.';
169
+ if ((settings.requirePhone || f.phone.trim()) && !isPhone(f.phone)) errs.phone = 'Numéro de téléphone invalide.';
170
+ if (f.password.length < MIN_PASSWORD) errs.password = `Au moins ${MIN_PASSWORD} caractères.`;
171
+ setErrors(errs);
172
+ if (Object.values(errs).some(Boolean)) return;
173
+ setBusy(true);
174
+ setError('');
175
+ try {
176
+ const r = await api.post<{ customer: Me }>('/register', { ...f, email: f.email.trim(), wishlist: wishlist.ids() });
177
+ applyMe(r.customer);
178
+ navigate(safeNext(search));
179
+ } catch (err) {
180
+ setError((err as Error).message);
181
+ setBusy(false);
182
+ }
183
+ };
184
+
185
+ return (
186
+ <AuthCard
187
+ title="Créer un compte"
188
+ subtitle={
189
+ <>
190
+ Déjà inscrit ?{' '}
191
+ <Link to={withNext(COMMERCE_PATHS.login, search)} navigate={navigate} className="font-medium text-px-primary hover:underline">
192
+ Connectez-vous
193
+ </Link>
194
+ </>
195
+ }
196
+ aside={<Benefits />}
197
+ >
198
+ <form onSubmit={submit} noValidate className="grid gap-5 sm:grid-cols-2">
199
+ {error ? (
200
+ <Notice tone="danger" className="sm:col-span-2">
201
+ {error}
202
+ </Notice>
203
+ ) : null}
204
+ <FormField label="Prénom" error={errors.firstName} required>
205
+ <TextInput autoComplete="given-name" autoFocus value={f.firstName} maxLength={60} invalid={!!errors.firstName} onChange={(e) => set('firstName', e.target.value)} />
206
+ </FormField>
207
+ <FormField label="Nom" error={errors.lastName} required>
208
+ <TextInput autoComplete="family-name" value={f.lastName} maxLength={60} invalid={!!errors.lastName} onChange={(e) => set('lastName', e.target.value)} />
209
+ </FormField>
210
+ <FormField label="Adresse e-mail" error={errors.email} required className="sm:col-span-2">
211
+ <TextInput type="email" autoComplete="email" value={f.email} maxLength={254} invalid={!!errors.email} onChange={(e) => set('email', e.target.value)} />
212
+ </FormField>
213
+ <FormField label="Téléphone" error={errors.phone} required={settings.requirePhone} className="sm:col-span-2">
214
+ <TextInput type="tel" autoComplete="tel" placeholder="+216 20 000 000" value={f.phone} maxLength={24} invalid={!!errors.phone} onChange={(e) => set('phone', e.target.value)} />
215
+ </FormField>
216
+ <FormField label="Mot de passe" error={errors.password} hint={errors.password ? undefined : `${MIN_PASSWORD} caractères minimum.`} required className="sm:col-span-2">
217
+ <PasswordInput autoComplete="new-password" value={f.password} invalid={!!errors.password} onChange={(v) => set('password', v)} />
218
+ </FormField>
219
+ <div className="sm:col-span-2">
220
+ <Button type="submit" size="lg" block loading={busy}>
221
+ Créer mon compte
222
+ </Button>
223
+ {settings.privacyPath ? (
224
+ <p className="mt-4 text-center text-xs text-px-muted">
225
+ En créant un compte, vous acceptez notre{' '}
226
+ <Link to={settings.privacyPath} navigate={navigate} className="underline hover:text-px-navy">
227
+ politique de confidentialité
228
+ </Link>
229
+ .
230
+ </p>
231
+ ) : null}
232
+ </div>
233
+ </form>
234
+ </AuthCard>
235
+ );
236
+ }
237
+
238
+ export function ForgotPage({ navigate, setMeta }: PublicPageProps<Settings>) {
239
+ const [email, setEmail] = useState('');
240
+ const [sent, setSent] = useState(false);
241
+ const [error, setError] = useState('');
242
+ const [busy, setBusy] = useState(false);
243
+ useEffect(() => setMeta({ title: 'Mot de passe oublié' }), [setMeta]);
244
+
245
+ const submit = async (e: React.FormEvent) => {
246
+ e.preventDefault();
247
+ if (!isEmail(email.trim())) return setError('Adresse e-mail invalide.');
248
+ setBusy(true);
249
+ setError('');
250
+ try {
251
+ await api.post('/password/forgot', { email: email.trim() });
252
+ setSent(true);
253
+ } catch (err) {
254
+ setError((err as Error).message);
255
+ } finally {
256
+ setBusy(false);
257
+ }
258
+ };
259
+
260
+ if (sent) {
261
+ return (
262
+ <AuthCard title="Vérifiez votre boîte mail">
263
+ <div className="flex gap-4">
264
+ <span className="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl bg-px-primary/10 text-px-primary">
265
+ <MailCheck className="h-6 w-6" />
266
+ </span>
267
+ <p className="text-sm leading-relaxed text-px-text">
268
+ Si un compte existe pour <strong className="text-px-navy">{email.trim()}</strong>, vous allez recevoir un lien pour choisir un nouveau mot de passe. Il est valable une heure. Pensez à regarder dans les courriers indésirables.
269
+ </p>
270
+ </div>
271
+ <Button className="mt-8" variant="outline" block onClick={() => navigate(COMMERCE_PATHS.login)}>
272
+ Retour à la connexion
273
+ </Button>
274
+ </AuthCard>
275
+ );
276
+ }
277
+
278
+ return (
279
+ <AuthCard title="Mot de passe oublié" subtitle="Indiquez l’adresse e-mail de votre compte : nous vous envoyons un lien de réinitialisation.">
280
+ <form onSubmit={submit} noValidate className="space-y-5">
281
+ {error ? <Notice tone="danger">{error}</Notice> : null}
282
+ <FormField label="Adresse e-mail" required>
283
+ <TextInput type="email" autoComplete="email" autoFocus value={email} maxLength={254} onChange={(e) => setEmail(e.target.value)} />
284
+ </FormField>
285
+ <Button type="submit" size="lg" block loading={busy}>
286
+ Envoyer le lien
287
+ </Button>
288
+ <p className="text-center text-sm">
289
+ <Link to={COMMERCE_PATHS.login} navigate={navigate} className="font-medium text-px-primary hover:underline">
290
+ Retour à la connexion
291
+ </Link>
292
+ </p>
293
+ </form>
294
+ </AuthCard>
295
+ );
296
+ }
297
+
298
+ export function ResetPage({ navigate, search, setMeta }: PublicPageProps<Settings>) {
299
+ const token = new URLSearchParams(search).get('t') || '';
300
+ const [password, setPassword] = useState('');
301
+ const [confirm, setConfirm] = useState('');
302
+ const [error, setError] = useState('');
303
+ const [busy, setBusy] = useState(false);
304
+ useEffect(() => setMeta({ title: 'Nouveau mot de passe' }), [setMeta]);
305
+
306
+ const submit = async (e: React.FormEvent) => {
307
+ e.preventDefault();
308
+ if (password.length < MIN_PASSWORD) return setError(`Le mot de passe doit contenir au moins ${MIN_PASSWORD} caractères.`);
309
+ if (password !== confirm) return setError('Les deux mots de passe ne correspondent pas.');
310
+ setBusy(true);
311
+ setError('');
312
+ try {
313
+ const r = await api.post<{ customer: Me }>('/password/reset', { token, password });
314
+ applyMe(r.customer);
315
+ navigate(COMMERCE_PATHS.account);
316
+ } catch (err) {
317
+ setError((err as Error).message);
318
+ setBusy(false);
319
+ }
320
+ };
321
+
322
+ if (!token) {
323
+ return (
324
+ <AuthCard title="Lien incomplet">
325
+ <Notice tone="warning">Ce lien de réinitialisation est incomplet. Ouvrez le lien reçu par e-mail ou demandez-en un nouveau.</Notice>
326
+ <Button className="mt-6" onClick={() => navigate('/compte/mot-de-passe-oublie/')}>
327
+ Demander un nouveau lien
328
+ </Button>
329
+ </AuthCard>
330
+ );
331
+ }
332
+
333
+ return (
334
+ <AuthCard title="Nouveau mot de passe" subtitle="Choisissez un mot de passe que vous n’utilisez pas ailleurs.">
335
+ <form onSubmit={submit} noValidate className="space-y-5">
336
+ {error ? <Notice tone="danger">{error}</Notice> : null}
337
+ <FormField label="Nouveau mot de passe" hint={`${MIN_PASSWORD} caractères minimum.`} required>
338
+ <PasswordInput autoComplete="new-password" value={password} onChange={setPassword} />
339
+ </FormField>
340
+ <FormField label="Confirmation" required>
341
+ <PasswordInput autoComplete="new-password" value={confirm} onChange={setConfirm} />
342
+ </FormField>
343
+ <Button type="submit" size="lg" block loading={busy}>
344
+ <ShieldCheck className="h-4 w-4" /> Enregistrer et me connecter
345
+ </Button>
346
+ </form>
347
+ </AuthCard>
348
+ );
349
+ }
@@ -0,0 +1,29 @@
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, useState } from 'react';
8
+ import { UserRound } from 'lucide-react';
9
+
10
+ interface Person {
11
+ firstName: string;
12
+ lastName: string;
13
+ avatarUrl?: string;
14
+ }
15
+
16
+ /** Photo du client, ou ses initiales sur fond foncé ; `className` fixe la taille et la police. */
17
+ export function CustomerAvatar({ person, className = 'h-10 w-10 text-sm' }: { person: Person; className?: string }) {
18
+ const [broken, setBroken] = useState(false);
19
+ useEffect(() => setBroken(false), [person.avatarUrl]);
20
+ if (person.avatarUrl && !broken) {
21
+ return <img src={person.avatarUrl} alt="" onError={() => setBroken(true)} className={`shrink-0 rounded-full object-cover ${className}`} />;
22
+ }
23
+ const initials = `${person.firstName[0] ?? ''}${person.lastName[0] ?? ''}`.toUpperCase();
24
+ return (
25
+ <span className={`flex shrink-0 items-center justify-center rounded-full bg-px-navy font-heading font-semibold text-white ${className}`}>
26
+ {initials || <UserRound className="h-1/2 w-1/2" />}
27
+ </span>
28
+ );
29
+ }
@@ -0,0 +1,291 @@
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 { useRef, useState } from 'react';
8
+ import { Camera, ImagePlus, Trash2 } from 'lucide-react';
9
+ import { isEmail, isPhone, wishlist } from '@snapify/sdk/commerce';
10
+ import { AVATAR_ACCEPT, prepareAvatar } from '@snapify/sdk/image';
11
+ import { Button, FormField, Notice, Spinner, TextInput } from '@snapify/sdk/ui/commerce';
12
+ import { PasswordInput } from './AuthPages';
13
+ import { CustomerAvatar } from './Avatar';
14
+ import { api, applyMe } from './session';
15
+ import type { Me } from '../types';
16
+
17
+ const MIN_PASSWORD = 8;
18
+
19
+ function Card({ title, text, children, danger }: { title: string; text?: string; children: React.ReactNode; danger?: boolean }) {
20
+ return (
21
+ <section className={danger ? 'rounded-2xl border border-rose-200 bg-rose-50/40 p-6 sm:p-8' : 'rounded-2xl border border-px-border-soft bg-white p-6 sm:p-8'}>
22
+ <h2 className="font-heading text-lg font-semibold text-px-navy">{title}</h2>
23
+ {text ? <p className="mt-1 text-sm text-px-muted">{text}</p> : null}
24
+ <div className="mt-6">{children}</div>
25
+ </section>
26
+ );
27
+ }
28
+
29
+ function InfoForm({ me }: { me: Me }) {
30
+ const [f, setF] = useState({ firstName: me.firstName, lastName: me.lastName, phone: me.phone, email: me.email, password: '' });
31
+ const [msg, setMsg] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);
32
+ const [busy, setBusy] = useState(false);
33
+ const emailChanged = f.email.trim().toLowerCase() !== me.email;
34
+ const dirty = emailChanged || f.firstName !== me.firstName || f.lastName !== me.lastName || f.phone !== me.phone;
35
+
36
+ const submit = async (e: React.FormEvent) => {
37
+ e.preventDefault();
38
+ if (!f.firstName.trim() || !f.lastName.trim()) return setMsg({ tone: 'danger', text: 'Indiquez votre prénom et votre nom.' });
39
+ if (!isEmail(f.email.trim())) return setMsg({ tone: 'danger', text: 'Adresse e-mail invalide.' });
40
+ if (f.phone.trim() && !isPhone(f.phone)) return setMsg({ tone: 'danger', text: 'Numéro de téléphone invalide.' });
41
+ if (emailChanged && !f.password) return setMsg({ tone: 'danger', text: 'Saisissez votre mot de passe actuel pour changer d’adresse e-mail.' });
42
+ setBusy(true);
43
+ setMsg(null);
44
+ try {
45
+ const r = await api.post<{ customer: Me }>('/profile', { ...f, email: f.email.trim() });
46
+ applyMe(r.customer);
47
+ setF((x) => ({ ...x, email: r.customer.email, password: '' }));
48
+ setMsg({ tone: 'success', text: 'Vos informations ont été mises à jour.' });
49
+ } catch (err) {
50
+ setMsg({ tone: 'danger', text: (err as Error).message });
51
+ } finally {
52
+ setBusy(false);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <form onSubmit={submit} noValidate className="grid gap-5 sm:grid-cols-2">
58
+ {msg ? (
59
+ <Notice tone={msg.tone} className="sm:col-span-2">
60
+ {msg.text}
61
+ </Notice>
62
+ ) : null}
63
+ <FormField label="Prénom" required>
64
+ <TextInput autoComplete="given-name" value={f.firstName} maxLength={60} onChange={(e) => setF({ ...f, firstName: e.target.value })} />
65
+ </FormField>
66
+ <FormField label="Nom" required>
67
+ <TextInput autoComplete="family-name" value={f.lastName} maxLength={60} onChange={(e) => setF({ ...f, lastName: e.target.value })} />
68
+ </FormField>
69
+ <FormField label="Adresse e-mail" required>
70
+ <TextInput type="email" autoComplete="email" value={f.email} maxLength={254} onChange={(e) => setF({ ...f, email: e.target.value })} />
71
+ </FormField>
72
+ <FormField label="Téléphone">
73
+ <TextInput type="tel" autoComplete="tel" value={f.phone} maxLength={24} onChange={(e) => setF({ ...f, phone: e.target.value })} />
74
+ </FormField>
75
+ {emailChanged ? (
76
+ <FormField label="Mot de passe actuel" hint="Demandé pour confirmer le changement d’adresse e-mail." required className="sm:col-span-2">
77
+ <PasswordInput autoComplete="current-password" value={f.password} onChange={(v) => setF({ ...f, password: v })} />
78
+ </FormField>
79
+ ) : null}
80
+ <div className="sm:col-span-2">
81
+ <Button type="submit" loading={busy} disabled={!dirty}>
82
+ Enregistrer
83
+ </Button>
84
+ </div>
85
+ </form>
86
+ );
87
+ }
88
+
89
+ function PasswordForm() {
90
+ const [f, setF] = useState({ current: '', next: '', confirm: '' });
91
+ const [msg, setMsg] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);
92
+ const [busy, setBusy] = useState(false);
93
+
94
+ const submit = async (e: React.FormEvent) => {
95
+ e.preventDefault();
96
+ if (!f.current) return setMsg({ tone: 'danger', text: 'Saisissez votre mot de passe actuel.' });
97
+ if (f.next.length < MIN_PASSWORD) return setMsg({ tone: 'danger', text: `Le nouveau mot de passe doit contenir au moins ${MIN_PASSWORD} caractères.` });
98
+ if (f.next !== f.confirm) return setMsg({ tone: 'danger', text: 'Les deux mots de passe ne correspondent pas.' });
99
+ setBusy(true);
100
+ setMsg(null);
101
+ try {
102
+ await api.post('/password', { current: f.current, next: f.next });
103
+ setF({ current: '', next: '', confirm: '' });
104
+ setMsg({ tone: 'success', text: 'Mot de passe modifié. Vos autres appareils ont été déconnectés.' });
105
+ } catch (err) {
106
+ setMsg({ tone: 'danger', text: (err as Error).message });
107
+ } finally {
108
+ setBusy(false);
109
+ }
110
+ };
111
+
112
+ return (
113
+ <form onSubmit={submit} noValidate className="grid gap-5 sm:grid-cols-2">
114
+ {msg ? (
115
+ <Notice tone={msg.tone} className="sm:col-span-2">
116
+ {msg.text}
117
+ </Notice>
118
+ ) : null}
119
+ <FormField label="Mot de passe actuel" required className="sm:col-span-2">
120
+ <PasswordInput autoComplete="current-password" value={f.current} onChange={(v) => setF({ ...f, current: v })} />
121
+ </FormField>
122
+ <FormField label="Nouveau mot de passe" hint={`${MIN_PASSWORD} caractères minimum.`} required>
123
+ <PasswordInput autoComplete="new-password" value={f.next} onChange={(v) => setF({ ...f, next: v })} />
124
+ </FormField>
125
+ <FormField label="Confirmation" required>
126
+ <PasswordInput autoComplete="new-password" value={f.confirm} onChange={(v) => setF({ ...f, confirm: v })} />
127
+ </FormField>
128
+ <div className="sm:col-span-2">
129
+ <Button type="submit" variant="dark" loading={busy}>
130
+ Changer le mot de passe
131
+ </Button>
132
+ </div>
133
+ </form>
134
+ );
135
+ }
136
+
137
+ function DeleteAccount({ navigate }: { navigate: (p: string) => void }) {
138
+ const [open, setOpen] = useState(false);
139
+ const [password, setPassword] = useState('');
140
+ const [error, setError] = useState('');
141
+ const [busy, setBusy] = useState(false);
142
+
143
+ const submit = async (e: React.FormEvent) => {
144
+ e.preventDefault();
145
+ setBusy(true);
146
+ setError('');
147
+ try {
148
+ await api.post('/delete-account', { password });
149
+ applyMe(null);
150
+ wishlist.set([]);
151
+ navigate('/');
152
+ } catch (err) {
153
+ setError((err as Error).message);
154
+ setBusy(false);
155
+ }
156
+ };
157
+
158
+ if (!open) {
159
+ return (
160
+ <Button variant="outline" className="border-rose-300 text-rose-700 hover:bg-rose-50" onClick={() => setOpen(true)}>
161
+ Supprimer mon compte
162
+ </Button>
163
+ );
164
+ }
165
+ return (
166
+ <form onSubmit={submit} noValidate className="space-y-4">
167
+ {error ? <Notice tone="danger">{error}</Notice> : null}
168
+ <FormField label="Confirmez avec votre mot de passe" required>
169
+ <PasswordInput autoComplete="current-password" value={password} onChange={setPassword} />
170
+ </FormField>
171
+ <div className="flex flex-wrap gap-3">
172
+ <Button type="submit" variant="danger" loading={busy} disabled={!password}>
173
+ Supprimer définitivement
174
+ </Button>
175
+ <Button type="button" variant="ghost" onClick={() => setOpen(false)}>
176
+ Annuler
177
+ </Button>
178
+ </div>
179
+ </form>
180
+ );
181
+ }
182
+
183
+ /** Photo de profil : recadrée en carré et allégée dans le navigateur avant l'envoi. */
184
+ function AvatarForm({ me }: { me: Me }) {
185
+ const input = useRef<HTMLInputElement>(null);
186
+ const [busy, setBusy] = useState<'upload' | 'remove' | null>(null);
187
+ const [preview, setPreview] = useState<string | null>(null);
188
+ const [dragging, setDragging] = useState(false);
189
+ const [msg, setMsg] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);
190
+
191
+ const upload = async (file: File | undefined) => {
192
+ if (!file || busy) return;
193
+ setBusy('upload');
194
+ setMsg(null);
195
+ try {
196
+ const image = await prepareAvatar(file);
197
+ setPreview(image.preview);
198
+ const r = await api.post<{ customer: Me }>('/avatar', { dataBase64: image.dataBase64, mimeType: image.mimeType });
199
+ applyMe(r.customer);
200
+ setMsg({ tone: 'success', text: 'Votre photo de profil a été mise à jour.' });
201
+ } catch (err) {
202
+ setMsg({ tone: 'danger', text: (err as Error).message });
203
+ } finally {
204
+ setPreview(null);
205
+ setBusy(null);
206
+ if (input.current) input.current.value = '';
207
+ }
208
+ };
209
+
210
+ const remove = async () => {
211
+ setBusy('remove');
212
+ setMsg(null);
213
+ try {
214
+ const r = await api.post<{ customer: Me }>('/avatar/delete');
215
+ applyMe(r.customer);
216
+ setMsg({ tone: 'success', text: 'Votre photo a été supprimée.' });
217
+ } catch (err) {
218
+ setMsg({ tone: 'danger', text: (err as Error).message });
219
+ } finally {
220
+ setBusy(null);
221
+ }
222
+ };
223
+
224
+ return (
225
+ <div className="space-y-4">
226
+ {msg ? <Notice tone={msg.tone}>{msg.text}</Notice> : null}
227
+ <div className="flex flex-col gap-5 sm:flex-row sm:items-center">
228
+ <button
229
+ type="button"
230
+ onClick={() => input.current?.click()}
231
+ onDragOver={(e) => {
232
+ e.preventDefault();
233
+ setDragging(true);
234
+ }}
235
+ onDragLeave={() => setDragging(false)}
236
+ onDrop={(e) => {
237
+ e.preventDefault();
238
+ setDragging(false);
239
+ void upload(e.dataTransfer.files[0]);
240
+ }}
241
+ disabled={!!busy}
242
+ aria-label="Changer la photo de profil"
243
+ className={`group relative w-fit shrink-0 rounded-full outline-none ring-offset-2 focus-visible:ring-2 focus-visible:ring-px-primary ${dragging ? 'ring-2 ring-px-primary' : ''}`}
244
+ >
245
+ <CustomerAvatar person={{ ...me, avatarUrl: preview ?? me.avatarUrl }} className="h-24 w-24 text-2xl" />
246
+ <span className={`absolute inset-0 flex items-center justify-center rounded-full bg-black/45 text-white transition ${busy === 'upload' ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`}>
247
+ {busy === 'upload' ? <Spinner className="h-6 w-6" /> : <Camera className="h-6 w-6" />}
248
+ </span>
249
+ <span className="absolute bottom-0 right-0 flex h-8 w-8 items-center justify-center rounded-full border-[3px] border-white bg-px-primary text-white shadow">
250
+ <Camera className="h-3.5 w-3.5" />
251
+ </span>
252
+ </button>
253
+ <input ref={input} type="file" accept={AVATAR_ACCEPT} className="hidden" onChange={(e) => void upload(e.target.files?.[0])} />
254
+ <div>
255
+ <p className="text-sm text-px-text">Cliquez sur la photo ou déposez une image. JPG, PNG ou WebP ; elle est recadrée en carré automatiquement.</p>
256
+ <div className="mt-3 flex flex-wrap gap-3">
257
+ <Button type="button" variant="outline" size="sm" onClick={() => input.current?.click()} disabled={!!busy}>
258
+ <ImagePlus className="h-4 w-4" /> {me.avatarUrl ? 'Changer la photo' : 'Ajouter une photo'}
259
+ </Button>
260
+ {me.avatarUrl ? (
261
+ <Button type="button" variant="ghost" size="sm" onClick={remove} loading={busy === 'remove'} disabled={!!busy}>
262
+ <Trash2 className="h-4 w-4" /> Supprimer
263
+ </Button>
264
+ ) : null}
265
+ </div>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ );
270
+ }
271
+
272
+ export function ProfileSection({ me, navigate, avatars = true }: { me: Me; navigate: (p: string) => void; avatars?: boolean }) {
273
+ return (
274
+ <div className="space-y-6">
275
+ {avatars ? (
276
+ <Card title="Photo de profil">
277
+ <AvatarForm me={me} />
278
+ </Card>
279
+ ) : null}
280
+ <Card title="Informations personnelles">
281
+ <InfoForm me={me} />
282
+ </Card>
283
+ <Card title="Mot de passe" text="Changer de mot de passe déconnecte vos autres appareils.">
284
+ <PasswordForm />
285
+ </Card>
286
+ <Card title="Supprimer mon compte" text="Votre compte, vos adresses et vos favoris sont effacés. Les commandes déjà passées restent enregistrées par la boutique pour sa comptabilité." danger>
287
+ <DeleteAccount navigate={navigate} />
288
+ </Card>
289
+ </div>
290
+ );
291
+ }