create-snapify-app 0.0.0-stage → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (614) hide show
  1. package/LICENSE.md +24 -0
  2. package/README.md +55 -2
  3. package/bin/create-snapify-app.mjs +174 -0
  4. package/lib/args.mjs +59 -0
  5. package/lib/copy.mjs +44 -0
  6. package/lib/env.mjs +84 -0
  7. package/lib/prompt.mjs +63 -0
  8. package/package.json +35 -4
  9. package/template/.env.example +79 -0
  10. package/template/.github/workflows/ci.yml +23 -0
  11. package/template/.github/workflows/deploy.yml +26 -0
  12. package/template/.github/workflows/ping.yml +8 -0
  13. package/template/.github/workflows/release.yml +67 -0
  14. package/template/.snapify-install.json +5 -0
  15. package/template/CHANGELOG.md +28 -0
  16. package/template/LICENSE +14 -0
  17. package/template/README.md +287 -0
  18. package/template/administrator.html +23 -0
  19. package/template/apps/client/src/App.tsx +765 -0
  20. package/template/apps/client/src/auth.css +400 -0
  21. package/template/apps/client/src/lib/api.ts +22 -0
  22. package/template/apps/client/src/main.tsx +20 -0
  23. package/template/apps/community/src/App.tsx +592 -0
  24. package/template/apps/community/src/main.tsx +20 -0
  25. package/template/apps/community/src/styles.css +24 -0
  26. package/template/apps/developers/src/App.tsx +643 -0
  27. package/template/apps/developers/src/main.tsx +17 -0
  28. package/template/apps/developers/src/pages.tsx +1587 -0
  29. package/template/apps/developers/src/styles.css +98 -0
  30. package/template/apps/developers/src/ui.tsx +179 -0
  31. package/template/apps/landing/src/App.tsx +598 -0
  32. package/template/apps/landing/src/LegalPages.tsx +158 -0
  33. package/template/apps/landing/src/OrderPage.tsx +381 -0
  34. package/template/apps/landing/src/SeoPages.tsx +923 -0
  35. package/template/apps/landing/src/copy.ts +195 -0
  36. package/template/apps/landing/src/main.tsx +20 -0
  37. package/template/apps/landing/src/styles.css +803 -0
  38. package/template/apps/landing/src/types.ts +19 -0
  39. package/template/apps/marketplace/src/App.tsx +1229 -0
  40. package/template/apps/marketplace/src/developer/DeveloperApp.tsx +1266 -0
  41. package/template/apps/marketplace/src/main.tsx +20 -0
  42. package/template/apps/marketplace/src/styles.css +1001 -0
  43. package/template/apps/shared/AuthContext.tsx +65 -0
  44. package/template/apps/shared/Logo.tsx +33 -0
  45. package/template/apps/shared/api.ts +47 -0
  46. package/template/apps/shared/ui.tsx +128 -0
  47. package/template/apps/superadmin/src/App.tsx +1264 -0
  48. package/template/apps/superadmin/src/lib/api.ts +20 -0
  49. package/template/apps/superadmin/src/lib/hooks.ts +20 -0
  50. package/template/apps/superadmin/src/main.tsx +16 -0
  51. package/template/apps/superadmin/src/pages/ClientsPage.tsx +185 -0
  52. package/template/apps/superadmin/src/pages/InvoicesPage.tsx +157 -0
  53. package/template/apps/superadmin/src/pages/NewsletterPage.tsx +212 -0
  54. package/template/apps/superadmin/src/pages/OrdersPage.tsx +195 -0
  55. package/template/apps/superadmin/src/pages/PaymentsPage.tsx +150 -0
  56. package/template/apps/superadmin/src/pages/PayoutsPage.tsx +133 -0
  57. package/template/apps/superadmin/src/pages/PublishersPage.tsx +151 -0
  58. package/template/apps/superadmin/src/pages/SeoControlPage.tsx +692 -0
  59. package/template/apps/superadmin/src/pages/SettingsPage.tsx +199 -0
  60. package/template/apps/superadmin/src/pages/SystemPage.tsx +109 -0
  61. package/template/apps/superadmin/src/pages/TicketsPage.tsx +201 -0
  62. package/template/apps/superadmin/src/pages/UsersPage.tsx +167 -0
  63. package/template/client.html +21 -0
  64. package/template/community/hamdiayari/akhbar/README.md +23 -0
  65. package/template/community/hamdiayari/akhbar/screenshot.jpg +0 -0
  66. package/template/community/hamdiayari/akhbar/snapify.theme.json +76 -0
  67. package/template/community/hamdiayari/akhbar/src/Brand.tsx +26 -0
  68. package/template/community/hamdiayari/akhbar/src/Footer.tsx +182 -0
  69. package/template/community/hamdiayari/akhbar/src/Header.tsx +191 -0
  70. package/template/community/hamdiayari/akhbar/src/Layout.tsx +49 -0
  71. package/template/community/hamdiayari/akhbar/src/Social.tsx +33 -0
  72. package/template/community/hamdiayari/akhbar/src/Ticker.tsx +86 -0
  73. package/template/community/hamdiayari/akhbar/src/Views.tsx +285 -0
  74. package/template/community/hamdiayari/akhbar/src/blocks/CollectionBlocks.tsx +672 -0
  75. package/template/community/hamdiayari/akhbar/src/blocks/ContentBlocks.tsx +305 -0
  76. package/template/community/hamdiayari/akhbar/src/blocks/ConversionBlocks.tsx +569 -0
  77. package/template/community/hamdiayari/akhbar/src/blocks/HeaderBlocks.tsx +287 -0
  78. package/template/community/hamdiayari/akhbar/src/index.tsx +51 -0
  79. package/template/community/hamdiayari/akhbar/src/kit.tsx +357 -0
  80. package/template/community/hamdiayari/akhbar/src/site.ts +96 -0
  81. package/template/community/hamdiayari/akhbar/src/styles.css +566 -0
  82. package/template/community/hamdiayari/akhbar-actualites/README.md +28 -0
  83. package/template/community/hamdiayari/akhbar-actualites/screenshot.jpg +0 -0
  84. package/template/community/hamdiayari/akhbar-actualites/snapify.module.json +107 -0
  85. package/template/community/hamdiayari/akhbar-actualites/src/admin/AudiencePage.tsx +185 -0
  86. package/template/community/hamdiayari/akhbar-actualites/src/admin/index.tsx +11 -0
  87. package/template/community/hamdiayari/akhbar-actualites/src/public/Archive.tsx +142 -0
  88. package/template/community/hamdiayari/akhbar-actualites/src/public/cards.tsx +152 -0
  89. package/template/community/hamdiayari/akhbar-actualites/src/public/index.tsx +71 -0
  90. package/template/community/hamdiayari/akhbar-actualites/src/public/posts.ts +103 -0
  91. package/template/community/hamdiayari/akhbar-actualites/src/public/styles.css +568 -0
  92. package/template/community/hamdiayari/akhbar-actualites/src/public/widgets.tsx +226 -0
  93. package/template/community/hamdiayari/akhbar-actualites/src/server.ts +119 -0
  94. package/template/community/hamdiayari/akhbar-actualites/src/types.ts +25 -0
  95. package/template/community/hamdiayari/akhbar-urgent/README.md +36 -0
  96. package/template/community/hamdiayari/akhbar-urgent/screenshot.jpg +0 -0
  97. package/template/community/hamdiayari/akhbar-urgent/snapify.module.json +64 -0
  98. package/template/community/hamdiayari/akhbar-urgent/src/admin/FlashesPage.tsx +229 -0
  99. package/template/community/hamdiayari/akhbar-urgent/src/admin/index.tsx +11 -0
  100. package/template/community/hamdiayari/akhbar-urgent/src/public/Ticker.tsx +81 -0
  101. package/template/community/hamdiayari/akhbar-urgent/src/public/index.tsx +63 -0
  102. package/template/community/hamdiayari/akhbar-urgent/src/public/styles.css +142 -0
  103. package/template/community/hamdiayari/akhbar-urgent/src/server.ts +101 -0
  104. package/template/community/hamdiayari/akhbar-urgent/src/types.ts +35 -0
  105. package/template/community.html +21 -0
  106. package/template/developers.html +24 -0
  107. package/template/docker-compose.db.yml +24 -0
  108. package/template/docs/BACKOFFICE_SNAPIFY.md +130 -0
  109. package/template/docs/CI_CD.md +66 -0
  110. package/template/docs/MODULES.md +346 -0
  111. package/template/docs/PLATFORM_DEPLOY.md +54 -0
  112. package/template/docs/logo.svg +10 -0
  113. package/template/drizzle/0000_init.sql +147 -0
  114. package/template/drizzle/0001_page_project.sql +5 -0
  115. package/template/drizzle/0002_modules.sql +23 -0
  116. package/template/drizzle/0003_themes.sql +4 -0
  117. package/template/drizzle/0004_security.sql +82 -0
  118. package/template/drizzle/0005_module_records.sql +16 -0
  119. package/template/drizzle/0006_avatars.sql +4 -0
  120. package/template/drizzle/0007_platform.sql +264 -0
  121. package/template/drizzle/0008_mp_validation.sql +3 -0
  122. package/template/drizzle/0009_saas_ops.sql +77 -0
  123. package/template/drizzle/0010_cms_chrome.sql +13 -0
  124. package/template/drizzle/0011_client_portal.sql +26 -0
  125. package/template/drizzle/meta/0000_snapshot.json +998 -0
  126. package/template/drizzle/meta/0001_snapshot.json +1004 -0
  127. package/template/drizzle/meta/_journal.json +90 -0
  128. package/template/drizzle.config.ts +16 -0
  129. package/template/ecosystem.snapify.cjs +24 -0
  130. package/template/for.html +1 -0
  131. package/template/index.html +29 -0
  132. package/template/landing.html +37 -0
  133. package/template/marketplace/catalog.json +373 -0
  134. package/template/marketplace/gallery/hamdiayari.preveris/01-home.jpg +0 -0
  135. package/template/marketplace/gallery/hamdiayari.recaptcha/01-reglages.jpg +0 -0
  136. package/template/marketplace/gallery/hamdiayari.recaptcha/02-logo.jpg +0 -0
  137. package/template/marketplace/gallery/novalabs.ai-articles/01-generer.jpg +0 -0
  138. package/template/marketplace/gallery/novalabs.ai-articles/02-fournisseur-ia.jpg +0 -0
  139. package/template/marketplace/gallery/novalabs.calendar/01-agenda-site.jpg +0 -0
  140. package/template/marketplace/gallery/novalabs.calendar/02-evenements-admin.jpg +0 -0
  141. package/template/marketplace/gallery/novalabs.calendar/03-reglages.jpg +0 -0
  142. package/template/marketplace/screenshots/hamdiayari.preveris-1.0.6.jpg +0 -0
  143. package/template/marketplace/screenshots/hamdiayari.recaptcha-1.0.0.jpg +0 -0
  144. package/template/marketplace/screenshots/hamdiayari.wp-seo-migrator-1.1.1.jpg +0 -0
  145. package/template/marketplace/screenshots/novalabs.ai-articles-1.0.1.jpg +0 -0
  146. package/template/marketplace/screenshots/novalabs.ai-chat-1.0.1.jpg +0 -0
  147. package/template/marketplace/screenshots/novalabs.calendar-1.0.1.jpg +0 -0
  148. package/template/marketplace/screenshots/novalabs.cart-1.0.0.jpg +0 -0
  149. package/template/marketplace/screenshots/novalabs.checkout-1.0.0.jpg +0 -0
  150. package/template/marketplace/screenshots/novalabs.customers-1.1.0.jpg +0 -0
  151. package/template/marketplace/screenshots/novalabs.dogga-1.0.0.jpg +0 -0
  152. package/template/marketplace/screenshots/novalabs.mahdia-1.0.0.jpg +0 -0
  153. package/template/marketplace/screenshots/novalabs.shop-1.0.0.jpg +0 -0
  154. package/template/marketplace/screenshots/novalabs.sidibou-1.0.0.jpg +0 -0
  155. package/template/marketplace/screenshots/novalabs.whatsapp-1.0.1.jpg +0 -0
  156. package/template/marketplace.html +21 -0
  157. package/template/metadata.json +7 -0
  158. package/template/modules/ai-articles/screenshot.jpg +0 -0
  159. package/template/modules/ai-articles/snapify.module.json +74 -0
  160. package/template/modules/ai-articles/src/admin/ArticlesPage.tsx +162 -0
  161. package/template/modules/ai-articles/src/admin/GeneratePage.tsx +118 -0
  162. package/template/modules/ai-articles/src/admin/index.tsx +12 -0
  163. package/template/modules/ai-articles/src/public/index.tsx +119 -0
  164. package/template/modules/ai-articles/src/server.ts +287 -0
  165. package/template/modules/ai-articles/src/types.ts +30 -0
  166. package/template/modules/ai-chat/screenshot.jpg +0 -0
  167. package/template/modules/ai-chat/snapify.module.json +217 -0
  168. package/template/modules/ai-chat/src/public.tsx +198 -0
  169. package/template/modules/ai-chat/src/server.ts +142 -0
  170. package/template/modules/ai-theme-studio/README.md +15 -0
  171. package/template/modules/ai-theme-studio/screenshot.jpg +0 -0
  172. package/template/modules/ai-theme-studio/snapify.module.json +175 -0
  173. package/template/modules/ai-theme-studio/src/admin/HistoryPage.tsx +91 -0
  174. package/template/modules/ai-theme-studio/src/admin/StudioPage.tsx +414 -0
  175. package/template/modules/ai-theme-studio/src/admin/index.tsx +11 -0
  176. package/template/modules/ai-theme-studio/src/lib/images.ts +240 -0
  177. package/template/modules/ai-theme-studio/src/lib/llm.ts +123 -0
  178. package/template/modules/ai-theme-studio/src/lib/normalize.ts +714 -0
  179. package/template/modules/ai-theme-studio/src/lib/prompts.ts +198 -0
  180. package/template/modules/ai-theme-studio/src/server.ts +137 -0
  181. package/template/modules/ai-theme-studio/src/types.ts +112 -0
  182. package/template/modules/calendar/screenshot.jpg +0 -0
  183. package/template/modules/calendar/snapify.module.json +45 -0
  184. package/template/modules/calendar/src/admin/EventsPage.tsx +217 -0
  185. package/template/modules/calendar/src/admin/index.tsx +11 -0
  186. package/template/modules/calendar/src/public/index.tsx +231 -0
  187. package/template/modules/calendar/src/server.ts +143 -0
  188. package/template/modules/calendar/src/types.ts +25 -0
  189. package/template/modules/cart/screenshot.jpg +0 -0
  190. package/template/modules/cart/snapify.module.json +90 -0
  191. package/template/modules/cart/src/admin/index.tsx +265 -0
  192. package/template/modules/cart/src/public/CartPage.tsx +198 -0
  193. package/template/modules/cart/src/public/index.tsx +261 -0
  194. package/template/modules/cart/src/public/useQuote.ts +67 -0
  195. package/template/modules/cart/src/server.ts +148 -0
  196. package/template/modules/cart/src/types.ts +31 -0
  197. package/template/modules/checkout/screenshot.jpg +0 -0
  198. package/template/modules/checkout/snapify.module.json +264 -0
  199. package/template/modules/checkout/src/admin/OrderDetailPage.tsx +304 -0
  200. package/template/modules/checkout/src/admin/badges.tsx +28 -0
  201. package/template/modules/checkout/src/admin/index.tsx +236 -0
  202. package/template/modules/checkout/src/public/CheckoutPage.tsx +461 -0
  203. package/template/modules/checkout/src/public/OrderPages.tsx +153 -0
  204. package/template/modules/checkout/src/public/index.tsx +23 -0
  205. package/template/modules/checkout/src/server.ts +383 -0
  206. package/template/modules/checkout/src/types.ts +70 -0
  207. package/template/modules/customers/screenshot.jpg +0 -0
  208. package/template/modules/customers/snapify.module.json +115 -0
  209. package/template/modules/customers/src/admin/CustomerDetailPage.tsx +274 -0
  210. package/template/modules/customers/src/admin/index.tsx +194 -0
  211. package/template/modules/customers/src/public/AccountPages.tsx +470 -0
  212. package/template/modules/customers/src/public/AddressForm.tsx +115 -0
  213. package/template/modules/customers/src/public/AuthPages.tsx +349 -0
  214. package/template/modules/customers/src/public/Avatar.tsx +29 -0
  215. package/template/modules/customers/src/public/ProfileSection.tsx +291 -0
  216. package/template/modules/customers/src/public/index.tsx +137 -0
  217. package/template/modules/customers/src/public/session.ts +51 -0
  218. package/template/modules/customers/src/server.ts +421 -0
  219. package/template/modules/customers/src/types.ts +53 -0
  220. package/template/modules/recaptcha/logo.jpg +0 -0
  221. package/template/modules/recaptcha/screenshot.jpg +0 -0
  222. package/template/modules/recaptcha/snapify.module.json +99 -0
  223. package/template/modules/recaptcha/src/public/index.tsx +94 -0
  224. package/template/modules/recaptcha/src/server.ts +65 -0
  225. package/template/modules/recaptcha/src/types.ts +18 -0
  226. package/template/modules/shop/screenshot.jpg +0 -0
  227. package/template/modules/shop/snapify.module.json +340 -0
  228. package/template/modules/shop/src/admin/CategoriesPage.tsx +224 -0
  229. package/template/modules/shop/src/admin/ProductEditor.tsx +313 -0
  230. package/template/modules/shop/src/admin/ProductsPage.tsx +307 -0
  231. package/template/modules/shop/src/admin/index.tsx +14 -0
  232. package/template/modules/shop/src/public/Catalog.tsx +244 -0
  233. package/template/modules/shop/src/public/NotFound.tsx +22 -0
  234. package/template/modules/shop/src/public/ProductPage.tsx +195 -0
  235. package/template/modules/shop/src/public/index.tsx +134 -0
  236. package/template/modules/shop/src/public/useFetch.ts +26 -0
  237. package/template/modules/shop/src/server.ts +493 -0
  238. package/template/modules/shop/src/shared.ts +41 -0
  239. package/template/modules/shop/src/types.ts +60 -0
  240. package/template/modules/whatsapp/screenshot.jpg +0 -0
  241. package/template/modules/whatsapp/snapify.module.json +121 -0
  242. package/template/modules/whatsapp/src/public.tsx +119 -0
  243. package/template/modules/wp-seo-migrator/ARCHITECTURE.md +113 -0
  244. package/template/modules/wp-seo-migrator/README.md +21 -0
  245. package/template/modules/wp-seo-migrator/fixtures/aioseo-sample.csv +3 -0
  246. package/template/modules/wp-seo-migrator/fixtures/sample-exporter.json +39 -0
  247. package/template/modules/wp-seo-migrator/fixtures/sample-page.html +12 -0
  248. package/template/modules/wp-seo-migrator/fixtures/sample.htaccess +3 -0
  249. package/template/modules/wp-seo-migrator/screenshot.jpg +0 -0
  250. package/template/modules/wp-seo-migrator/snapify.module.json +87 -0
  251. package/template/modules/wp-seo-migrator/src/admin/HistoryPage.tsx +64 -0
  252. package/template/modules/wp-seo-migrator/src/admin/WizardPage.tsx +788 -0
  253. package/template/modules/wp-seo-migrator/src/admin/i18n.ts +156 -0
  254. package/template/modules/wp-seo-migrator/src/admin/index.tsx +11 -0
  255. package/template/modules/wp-seo-migrator/src/connectors/CUSTOM_CONNECTOR.md +31 -0
  256. package/template/modules/wp-seo-migrator/src/connectors/csvSeoFactory.ts +106 -0
  257. package/template/modules/wp-seo-migrator/src/connectors/exporter.ts +96 -0
  258. package/template/modules/wp-seo-migrator/src/connectors/gscCsv.ts +53 -0
  259. package/template/modules/wp-seo-migrator/src/connectors/htaccess.ts +65 -0
  260. package/template/modules/wp-seo-migrator/src/connectors/htmlCrawler.ts +90 -0
  261. package/template/modules/wp-seo-migrator/src/connectors/index.ts +113 -0
  262. package/template/modules/wp-seo-migrator/src/connectors/pluginCsv.ts +77 -0
  263. package/template/modules/wp-seo-migrator/src/connectors/rankMathCsv.ts +77 -0
  264. package/template/modules/wp-seo-migrator/src/connectors/redirection.ts +62 -0
  265. package/template/modules/wp-seo-migrator/src/connectors/sitemap.ts +44 -0
  266. package/template/modules/wp-seo-migrator/src/connectors/utils.ts +111 -0
  267. package/template/modules/wp-seo-migrator/src/connectors/wxr.ts +103 -0
  268. package/template/modules/wp-seo-migrator/src/connectors/yoastCsv.ts +56 -0
  269. package/template/modules/wp-seo-migrator/src/pipeline/analyze.ts +158 -0
  270. package/template/modules/wp-seo-migrator/src/pipeline/htmlParse.ts +58 -0
  271. package/template/modules/wp-seo-migrator/src/pipeline/remoteAuto.ts +328 -0
  272. package/template/modules/wp-seo-migrator/src/server.ts +492 -0
  273. package/template/modules/wp-seo-migrator/src/types.ts +151 -0
  274. package/template/modules/wp-seo-migrator/tests/connectors-extra.test.ts +39 -0
  275. package/template/modules/wp-seo-migrator/tests/merge.test.ts +25 -0
  276. package/template/package-lock.json +6016 -0
  277. package/template/package.json +74 -0
  278. package/template/public/images/photos/caravane-chameaux-desert.webp +0 -0
  279. package/template/public/images/photos/hammamet-medina-plage.jpg +0 -0
  280. package/template/public/images/photos/maison-campagne-collines.webp +0 -0
  281. package/template/public/images/photos/maisons-troglodytes-desert.jpg +0 -0
  282. package/template/public/images/photos/panorama-ville.jpg +0 -0
  283. package/template/public/images/photos/preveris-accessibilite-erp.jpg +0 -0
  284. package/template/public/images/photos/preveris-collectivites.jpg +0 -0
  285. package/template/public/images/photos/preveris-etudes-plans.jpg +0 -0
  286. package/template/public/images/photos/preveris-hero-erp.jpg +0 -0
  287. package/template/public/images/photos/preveris-ssi-incendie.jpg +0 -0
  288. package/template/public/images/photos/preveris-suivi-hotel.jpg +0 -0
  289. package/template/public/images/photos/ruelle-bleue-medina.jpg +0 -0
  290. package/template/public/images/photos/sidi-bou-said-port.webp +0 -0
  291. package/template/public/images/photos/terrasse-blanche-mer-voilier.jpg +0 -0
  292. package/template/public/landing/hero-ai-cms.jpg +0 -0
  293. package/template/public/landing/shot-dogga.jpg +0 -0
  294. package/template/public/landing/shot-mahdia.jpg +0 -0
  295. package/template/public/landing/shot-sidibou.jpg +0 -0
  296. package/template/public/llms.txt +35 -0
  297. package/template/public/logo.svg +10 -0
  298. package/template/scripts/app-pack.ts +155 -0
  299. package/template/scripts/app-publish.ts +123 -0
  300. package/template/scripts/apply-ecommerce-home.ts +196 -0
  301. package/template/scripts/auto-deploy-poll.sh +13 -0
  302. package/template/scripts/db-backup.sh +16 -0
  303. package/template/scripts/db-restore.sh +11 -0
  304. package/template/scripts/deploy.sh +34 -0
  305. package/template/scripts/github-webhook-deploy.sh +7 -0
  306. package/template/scripts/install-new-modules.ts +96 -0
  307. package/template/scripts/refresh-demo-baseline.ts +9 -0
  308. package/template/scripts/reinstall-marketplace-packs.ts +51 -0
  309. package/template/scripts/restore-classic-home.ts +13 -0
  310. package/template/scripts/sdk-pack.ts +289 -0
  311. package/template/scripts/sdk-publish.ts +75 -0
  312. package/template/scripts/seed-demo-products.ts +120 -0
  313. package/template/scripts/seed-theme-content-packs.ts +42 -0
  314. package/template/scripts/snapify-module.ts +207 -0
  315. package/template/scripts/test-theme-switch.ts +24 -0
  316. package/template/sdk/admin.css +18 -0
  317. package/template/sdk/admin.ts +95 -0
  318. package/template/sdk/build.ts +249 -0
  319. package/template/sdk/commerce.ts +512 -0
  320. package/template/sdk/content.ts +76 -0
  321. package/template/sdk/icons.ts +101 -0
  322. package/template/sdk/image.ts +60 -0
  323. package/template/sdk/index.ts +500 -0
  324. package/template/sdk/package.ts +298 -0
  325. package/template/sdk/recaptcha.tsx +122 -0
  326. package/template/sdk/settings.ts +84 -0
  327. package/template/sdk/theme.css +29 -0
  328. package/template/sdk/theme.ts +100 -0
  329. package/template/sdk/ui/commerce.tsx +385 -0
  330. package/template/sdk/ui/orders.tsx +136 -0
  331. package/template/sdk-kit/CHANGELOG.md +17 -0
  332. package/template/sdk-kit/LICENSE.md +47 -0
  333. package/template/sdk-kit/README.md +81 -0
  334. package/template/sdk-kit/cli.ts +339 -0
  335. package/template/sdk-kit/create-snapify/README.md +28 -0
  336. package/template/sdk-kit/create-snapify/create-snapify.mjs +11 -0
  337. package/template/sdk-kit/create-snapify/package.template.json +16 -0
  338. package/template/sdk-kit/package.template.json +95 -0
  339. package/template/sdk-kit/templates/common/README.md.tpl +27 -0
  340. package/template/sdk-kit/templates/common/_gitignore +3 -0
  341. package/template/sdk-kit/templates/common/package.json.tpl +19 -0
  342. package/template/sdk-kit/templates/common/src/snapify-env.d.ts +2 -0
  343. package/template/sdk-kit/templates/common/tsconfig.json +15 -0
  344. package/template/sdk-kit/templates/module/snapify.module.json.tpl +33 -0
  345. package/template/sdk-kit/templates/module/src/admin.tsx +53 -0
  346. package/template/sdk-kit/templates/module/src/public.tsx +49 -0
  347. package/template/sdk-kit/templates/module/src/server.ts +30 -0
  348. package/template/sdk-kit/templates/module/src/types.ts +19 -0
  349. package/template/sdk-kit/templates/theme/snapify.theme.json.tpl +22 -0
  350. package/template/sdk-kit/templates/theme/src/Footer.tsx +40 -0
  351. package/template/sdk-kit/templates/theme/src/Header.tsx +47 -0
  352. package/template/sdk-kit/templates/theme/src/Views.tsx +36 -0
  353. package/template/sdk-kit/templates/theme/src/blocks.tsx +93 -0
  354. package/template/sdk-kit/templates/theme/src/index.tsx +30 -0
  355. package/template/sdk-kit/templates/theme/src/site.tsx +59 -0
  356. package/template/sdk-kit/templates/theme/src/styles.css +71 -0
  357. package/template/server/client/routes.ts +252 -0
  358. package/template/server/cms/analyticsStore.ts +180 -0
  359. package/template/server/cms/auth.ts +155 -0
  360. package/template/server/cms/blockCatalog.ts +342 -0
  361. package/template/server/cms/blocks.ts +521 -0
  362. package/template/server/cms/contentMigration.ts +1119 -0
  363. package/template/server/cms/defaults.ts +303 -0
  364. package/template/server/cms/permissions.ts +154 -0
  365. package/template/server/cms/restoreClassicHome.ts +183 -0
  366. package/template/server/cms/revisions.ts +99 -0
  367. package/template/server/cms/scheduler.ts +78 -0
  368. package/template/server/cms/seed.ts +72 -0
  369. package/template/server/cms/seoAnalyzer.ts +213 -0
  370. package/template/server/cms/store.ts +122 -0
  371. package/template/server/cms/themeContent.ts +1386 -0
  372. package/template/server/cms/themePacks/sidibou-branding.json +1 -0
  373. package/template/server/cms/themePacks/sidibou-home.json +1 -0
  374. package/template/server/cms/themePacks/sidibou-menus.json +1 -0
  375. package/template/server/cms/types.ts +432 -0
  376. package/template/server/community/routes.ts +410 -0
  377. package/template/server/db/client.ts +43 -0
  378. package/template/server/db/repository.ts +389 -0
  379. package/template/server/db/schema.ts +726 -0
  380. package/template/server/demo/cmsReset.ts +196 -0
  381. package/template/server/developers/sdk.ts +85 -0
  382. package/template/server/geoip.ts +15 -0
  383. package/template/server/http.ts +53 -0
  384. package/template/server/mail.ts +216 -0
  385. package/template/server/marketplace/catalog.ts +74 -0
  386. package/template/server/marketplace/packageValidate.ts +475 -0
  387. package/template/server/marketplace/payments.ts +193 -0
  388. package/template/server/marketplace/routes.ts +850 -0
  389. package/template/server/marketplace/signing.ts +116 -0
  390. package/template/server/marketplace/syncOfficial.ts +282 -0
  391. package/template/server/media/images.ts +63 -0
  392. package/template/server/middleware/adminAuth.ts +134 -0
  393. package/template/server/modules/legacy.ts +44 -0
  394. package/template/server/modules/manager.ts +1122 -0
  395. package/template/server/modules/marketplace.ts +178 -0
  396. package/template/server/modules/outbound.ts +178 -0
  397. package/template/server/modules/purchases.ts +149 -0
  398. package/template/server/modules/records.ts +168 -0
  399. package/template/server/modules/themeHtml.ts +32 -0
  400. package/template/server/modules/trust.ts +16 -0
  401. package/template/server/modules/types.ts +147 -0
  402. package/template/server/platform/auth.ts +223 -0
  403. package/template/server/platform/clientWelcome.ts +168 -0
  404. package/template/server/platform/clients.ts +139 -0
  405. package/template/server/platform/dailyImprove.ts +356 -0
  406. package/template/server/platform/flouci.ts +126 -0
  407. package/template/server/platform/host.ts +69 -0
  408. package/template/server/platform/hostingProviders.ts +143 -0
  409. package/template/server/platform/licenseOrders.ts +291 -0
  410. package/template/server/platform/money.ts +42 -0
  411. package/template/server/platform/passwords.ts +41 -0
  412. package/template/server/platform/region.ts +86 -0
  413. package/template/server/platform/routes.ts +225 -0
  414. package/template/server/platform/seed.ts +82 -0
  415. package/template/server/platform/settings.ts +72 -0
  416. package/template/server/routes/adminAccount.ts +403 -0
  417. package/template/server/routes/adminCms.ts +808 -0
  418. package/template/server/routes/adminSystem.ts +243 -0
  419. package/template/server/routes/adminUsers.ts +449 -0
  420. package/template/server/routes/googleOAuth.ts +327 -0
  421. package/template/server/routes/modules.ts +323 -0
  422. package/template/server/routes/publicCms.ts +313 -0
  423. package/template/server/routes/seoPublic.ts +158 -0
  424. package/template/server/security/accounts.ts +121 -0
  425. package/template/server/security/audit.ts +143 -0
  426. package/template/server/security/authTokens.ts +74 -0
  427. package/template/server/security/ip.ts +68 -0
  428. package/template/server/security/rateLimit.ts +85 -0
  429. package/template/server/security/retention.ts +46 -0
  430. package/template/server/security/secrets.ts +44 -0
  431. package/template/server/security/sessions.ts +140 -0
  432. package/template/server/security/totp.ts +101 -0
  433. package/template/server/superadmin/analytics.ts +109 -0
  434. package/template/server/superadmin/auth.ts +92 -0
  435. package/template/server/superadmin/backlinksCatalog.ts +210 -0
  436. package/template/server/superadmin/frSeoContent.ts +165 -0
  437. package/template/server/superadmin/landingSeo.ts +675 -0
  438. package/template/server/superadmin/ops.ts +238 -0
  439. package/template/server/superadmin/routes.ts +1095 -0
  440. package/template/server/superadmin/tnSeoContent.ts +130 -0
  441. package/template/server.ts +522 -0
  442. package/template/src/App.tsx +263 -0
  443. package/template/src/admin/AdminApp.tsx +162 -0
  444. package/template/src/admin/account/AuthLayout.tsx +57 -0
  445. package/template/src/admin/account/SecurityForms.tsx +276 -0
  446. package/template/src/admin/admin.css +127 -0
  447. package/template/src/admin/context/AuthContext.tsx +147 -0
  448. package/template/src/admin/context/SiteContext.tsx +50 -0
  449. package/template/src/admin/editor/BlockLibrary.tsx +173 -0
  450. package/template/src/admin/editor/BlocksPanel.tsx +396 -0
  451. package/template/src/admin/editor/EditorBits.tsx +106 -0
  452. package/template/src/admin/editor/PreviewFrame.tsx +145 -0
  453. package/template/src/admin/editor/RichTextEditor.tsx +276 -0
  454. package/template/src/admin/editor/SeoPanel.tsx +349 -0
  455. package/template/src/admin/editor/StyleEditor.tsx +114 -0
  456. package/template/src/admin/editor/blockSchemas.ts +699 -0
  457. package/template/src/admin/editor/fields.tsx +486 -0
  458. package/template/src/admin/layout/AdminShell.tsx +418 -0
  459. package/template/src/admin/layout/nav.ts +95 -0
  460. package/template/src/admin/lib/api.ts +408 -0
  461. package/template/src/admin/lib/audit.ts +141 -0
  462. package/template/src/admin/lib/format.ts +84 -0
  463. package/template/src/admin/lib/hooks.ts +125 -0
  464. package/template/src/admin/main.tsx +16 -0
  465. package/template/src/admin/modules/InstallDialog.tsx +160 -0
  466. package/template/src/admin/modules/SettingsForm.tsx +145 -0
  467. package/template/src/admin/modules/kit.tsx +127 -0
  468. package/template/src/admin/modules/labels.tsx +226 -0
  469. package/template/src/admin/pages/AccountSetupPage.tsx +62 -0
  470. package/template/src/admin/pages/AnalyticsPage.tsx +206 -0
  471. package/template/src/admin/pages/BrandingPage.tsx +154 -0
  472. package/template/src/admin/pages/DashboardPage.tsx +239 -0
  473. package/template/src/admin/pages/EngagementPage.tsx +168 -0
  474. package/template/src/admin/pages/FormsPage.tsx +435 -0
  475. package/template/src/admin/pages/GeneralPage.tsx +120 -0
  476. package/template/src/admin/pages/LoginPage.tsx +249 -0
  477. package/template/src/admin/pages/MaintenancePage.tsx +125 -0
  478. package/template/src/admin/pages/MarketplacePage.tsx +426 -0
  479. package/template/src/admin/pages/MediaPage.tsx +251 -0
  480. package/template/src/admin/pages/MenusPage.tsx +446 -0
  481. package/template/src/admin/pages/ModuleAdminPage.tsx +113 -0
  482. package/template/src/admin/pages/ModuleDetailPage.tsx +293 -0
  483. package/template/src/admin/pages/ModulesPage.tsx +135 -0
  484. package/template/src/admin/pages/NewsletterPage.tsx +193 -0
  485. package/template/src/admin/pages/PageEditorPage.tsx +322 -0
  486. package/template/src/admin/pages/PagesListPage.tsx +277 -0
  487. package/template/src/admin/pages/PostEditorPage.tsx +292 -0
  488. package/template/src/admin/pages/PostsListPage.tsx +187 -0
  489. package/template/src/admin/pages/ProfilePage.tsx +489 -0
  490. package/template/src/admin/pages/ResetPasswordPage.tsx +110 -0
  491. package/template/src/admin/pages/SeoPage.tsx +670 -0
  492. package/template/src/admin/pages/SettingsPage.tsx +189 -0
  493. package/template/src/admin/pages/ThemeCustomizePage.tsx +170 -0
  494. package/template/src/admin/pages/ThemesPage.tsx +292 -0
  495. package/template/src/admin/pages/UsersPage.tsx +57 -0
  496. package/template/src/admin/pages/WidgetsPage.tsx +200 -0
  497. package/template/src/admin/seo/GoogleSetup.tsx +529 -0
  498. package/template/src/admin/settings/EmailTab.tsx +293 -0
  499. package/template/src/admin/settings/OtherTabs.tsx +270 -0
  500. package/template/src/admin/settings/SecurityTab.tsx +172 -0
  501. package/template/src/admin/settings/controls.tsx +133 -0
  502. package/template/src/admin/ui/Pagination.tsx +170 -0
  503. package/template/src/admin/ui/SaveBar.tsx +77 -0
  504. package/template/src/admin/ui/TagInput.tsx +54 -0
  505. package/template/src/admin/ui/charts.tsx +108 -0
  506. package/template/src/admin/ui/media.tsx +265 -0
  507. package/template/src/admin/ui/overlays.tsx +239 -0
  508. package/template/src/admin/ui/primitives.tsx +544 -0
  509. package/template/src/admin/users/AuditTab.tsx +104 -0
  510. package/template/src/admin/users/RolesTab.tsx +368 -0
  511. package/template/src/admin/users/UsersTab.tsx +723 -0
  512. package/template/src/admin/users/shared.tsx +278 -0
  513. package/template/src/cms/icons.ts +7 -0
  514. package/template/src/components/CookieBanner.tsx +166 -0
  515. package/template/src/components/MaintenanceGate.tsx +66 -0
  516. package/template/src/components/SiteAnalytics.tsx +64 -0
  517. package/template/src/components/SnapifyLogo.tsx +44 -0
  518. package/template/src/components/cms/BlockRenderer.tsx +154 -0
  519. package/template/src/components/cms/WidgetArea.tsx +193 -0
  520. package/template/src/context/SiteConfigContext.tsx +130 -0
  521. package/template/src/demoBase.ts +23 -0
  522. package/template/src/index.css +90 -0
  523. package/template/src/main.tsx +16 -0
  524. package/template/src/modules/ModuleHost.tsx +197 -0
  525. package/template/src/preview.ts +63 -0
  526. package/template/src/runtime.ts +38 -0
  527. package/template/src/themes/ThemeHost.tsx +168 -0
  528. package/template/src/themes/fallback.tsx +71 -0
  529. package/template/src/types.ts +94 -0
  530. package/template/src/views/BlogPostView.tsx +68 -0
  531. package/template/src/views/CmsPageView.tsx +34 -0
  532. package/template/start-pm2.sh +3 -0
  533. package/template/superadmin.html +20 -0
  534. package/template/themes/ai-canvas/screenshot.jpg +0 -0
  535. package/template/themes/ai-canvas/snapify.theme.json +96 -0
  536. package/template/themes/ai-canvas/src/CeeSimulator.tsx +247 -0
  537. package/template/themes/ai-canvas/src/Footer.tsx +190 -0
  538. package/template/themes/ai-canvas/src/Header.tsx +324 -0
  539. package/template/themes/ai-canvas/src/Layout.tsx +56 -0
  540. package/template/themes/ai-canvas/src/NotFound.tsx +58 -0
  541. package/template/themes/ai-canvas/src/Post.tsx +26 -0
  542. package/template/themes/ai-canvas/src/SnapifyLogo.tsx +44 -0
  543. package/template/themes/ai-canvas/src/blocks/CollectionBlocks.tsx +711 -0
  544. package/template/themes/ai-canvas/src/blocks/ContentBlocks.tsx +318 -0
  545. package/template/themes/ai-canvas/src/blocks/ConversionBlocks.tsx +559 -0
  546. package/template/themes/ai-canvas/src/blocks/HeaderBlocks.tsx +341 -0
  547. package/template/themes/ai-canvas/src/index.tsx +54 -0
  548. package/template/themes/ai-canvas/src/kit.tsx +398 -0
  549. package/template/themes/ai-canvas/src/site.ts +29 -0
  550. package/template/themes/ai-canvas/src/styles.css +317 -0
  551. package/template/themes/dogga/screenshot.jpg +0 -0
  552. package/template/themes/dogga/snapify.theme.json +55 -0
  553. package/template/themes/dogga/src/Brand.tsx +41 -0
  554. package/template/themes/dogga/src/Footer.tsx +202 -0
  555. package/template/themes/dogga/src/Header.tsx +239 -0
  556. package/template/themes/dogga/src/Layout.tsx +43 -0
  557. package/template/themes/dogga/src/Views.tsx +71 -0
  558. package/template/themes/dogga/src/blocks/CollectionBlocks.tsx +674 -0
  559. package/template/themes/dogga/src/blocks/ContentBlocks.tsx +307 -0
  560. package/template/themes/dogga/src/blocks/ConversionBlocks.tsx +571 -0
  561. package/template/themes/dogga/src/blocks/HeaderBlocks.tsx +289 -0
  562. package/template/themes/dogga/src/index.tsx +53 -0
  563. package/template/themes/dogga/src/kit.tsx +362 -0
  564. package/template/themes/dogga/src/site.ts +56 -0
  565. package/template/themes/dogga/src/styles.css +396 -0
  566. package/template/themes/mahdia/screenshot.jpg +0 -0
  567. package/template/themes/mahdia/snapify.theme.json +65 -0
  568. package/template/themes/mahdia/src/Brand.tsx +39 -0
  569. package/template/themes/mahdia/src/Footer.tsx +210 -0
  570. package/template/themes/mahdia/src/Header.tsx +291 -0
  571. package/template/themes/mahdia/src/Layout.tsx +44 -0
  572. package/template/themes/mahdia/src/Views.tsx +79 -0
  573. package/template/themes/mahdia/src/blocks/CollectionBlocks.tsx +674 -0
  574. package/template/themes/mahdia/src/blocks/ContentBlocks.tsx +307 -0
  575. package/template/themes/mahdia/src/blocks/ConversionBlocks.tsx +571 -0
  576. package/template/themes/mahdia/src/blocks/HeaderBlocks.tsx +289 -0
  577. package/template/themes/mahdia/src/index.tsx +53 -0
  578. package/template/themes/mahdia/src/kit.tsx +362 -0
  579. package/template/themes/mahdia/src/site.ts +75 -0
  580. package/template/themes/mahdia/src/styles.css +446 -0
  581. package/template/themes/preveris/screenshot.jpg +0 -0
  582. package/template/themes/preveris/snapify.theme.json +126 -0
  583. package/template/themes/preveris/src/Brand.tsx +44 -0
  584. package/template/themes/preveris/src/Footer.tsx +202 -0
  585. package/template/themes/preveris/src/Header.tsx +239 -0
  586. package/template/themes/preveris/src/Layout.tsx +42 -0
  587. package/template/themes/preveris/src/Views.tsx +71 -0
  588. package/template/themes/preveris/src/blocks/CollectionBlocks.tsx +678 -0
  589. package/template/themes/preveris/src/blocks/ContentBlocks.tsx +307 -0
  590. package/template/themes/preveris/src/blocks/ConversionBlocks.tsx +571 -0
  591. package/template/themes/preveris/src/blocks/HeaderBlocks.tsx +299 -0
  592. package/template/themes/preveris/src/index.tsx +53 -0
  593. package/template/themes/preveris/src/kit.tsx +365 -0
  594. package/template/themes/preveris/src/site.ts +79 -0
  595. package/template/themes/preveris/src/styles.css +446 -0
  596. package/template/themes/sidibou/screenshot.jpg +0 -0
  597. package/template/themes/sidibou/snapify.theme.json +21 -0
  598. package/template/themes/sidibou/src/CeeSimulator.tsx +247 -0
  599. package/template/themes/sidibou/src/Footer.tsx +203 -0
  600. package/template/themes/sidibou/src/Header.tsx +324 -0
  601. package/template/themes/sidibou/src/Layout.tsx +53 -0
  602. package/template/themes/sidibou/src/NotFound.tsx +58 -0
  603. package/template/themes/sidibou/src/Post.tsx +26 -0
  604. package/template/themes/sidibou/src/SnapifyLogo.tsx +44 -0
  605. package/template/themes/sidibou/src/blocks/CollectionBlocks.tsx +711 -0
  606. package/template/themes/sidibou/src/blocks/ContentBlocks.tsx +318 -0
  607. package/template/themes/sidibou/src/blocks/ConversionBlocks.tsx +559 -0
  608. package/template/themes/sidibou/src/blocks/HeaderBlocks.tsx +341 -0
  609. package/template/themes/sidibou/src/index.tsx +54 -0
  610. package/template/themes/sidibou/src/kit.tsx +398 -0
  611. package/template/themes/sidibou/src/site.ts +28 -0
  612. package/template/themes/sidibou/src/styles.css +317 -0
  613. package/template/tsconfig.json +33 -0
  614. package/template/vite.config.ts +42 -0
@@ -0,0 +1,1266 @@
1
+ /**
2
+ * Snapify Marketplace — Espace développeur pro
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ *
5
+ * Chrome aligné sur le backoffice CMS / superadmin : primitives admin,
6
+ * sidebar navy-950, canvas slate-50. Auth & API restent marketplace.
7
+ */
8
+ import React, { useCallback, useEffect, useState } from 'react';
9
+ import { Link, NavLink, Navigate, Route, Routes, useLocation, useNavigate, useParams } from 'react-router-dom';
10
+ import {
11
+ AlertTriangle,
12
+ ArrowLeft,
13
+ BadgeCheck,
14
+ BarChart3,
15
+ CheckCircle2,
16
+ ChevronDown,
17
+ ExternalLink,
18
+ FileUp,
19
+ ImagePlus,
20
+ LayoutDashboard,
21
+ LogOut,
22
+ Menu as MenuIcon,
23
+ Package,
24
+ Settings,
25
+ ShoppingBag,
26
+ Trash2,
27
+ Upload,
28
+ Wallet,
29
+ X,
30
+ XCircle,
31
+ } from 'lucide-react';
32
+ import { api, ApiError } from '../../../shared/api';
33
+ import { useAuth } from '../../../shared/AuthContext';
34
+ import { DtInput, formatDt } from '../../../shared/ui';
35
+ import { SnapifyMark } from '../../../../src/components/SnapifyLogo';
36
+ import {
37
+ Alert,
38
+ Badge,
39
+ Button,
40
+ Card,
41
+ cx,
42
+ EmptyState,
43
+ ErrorState,
44
+ Field,
45
+ Input,
46
+ PageHeader,
47
+ PageSkeleton,
48
+ Section,
49
+ StatCard,
50
+ Table,
51
+ Td,
52
+ Textarea,
53
+ Th,
54
+ } from '../../../../src/admin/ui/primitives';
55
+
56
+ type Product = {
57
+ id: string;
58
+ slug: string;
59
+ name: string;
60
+ kind: 'module' | 'theme';
61
+ shortDesc: string;
62
+ longDesc: string;
63
+ category: string;
64
+ priceMillimes: number;
65
+ priceLabel: string;
66
+ status: string;
67
+ icon: string;
68
+ screenshots: string[];
69
+ salesCount: number;
70
+ rejectionReason?: string | null;
71
+ permissions?: string[];
72
+ engines?: { snapify: string };
73
+ };
74
+
75
+ function isMediaUrl(s: string) {
76
+ return /^https?:\/\//.test(s) || s.startsWith('/uploads/');
77
+ }
78
+
79
+ type CheckStatus = 'pass' | 'fail' | 'warn';
80
+
81
+ type Validation = {
82
+ ok: boolean;
83
+ checks: { id: string; label: string; status: CheckStatus; detail?: string; group?: string }[];
84
+ manifest?: {
85
+ id: string;
86
+ version: string;
87
+ name: string;
88
+ description: string;
89
+ kind?: string;
90
+ pricing?: string;
91
+ icon?: string;
92
+ permissions?: string[];
93
+ engines?: { snapify: string };
94
+ };
95
+ signed: boolean;
96
+ fileCount: number;
97
+ size: number;
98
+ sha256: string;
99
+ errors: string[];
100
+ warnings: string[];
101
+ analysis?: { capabilities: string[]; envVars: string[]; hosts: string[]; files: { name: string; size: number }[] };
102
+ };
103
+
104
+ type DashData = {
105
+ publisher: { id: string; displayName: string; website: string | null; payoutIban: string | null; status: string; paidAt: string | null };
106
+ products: Product[];
107
+ earnings: { grossMillimes: number; netMillimes: number; commissionBps: number; salesCount: number };
108
+ stats: { draft: number; pending: number; published: number; rejected: number; productCount: number };
109
+ recentSales: { orderId: string; productName: string; unitPriceMillimes: number; netMillimes: number; paidAt: string | null; licenseKey: string }[];
110
+ payouts: { id: string; amountMillimes: number; status: string; period: string }[];
111
+ };
112
+
113
+ async function fileToBase64(file: File): Promise<string> {
114
+ const buf = new Uint8Array(await file.arrayBuffer());
115
+ let binary = '';
116
+ const chunk = 0x8000;
117
+ for (let i = 0; i < buf.length; i += chunk) binary += String.fromCharCode(...buf.subarray(i, i + chunk));
118
+ return btoa(binary);
119
+ }
120
+
121
+ async function uploadMedia(file: File): Promise<string> {
122
+ const imageBase64 = await fileToBase64(file);
123
+ const mime = file.type || 'image/png';
124
+ const res = await api<{ url: string }>('/api/marketplace/developer/media', {
125
+ method: 'POST',
126
+ body: JSON.stringify({ imageBase64, mime }),
127
+ });
128
+ return res.url;
129
+ }
130
+
131
+ const STATUS_TONE: Record<string, ['success' | 'warning' | 'danger' | 'neutral' | 'info', string]> = {
132
+ draft: ['neutral', 'Brouillon'],
133
+ pending: ['warning', 'En review'],
134
+ published: ['success', 'Publié'],
135
+ rejected: ['danger', 'Rejeté'],
136
+ active: ['success', 'Actif'],
137
+ };
138
+
139
+ function StatusPill({ status }: { status: string }) {
140
+ const [tone, label] = STATUS_TONE[status] || ['neutral', status];
141
+ return (
142
+ <Badge tone={tone} dot>
143
+ {label}
144
+ </Badge>
145
+ );
146
+ }
147
+
148
+ const CHECK_GROUPS = ['Paquet', 'Manifeste', 'Code', 'Permissions', 'Sécurité', 'Présentation', 'Marketplace'];
149
+
150
+ function formatBytes(n: number) {
151
+ return n < 1024 ? `${n} o` : n < 1024 * 1024 ? `${(n / 1024).toFixed(1)} Ko` : `${(n / 1024 / 1024).toFixed(2)} Mo`;
152
+ }
153
+
154
+ function CheckIcon({ status }: { status: CheckStatus }) {
155
+ if (status === 'pass') return <CheckCircle2 className="h-4 w-4 text-emerald-600 shrink-0 mt-0.5" />;
156
+ if (status === 'warn') return <AlertTriangle className="h-4 w-4 text-amber-500 shrink-0 mt-0.5" />;
157
+ return <XCircle className="h-4 w-4 text-red-600 shrink-0 mt-0.5" />;
158
+ }
159
+
160
+ function CheckList({ validation }: { validation: Validation | null }) {
161
+ if (!validation) return null;
162
+ const count = (st: CheckStatus) => validation.checks.filter((c) => c.status === st).length;
163
+ const groups = [...CHECK_GROUPS, ...new Set(validation.checks.map((c) => c.group || 'Paquet'))]
164
+ .filter((g, i, all) => all.indexOf(g) === i)
165
+ .map((g) => ({ name: g, checks: validation.checks.filter((c) => (c.group || 'Paquet') === g) }))
166
+ .filter((g) => g.checks.length);
167
+ const worst = (checks: Validation['checks']): CheckStatus =>
168
+ checks.some((c) => c.status === 'fail') ? 'fail' : checks.some((c) => c.status === 'warn') ? 'warn' : 'pass';
169
+ const a = validation.analysis;
170
+ const label = { color: '#6c6c6c', fontSize: 12 } as const;
171
+ const chip = { fontFamily: 'monospace', fontSize: 11.5, background: '#eef2f7', borderRadius: 4, padding: '2px 6px' } as const;
172
+
173
+ return (
174
+ <div style={{ border: '1px solid #e5e7eb', borderRadius: 8, overflow: 'hidden', marginTop: 16 }}>
175
+ <div style={{ padding: '12px 16px', background: validation.ok ? '#ecfdf5' : '#fef2f2', display: 'flex', flexWrap: 'wrap', gap: 8, alignItems: 'center', fontWeight: 700 }}>
176
+ {validation.ok ? <CheckCircle2 className="h-5 w-5 text-emerald-600" /> : <XCircle className="h-5 w-5 text-red-600" />}
177
+ <span style={{ flex: 1, minWidth: 180 }}>
178
+ {validation.ok ? 'Analyse réussie — prêt à publier' : `Analyse : ${validation.errors.length} problème(s) à corriger`}
179
+ </span>
180
+ <span style={{ fontSize: 12, fontWeight: 600, color: '#475569' }}>
181
+ {count('pass')} réussi(s) · {count('warn')} avertissement(s) · {count('fail')} erreur(s)
182
+ </span>
183
+ </div>
184
+ {groups.map((g) => (
185
+ <details key={g.name} open={worst(g.checks) !== 'pass'} style={{ borderTop: '1px solid #f1f5f9' }}>
186
+ <summary style={{ display: 'flex', gap: 8, alignItems: 'center', padding: '10px 16px', cursor: 'pointer', fontSize: 13, fontWeight: 700, background: '#fafafa' }}>
187
+ <CheckIcon status={worst(g.checks)} />
188
+ <span style={{ flex: 1 }}>{g.name}</span>
189
+ <span style={{ fontWeight: 500, color: '#6c6c6c' }}>
190
+ {g.checks.filter((c) => c.status === 'pass').length}/{g.checks.length}
191
+ </span>
192
+ </summary>
193
+ <ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
194
+ {g.checks.map((c, i) => (
195
+ <li key={`${c.id}-${i}`} style={{ display: 'flex', gap: 10, padding: '8px 16px 8px 40px', borderTop: '1px solid #f8fafc', fontSize: 13, alignItems: 'flex-start' }}>
196
+ <CheckIcon status={c.status} />
197
+ <div style={{ minWidth: 0 }}>
198
+ <strong>{c.label}</strong>
199
+ {c.detail && <div style={{ color: c.status === 'fail' ? '#b91c1c' : '#6c6c6c', overflowWrap: 'anywhere' }}>{c.detail}</div>}
200
+ </div>
201
+ </li>
202
+ ))}
203
+ </ul>
204
+ </details>
205
+ ))}
206
+ {validation.manifest && (
207
+ <div style={{ padding: 14, background: '#f8fafc', fontSize: 13, display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(180px, 1fr))', gap: 12, borderTop: '1px solid #e5e7eb' }}>
208
+ <div>
209
+ <div style={label}>Identifiant</div>
210
+ <div style={{ fontFamily: 'monospace', overflowWrap: 'anywhere' }}>{validation.manifest.id}</div>
211
+ </div>
212
+ <div>
213
+ <div style={label}>Version · compatibilité</div>
214
+ <div style={{ fontFamily: 'monospace' }}>
215
+ {validation.manifest.version} · {validation.manifest.engines?.snapify || '*'}
216
+ </div>
217
+ </div>
218
+ <div>
219
+ <div style={label}>Permissions demandées</div>
220
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 2 }}>
221
+ {(validation.manifest.permissions || []).length ? validation.manifest.permissions!.map((p) => <span key={p} style={chip}>{p}</span>) : '—'}
222
+ </div>
223
+ </div>
224
+ {a && a.capabilities.length > 0 && (
225
+ <div>
226
+ <div style={label}>Utilisées par le code serveur</div>
227
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 2 }}>
228
+ {a.capabilities.map((p) => <span key={p} style={chip}>{p}</span>)}
229
+ </div>
230
+ </div>
231
+ )}
232
+ {a && a.files.length > 0 && (
233
+ <div>
234
+ <div style={label}>Code analysé · paquet de {formatBytes(validation.size)}</div>
235
+ {a.files.map((f) => (
236
+ <div key={f.name} style={{ display: 'flex', justifyContent: 'space-between', gap: 8, fontFamily: 'monospace', fontSize: 12 }}>
237
+ <span>{f.name}</span>
238
+ <span style={{ color: '#6c6c6c' }}>{formatBytes(f.size)}</span>
239
+ </div>
240
+ ))}
241
+ </div>
242
+ )}
243
+ <div>
244
+ <div style={label}>SHA-256</div>
245
+ <div style={{ fontFamily: 'monospace', fontSize: 11, wordBreak: 'break-all' }}>{validation.sha256.slice(0, 32)}…</div>
246
+ </div>
247
+ </div>
248
+ )}
249
+ </div>
250
+ );
251
+ }
252
+
253
+ const DEV_NAV = [
254
+ { to: '/developer/dashboard', end: true, icon: LayoutDashboard, label: 'Vue d’ensemble' },
255
+ { to: '/developer/dashboard/products', end: false, icon: Package, label: 'Mes produits' },
256
+ { to: '/developer/dashboard/upload', end: true, icon: Upload, label: 'Uploader un paquet' },
257
+ { to: '/developer/dashboard/sales', end: true, icon: ShoppingBag, label: 'Ventes & payouts' },
258
+ { to: '/developer/dashboard/settings', end: true, icon: Settings, label: 'Profil éditeur' },
259
+ ];
260
+
261
+ function DevSidebar({
262
+ data,
263
+ onNavigate,
264
+ onLogout,
265
+ }: {
266
+ data: DashData;
267
+ onNavigate?: () => void;
268
+ onLogout: () => void;
269
+ }) {
270
+ const { user } = useAuth();
271
+ return (
272
+ <div className="flex h-full flex-col bg-navy-950 text-slate-300">
273
+ <div className="flex h-16 shrink-0 items-center gap-3 px-5">
274
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-brand-600">
275
+ <SnapifyMark className="h-6 w-6" accent="#FFFFFF" />
276
+ </div>
277
+ <div className="min-w-0 leading-tight">
278
+ <p className="truncate text-sm font-semibold text-white">Snapify</p>
279
+ <p className="text-[11px] text-slate-500">Espace développeur</p>
280
+ </div>
281
+ </div>
282
+ <div className="border-b border-white/5 px-5 py-3">
283
+ <p className="truncate text-[13px] font-medium text-white">{data.publisher.displayName}</p>
284
+ <p className="truncate text-[11px] text-slate-500">{user?.email}</p>
285
+ </div>
286
+ <nav className="flex-1 space-y-0.5 overflow-y-auto px-3 py-4 scrollbar-thin">
287
+ {DEV_NAV.map((item) => (
288
+ <NavLink
289
+ key={item.to}
290
+ to={item.to}
291
+ end={item.end}
292
+ onClick={onNavigate}
293
+ className={({ isActive }) =>
294
+ cx('group flex items-center gap-3 rounded-lg px-3 py-2 text-sm transition', isActive ? 'bg-white/10 font-medium text-white' : 'text-slate-400 hover:bg-white/5 hover:text-slate-100')
295
+ }
296
+ >
297
+ {({ isActive }) => (
298
+ <>
299
+ <item.icon className={cx('h-4 w-4 shrink-0', isActive ? 'text-brand-400' : 'text-slate-500 group-hover:text-slate-300')} />
300
+ {item.label}
301
+ </>
302
+ )}
303
+ </NavLink>
304
+ ))}
305
+ </nav>
306
+ <div className="space-y-0.5 border-t border-white/5 p-3">
307
+ <a
308
+ href="https://developers.snapify.tn"
309
+ target="_blank"
310
+ rel="noreferrer"
311
+ className="flex items-center justify-between rounded-lg px-3 py-2 text-sm text-slate-400 transition hover:bg-white/5 hover:text-white"
312
+ >
313
+ Documentation <ExternalLink className="h-3.5 w-3.5" />
314
+ </a>
315
+ <Link to="/" onClick={onNavigate} className="flex items-center rounded-lg px-3 py-2 text-sm text-slate-400 transition hover:bg-white/5 hover:text-white">
316
+ ← Marketplace
317
+ </Link>
318
+ <button
319
+ type="button"
320
+ onClick={onLogout}
321
+ className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-slate-400 transition hover:bg-white/5 hover:text-white"
322
+ >
323
+ <LogOut className="h-3.5 w-3.5" /> Déconnexion
324
+ </button>
325
+ </div>
326
+ </div>
327
+ );
328
+ }
329
+
330
+ function DevShell({ children, data }: { children: React.ReactNode; data: DashData }) {
331
+ const { logout } = useAuth();
332
+ const location = useLocation();
333
+ const [mobileOpen, setMobileOpen] = useState(false);
334
+ const [menuOpen, setMenuOpen] = useState(false);
335
+ useEffect(() => {
336
+ setMobileOpen(false);
337
+ setMenuOpen(false);
338
+ }, [location.pathname]);
339
+
340
+ const current = DEV_NAV.filter((n) => (n.end ? location.pathname === n.to : location.pathname.startsWith(n.to))).sort((a, b) => b.to.length - a.to.length)[0];
341
+ useEffect(() => {
342
+ document.title = `${current?.label || 'Dashboard'} · Développeur Snapify`;
343
+ }, [current]);
344
+
345
+ const doLogout = () => void logout();
346
+
347
+ return (
348
+ <div className="mp-dev min-h-screen bg-slate-50">
349
+ <aside className="fixed inset-y-0 left-0 z-30 hidden w-64 lg:block">
350
+ <DevSidebar data={data} onLogout={doLogout} />
351
+ </aside>
352
+ {mobileOpen ? (
353
+ <div className="fixed inset-0 z-50 lg:hidden">
354
+ <div className="absolute inset-0 bg-slate-900/50" onClick={() => setMobileOpen(false)} />
355
+ <div className="absolute inset-y-0 left-0 w-72 shadow-2xl">
356
+ <DevSidebar data={data} onNavigate={() => setMobileOpen(false)} onLogout={doLogout} />
357
+ <button type="button" onClick={() => setMobileOpen(false)} className="absolute right-3 top-4 rounded-lg p-1.5 text-slate-400 hover:bg-white/10" aria-label="Fermer le menu">
358
+ <X className="h-5 w-5" />
359
+ </button>
360
+ </div>
361
+ </div>
362
+ ) : null}
363
+
364
+ <div className="lg:pl-64">
365
+ <header className="sticky top-0 z-20 flex h-16 items-center gap-3 border-b border-slate-200/80 bg-white/85 px-4 backdrop-blur sm:px-6">
366
+ <Button variant="ghost" size="icon" className="lg:hidden" onClick={() => setMobileOpen(true)} aria-label="Ouvrir le menu">
367
+ <MenuIcon className="h-5 w-5" />
368
+ </Button>
369
+ <div className="min-w-0 text-sm">
370
+ <span className="text-slate-400">Développeur</span>
371
+ <span className="mx-1.5 text-slate-300">/</span>
372
+ <span className="font-medium text-slate-800">{current?.label || 'Tableau de bord'}</span>
373
+ </div>
374
+ <div className="ml-auto flex items-center gap-2">
375
+ <a href="https://developers.snapify.tn" target="_blank" rel="noreferrer" className="hidden sm:block">
376
+ <Button size="sm" icon={ExternalLink}>
377
+ Docs
378
+ </Button>
379
+ </a>
380
+ <div className="relative">
381
+ <button type="button" onClick={() => setMenuOpen((v) => !v)} className="flex items-center gap-2 rounded-lg py-1 pl-1 pr-2 transition hover:bg-slate-100">
382
+ <span className="flex h-8 w-8 items-center justify-center rounded-full bg-navy-900 text-xs font-semibold text-white">
383
+ {data.publisher.displayName.slice(0, 2).toUpperCase()}
384
+ </span>
385
+ <span className="hidden text-left leading-tight md:block">
386
+ <span className="block max-w-[140px] truncate text-[13px] font-medium text-slate-800">{data.publisher.displayName}</span>
387
+ <span className="block text-[11px] text-slate-500">Éditeur</span>
388
+ </span>
389
+ <ChevronDown className="h-4 w-4 text-slate-400" />
390
+ </button>
391
+ {menuOpen ? (
392
+ <div className="absolute right-0 mt-1 w-48 rounded-xl border border-slate-200 bg-white py-1 shadow-lg ring-1 ring-slate-900/5">
393
+ <Link to="/developer/dashboard/settings" className="block px-3 py-2 text-sm text-slate-700 hover:bg-slate-50" onClick={() => setMenuOpen(false)}>
394
+ Profil éditeur
395
+ </Link>
396
+ <button type="button" className="block w-full px-3 py-2 text-left text-sm text-slate-700 hover:bg-slate-50" onClick={doLogout}>
397
+ Déconnexion
398
+ </button>
399
+ </div>
400
+ ) : null}
401
+ </div>
402
+ </div>
403
+ </header>
404
+ <main className="mp-dev-main px-4 py-6 sm:px-6 lg:px-8 lg:py-8">{children}</main>
405
+ </div>
406
+ </div>
407
+ );
408
+ }
409
+
410
+ function Overview({ data }: { data: DashData }) {
411
+ return (
412
+ <div>
413
+ <PageHeader
414
+ title="Vue d’ensemble"
415
+ description={`Tableau de bord vendeur — commission ${(data.earnings.commissionBps / 100).toFixed(0)} %`}
416
+ actions={
417
+ <Link to="/developer/dashboard/upload">
418
+ <Button variant="secondary" icon={Upload}>
419
+ Nouvel upload
420
+ </Button>
421
+ </Link>
422
+ }
423
+ />
424
+ <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
425
+ <StatCard label="Revenu brut" value={formatDt(data.earnings.grossMillimes)} icon={Wallet} tone="brand" />
426
+ <StatCard label="Revenu net" value={formatDt(data.earnings.netMillimes)} icon={BarChart3} tone="sky" />
427
+ <StatCard label="Ventes" value={String(data.earnings.salesCount)} icon={ShoppingBag} tone="violet" />
428
+ <StatCard label="Produits" value={String(data.stats.productCount)} icon={Package} tone="amber" />
429
+ </div>
430
+ <div className="mt-4 grid gap-4 lg:grid-cols-2">
431
+ <Section
432
+ title="Pipeline"
433
+ description="Statut de publication de vos paquets"
434
+ actions={
435
+ <Link to="/developer/dashboard/products">
436
+ <Button size="sm">Gérer les produits</Button>
437
+ </Link>
438
+ }
439
+ >
440
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
441
+ {[
442
+ ['Brouillons', data.stats.draft],
443
+ ['Review', data.stats.pending],
444
+ ['Publiés', data.stats.published],
445
+ ['Rejetés', data.stats.rejected],
446
+ ].map(([l, n]) => (
447
+ <div key={String(l)} className="rounded-lg bg-slate-50 px-3 py-4 text-center">
448
+ <div className="text-2xl font-semibold tabular-nums text-slate-900">{n}</div>
449
+ <div className="mt-0.5 text-[11px] font-medium uppercase tracking-wide text-slate-500">{l}</div>
450
+ </div>
451
+ ))}
452
+ </div>
453
+ </Section>
454
+ <Section title="Dernières ventes" description="Les 5 plus récentes">
455
+ {data.recentSales.length === 0 ? (
456
+ <EmptyState icon={ShoppingBag} title="Aucune vente" description="Vos ventes apparaîtront ici dès la première commande." />
457
+ ) : (
458
+ <ul className="divide-y divide-slate-100">
459
+ {data.recentSales.slice(0, 5).map((s, i) => (
460
+ <li key={i} className="flex items-center justify-between gap-3 py-2.5 text-sm">
461
+ <span className="truncate text-slate-700">{s.productName}</span>
462
+ <strong className="shrink-0 tabular-nums text-slate-900">{formatDt(s.netMillimes)}</strong>
463
+ </li>
464
+ ))}
465
+ </ul>
466
+ )}
467
+ </Section>
468
+ </div>
469
+ </div>
470
+ );
471
+ }
472
+
473
+ function ProductsList({ data }: { data: DashData; refresh: () => void }) {
474
+ const nav = useNavigate();
475
+ return (
476
+ <div>
477
+ <PageHeader
478
+ title="Mes produits"
479
+ description="Profils d’apps, versions et publication"
480
+ actions={
481
+ <Link to="/developer/dashboard/upload">
482
+ <Button variant="secondary" icon={FileUp}>
483
+ Uploader
484
+ </Button>
485
+ </Link>
486
+ }
487
+ />
488
+ <Card padded={false}>
489
+ {!data.products.length ? (
490
+ <EmptyState
491
+ icon={Package}
492
+ title="Aucun produit"
493
+ description="Uploadez votre premier paquet .snapmod ou .snaptheme."
494
+ action={
495
+ <Link to="/developer/dashboard/upload">
496
+ <Button variant="secondary" icon={Upload}>
497
+ Uploader un paquet
498
+ </Button>
499
+ </Link>
500
+ }
501
+ />
502
+ ) : (
503
+ <Table>
504
+ <thead>
505
+ <tr>
506
+ <Th>Produit</Th>
507
+ <Th>Type</Th>
508
+ <Th>Prix</Th>
509
+ <Th>Statut</Th>
510
+ <Th>Ventes</Th>
511
+ <Th />
512
+ </tr>
513
+ </thead>
514
+ <tbody>
515
+ {data.products.map((p) => (
516
+ <tr key={p.id}>
517
+ <Td>
518
+ <div className="flex items-center gap-3">
519
+ {isMediaUrl(p.icon) ? (
520
+ <img src={p.icon} alt="" className="h-10 w-10 rounded-lg border border-slate-200 object-cover" />
521
+ ) : (
522
+ <div className="grid h-10 w-10 place-items-center rounded-lg bg-slate-100">
523
+ <Package className="h-4 w-4 text-slate-400" />
524
+ </div>
525
+ )}
526
+ <div className="min-w-0">
527
+ <p className="truncate font-medium text-slate-900">{p.name}</p>
528
+ <p className="truncate font-mono text-xs text-slate-500">{p.slug}</p>
529
+ </div>
530
+ </div>
531
+ </Td>
532
+ <Td className="capitalize">{p.kind}</Td>
533
+ <Td>{p.priceLabel}</Td>
534
+ <Td>
535
+ <StatusPill status={p.status} />
536
+ </Td>
537
+ <Td className="tabular-nums">{p.salesCount}</Td>
538
+ <Td className="text-right">
539
+ <Button size="sm" onClick={() => nav(`/developer/dashboard/products/${p.id}`)}>
540
+ Profil
541
+ </Button>
542
+ </Td>
543
+ </tr>
544
+ ))}
545
+ </tbody>
546
+ </Table>
547
+ )}
548
+ </Card>
549
+ </div>
550
+ );
551
+ }
552
+
553
+ function ProductProfile({ refresh }: { refresh: () => void }) {
554
+ const { id } = useParams();
555
+ const nav = useNavigate();
556
+ const [product, setProduct] = useState<Product | null>(null);
557
+ const [versions, setVersions] = useState<
558
+ { id: string; version: string; changelog: string; size: number; signed: boolean; sha256: string; validationReport?: Validation; moduleId?: string | null; createdAt: string }[]
559
+ >([]);
560
+ const [form, setForm] = useState({
561
+ name: '',
562
+ shortDesc: '',
563
+ longDesc: '',
564
+ category: '',
565
+ priceMillimes: 0,
566
+ icon: '',
567
+ screenshots: [] as string[],
568
+ });
569
+ const [msg, setMsg] = useState('');
570
+ const [file, setFile] = useState<File | null>(null);
571
+ const [changelog, setChangelog] = useState('');
572
+ const [validation, setValidation] = useState<Validation | null>(null);
573
+ const [busy, setBusy] = useState(false);
574
+ const [mediaBusy, setMediaBusy] = useState(false);
575
+
576
+ const load = useCallback(async () => {
577
+ const d = await api<{ product: Product; versions: typeof versions }>(`/api/marketplace/developer/products/${id}`);
578
+ setProduct(d.product);
579
+ setVersions(d.versions);
580
+ setForm({
581
+ name: d.product.name,
582
+ shortDesc: d.product.shortDesc,
583
+ longDesc: d.product.longDesc,
584
+ category: d.product.category,
585
+ priceMillimes: d.product.priceMillimes,
586
+ icon: d.product.icon || '',
587
+ screenshots: Array.isArray(d.product.screenshots) ? d.product.screenshots : [],
588
+ });
589
+ }, [id]);
590
+
591
+ useEffect(() => {
592
+ void load();
593
+ }, [load]);
594
+
595
+ const saveProfile = async (extra?: Partial<typeof form>) => {
596
+ if (!product) return;
597
+ const body = { ...form, ...extra };
598
+ await api(`/api/marketplace/developer/products/${product.id}`, { method: 'PATCH', body: JSON.stringify(body) });
599
+ setMsg('Profil enregistré');
600
+ void load();
601
+ refresh();
602
+ };
603
+
604
+ if (!product) return <PageSkeleton />;
605
+
606
+ return (
607
+ <div>
608
+ <button
609
+ type="button"
610
+ onClick={() => nav('/developer/dashboard/products')}
611
+ className="mb-3 inline-flex items-center gap-1.5 text-[13px] text-slate-500 transition hover:text-slate-800"
612
+ >
613
+ <ArrowLeft className="h-3.5 w-3.5" /> Produits
614
+ </button>
615
+ <PageHeader
616
+ title={
617
+ <span className="flex items-center gap-3">
618
+ {isMediaUrl(form.icon) ? (
619
+ <img src={form.icon} alt="" className="h-12 w-12 rounded-xl border border-slate-200 object-cover" />
620
+ ) : (
621
+ <span className="grid h-12 w-12 place-items-center rounded-xl bg-slate-100 text-[10px] font-semibold uppercase text-slate-500">{product.kind}</span>
622
+ )}
623
+ {product.name}
624
+ </span>
625
+ }
626
+ description={
627
+ <span className="inline-flex flex-wrap items-center gap-2">
628
+ <StatusPill status={product.status} />
629
+ <span className="font-mono text-xs">{product.slug}</span>
630
+ </span>
631
+ }
632
+ actions={
633
+ <>
634
+ {product.status === 'published' ? (
635
+ <Link to={`/product/${product.slug}`}>
636
+ <Button icon={ExternalLink}>Voir sur le store</Button>
637
+ </Link>
638
+ ) : null}
639
+ {product.status === 'draft' || product.status === 'rejected' ? (
640
+ <Button
641
+ variant="secondary"
642
+ onClick={async () => {
643
+ if (!versions.length) return setMsg('Uploadez au moins une version avant de soumettre');
644
+ if (!isMediaUrl(form.icon)) return setMsg('Ajoutez une icône (image) avant de soumettre');
645
+ if (form.screenshots.length < 1) return setMsg('Ajoutez au moins une capture d’écran');
646
+ await api(`/api/marketplace/developer/products/${product.id}`, { method: 'PATCH', body: JSON.stringify({ ...form, submit: true }) });
647
+ setMsg('Soumis pour validation Snapify');
648
+ void load();
649
+ refresh();
650
+ }}
651
+ >
652
+ Soumettre pour review
653
+ </Button>
654
+ ) : null}
655
+ </>
656
+ }
657
+ />
658
+ {product.rejectionReason ? (
659
+ <div className="mb-4">
660
+ <Alert tone="danger" title="Rejeté">
661
+ {product.rejectionReason}
662
+ </Alert>
663
+ </div>
664
+ ) : null}
665
+
666
+ <div style={{ display: 'grid', gridTemplateColumns: '1.15fr 0.85fr', gap: 16, marginTop: 20 }}>
667
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
668
+ <div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 20 }}>
669
+ <h2 style={{ marginTop: 0, fontSize: 15 }}>Profil de l’application</h2>
670
+ <p style={{ fontSize: 13, color: '#6c6c6c', marginTop: 0 }}>Comme sur PrestaShop Addons : fiche complète visible sur le store.</p>
671
+ <label style={{ display: 'block', fontSize: 13, fontWeight: 600, marginTop: 10 }}>
672
+ Nom
673
+ <input className="mp-input" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
674
+ </label>
675
+ <label style={{ display: 'block', fontSize: 13, fontWeight: 600, marginTop: 10 }}>
676
+ Catégorie
677
+ <input className="mp-input" value={form.category} onChange={(e) => setForm({ ...form, category: e.target.value })} />
678
+ </label>
679
+ <label style={{ display: 'block', fontSize: 13, fontWeight: 600, marginTop: 10 }}>
680
+ Prix (0 = gratuit)
681
+ <DtInput millimes={form.priceMillimes} onMillimes={(m) => setForm({ ...form, priceMillimes: m })} />
682
+ </label>
683
+ <label style={{ display: 'block', fontSize: 13, fontWeight: 600, marginTop: 10 }}>
684
+ Description courte
685
+ <textarea className="mp-input" rows={2} value={form.shortDesc} onChange={(e) => setForm({ ...form, shortDesc: e.target.value })} />
686
+ </label>
687
+ <label style={{ display: 'block', fontSize: 13, fontWeight: 600, marginTop: 10 }}>
688
+ Description longue (HTML)
689
+ <textarea className="mp-input" rows={6} value={form.longDesc} onChange={(e) => setForm({ ...form, longDesc: e.target.value })} />
690
+ </label>
691
+ <button type="button" className="btn-mp" style={{ marginTop: 14 }} onClick={() => void saveProfile()}>
692
+ Enregistrer le profil
693
+ </button>
694
+ {product.permissions && product.permissions.length > 0 && (
695
+ <div style={{ marginTop: 16, fontSize: 13 }}>
696
+ <strong>Permissions</strong>
697
+ <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6, marginTop: 6 }}>
698
+ {product.permissions.map((p) => (
699
+ <span key={p} style={{ background: '#f1f5f9', padding: '4px 8px', borderRadius: 4, fontFamily: 'monospace', fontSize: 11 }}>
700
+ {p}
701
+ </span>
702
+ ))}
703
+ </div>
704
+ </div>
705
+ )}
706
+ </div>
707
+
708
+ <div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 20 }}>
709
+ <h2 style={{ marginTop: 0, fontSize: 15 }}>Icône du module / thème</h2>
710
+ <p style={{ fontSize: 13, color: '#6c6c6c', marginTop: 0 }}>PNG/JPG/WebP · carré recommandé 512×512 · max 5 Mo</p>
711
+ <div style={{ display: 'flex', gap: 16, alignItems: 'center', flexWrap: 'wrap' }}>
712
+ {isMediaUrl(form.icon) ? <img src={form.icon} alt="Icône" className="mp-dev-icon-preview" /> : <div className="mp-dev-icon-fallback">Pas d’icône</div>}
713
+ <div>
714
+ <label className="btn-mp" style={{ cursor: mediaBusy ? 'wait' : 'pointer' }}>
715
+ <ImagePlus className="h-4 w-4" /> {mediaBusy ? 'Upload…' : 'Choisir une image'}
716
+ <input
717
+ type="file"
718
+ accept="image/png,image/jpeg,image/webp,image/gif"
719
+ hidden
720
+ disabled={mediaBusy}
721
+ onChange={async (e) => {
722
+ const f = e.target.files?.[0];
723
+ e.target.value = '';
724
+ if (!f) return;
725
+ setMediaBusy(true);
726
+ setMsg('');
727
+ try {
728
+ const url = await uploadMedia(f);
729
+ setForm((prev) => ({ ...prev, icon: url }));
730
+ await api(`/api/marketplace/developer/products/${product.id}`, {
731
+ method: 'PATCH',
732
+ body: JSON.stringify({ icon: url }),
733
+ });
734
+ setMsg('Icône mise à jour');
735
+ refresh();
736
+ } catch (err) {
737
+ setMsg(err instanceof ApiError ? err.message : 'Erreur upload icône');
738
+ } finally {
739
+ setMediaBusy(false);
740
+ }
741
+ }}
742
+ />
743
+ </label>
744
+ {isMediaUrl(form.icon) && (
745
+ <button
746
+ type="button"
747
+ className="mp-btn-ghost"
748
+ style={{ width: 'auto', padding: '0 14px', marginTop: 8, display: 'inline-flex', alignItems: 'center', gap: 6 }}
749
+ onClick={async () => {
750
+ setForm((prev) => ({ ...prev, icon: 'package' }));
751
+ await api(`/api/marketplace/developer/products/${product.id}`, {
752
+ method: 'PATCH',
753
+ body: JSON.stringify({ icon: 'package' }),
754
+ });
755
+ setMsg('Icône retirée');
756
+ refresh();
757
+ }}
758
+ >
759
+ <Trash2 className="h-3.5 w-3.5" /> Retirer
760
+ </button>
761
+ )}
762
+ </div>
763
+ </div>
764
+ </div>
765
+
766
+ <div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 20 }}>
767
+ <h2 style={{ marginTop: 0, fontSize: 15 }}>Captures d’écran</h2>
768
+ <p style={{ fontSize: 13, color: '#6c6c6c', marginTop: 0 }}>Jusqu’à 8 images · affichées en galerie sur la fiche produit</p>
769
+ <div className="mp-dev-media-grid">
770
+ {form.screenshots.map((url, i) => (
771
+ <div key={`${url}-${i}`} className="mp-dev-shot">
772
+ <img src={url} alt={`Capture ${i + 1}`} />
773
+ <button
774
+ type="button"
775
+ title="Supprimer"
776
+ onClick={async () => {
777
+ const next = form.screenshots.filter((_, j) => j !== i);
778
+ setForm((prev) => ({ ...prev, screenshots: next }));
779
+ await api(`/api/marketplace/developer/products/${product.id}`, {
780
+ method: 'PATCH',
781
+ body: JSON.stringify({ screenshots: next }),
782
+ });
783
+ setMsg('Capture retirée');
784
+ refresh();
785
+ }}
786
+ >
787
+ ×
788
+ </button>
789
+ </div>
790
+ ))}
791
+ {form.screenshots.length < 8 && (
792
+ <label
793
+ className="mp-dev-shot"
794
+ style={{ display: 'grid', placeItems: 'center', cursor: mediaBusy ? 'wait' : 'pointer', borderStyle: 'dashed' }}
795
+ >
796
+ <span style={{ textAlign: 'center', fontSize: 12, color: '#6c6c6c', fontWeight: 600 }}>
797
+ <ImagePlus className="mx-auto mb-1 h-5 w-5" />
798
+ {mediaBusy ? 'Upload…' : 'Ajouter'}
799
+ </span>
800
+ <input
801
+ type="file"
802
+ accept="image/png,image/jpeg,image/webp,image/gif"
803
+ hidden
804
+ disabled={mediaBusy}
805
+ onChange={async (e) => {
806
+ const f = e.target.files?.[0];
807
+ e.target.value = '';
808
+ if (!f) return;
809
+ setMediaBusy(true);
810
+ setMsg('');
811
+ try {
812
+ const url = await uploadMedia(f);
813
+ const next = [...form.screenshots, url].slice(0, 8);
814
+ setForm((prev) => ({ ...prev, screenshots: next }));
815
+ await api(`/api/marketplace/developer/products/${product.id}`, {
816
+ method: 'PATCH',
817
+ body: JSON.stringify({ screenshots: next }),
818
+ });
819
+ setMsg('Capture ajoutée');
820
+ refresh();
821
+ } catch (err) {
822
+ setMsg(err instanceof ApiError ? err.message : 'Erreur upload');
823
+ } finally {
824
+ setMediaBusy(false);
825
+ }
826
+ }}
827
+ />
828
+ </label>
829
+ )}
830
+ </div>
831
+ </div>
832
+ </div>
833
+
834
+ <div>
835
+ <div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 20 }}>
836
+ <h2 style={{ marginTop: 0, fontSize: 15 }}>Nouvelle version</h2>
837
+ <p style={{ fontSize: 13, color: '#6c6c6c' }}>Le paquet est analysé automatiquement : intégrité, compatibilité, code, permissions, sécurité et numéro de version.</p>
838
+ <input
839
+ type="file"
840
+ accept=".snapmod,.snaptheme"
841
+ onChange={async (e) => {
842
+ const f = e.target.files?.[0] || null;
843
+ setFile(f);
844
+ setValidation(null);
845
+ if (!f) return;
846
+ setBusy(true);
847
+ try {
848
+ const packageBase64 = await fileToBase64(f);
849
+ const res = await api<{ validation: Validation }>('/api/marketplace/developer/validate-package', {
850
+ method: 'POST',
851
+ body: JSON.stringify({ packageBase64, productId: product.id }),
852
+ });
853
+ setValidation(res.validation);
854
+ } catch (err) {
855
+ setMsg(err instanceof ApiError ? err.message : 'Erreur validation');
856
+ } finally {
857
+ setBusy(false);
858
+ }
859
+ }}
860
+ />
861
+ <label style={{ display: 'block', fontSize: 13, fontWeight: 600, marginTop: 10 }}>
862
+ Changelog
863
+ <textarea className="mp-input" rows={3} value={changelog} onChange={(e) => setChangelog(e.target.value)} placeholder="Nouveautés de cette version…" />
864
+ </label>
865
+ <CheckList validation={validation} />
866
+ <button
867
+ type="button"
868
+ className="btn-mp"
869
+ style={{ marginTop: 12, width: '100%' }}
870
+ disabled={!file || busy || (validation !== null && !validation.ok)}
871
+ onClick={async () => {
872
+ if (!file) return;
873
+ setBusy(true);
874
+ try {
875
+ const packageBase64 = await fileToBase64(file);
876
+ const res = await api<{ validation: Validation }>(`/api/marketplace/developer/products/${product.id}/versions`, {
877
+ method: 'POST',
878
+ body: JSON.stringify({ packageBase64, changelog }),
879
+ });
880
+ setValidation(res.validation);
881
+ setMsg(`Version ${res.validation.manifest?.version || ''} uploadée`);
882
+ setFile(null);
883
+ setChangelog('');
884
+ void load();
885
+ refresh();
886
+ } catch (err) {
887
+ setMsg(err instanceof ApiError ? err.message : 'Erreur upload');
888
+ } finally {
889
+ setBusy(false);
890
+ }
891
+ }}
892
+ >
893
+ {busy ? 'Traitement…' : 'Valider & enregistrer la version'}
894
+ </button>
895
+ </div>
896
+
897
+ <div style={{ background: '#fff', border: '1px solid #e5e7eb', borderRadius: 8, padding: 20, marginTop: 14 }}>
898
+ <h2 style={{ marginTop: 0, fontSize: 15 }}>Historique des versions</h2>
899
+ {versions.map((v) => (
900
+ <div key={v.id} style={{ borderTop: '1px solid #f1f5f9', padding: '12px 0', fontSize: 13 }}>
901
+ <div style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
902
+ <strong>v{v.version}</strong>
903
+ <span style={{ color: '#6c6c6c' }}>{new Date(v.createdAt).toLocaleString('fr-TN')}</span>
904
+ </div>
905
+ <div style={{ color: '#6c6c6c', marginTop: 4 }}>{v.changelog || '—'}</div>
906
+ <div style={{ marginTop: 6, display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
907
+ {v.signed ? (
908
+ <span style={{ color: '#047857', display: 'inline-flex', gap: 4, alignItems: 'center' }}>
909
+ <BadgeCheck className="h-3.5 w-3.5" /> Signé
910
+ </span>
911
+ ) : (
912
+ <span style={{ color: '#b45309' }}>Non signé</span>
913
+ )}
914
+ <span style={{ fontFamily: 'monospace', fontSize: 11 }}>{(v.size / 1024).toFixed(1)} Ko</span>
915
+ {v.moduleId && <span style={{ fontFamily: 'monospace', fontSize: 11 }}>{v.moduleId}</span>}
916
+ </div>
917
+ </div>
918
+ ))}
919
+ {!versions.length && <div style={{ color: '#6c6c6c' }}>Aucune version.</div>}
920
+ </div>
921
+ </div>
922
+ </div>
923
+ {msg && <p style={{ color: '#00a89d', marginTop: 12 }}>{msg}</p>}
924
+ </div>
925
+ );
926
+ }
927
+
928
+ function UploadWizard({ refresh }: { refresh: () => void }) {
929
+ const nav = useNavigate();
930
+ const [file, setFile] = useState<File | null>(null);
931
+ const [validation, setValidation] = useState<Validation | null>(null);
932
+ const [category, setCategory] = useState('Autres');
933
+ const [priceMillimes, setPriceMillimes] = useState(0);
934
+ const [changelog, setChangelog] = useState('Version initiale');
935
+ const [busy, setBusy] = useState(false);
936
+ const [drag, setDrag] = useState(false);
937
+ const [msg, setMsg] = useState('');
938
+
939
+ const runValidate = async (f: File) => {
940
+ setFile(f);
941
+ setBusy(true);
942
+ setMsg('');
943
+ try {
944
+ const packageBase64 = await fileToBase64(f);
945
+ const res = await api<{ validation: Validation }>('/api/marketplace/developer/validate-package', {
946
+ method: 'POST',
947
+ body: JSON.stringify({ packageBase64 }),
948
+ });
949
+ setValidation(res.validation);
950
+ if (res.validation.manifest?.pricing === 'paid' && priceMillimes === 0) setPriceMillimes(49_000);
951
+ if (res.validation.manifest?.pricing === 'free') setPriceMillimes(0);
952
+ } catch (e) {
953
+ setValidation(null);
954
+ setMsg(e instanceof ApiError ? e.message : 'Erreur');
955
+ } finally {
956
+ setBusy(false);
957
+ }
958
+ };
959
+
960
+ return (
961
+ <div className="max-w-3xl">
962
+ <PageHeader
963
+ title="Uploader un paquet"
964
+ description={
965
+ <>
966
+ Déposez un fichier <code className="rounded bg-slate-100 px-1 font-mono text-[13px]">.snapmod</code> ou{' '}
967
+ <code className="rounded bg-slate-100 px-1 font-mono text-[13px]">.snaptheme</code>. Analyse automatique avant création — le code n’est jamais exécuté.
968
+ </>
969
+ }
970
+ />
971
+
972
+ <Card
973
+ className={cx('border-2 border-dashed text-center transition', drag ? 'border-brand-500 bg-brand-50' : 'border-slate-200')}
974
+ padded={false}
975
+ >
976
+ <div
977
+ className="px-6 py-12"
978
+ onDragOver={(e) => {
979
+ e.preventDefault();
980
+ setDrag(true);
981
+ }}
982
+ onDragLeave={() => setDrag(false)}
983
+ onDrop={(e) => {
984
+ e.preventDefault();
985
+ setDrag(false);
986
+ const f = e.dataTransfer.files?.[0];
987
+ if (f) void runValidate(f);
988
+ }}
989
+ >
990
+ <Upload className="mx-auto h-10 w-10 text-brand-600 opacity-80" />
991
+ <p className="mt-3 text-[15px] font-semibold text-slate-900">Glissez-déposez votre paquet ici</p>
992
+ <p className="mt-1 text-sm text-slate-500">ou</p>
993
+ <label className="mt-3 inline-flex cursor-pointer">
994
+ <span className="inline-flex h-9 items-center rounded-lg bg-navy-900 px-3.5 text-sm font-medium text-white shadow-sm hover:bg-navy-800">Choisir un fichier</span>
995
+ <input
996
+ type="file"
997
+ accept=".snapmod,.snaptheme"
998
+ hidden
999
+ onChange={(e) => {
1000
+ const f = e.target.files?.[0];
1001
+ if (f) void runValidate(f);
1002
+ }}
1003
+ />
1004
+ </label>
1005
+ {file ? (
1006
+ <p className="mt-3 text-sm text-slate-500">
1007
+ {file.name} · {(file.size / 1024).toFixed(1)} Ko {busy ? '· Analyse…' : ''}
1008
+ </p>
1009
+ ) : null}
1010
+ </div>
1011
+ </Card>
1012
+
1013
+ <CheckList validation={validation} />
1014
+
1015
+ {validation?.ok && validation.manifest ? (
1016
+ <div className="mt-4">
1017
+ <Section
1018
+ title="Créer le produit sur la marketplace"
1019
+ description={
1020
+ <>
1021
+ <strong>{validation.manifest.name}</strong> — {validation.manifest.id}@{validation.manifest.version}
1022
+ </>
1023
+ }
1024
+ >
1025
+ <div className="grid gap-4 sm:grid-cols-2">
1026
+ <Field label="Catégorie">
1027
+ <Input value={category} onChange={(e) => setCategory(e.target.value)} />
1028
+ </Field>
1029
+ <Field label="Prix (0 = gratuit)">
1030
+ <DtInput millimes={priceMillimes} onMillimes={setPriceMillimes} />
1031
+ </Field>
1032
+ </div>
1033
+ <Field label="Changelog">
1034
+ <Textarea rows={2} value={changelog} onChange={(e) => setChangelog(e.target.value)} />
1035
+ </Field>
1036
+ <Button
1037
+ variant="secondary"
1038
+ loading={busy}
1039
+ onClick={async () => {
1040
+ if (!file) return;
1041
+ setBusy(true);
1042
+ try {
1043
+ const packageBase64 = await fileToBase64(file);
1044
+ const res = await api<{ product: Product }>('/api/marketplace/developer/products/from-package', {
1045
+ method: 'POST',
1046
+ body: JSON.stringify({ packageBase64, category, priceMillimes, changelog }),
1047
+ });
1048
+ refresh();
1049
+ nav(`/developer/dashboard/products/${res.product.id}`);
1050
+ } catch (e) {
1051
+ setMsg(e instanceof ApiError ? e.message : 'Erreur');
1052
+ } finally {
1053
+ setBusy(false);
1054
+ }
1055
+ }}
1056
+ >
1057
+ Créer le produit + version
1058
+ </Button>
1059
+ </Section>
1060
+ </div>
1061
+ ) : null}
1062
+ {msg ? (
1063
+ <div className="mt-4">
1064
+ <Alert tone="danger">{msg}</Alert>
1065
+ </div>
1066
+ ) : null}
1067
+ </div>
1068
+ );
1069
+ }
1070
+
1071
+ function SalesPage({ data }: { data: DashData }) {
1072
+ const [amount, setAmount] = useState(50_000);
1073
+ const [msg, setMsg] = useState('');
1074
+ const [busy, setBusy] = useState(false);
1075
+ return (
1076
+ <div>
1077
+ <PageHeader
1078
+ title="Ventes & payouts"
1079
+ description={
1080
+ <>
1081
+ Net disponible estimé : <strong className="text-slate-800">{formatDt(data.earnings.netMillimes)}</strong>
1082
+ </>
1083
+ }
1084
+ />
1085
+ <div className="grid gap-4 lg:grid-cols-[1fr_360px]">
1086
+ <Card padded={false}>
1087
+ {!data.recentSales.length ? (
1088
+ <EmptyState icon={ShoppingBag} title="Pas encore de ventes" description="Les commandes de vos produits apparaîtront ici." />
1089
+ ) : (
1090
+ <Table>
1091
+ <thead>
1092
+ <tr>
1093
+ <Th>Produit</Th>
1094
+ <Th>Brut</Th>
1095
+ <Th>Net</Th>
1096
+ <Th>Date</Th>
1097
+ <Th>Licence</Th>
1098
+ </tr>
1099
+ </thead>
1100
+ <tbody>
1101
+ {data.recentSales.map((s, i) => (
1102
+ <tr key={i}>
1103
+ <Td>{s.productName}</Td>
1104
+ <Td className="tabular-nums">{formatDt(s.unitPriceMillimes)}</Td>
1105
+ <Td className="tabular-nums font-medium">{formatDt(s.netMillimes)}</Td>
1106
+ <Td className="text-slate-500">{s.paidAt ? new Date(s.paidAt).toLocaleString('fr-TN') : '—'}</Td>
1107
+ <Td className="font-mono text-xs">{s.licenseKey}</Td>
1108
+ </tr>
1109
+ ))}
1110
+ </tbody>
1111
+ </Table>
1112
+ )}
1113
+ </Card>
1114
+ <Section title="Demander un payout" description="Minimum 10 DT · virement sur l’IBAN du profil">
1115
+ <Field label="Montant">
1116
+ <DtInput millimes={amount} onMillimes={setAmount} />
1117
+ </Field>
1118
+ <Button
1119
+ variant="secondary"
1120
+ loading={busy}
1121
+ className="w-full justify-center"
1122
+ onClick={async () => {
1123
+ setBusy(true);
1124
+ setMsg('');
1125
+ try {
1126
+ await api('/api/marketplace/developer/payouts', { method: 'POST', body: JSON.stringify({ amountMillimes: amount }) });
1127
+ setMsg('Demande envoyée');
1128
+ } catch (e) {
1129
+ setMsg(e instanceof ApiError ? e.message : 'Erreur');
1130
+ } finally {
1131
+ setBusy(false);
1132
+ }
1133
+ }}
1134
+ >
1135
+ Envoyer la demande
1136
+ </Button>
1137
+ {msg ? <Alert tone={msg === 'Demande envoyée' ? 'success' : 'danger'}>{msg}</Alert> : null}
1138
+ {data.payouts.length > 0 ? (
1139
+ <ul className="space-y-2 text-[13px] text-slate-600">
1140
+ {data.payouts.map((p) => (
1141
+ <li key={p.id} className="flex justify-between gap-2 border-t border-slate-100 pt-2">
1142
+ <span>{formatDt(p.amountMillimes)}</span>
1143
+ <span className="text-slate-500">
1144
+ {p.status} · {p.period}
1145
+ </span>
1146
+ </li>
1147
+ ))}
1148
+ </ul>
1149
+ ) : null}
1150
+ </Section>
1151
+ </div>
1152
+ </div>
1153
+ );
1154
+ }
1155
+
1156
+ function PublisherSettings({ data, refresh }: { data: DashData; refresh: () => void }) {
1157
+ const [displayName, setDisplayName] = useState(data.publisher.displayName);
1158
+ const [website, setWebsite] = useState(data.publisher.website || '');
1159
+ const [payoutIban, setPayoutIban] = useState(data.publisher.payoutIban || '');
1160
+ const [msg, setMsg] = useState('');
1161
+ const [busy, setBusy] = useState(false);
1162
+ return (
1163
+ <div className="max-w-xl">
1164
+ <PageHeader
1165
+ title="Profil éditeur"
1166
+ description={
1167
+ <span className="inline-flex flex-wrap items-center gap-2">
1168
+ <StatusPill status={data.publisher.status === 'active' ? 'published' : 'pending'} />
1169
+ <span>
1170
+ Payé le {data.publisher.paidAt ? new Date(data.publisher.paidAt).toLocaleDateString('fr-TN') : '—'}
1171
+ </span>
1172
+ </span>
1173
+ }
1174
+ />
1175
+ <Section title="Identité publique" description="Affiché sur vos fiches produit de la marketplace">
1176
+ <Field label="Nom public">
1177
+ <Input value={displayName} onChange={(e) => setDisplayName(e.target.value)} />
1178
+ </Field>
1179
+ <Field label="Site web">
1180
+ <Input value={website} onChange={(e) => setWebsite(e.target.value)} placeholder="https://" />
1181
+ </Field>
1182
+ <Field label="IBAN payout" hint="Utilisé pour les virements de commissions.">
1183
+ <Input value={payoutIban} onChange={(e) => setPayoutIban(e.target.value)} className="font-mono" />
1184
+ </Field>
1185
+ <Button
1186
+ variant="secondary"
1187
+ loading={busy}
1188
+ onClick={async () => {
1189
+ setBusy(true);
1190
+ setMsg('');
1191
+ try {
1192
+ await api('/api/marketplace/developer/profile', {
1193
+ method: 'PATCH',
1194
+ body: JSON.stringify({ displayName, website: website || null, payoutIban: payoutIban || null }),
1195
+ });
1196
+ setMsg('Profil mis à jour');
1197
+ refresh();
1198
+ } catch (e) {
1199
+ setMsg(e instanceof ApiError ? e.message : 'Erreur');
1200
+ } finally {
1201
+ setBusy(false);
1202
+ }
1203
+ }}
1204
+ >
1205
+ Enregistrer
1206
+ </Button>
1207
+ {msg ? <Alert tone={msg.includes('mis à jour') ? 'success' : 'danger'}>{msg}</Alert> : null}
1208
+ </Section>
1209
+ </div>
1210
+ );
1211
+ }
1212
+
1213
+ export function DeveloperRoutes() {
1214
+ const { user, loading } = useAuth();
1215
+ const [data, setData] = useState<DashData | null>(null);
1216
+ const [err, setErr] = useState('');
1217
+
1218
+ const refresh = useCallback(() => {
1219
+ setErr('');
1220
+ api<DashData>('/api/marketplace/developer/me')
1221
+ .then(setData)
1222
+ .catch((e) => setErr(e instanceof ApiError ? e.message : 'Erreur'));
1223
+ }, []);
1224
+
1225
+ useEffect(() => {
1226
+ if (user) refresh();
1227
+ }, [user, refresh]);
1228
+
1229
+ if (loading) {
1230
+ return (
1231
+ <div className="mp-dev min-h-screen bg-slate-50 px-6 py-10 lg:pl-64">
1232
+ <PageSkeleton />
1233
+ </div>
1234
+ );
1235
+ }
1236
+ if (!user) return <Navigate to="/login" replace />;
1237
+ if (err && !data) {
1238
+ return (
1239
+ <div className="mp-dev mx-auto max-w-lg px-4 py-16">
1240
+ <ErrorState message={err} onRetry={refresh} />
1241
+ </div>
1242
+ );
1243
+ }
1244
+ if (!data) {
1245
+ return (
1246
+ <div className="mp-dev min-h-screen bg-slate-50 px-6 py-10 lg:pl-64">
1247
+ <PageSkeleton />
1248
+ </div>
1249
+ );
1250
+ }
1251
+ if (!data.publisher || data.publisher.status !== 'active') return <Navigate to="/developer" replace />;
1252
+
1253
+ return (
1254
+ <DevShell data={data}>
1255
+ <Routes>
1256
+ <Route index element={<Overview data={data} />} />
1257
+ <Route path="products" element={<ProductsList data={data} refresh={refresh} />} />
1258
+ <Route path="products/:id" element={<ProductProfile refresh={refresh} />} />
1259
+ <Route path="upload" element={<UploadWizard refresh={refresh} />} />
1260
+ <Route path="sales" element={<SalesPage data={data} />} />
1261
+ <Route path="settings" element={<PublisherSettings data={data} refresh={refresh} />} />
1262
+ <Route path="*" element={<Navigate to="/developer/dashboard" replace />} />
1263
+ </Routes>
1264
+ </DevShell>
1265
+ );
1266
+ }