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,1264 @@
1
+ /**
2
+ * Snapify Superadmin — /hamdisup90
3
+ * © 2026 NOVALABS WEB DESIGN — Tunisie
4
+ */
5
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
6
+ import { Navigate, NavLink, Route, Routes, useLocation, useNavigate } from 'react-router-dom';
7
+ import {
8
+ BarChart3,
9
+ Banknote,
10
+ ChevronDown,
11
+ CreditCard,
12
+ ExternalLink,
13
+ Eye,
14
+ FileCheck2,
15
+ FileText,
16
+ Globe2,
17
+ Inbox,
18
+ LayoutDashboard,
19
+ LifeBuoy,
20
+ Lock,
21
+ LogOut,
22
+ Mail,
23
+ Menu as MenuIcon,
24
+ Package,
25
+ Percent,
26
+ RefreshCw,
27
+ Search,
28
+ Send,
29
+ Server,
30
+ Settings2,
31
+ ShieldCheck,
32
+ ShoppingBag,
33
+ Store,
34
+ Trash2,
35
+ User,
36
+ Users,
37
+ Wallet,
38
+ X,
39
+ } from 'lucide-react';
40
+ import { SnapifyMark } from '../../../src/components/SnapifyLogo';
41
+ import { useDtField } from '../../shared/ui';
42
+ import {
43
+ Alert,
44
+ Badge,
45
+ Button,
46
+ Card,
47
+ cx,
48
+ EmptyState,
49
+ Field,
50
+ Input,
51
+ PageHeader,
52
+ PageSkeleton,
53
+ Section,
54
+ StatCard,
55
+ Switch,
56
+ Table,
57
+ Tabs,
58
+ Td,
59
+ Textarea,
60
+ Th,
61
+ } from '../../../src/admin/ui/primitives';
62
+ import { sa, dt, when } from './lib/api';
63
+ import { UsersPage } from './pages/UsersPage';
64
+ import { PublishersPage } from './pages/PublishersPage';
65
+ import { OrdersPage } from './pages/OrdersPage';
66
+ import { PayoutsPage } from './pages/PayoutsPage';
67
+ import { SystemPage } from './pages/SystemPage';
68
+ import { TicketsPage } from './pages/TicketsPage';
69
+ import { NewsletterPage } from './pages/NewsletterPage';
70
+ import { InvoicesPage } from './pages/InvoicesPage';
71
+ import { PaymentsPage } from './pages/PaymentsPage';
72
+ import { SettingsPage } from './pages/SettingsPage';
73
+ import { SeoControlPage } from './pages/SeoControlPage';
74
+ import { ClientsPage } from './pages/ClientsPage';
75
+
76
+ const STATUS: Record<string, ['success' | 'warning' | 'danger' | 'neutral' | 'info', string]> = {
77
+ paid: ['success', 'Payée'],
78
+ published: ['success', 'Publié'],
79
+ active: ['success', 'Actif'],
80
+ pending: ['warning', 'En attente'],
81
+ draft: ['neutral', 'Brouillon'],
82
+ rejected: ['danger', 'Rejeté'],
83
+ failed: ['danger', 'Échouée'],
84
+ };
85
+
86
+ function StatusBadge({ status }: { status: string }) {
87
+ const [tone, label] = STATUS[status] || ['neutral', status];
88
+ return (
89
+ <Badge tone={tone} dot>
90
+ {label}
91
+ </Badge>
92
+ );
93
+ }
94
+
95
+ /* ------------------------------------------------------------------ Shell */
96
+
97
+ const NAV = [
98
+ {
99
+ title: '',
100
+ items: [{ to: '/', label: 'Tableau de bord', icon: LayoutDashboard }],
101
+ },
102
+ {
103
+ title: 'Ventes',
104
+ items: [
105
+ { to: '/demandes', label: 'Demandes licence', icon: Inbox },
106
+ { to: '/clients', label: 'Clients', icon: User },
107
+ { to: '/commandes', label: 'Commandes MP', icon: ShoppingBag },
108
+ { to: '/factures', label: 'Factures', icon: FileText },
109
+ { to: '/paiements', label: 'Paiements Flouci', icon: CreditCard },
110
+ { to: '/payouts', label: 'Payouts', icon: Banknote },
111
+ { to: '/finances', label: 'Commissions & frais', icon: Wallet },
112
+ ],
113
+ },
114
+ {
115
+ title: 'Marketplace',
116
+ items: [
117
+ { to: '/catalogue', label: 'Modules & thèmes', icon: Package },
118
+ { to: '/editeurs', label: 'Éditeurs', icon: Store },
119
+ ],
120
+ },
121
+ {
122
+ title: 'Ops',
123
+ items: [
124
+ { to: '/tickets', label: 'Tickets', icon: LifeBuoy },
125
+ { to: '/newsletter', label: 'Newsletter', icon: Mail },
126
+ { to: '/parametrage', label: 'Paramétrage', icon: Settings2 },
127
+ ],
128
+ },
129
+ {
130
+ title: 'Plateforme',
131
+ items: [
132
+ { to: '/utilisateurs', label: 'Utilisateurs', icon: Users },
133
+ { to: '/systeme', label: 'Système', icon: Server },
134
+ ],
135
+ },
136
+ {
137
+ title: 'Croissance',
138
+ items: [
139
+ { to: '/visites', label: 'Visites', icon: BarChart3 },
140
+ { to: '/seo', label: 'SEO & Backlinks', icon: Search },
141
+ ],
142
+ },
143
+ ];
144
+ const ALL_NAV = NAV.flatMap((g) => g.items);
145
+
146
+ function Sidebar({ onNavigate }: { onNavigate?: () => void }) {
147
+ return (
148
+ <div className="flex h-full flex-col bg-navy-950 text-slate-300">
149
+ <div className="flex h-16 shrink-0 items-center gap-3 px-5">
150
+ <div className="flex h-8 w-8 items-center justify-center rounded-lg bg-brand-600">
151
+ <SnapifyMark className="h-6 w-6" accent="#FFFFFF" />
152
+ </div>
153
+ <div className="min-w-0 leading-tight">
154
+ <p className="truncate text-sm font-semibold text-white">Snapify</p>
155
+ <p className="text-[11px] text-slate-500">Superadmin plateforme</p>
156
+ </div>
157
+ </div>
158
+ <nav className="flex-1 space-y-6 overflow-y-auto px-3 py-4 scrollbar-thin">
159
+ {NAV.map((group) => (
160
+ <div key={group.title || 'root'}>
161
+ {group.title ? <p className="mb-1.5 px-3 text-[11px] font-semibold uppercase tracking-wider text-slate-500">{group.title}</p> : null}
162
+ <div className="space-y-0.5">
163
+ {group.items.map((item) => (
164
+ <NavLink
165
+ key={item.to}
166
+ to={item.to}
167
+ end={item.to === '/'}
168
+ onClick={onNavigate}
169
+ className={({ isActive }) =>
170
+ 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')
171
+ }
172
+ >
173
+ {({ isActive }) => (
174
+ <>
175
+ <item.icon className={cx('h-4 w-4', isActive ? 'text-brand-400' : 'text-slate-500 group-hover:text-slate-300')} />
176
+ {item.label}
177
+ </>
178
+ )}
179
+ </NavLink>
180
+ ))}
181
+ </div>
182
+ </div>
183
+ ))}
184
+ </nav>
185
+ <div className="space-y-0.5 border-t border-white/5 p-3">
186
+ {[
187
+ { href: 'https://snapify.tn/', label: 'snapify.tn' },
188
+ { href: 'https://marketplace.snapify.tn/', label: 'Marketplace' },
189
+ { href: 'https://community.snapify.tn/', label: 'Communauté' },
190
+ ].map((l) => (
191
+ <a
192
+ key={l.href}
193
+ href={l.href}
194
+ target="_blank"
195
+ rel="noreferrer"
196
+ 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"
197
+ >
198
+ {l.label} <ExternalLink className="h-3.5 w-3.5" />
199
+ </a>
200
+ ))}
201
+ </div>
202
+ </div>
203
+ );
204
+ }
205
+
206
+ function Shell({ children, onLogout }: { children: React.ReactNode; onLogout: () => void }) {
207
+ const location = useLocation();
208
+ const [mobileOpen, setMobileOpen] = useState(false);
209
+ const [menuOpen, setMenuOpen] = useState(false);
210
+ useEffect(() => {
211
+ setMobileOpen(false);
212
+ setMenuOpen(false);
213
+ }, [location.pathname]);
214
+
215
+ const current = ALL_NAV.filter((n) => (n.to === '/' ? location.pathname === '/' : location.pathname.startsWith(n.to))).sort((a, b) => b.to.length - a.to.length)[0];
216
+ useEffect(() => {
217
+ document.title = `${current?.label || 'Superadmin'} · Snapify`;
218
+ }, [current]);
219
+
220
+ return (
221
+ <div className="min-h-screen bg-slate-50">
222
+ <aside className="fixed inset-y-0 left-0 z-30 hidden w-64 lg:block">
223
+ <Sidebar />
224
+ </aside>
225
+ {mobileOpen ? (
226
+ <div className="fixed inset-0 z-50 lg:hidden">
227
+ <div className="absolute inset-0 bg-slate-900/50" onClick={() => setMobileOpen(false)} />
228
+ <div className="absolute inset-y-0 left-0 w-72 shadow-2xl">
229
+ <Sidebar onNavigate={() => setMobileOpen(false)} />
230
+ <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">
231
+ <X className="h-5 w-5" />
232
+ </button>
233
+ </div>
234
+ </div>
235
+ ) : null}
236
+
237
+ <div className="lg:pl-64">
238
+ <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">
239
+ <Button variant="ghost" size="icon" className="lg:hidden" onClick={() => setMobileOpen(true)} aria-label="Ouvrir le menu">
240
+ <MenuIcon className="h-5 w-5" />
241
+ </Button>
242
+ <div className="min-w-0 text-sm">
243
+ <span className="text-slate-400">Superadmin</span>
244
+ <span className="mx-1.5 text-slate-300">/</span>
245
+ <span className="font-medium text-slate-800">{current?.label || 'Tableau de bord'}</span>
246
+ </div>
247
+ <div className="ml-auto flex items-center gap-2">
248
+ <a href="https://snapify.tn/" target="_blank" rel="noreferrer" className="hidden sm:block">
249
+ <Button size="sm" icon={ExternalLink}>
250
+ Voir snapify.tn
251
+ </Button>
252
+ </a>
253
+ <div className="relative">
254
+ <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">
255
+ <span className="flex h-8 w-8 items-center justify-center rounded-full bg-navy-900 text-xs font-semibold text-white">SA</span>
256
+ <span className="hidden text-left leading-tight md:block">
257
+ <span className="block text-[13px] font-medium text-slate-800">hamdisup90</span>
258
+ <span className="block text-[11px] text-slate-500">Superadmin</span>
259
+ </span>
260
+ <ChevronDown className="h-4 w-4 text-slate-400" />
261
+ </button>
262
+ {menuOpen ? (
263
+ <>
264
+ <div className="fixed inset-0 z-40" onClick={() => setMenuOpen(false)} />
265
+ <div className="absolute right-0 z-50 mt-1.5 w-56 rounded-xl border border-slate-200 bg-white p-1.5 shadow-lg">
266
+ <div className="border-b border-slate-100 px-2.5 pb-2 pt-1.5 text-xs text-slate-500">Accès plateforme complet</div>
267
+ <button type="button" onClick={onLogout} className="mt-1 flex w-full items-center gap-2 rounded-lg px-2.5 py-2 text-sm text-rose-600 hover:bg-rose-50">
268
+ <LogOut className="h-4 w-4" /> Se déconnecter
269
+ </button>
270
+ </div>
271
+ </>
272
+ ) : null}
273
+ </div>
274
+ </div>
275
+ </header>
276
+ <main className="mx-auto max-w-[1400px] px-4 pb-28 pt-6 sm:px-6 lg:px-8">{children}</main>
277
+ </div>
278
+ </div>
279
+ );
280
+ }
281
+
282
+ /* ------------------------------------------------------------------ Types */
283
+
284
+ type Visits = {
285
+ last24h: number;
286
+ last7d: number;
287
+ total: number;
288
+ topPaths: { path: string; n: number }[];
289
+ topCountries: { country: string; n: number }[];
290
+ recent: { ts: string; path: string; country?: string; ref?: string }[];
291
+ };
292
+
293
+ type OverviewData = {
294
+ visits: Visits;
295
+ marketplace: {
296
+ products: number;
297
+ publishers: number;
298
+ activePublishers?: number;
299
+ pendingReview: number;
300
+ commissionLabel: string;
301
+ developerFeeLabel: string;
302
+ paidOrders: number;
303
+ revenue30dMillimes?: number;
304
+ commission30dMillimes?: number;
305
+ };
306
+ licenses: { total: number; paid: number; pending: number; revenueMillimes: number };
307
+ users?: { total: number };
308
+ payouts?: { pending: number; pendingMillimes: number };
309
+ };
310
+
311
+ type LicenseOrder = {
312
+ id: string;
313
+ name: string;
314
+ email: string;
315
+ domain: string;
316
+ hostingNotes: string;
317
+ status: string;
318
+ amountMillimes: number;
319
+ createdAt: string;
320
+ paidAt?: string;
321
+ details?: {
322
+ company?: string;
323
+ phone: string;
324
+ gouvernorat: string;
325
+ city?: string;
326
+ siteType: string;
327
+ domainStatus: 'owned' | 'to-register';
328
+ providerId: string;
329
+ providerName: string;
330
+ hostingType: string;
331
+ accessType: string;
332
+ serverIp?: string;
333
+ theme?: string;
334
+ message?: string;
335
+ };
336
+ };
337
+
338
+ function useLoad<T>(load: () => Promise<T>, deps: unknown[] = []) {
339
+ const [data, setData] = useState<T | null>(null);
340
+ const [error, setError] = useState('');
341
+ const reload = useCallback(() => {
342
+ setError('');
343
+ load()
344
+ .then(setData)
345
+ .catch((e: Error) => setError(e.message));
346
+ // eslint-disable-next-line react-hooks/exhaustive-deps
347
+ }, deps);
348
+ useEffect(() => {
349
+ reload();
350
+ }, [reload]);
351
+ return { data, error, reload };
352
+ }
353
+
354
+ function BarList({ rows, empty }: { rows: { label: string; n: number }[]; empty: string }) {
355
+ const max = Math.max(1, ...rows.map((r) => r.n));
356
+ if (!rows.length) return <p className="py-6 text-center text-sm text-slate-500">{empty}</p>;
357
+ return (
358
+ <ul className="space-y-2.5">
359
+ {rows.map((r) => (
360
+ <li key={r.label}>
361
+ <div className="flex items-center justify-between gap-3 text-sm">
362
+ <span className="truncate text-slate-700">{r.label}</span>
363
+ <span className="tabular-nums font-medium text-slate-900">{r.n}</span>
364
+ </div>
365
+ <div className="mt-1 h-1.5 overflow-hidden rounded-full bg-slate-100">
366
+ <div className="h-full rounded-full bg-brand-500" style={{ width: `${(r.n / max) * 100}%` }} />
367
+ </div>
368
+ </li>
369
+ ))}
370
+ </ul>
371
+ );
372
+ }
373
+
374
+ /* ------------------------------------------------------------------ Pages */
375
+
376
+ function Overview() {
377
+ const { data, error, reload } = useLoad(() => sa<OverviewData>('/overview'));
378
+ const orders = useLoad(() => sa<{ items: LicenseOrder[] }>('/licenses'));
379
+ const navigate = useNavigate();
380
+ if (error) return <Alert tone="danger" title="Chargement impossible" action={<Button size="sm" onClick={reload}>Réessayer</Button>}>{error}</Alert>;
381
+ if (!data) return <PageSkeleton />;
382
+ const { visits: v, marketplace: m, licenses: l } = data;
383
+ const payouts = data.payouts || { pending: 0, pendingMillimes: 0 };
384
+ return (
385
+ <div className="space-y-6">
386
+ <PageHeader title="Tableau de bord" description="Vue d’ensemble SaaS : licences, marketplace, payouts et trafic landing." />
387
+ <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
388
+ <StatCard label="Licences payées" value={l.paid} icon={FileCheck2} hint={`${l.pending} en attente de paiement`} />
389
+ <StatCard label="CA licences" value={dt(l.revenueMillimes)} icon={Banknote} tone="violet" hint={`${l.total} demandes au total`} />
390
+ <StatCard
391
+ label="CA marketplace (30 j)"
392
+ value={dt(m.revenue30dMillimes || 0)}
393
+ icon={ShoppingBag}
394
+ tone="sky"
395
+ hint={`Commission ${dt(m.commission30dMillimes || 0)}`}
396
+ />
397
+ <StatCard
398
+ label="Payouts à traiter"
399
+ value={payouts.pending}
400
+ icon={Wallet}
401
+ tone="amber"
402
+ hint={payouts.pending ? dt(payouts.pendingMillimes) : 'File vide'}
403
+ />
404
+ </div>
405
+ <div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
406
+ <StatCard label="Visites (7 j)" value={v.last7d} icon={Eye} hint={`${v.last24h} / 24 h`} />
407
+ <StatCard label="Produits" value={m.products} icon={Package} tone="sky" hint={`${m.pendingReview} en validation`} />
408
+ <StatCard label="Éditeurs actifs" value={m.activePublishers ?? m.publishers} icon={Store} tone="violet" hint={`${m.publishers} au total`} />
409
+ <StatCard label="Utilisateurs" value={data.users?.total ?? '—'} icon={Users} tone="amber" />
410
+ </div>
411
+ <div className="grid gap-4 lg:grid-cols-3">
412
+ <Section
413
+ className="lg:col-span-2"
414
+ title="Dernières demandes de licence"
415
+ description="Formulaire de commande snapify.tn/commander"
416
+ actions={
417
+ <Button size="sm" onClick={() => navigate('/demandes')}>
418
+ Tout voir
419
+ </Button>
420
+ }
421
+ >
422
+ {!orders.data ? (
423
+ <p className="text-sm text-slate-500">Chargement…</p>
424
+ ) : orders.data.items.length ? (
425
+ <ul className="-my-2 divide-y divide-slate-100">
426
+ {orders.data.items.slice(0, 5).map((o) => (
427
+ <li key={o.id} className="flex flex-wrap items-center gap-3 py-3">
428
+ <div className="min-w-0 flex-1">
429
+ <p className="truncate text-sm font-medium text-slate-900">
430
+ {o.name}
431
+ {o.details?.company ? <span className="font-normal text-slate-500"> · {o.details.company}</span> : null}
432
+ </p>
433
+ <p className="truncate text-xs text-slate-500">
434
+ {o.domain} · {o.details?.providerName || 'hébergeur non renseigné'}
435
+ </p>
436
+ </div>
437
+ <StatusBadge status={o.status} />
438
+ <span className="w-28 text-right text-xs text-slate-400">{when(o.createdAt)}</span>
439
+ </li>
440
+ ))}
441
+ </ul>
442
+ ) : (
443
+ <EmptyState icon={Inbox} title="Aucune demande" description="Les commandes passées depuis la landing apparaîtront ici." />
444
+ )}
445
+ </Section>
446
+ <Section
447
+ title="Marketplace"
448
+ description="Réglages et activité"
449
+ actions={
450
+ payouts.pending ? (
451
+ <Button size="sm" onClick={() => navigate('/payouts')}>
452
+ Voir payouts
453
+ </Button>
454
+ ) : (
455
+ <Button size="sm" onClick={() => navigate('/catalogue')}>
456
+ Catalogue
457
+ </Button>
458
+ )
459
+ }
460
+ >
461
+ <dl className="space-y-3 text-sm">
462
+ {[
463
+ ['Commission', m.commissionLabel],
464
+ ['Frais développeur', m.developerFeeLabel],
465
+ ['Éditeurs', String(m.publishers)],
466
+ ['Commandes payées', String(m.paidOrders)],
467
+ ['En validation', String(m.pendingReview)],
468
+ ['Payouts en attente', String(payouts.pending)],
469
+ ].map(([k, val]) => (
470
+ <div key={k} className="flex items-center justify-between">
471
+ <dt className="text-slate-500">{k}</dt>
472
+ <dd className="font-medium tabular-nums text-slate-900">{val}</dd>
473
+ </div>
474
+ ))}
475
+ </dl>
476
+ </Section>
477
+ </div>
478
+ <div className="grid gap-4 lg:grid-cols-2">
479
+ <Section title="Pages les plus vues" description="7 derniers jours">
480
+ <BarList rows={v.topPaths.slice(0, 6).map((p) => ({ label: p.path, n: p.n }))} empty="Pas encore de visites." />
481
+ </Section>
482
+ <Section title="Pays" description="7 derniers jours">
483
+ <BarList rows={v.topCountries.slice(0, 6).map((p) => ({ label: p.country || 'Inconnu', n: p.n }))} empty="Pas encore de visites." />
484
+ </Section>
485
+ </div>
486
+ </div>
487
+ );
488
+ }
489
+
490
+ function Finances() {
491
+ const navigate = useNavigate();
492
+ const [form, setForm] = useState({ commissionBps: 3000, developerFeeMillimes: 100000, flouciPublicKey: '', flouciPrivateKey: '', bankFallback: '', hasFlouciPrivateKey: false });
493
+ const developerFee = useDtField(form.developerFeeMillimes, (m) => setForm((f) => ({ ...f, developerFeeMillimes: m })));
494
+ const [msg, setMsg] = useState<{ tone: 'success' | 'danger'; text: string } | null>(null);
495
+ const [saving, setSaving] = useState(false);
496
+ const [rev, setRev] = useState<{ grossLabel: string; commissionLabel: string; orders: number } | null>(null);
497
+ const load = useCallback(() => {
498
+ void sa<typeof form>('/settings').then((s) => setForm((f) => ({ ...f, ...s, flouciPrivateKey: '' })));
499
+ void sa<typeof rev>('/revenue').then(setRev);
500
+ }, []);
501
+ useEffect(() => {
502
+ load();
503
+ }, [load]);
504
+
505
+ const save = async () => {
506
+ setSaving(true);
507
+ setMsg(null);
508
+ try {
509
+ const body: Record<string, unknown> = {
510
+ commissionBps: form.commissionBps,
511
+ developerFeeMillimes: form.developerFeeMillimes,
512
+ flouciPublicKey: form.flouciPublicKey,
513
+ bankFallback: form.bankFallback,
514
+ };
515
+ if (form.flouciPrivateKey) body.flouciPrivateKey = form.flouciPrivateKey;
516
+ await sa('/settings', { method: 'PUT', body: JSON.stringify(body) });
517
+ setMsg({ tone: 'success', text: 'Réglages enregistrés.' });
518
+ load();
519
+ } catch (e) {
520
+ setMsg({ tone: 'danger', text: e instanceof Error ? e.message : 'Erreur' });
521
+ } finally {
522
+ setSaving(false);
523
+ }
524
+ };
525
+
526
+ return (
527
+ <div className="space-y-6">
528
+ <PageHeader
529
+ title="Commissions & frais"
530
+ description="Pourcentage prélevé sur les ventes marketplace, frais d’inscription développeur et compte de paiement Flouci."
531
+ actions={
532
+ <>
533
+ <Button onClick={() => navigate('/payouts')}>Payouts</Button>
534
+ <Button variant="primary" loading={saving} onClick={save}>
535
+ Enregistrer
536
+ </Button>
537
+ </>
538
+ }
539
+ />
540
+ {rev ? (
541
+ <div className="grid gap-4 sm:grid-cols-3">
542
+ <StatCard label="Chiffre d’affaires (30 j)" value={rev.grossLabel} icon={Banknote} />
543
+ <StatCard label="Commissions (30 j)" value={rev.commissionLabel} icon={Percent} tone="violet" />
544
+ <StatCard label="Commandes payées" value={rev.orders} icon={FileCheck2} tone="sky" />
545
+ </div>
546
+ ) : null}
547
+ {msg ? <Alert tone={msg.tone}>{msg.text}</Alert> : null}
548
+ <div className="grid gap-4 lg:grid-cols-2">
549
+ <Section title="Marketplace" description="S’applique aux nouvelles commandes.">
550
+ <Field label="Commission marketplace" hint="Part prélevée par NOVALABS sur chaque vente (le reste revient au développeur).">
551
+ <Input
552
+ type="number"
553
+ prefix="%"
554
+ min={0}
555
+ max={90}
556
+ step={0.5}
557
+ value={form.commissionBps / 100}
558
+ onChange={(e) => setForm({ ...form, commissionBps: Math.round(Number(e.target.value) * 100) })}
559
+ />
560
+ </Field>
561
+ <Field label="Frais d’inscription développeur" hint={`Payé une fois via Flouci : ${dt(form.developerFeeMillimes)}. 0 = inscription gratuite.`}>
562
+ <Input prefix="DT" {...developerFee} />
563
+ </Field>
564
+ </Section>
565
+ <Section title="Paiement Flouci" description="Clés du compte marchand utilisé pour les licences et la marketplace.">
566
+ <Field label="Clé publique (app token)">
567
+ <Input value={form.flouciPublicKey} onChange={(e) => setForm({ ...form, flouciPublicKey: e.target.value })} className="font-mono" />
568
+ </Field>
569
+ <Field label="Clé privée (app secret)" hint={form.hasFlouciPrivateKey ? 'Une clé est déjà enregistrée — laissez vide pour la conserver.' : 'Aucune clé enregistrée.'}>
570
+ <Input type="password" value={form.flouciPrivateKey} onChange={(e) => setForm({ ...form, flouciPrivateKey: e.target.value })} placeholder="••••••••" autoComplete="new-password" />
571
+ </Field>
572
+ <Field label="IBAN / virement de secours">
573
+ <Input value={form.bankFallback} onChange={(e) => setForm({ ...form, bankFallback: e.target.value })} />
574
+ </Field>
575
+ </Section>
576
+ </div>
577
+ </div>
578
+ );
579
+ }
580
+
581
+ function Visites() {
582
+ const { data, error } = useLoad(() => sa<OverviewData>('/overview').then((d) => d.visits));
583
+ if (error) return <Alert tone="danger">{error}</Alert>;
584
+ if (!data) return <PageSkeleton />;
585
+ return (
586
+ <div className="space-y-6">
587
+ <PageHeader title="Visites" description="Trafic de la landing snapify.tn et des pages marketing." />
588
+ <div className="grid gap-4 sm:grid-cols-3">
589
+ <StatCard label="Dernières 24 h" value={data.last24h} icon={Eye} />
590
+ <StatCard label="7 derniers jours" value={data.last7d} icon={BarChart3} tone="sky" />
591
+ <StatCard label="Total enregistré" value={data.total} icon={Globe2} tone="violet" />
592
+ </div>
593
+ <div className="grid gap-4 lg:grid-cols-2">
594
+ <Section title="Pages les plus vues" description="7 derniers jours">
595
+ <BarList rows={data.topPaths.map((p) => ({ label: p.path, n: p.n }))} empty="Pas encore de visites." />
596
+ </Section>
597
+ <Section title="Pays" description="7 derniers jours">
598
+ <BarList rows={data.topCountries.map((p) => ({ label: p.country || 'Inconnu', n: p.n }))} empty="Pas encore de visites." />
599
+ </Section>
600
+ </div>
601
+ <Card padded={false}>
602
+ <div className="border-b border-slate-100 px-5 py-4">
603
+ <h2 className="text-[15px] font-semibold text-slate-900">Visites récentes</h2>
604
+ </div>
605
+ <Table>
606
+ <thead>
607
+ <tr>
608
+ <Th>Date</Th>
609
+ <Th>Page</Th>
610
+ <Th>Pays</Th>
611
+ <Th>Provenance</Th>
612
+ </tr>
613
+ </thead>
614
+ <tbody>
615
+ {data.recent.map((r, i) => (
616
+ <tr key={i} className="hover:bg-slate-50/60">
617
+ <Td className="whitespace-nowrap text-slate-500">{when(r.ts)}</Td>
618
+ <Td className="font-mono text-xs">{r.path}</Td>
619
+ <Td>{r.country || '—'}</Td>
620
+ <Td className="max-w-[260px] truncate text-slate-500">{r.ref || '—'}</Td>
621
+ </tr>
622
+ ))}
623
+ {!data.recent.length ? (
624
+ <tr>
625
+ <Td colSpan={4} className="py-10 text-center text-slate-500">
626
+ Aucune visite enregistrée.
627
+ </Td>
628
+ </tr>
629
+ ) : null}
630
+ </tbody>
631
+ </Table>
632
+ </Card>
633
+ </div>
634
+ );
635
+ }
636
+
637
+ const SITE_TYPE: Record<string, string> = { vitrine: 'Site vitrine', boutique: 'Boutique en ligne', blog: 'Blog / média', multi: 'Multi-sites / agence' };
638
+ const HOSTING_TYPE: Record<string, string> = { mutualise: 'Mutualisé', vps: 'VPS', dedie: 'Serveur dédié', cloud: 'Cloud', unknown: 'Non précisé' };
639
+ const ACCESS_TYPE: Record<string, string> = { cpanel: 'cPanel', plesk: 'Plesk', directadmin: 'DirectAdmin', ssh: 'SSH / root', unknown: 'Non précisé' };
640
+
641
+ function OrderDetails({ o }: { o: LicenseOrder }) {
642
+ const d = o.details;
643
+ if (!d) return <p className="whitespace-pre-wrap text-sm text-slate-600">{o.hostingNotes || 'Aucun détail.'}</p>;
644
+ const block = (title: string, icon: React.ReactNode, rows: [string, React.ReactNode][]) => (
645
+ <div>
646
+ <p className="mb-2 flex items-center gap-1.5 text-xs font-semibold uppercase tracking-wide text-slate-500">
647
+ {icon} {title}
648
+ </p>
649
+ <dl className="space-y-1.5 text-sm">
650
+ {rows.map(([k, v]) => (
651
+ <div key={k} className="flex gap-3">
652
+ <dt className="w-28 shrink-0 text-slate-500">{k}</dt>
653
+ <dd className="min-w-0 break-words font-medium text-slate-800">{v || '—'}</dd>
654
+ </div>
655
+ ))}
656
+ </dl>
657
+ </div>
658
+ );
659
+ return (
660
+ <div className="grid gap-6 md:grid-cols-3">
661
+ {block('Client', <User className="h-3.5 w-3.5" />, [
662
+ ['Nom', o.name],
663
+ ['Société', d.company],
664
+ ['E-mail', <a className="text-brand-700 hover:underline" href={`mailto:${o.email}`}>{o.email}</a>],
665
+ ['Téléphone', <a className="text-brand-700 hover:underline" href={`tel:${d.phone.replace(/\s/g, '')}`}>{d.phone}</a>],
666
+ ['Localisation', [d.city, d.gouvernorat].filter(Boolean).join(', ')],
667
+ ])}
668
+ {block('Projet', <Globe2 className="h-3.5 w-3.5" />, [
669
+ ['Type', SITE_TYPE[d.siteType] || d.siteType],
670
+ ['Domaine', o.domain],
671
+ ['Statut domaine', d.domainStatus === 'owned' ? 'Déjà enregistré' : 'À enregistrer'],
672
+ ['Thème', d.theme ? d.theme.charAt(0).toUpperCase() + d.theme.slice(1) : '—'],
673
+ ['Montant', dt(o.amountMillimes)],
674
+ ])}
675
+ {block('Hébergement', <Lock className="h-3.5 w-3.5" />, [
676
+ ['Hébergeur', d.providerName],
677
+ ['Type', HOSTING_TYPE[d.hostingType] || d.hostingType],
678
+ ['Accès', ACCESS_TYPE[d.accessType] || d.accessType],
679
+ ['IP serveur', d.serverIp ? <span className="font-mono text-xs">{d.serverIp}</span> : '—'],
680
+ ])}
681
+ {d.message ? (
682
+ <div className="md:col-span-3">
683
+ <p className="mb-1.5 text-xs font-semibold uppercase tracking-wide text-slate-500">Précisions du client</p>
684
+ <p className="whitespace-pre-wrap rounded-lg bg-slate-50 p-3 text-sm text-slate-700">{d.message}</p>
685
+ </div>
686
+ ) : null}
687
+ </div>
688
+ );
689
+ }
690
+
691
+ function Demandes() {
692
+ const { data, error, reload } = useLoad(() => sa<{ items: LicenseOrder[] }>('/licenses'));
693
+ const [tab, setTab] = useState<'all' | 'paid' | 'pending' | 'failed'>('all');
694
+ const [q, setQ] = useState('');
695
+ const [open, setOpen] = useState<string | null>(null);
696
+ const [busy, setBusy] = useState<string | null>(null);
697
+ const [flash, setFlash] = useState('');
698
+ const items = data?.items || [];
699
+ const filtered = useMemo(() => {
700
+ const nq = q.trim().toLowerCase();
701
+ return items.filter(
702
+ (o) =>
703
+ (tab === 'all' || o.status === tab) &&
704
+ (!nq || [o.name, o.email, o.domain, o.details?.company, o.details?.providerName, o.details?.phone].some((x) => x?.toLowerCase().includes(nq)))
705
+ );
706
+ }, [items, tab, q]);
707
+ const count = (s: string) => items.filter((o) => o.status === s).length;
708
+ const setStatus = async (o: LicenseOrder, status: 'paid' | 'pending' | 'failed') => {
709
+ setBusy(o.id);
710
+ setFlash('');
711
+ try {
712
+ await sa(`/licenses/${o.id}`, { method: 'PATCH', body: JSON.stringify({ status }) });
713
+ setFlash(`${o.name} → ${status}`);
714
+ reload();
715
+ } catch (e) {
716
+ setFlash(e instanceof Error ? e.message : 'Erreur');
717
+ } finally {
718
+ setBusy(null);
719
+ }
720
+ };
721
+
722
+ return (
723
+ <div>
724
+ <PageHeader
725
+ title="Demandes de licence"
726
+ description="Commandes d’installation Snapify (699 DT / an) passées depuis snapify.tn/commander."
727
+ actions={
728
+ <Button icon={RefreshCw} onClick={reload}>
729
+ Actualiser
730
+ </Button>
731
+ }
732
+ />
733
+ {error ? <Alert tone="danger">{error}</Alert> : null}
734
+ {flash ? (
735
+ <div className="mb-4">
736
+ <Alert tone={flash.includes('Erreur') ? 'danger' : 'success'}>{flash}</Alert>
737
+ </div>
738
+ ) : null}
739
+ <Tabs
740
+ value={tab}
741
+ onChange={setTab}
742
+ tabs={[
743
+ { value: 'all', label: 'Toutes', badge: <Badge>{items.length}</Badge> },
744
+ { value: 'paid', label: 'Payées', badge: <Badge tone="success">{count('paid')}</Badge> },
745
+ { value: 'pending', label: 'En attente', badge: <Badge tone="warning">{count('pending')}</Badge> },
746
+ { value: 'failed', label: 'Échouées', badge: <Badge tone="danger">{count('failed')}</Badge> },
747
+ ]}
748
+ />
749
+ <Card padded={false}>
750
+ <div className="border-b border-slate-100 p-3">
751
+ <div className="relative max-w-sm">
752
+ <Search className="pointer-events-none absolute left-3 top-2.5 h-4 w-4 text-slate-400" />
753
+ <Input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Nom, e-mail, domaine, hébergeur…" className="pl-9" />
754
+ </div>
755
+ </div>
756
+ {!data ? (
757
+ <div className="p-5">
758
+ <PageSkeleton />
759
+ </div>
760
+ ) : filtered.length ? (
761
+ <Table>
762
+ <thead>
763
+ <tr>
764
+ <Th>Client</Th>
765
+ <Th>Domaine</Th>
766
+ <Th className="hidden md:table-cell">Hébergeur</Th>
767
+ <Th>Statut</Th>
768
+ <Th className="hidden sm:table-cell">Date</Th>
769
+ <Th />
770
+ </tr>
771
+ </thead>
772
+ <tbody>
773
+ {filtered.map((o) => (
774
+ <React.Fragment key={o.id}>
775
+ <tr className={cx('cursor-pointer hover:bg-slate-50/60', open === o.id && 'bg-slate-50/60')} onClick={() => setOpen(open === o.id ? null : o.id)}>
776
+ <Td>
777
+ <p className="font-medium text-slate-900">{o.name}</p>
778
+ <p className="text-xs text-slate-500">{o.details?.company || o.email}</p>
779
+ </Td>
780
+ <Td className="font-mono text-xs">{o.domain}</Td>
781
+ <Td className="hidden md:table-cell">
782
+ {o.details ? (
783
+ <>
784
+ <p className="text-slate-800">{o.details.providerName}</p>
785
+ <p className="text-xs text-slate-500">{HOSTING_TYPE[o.details.hostingType] || o.details.hostingType}</p>
786
+ </>
787
+ ) : (
788
+ <span className="text-slate-400">—</span>
789
+ )}
790
+ </Td>
791
+ <Td>
792
+ <StatusBadge status={o.status} />
793
+ </Td>
794
+ <Td className="hidden whitespace-nowrap text-slate-500 sm:table-cell">{when(o.createdAt)}</Td>
795
+ <Td className="text-right">
796
+ <ChevronDown className={cx('ml-auto h-4 w-4 text-slate-400 transition-transform', open === o.id && 'rotate-180')} />
797
+ </Td>
798
+ </tr>
799
+ {open === o.id ? (
800
+ <tr>
801
+ <td colSpan={6} className="border-b border-slate-100 bg-slate-50/40 px-5 py-5">
802
+ <OrderDetails o={o} />
803
+ <div className="mt-4 flex flex-wrap gap-2 border-t border-slate-200 pt-4">
804
+ {o.status !== 'paid' ? (
805
+ <Button size="sm" variant="secondary" loading={busy === o.id} onClick={() => void setStatus(o, 'paid')}>
806
+ Marquer payée (virement)
807
+ </Button>
808
+ ) : null}
809
+ {o.status === 'paid' ? (
810
+ <Button size="sm" loading={busy === o.id} onClick={() => void setStatus(o, 'pending')}>
811
+ Remettre en attente
812
+ </Button>
813
+ ) : null}
814
+ {o.status !== 'failed' ? (
815
+ <Button size="sm" variant="danger" disabled={busy === o.id} onClick={() => void setStatus(o, 'failed')}>
816
+ Marquer échouée
817
+ </Button>
818
+ ) : null}
819
+ {o.status === 'pending' ? (
820
+ <Button
821
+ size="sm"
822
+ icon={Send}
823
+ loading={busy === o.id}
824
+ onClick={async () => {
825
+ setBusy(o.id);
826
+ setFlash('');
827
+ try {
828
+ const r = await sa<{ mailed: boolean; reason?: string }>(`/licenses/${o.id}/relaunch`, { method: 'POST', body: '{}' });
829
+ setFlash(r.mailed ? `Relance envoyée à ${o.email}` : `Relance non envoyée : ${r.reason || 'SMTP'}`);
830
+ } catch (e) {
831
+ setFlash(e instanceof Error ? e.message : 'Erreur');
832
+ } finally {
833
+ setBusy(null);
834
+ }
835
+ }}
836
+ >
837
+ Relancer par e-mail
838
+ </Button>
839
+ ) : null}
840
+ <Button
841
+ size="sm"
842
+ icon={FileText}
843
+ loading={busy === o.id}
844
+ onClick={async () => {
845
+ setBusy(o.id);
846
+ setFlash('');
847
+ try {
848
+ const r = await sa<{ invoice: { number: string } }>(`/licenses/${o.id}/invoice`, { method: 'POST', body: '{}' });
849
+ setFlash(`Facture ${r.invoice.number} créée.`);
850
+ } catch (e) {
851
+ setFlash(e instanceof Error ? e.message : 'Erreur');
852
+ } finally {
853
+ setBusy(null);
854
+ }
855
+ }}
856
+ >
857
+ Générer facture
858
+ </Button>
859
+ <Button
860
+ size="sm"
861
+ onClick={async () => {
862
+ const name = window.prompt('Nom client', o.name);
863
+ if (name === null) return;
864
+ const email = window.prompt('E-mail', o.email);
865
+ if (email === null) return;
866
+ const domain = window.prompt('Domaine', o.domain);
867
+ if (domain === null) return;
868
+ setBusy(o.id);
869
+ setFlash('');
870
+ try {
871
+ await sa(`/licenses/${o.id}`, { method: 'PATCH', body: JSON.stringify({ name, email, domain }) });
872
+ setFlash('Demande mise à jour.');
873
+ reload();
874
+ } catch (e) {
875
+ setFlash(e instanceof Error ? e.message : 'Erreur');
876
+ } finally {
877
+ setBusy(null);
878
+ }
879
+ }}
880
+ >
881
+ Éditer
882
+ </Button>
883
+ <Button
884
+ size="sm"
885
+ variant="danger"
886
+ icon={Trash2}
887
+ disabled={busy === o.id}
888
+ onClick={async () => {
889
+ if (!window.confirm(`Supprimer la demande de ${o.name} ?`)) return;
890
+ setBusy(o.id);
891
+ setFlash('');
892
+ try {
893
+ await sa(`/licenses/${o.id}`, { method: 'DELETE' });
894
+ setFlash('Demande supprimée.');
895
+ setOpen(null);
896
+ reload();
897
+ } catch (e) {
898
+ setFlash(e instanceof Error ? e.message : 'Erreur');
899
+ } finally {
900
+ setBusy(null);
901
+ }
902
+ }}
903
+ >
904
+ Supprimer
905
+ </Button>
906
+ </div>
907
+ </td>
908
+ </tr>
909
+ ) : null}
910
+ </React.Fragment>
911
+ ))}
912
+ </tbody>
913
+ </Table>
914
+ ) : (
915
+ <EmptyState icon={Inbox} title="Aucune demande" description={q || tab !== 'all' ? 'Aucun résultat pour ce filtre.' : 'Les commandes passées depuis la landing apparaîtront ici.'} />
916
+ )}
917
+ </Card>
918
+ </div>
919
+ );
920
+ }
921
+
922
+ type Product = {
923
+ id: string;
924
+ name: string;
925
+ slug: string;
926
+ kind: string;
927
+ status: string;
928
+ priceLabel: string;
929
+ latestVersion: { version: string; signed: boolean; sha256: string } | null;
930
+ unsignedVersions: string[];
931
+ };
932
+
933
+ function Catalogue() {
934
+ const [filter, setFilter] = useState('all');
935
+ const { data, error, reload } = useLoad(() => sa<{ items: Product[] }>(`/products?status=${filter}`), [filter]);
936
+ const [msg, setMsg] = useState('');
937
+ const [failure, setFailure] = useState('');
938
+ const [busy, setBusy] = useState<string | null>(null);
939
+ const [syncing, setSyncing] = useState(false);
940
+ const review = async (p: Product, action: 'approve' | 'reject' | 'unpublish') => {
941
+ setBusy(p.id);
942
+ setMsg('');
943
+ setFailure('');
944
+ try {
945
+ const r = await sa<{ signed?: string[] }>(`/products/${p.id}/review`, {
946
+ method: 'POST',
947
+ body: JSON.stringify(action === 'approve' ? { action } : action === 'reject' ? { action, reason: 'Non conforme' } : { action }),
948
+ });
949
+ setMsg(
950
+ action === 'reject'
951
+ ? `${p.name} rejeté.`
952
+ : action === 'unpublish'
953
+ ? `${p.name} dépublié (brouillon).`
954
+ : r.signed?.length
955
+ ? `${p.name} publié : version${r.signed.length > 1 ? 's' : ''} ${r.signed.join(', ')} signée${r.signed.length > 1 ? 's' : ''} par NOVALABS.`
956
+ : `${p.name} publié.`
957
+ );
958
+ reload();
959
+ } catch (e) {
960
+ setFailure(`${p.name} : ${e instanceof Error ? e.message : 'action impossible'}`);
961
+ } finally {
962
+ setBusy(null);
963
+ }
964
+ };
965
+ return (
966
+ <div>
967
+ <PageHeader
968
+ title="Modules & thèmes"
969
+ description="Validation des soumissions, signatures et synchronisation du catalogue officiel NOVALABS."
970
+ actions={
971
+ <Button
972
+ variant="secondary"
973
+ icon={RefreshCw}
974
+ loading={syncing}
975
+ onClick={async () => {
976
+ setSyncing(true);
977
+ try {
978
+ const r = await sa<{ synced: number }>('/sync-official', { method: 'POST', body: '{}' });
979
+ setMsg(`Catalogue officiel synchronisé : ${r.synced} produits.`);
980
+ reload();
981
+ } finally {
982
+ setSyncing(false);
983
+ }
984
+ }}
985
+ >
986
+ Synchroniser le catalogue
987
+ </Button>
988
+ }
989
+ />
990
+ {msg ? (
991
+ <div className="mb-4">
992
+ <Alert tone="success">{msg}</Alert>
993
+ </div>
994
+ ) : null}
995
+ {error ? <Alert tone="danger">{error}</Alert> : null}
996
+ {failure ? (
997
+ <div className="mb-4">
998
+ <Alert tone="danger">{failure}</Alert>
999
+ </div>
1000
+ ) : null}
1001
+ <Tabs
1002
+ value={filter}
1003
+ onChange={setFilter}
1004
+ tabs={[
1005
+ { value: 'all', label: 'Tous' },
1006
+ { value: 'pending', label: 'En validation' },
1007
+ { value: 'unsigned', label: 'À signer' },
1008
+ { value: 'published', label: 'Publiés' },
1009
+ { value: 'draft', label: 'Brouillons' },
1010
+ { value: 'rejected', label: 'Rejetés' },
1011
+ ]}
1012
+ />
1013
+ <Card padded={false}>
1014
+ {!data ? (
1015
+ <div className="p-5">
1016
+ <PageSkeleton />
1017
+ </div>
1018
+ ) : data.items.length ? (
1019
+ <Table>
1020
+ <thead>
1021
+ <tr>
1022
+ <Th>Produit</Th>
1023
+ <Th>Type</Th>
1024
+ <Th>Prix</Th>
1025
+ <Th className="hidden md:table-cell">Version</Th>
1026
+ <Th className="hidden md:table-cell">Signature</Th>
1027
+ <Th>Statut</Th>
1028
+ <Th />
1029
+ </tr>
1030
+ </thead>
1031
+ <tbody>
1032
+ {data.items.map((p) => (
1033
+ <tr key={p.id} className="hover:bg-slate-50/60">
1034
+ <Td>
1035
+ <p className="font-medium text-slate-900">{p.name}</p>
1036
+ <p className="font-mono text-[11px] text-slate-500">{p.slug}</p>
1037
+ </Td>
1038
+ <Td>
1039
+ <Badge tone={p.kind === 'theme' ? 'info' : 'brand'}>{p.kind === 'theme' ? 'Thème' : 'Module'}</Badge>
1040
+ </Td>
1041
+ <Td className="whitespace-nowrap">{p.priceLabel}</Td>
1042
+ <Td className="hidden md:table-cell">{p.latestVersion ? `v${p.latestVersion.version}` : '—'}</Td>
1043
+ <Td className="hidden md:table-cell">
1044
+ {p.unsignedVersions.length ? (
1045
+ <Badge tone="warning">À signer : v{p.unsignedVersions.join(', v')}</Badge>
1046
+ ) : p.latestVersion?.signed ? (
1047
+ <Badge tone="success">
1048
+ <ShieldCheck className="h-3 w-3" /> Signé
1049
+ </Badge>
1050
+ ) : (
1051
+ <Badge tone="neutral">—</Badge>
1052
+ )}
1053
+ </Td>
1054
+ <Td>
1055
+ <StatusBadge status={p.status} />
1056
+ </Td>
1057
+ <Td className="text-right">
1058
+ <div className="flex flex-wrap justify-end gap-1.5">
1059
+ {p.status === 'pending' ? (
1060
+ <>
1061
+ <Button size="sm" variant="secondary" loading={busy === p.id} onClick={() => void review(p, 'approve')}>
1062
+ Approuver et signer
1063
+ </Button>
1064
+ <Button size="sm" variant="danger" disabled={busy === p.id} onClick={() => void review(p, 'reject')}>
1065
+ Rejeter
1066
+ </Button>
1067
+ </>
1068
+ ) : null}
1069
+ {p.status === 'published' && p.unsignedVersions.length ? (
1070
+ <Button size="sm" variant="secondary" icon={ShieldCheck} loading={busy === p.id} onClick={() => void review(p, 'approve')}>
1071
+ Signer v{p.unsignedVersions.join(', v')}
1072
+ </Button>
1073
+ ) : null}
1074
+ {p.status === 'published' ? (
1075
+ <Button
1076
+ size="sm"
1077
+ loading={busy === p.id}
1078
+ onClick={() => void review(p, 'unpublish')}
1079
+ >
1080
+ Dépublier
1081
+ </Button>
1082
+ ) : null}
1083
+ {p.status !== 'published' ? (
1084
+ <Button
1085
+ size="sm"
1086
+ variant="danger"
1087
+ icon={Trash2}
1088
+ disabled={busy === p.id}
1089
+ onClick={async () => {
1090
+ if (!window.confirm(`Supprimer définitivement « ${p.name} » ?`)) return;
1091
+ setBusy(p.id);
1092
+ setMsg('');
1093
+ setFailure('');
1094
+ try {
1095
+ await sa(`/products/${p.id}`, { method: 'DELETE' });
1096
+ setMsg(`${p.name} supprimé.`);
1097
+ reload();
1098
+ } catch (e) {
1099
+ setFailure(e instanceof Error ? e.message : 'Suppression impossible');
1100
+ } finally {
1101
+ setBusy(null);
1102
+ }
1103
+ }}
1104
+ >
1105
+ Supprimer
1106
+ </Button>
1107
+ ) : null}
1108
+ </div>
1109
+ </Td>
1110
+ </tr>
1111
+ ))}
1112
+ </tbody>
1113
+ </Table>
1114
+ ) : (
1115
+ <EmptyState icon={Package} title="Aucun produit" description="Aucun module ni thème pour ce filtre." />
1116
+ )}
1117
+ </Card>
1118
+ </div>
1119
+ );
1120
+ }
1121
+
1122
+ /* ------------------------------------------------------------------ Auth */
1123
+
1124
+ function Login({ onOk }: { onOk: () => void }) {
1125
+ const [username, setUsername] = useState('');
1126
+ const [password, setPassword] = useState('');
1127
+ const [err, setErr] = useState('');
1128
+ const [loading, setLoading] = useState(false);
1129
+ return (
1130
+ <div className="grid min-h-screen bg-white lg:grid-cols-[1.1fr_1fr]">
1131
+ <div className="relative hidden overflow-hidden bg-navy-950 p-12 text-white lg:flex lg:flex-col">
1132
+ <div className="absolute -right-32 -top-32 h-[480px] w-[480px] rounded-full bg-brand-600/20 blur-3xl" />
1133
+ <div className="absolute -bottom-40 -left-20 h-[420px] w-[420px] rounded-full bg-sky-500/10 blur-3xl" />
1134
+ <div className="relative flex items-center gap-3">
1135
+ <div className="flex h-9 w-9 items-center justify-center rounded-lg bg-brand-600">
1136
+ <SnapifyMark className="h-7 w-7" accent="#FFFFFF" />
1137
+ </div>
1138
+ <span className="text-lg font-semibold tracking-tight">Snapify</span>
1139
+ </div>
1140
+ <div className="relative mt-auto max-w-md">
1141
+ <h1 className="text-3xl font-semibold leading-tight tracking-tight">Pilotez toute la plateforme Snapify depuis un seul espace.</h1>
1142
+ <ul className="mt-10 space-y-5 text-sm text-slate-300">
1143
+ {[
1144
+ { icon: Inbox, t: 'Licences, commandes marketplace et payouts' },
1145
+ { icon: Users, t: 'Utilisateurs, éditeurs et rôles plateforme' },
1146
+ { icon: Package, t: 'Validation, signature et catalogue' },
1147
+ { icon: Server, t: 'Santé système, Flouci et SEO landing' },
1148
+ ].map(({ icon: Icon, t }) => (
1149
+ <li key={t} className="flex items-center gap-3">
1150
+ <span className="flex h-9 w-9 items-center justify-center rounded-lg bg-white/5 ring-1 ring-white/10">
1151
+ <Icon className="h-4 w-4 text-brand-400" />
1152
+ </span>
1153
+ {t}
1154
+ </li>
1155
+ ))}
1156
+ </ul>
1157
+ </div>
1158
+ <p className="relative mt-16 text-xs text-slate-500">© {new Date().getFullYear()} Snapify — Superadmin plateforme</p>
1159
+ </div>
1160
+ <div className="flex items-center justify-center px-6 py-12">
1161
+ <div className="w-full max-w-sm">
1162
+ <div className="mb-8 lg:hidden">
1163
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-brand-600">
1164
+ <SnapifyMark className="h-8 w-8" accent="#FFFFFF" />
1165
+ </div>
1166
+ </div>
1167
+ <form
1168
+ onSubmit={async (e) => {
1169
+ e.preventDefault();
1170
+ setErr('');
1171
+ setLoading(true);
1172
+ try {
1173
+ await sa('/login', { method: 'POST', body: JSON.stringify({ username, password }) });
1174
+ onOk();
1175
+ } catch (ex) {
1176
+ setErr(ex instanceof Error ? ex.message : 'Erreur');
1177
+ } finally {
1178
+ setLoading(false);
1179
+ }
1180
+ }}
1181
+ >
1182
+ <h2 className="text-2xl font-semibold tracking-tight text-slate-900">Superadmin</h2>
1183
+ <p className="mt-1.5 text-sm text-slate-500">Accès réservé à l’administration de la plateforme.</p>
1184
+ <div className="mt-8 space-y-5">
1185
+ {err ? <Alert tone="danger">{err}</Alert> : null}
1186
+ <Field label="Identifiant" htmlFor="sa-user">
1187
+ <div className="relative">
1188
+ <User className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-slate-400" />
1189
+ <Input id="sa-user" autoFocus value={username} onChange={(e) => setUsername(e.target.value)} autoComplete="username" className="h-10 pl-9" />
1190
+ </div>
1191
+ </Field>
1192
+ <Field label="Mot de passe" htmlFor="sa-pass">
1193
+ <div className="relative">
1194
+ <Lock className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-slate-400" />
1195
+ <Input id="sa-pass" type="password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" className="h-10 pl-9" />
1196
+ </div>
1197
+ </Field>
1198
+ <Button type="submit" variant="primary" size="lg" loading={loading} className="w-full justify-center">
1199
+ Se connecter
1200
+ </Button>
1201
+ </div>
1202
+ <p className="mt-8 text-center text-xs text-slate-400">Accès réservé aux personnes autorisées. Les connexions sont journalisées.</p>
1203
+ </form>
1204
+ </div>
1205
+ </div>
1206
+ </div>
1207
+ );
1208
+ }
1209
+
1210
+ export function App() {
1211
+ const [auth, setAuth] = useState<boolean | null>(null);
1212
+ const nav = useNavigate();
1213
+ useEffect(() => {
1214
+ void sa<{ ok: boolean }>('/me')
1215
+ .then((d) => setAuth(d.ok))
1216
+ .catch(() => setAuth(false));
1217
+ }, []);
1218
+
1219
+ if (auth === null)
1220
+ return (
1221
+ <div className="flex min-h-screen items-center justify-center bg-slate-50">
1222
+ <RefreshCw className="h-5 w-5 animate-spin text-slate-400" />
1223
+ </div>
1224
+ );
1225
+ if (!auth)
1226
+ return (
1227
+ <Login
1228
+ onOk={() => {
1229
+ setAuth(true);
1230
+ nav('/');
1231
+ }}
1232
+ />
1233
+ );
1234
+
1235
+ return (
1236
+ <Shell
1237
+ onLogout={async () => {
1238
+ await sa('/logout', { method: 'POST', body: '{}' });
1239
+ setAuth(false);
1240
+ }}
1241
+ >
1242
+ <Routes>
1243
+ <Route index element={<Overview />} />
1244
+ <Route path="finances" element={<Finances />} />
1245
+ <Route path="visites" element={<Visites />} />
1246
+ <Route path="demandes" element={<Demandes />} />
1247
+ <Route path="clients" element={<ClientsPage />} />
1248
+ <Route path="commandes" element={<OrdersPage />} />
1249
+ <Route path="factures" element={<InvoicesPage />} />
1250
+ <Route path="paiements" element={<PaymentsPage />} />
1251
+ <Route path="payouts" element={<PayoutsPage />} />
1252
+ <Route path="catalogue" element={<Catalogue />} />
1253
+ <Route path="editeurs" element={<PublishersPage />} />
1254
+ <Route path="tickets" element={<TicketsPage />} />
1255
+ <Route path="newsletter" element={<NewsletterPage />} />
1256
+ <Route path="parametrage" element={<SettingsPage />} />
1257
+ <Route path="utilisateurs" element={<UsersPage />} />
1258
+ <Route path="systeme" element={<SystemPage />} />
1259
+ <Route path="seo" element={<SeoControlPage />} />
1260
+ <Route path="*" element={<Navigate to="/" replace />} />
1261
+ </Routes>
1262
+ </Shell>
1263
+ );
1264
+ }