amliq-dashboard 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +3 -0
- package/.env.production +1 -0
- package/Dockerfile +26 -0
- package/QUICK_START.md +267 -0
- package/README.md +161 -0
- package/TESTING_GUIDE.md +322 -0
- package/dist/.well-known/ai-plugin.json +15 -0
- package/dist/_headers +8 -0
- package/dist/_redirects +1 -0
- package/dist/assets/APIKeys-BvwFauIs.js +6 -0
- package/dist/assets/APIKeys-DaMSBZQL.js +1 -0
- package/dist/assets/AdverseMedia-DckXfMzS.js +4 -0
- package/dist/assets/AlertDetailPage-Bc2_zwu_.js +1 -0
- package/dist/assets/AlertQueue-DFtBiRoM.js +8 -0
- package/dist/assets/Analytics-DtruB5lQ.js +1 -0
- package/dist/assets/AuditTrail-BK68ePu0.js +1 -0
- package/dist/assets/AuthDivider-gRHEt7gx.js +1 -0
- package/dist/assets/Badge-B9VFkofy.js +1 -0
- package/dist/assets/BatchJobs-Bz6KFnXD.js +6 -0
- package/dist/assets/BillingPage-R9nQ5nFb.js +11 -0
- package/dist/assets/Card-Cw-T_-oU.js +1 -0
- package/dist/assets/CaseDetail-DYs7Zg_y.js +7 -0
- package/dist/assets/CaseManagement-B1Q4Dp1Y.js +1 -0
- package/dist/assets/ComplianceReport-Dhssqc9T.js +1 -0
- package/dist/assets/Configuration-B92kl9T0.js +1 -0
- package/dist/assets/CryptoScreening-C7yFlskC.js +1 -0
- package/dist/assets/Dashboard-w4-Nhv9q.js +17 -0
- package/dist/assets/DataSources-u3-Ri_5_.js +3 -0
- package/dist/assets/EDDWorkflow-CFzlEO0e.js +1 -0
- package/dist/assets/EmptyState-Do0xJAT9.js +6 -0
- package/dist/assets/ForgotPassword-BPYyQFQp.js +1 -0
- package/dist/assets/LandingPage-CjaP0zw4.js +41 -0
- package/dist/assets/ListsMarketplace-CG6KkT9B.js +6 -0
- package/dist/assets/Login-677LEGP1.js +1 -0
- package/dist/assets/MFASetup-BxqCPvui.js +1 -0
- package/dist/assets/Monitoring-9TN9MK4y.js +1 -0
- package/dist/assets/Onboarding-Cf0Y83lX.js +1 -0
- package/dist/assets/Operations-CcXwc2xw.js +10 -0
- package/dist/assets/Overview-B_Ky0VWV.js +1 -0
- package/dist/assets/PEPScreening-pUQjTH9c.js +1 -0
- package/dist/assets/PageHeader-BObHFn0i.js +1 -0
- package/dist/assets/PrivacyPolicy-DZ2xrKir.js +1 -0
- package/dist/assets/ResetPassword-AtxtpIG1.js +1 -0
- package/dist/assets/RiskAssessment-Dzuh1Usv.js +1 -0
- package/dist/assets/SARForm-D8w2ZGe-.js +1 -0
- package/dist/assets/SanctionsLists-CPA3UH2v.js +1 -0
- package/dist/assets/ScheduledTasks-Dsjk-6UR.js +1 -0
- package/dist/assets/ScreenEntity-D1U0YL7v.js +3 -0
- package/dist/assets/ScreeningProgress-BW49PzoB.js +66 -0
- package/dist/assets/SearchField-CulFKdiI.js +1 -0
- package/dist/assets/Signup-C0FmFOo_.js +1 -0
- package/dist/assets/SystemHealth-B_8u4eva.js +1 -0
- package/dist/assets/TaskHistory-BCmEM89q.js +3 -0
- package/dist/assets/Team-CUo_FlU7.js +1 -0
- package/dist/assets/TenantDetail-CgZQHUY8.js +1 -0
- package/dist/assets/Tenants-RB9E9ick.js +2 -0
- package/dist/assets/TermsOfService-BfL-kndX.js +1 -0
- package/dist/assets/TransactionMonitoring-DGh_T22s.js +14 -0
- package/dist/assets/TxnScreening-B7cm2-eh.js +1 -0
- package/dist/assets/UBOChain-falXAsCo.js +1 -0
- package/dist/assets/Users-Doakpg9c.js +1 -0
- package/dist/assets/Webhooks-BZpAfaxv.js +1 -0
- package/dist/assets/alert-triangle-YCFVm7B_.js +6 -0
- package/dist/assets/alerts-COUnTwWY.js +1 -0
- package/dist/assets/arrow-left-9ApLqP95.js +6 -0
- package/dist/assets/check-VTcvVtIU.js +6 -0
- package/dist/assets/check-circle-2-Dn-lG5YQ.js +6 -0
- package/dist/assets/code-DIJRWFVv.js +6 -0
- package/dist/assets/fingerprint-C0MGVtax.js +6 -0
- package/dist/assets/flag-B8_Gwxh6.js +6 -0
- package/dist/assets/index-CfdYcqRM.js +289 -0
- package/dist/assets/index-HPU_Rhdu.css +1 -0
- package/dist/assets/layers-zHz2o4vo.js +6 -0
- package/dist/assets/loader-B9_dNihg.js +6 -0
- package/dist/assets/mail-BijL2qwp.js +6 -0
- package/dist/assets/plus-i0oBIIPS.js +6 -0
- package/dist/assets/refresh-cw-CSuAwutt.js +6 -0
- package/dist/assets/useAnalytics-Bj8IONcw.js +72 -0
- package/dist/assets/x-circle-DLGKvijE.js +6 -0
- package/dist/favicon.svg +15 -0
- package/dist/index.html +51 -0
- package/dist/llms.txt +28 -0
- package/dist/logo.png +0 -0
- package/dist/logo.svg +17 -0
- package/dist/manifest.json +44 -0
- package/dist/robots.txt +15 -0
- package/dist/schema.json +13 -0
- package/dist/sitemap.xml +28 -0
- package/dist/sw.js +67 -0
- package/e2e/auth-setup.ts +20 -0
- package/e2e/billing.spec.ts +50 -0
- package/e2e/cases.spec.ts +53 -0
- package/e2e/compliance.spec.ts +65 -0
- package/e2e/config.spec.ts +56 -0
- package/e2e/dashboard.spec.ts +47 -0
- package/e2e/fixtures.ts +33 -0
- package/e2e/lists.spec.ts +43 -0
- package/e2e/login.spec.ts +64 -0
- package/e2e/media.spec.ts +48 -0
- package/e2e/mocks.ts +51 -0
- package/e2e/monitoring.spec.ts +44 -0
- package/e2e/navigation.spec.ts +56 -0
- package/e2e/onboarding.spec.ts +49 -0
- package/e2e/responsive.spec.ts +40 -0
- package/e2e/risk.spec.ts +61 -0
- package/e2e/screening.spec.ts +76 -0
- package/e2e/team.spec.ts +53 -0
- package/index.html +50 -0
- package/package.json +47 -0
- package/playwright.config.ts +35 -0
- package/postcss.config.js +6 -0
- package/public/.well-known/ai-plugin.json +15 -0
- package/public/_headers +8 -0
- package/public/_redirects +1 -0
- package/public/favicon.svg +15 -0
- package/public/llms.txt +28 -0
- package/public/logo.png +0 -0
- package/public/logo.svg +17 -0
- package/public/manifest.json +44 -0
- package/public/robots.txt +15 -0
- package/public/schema.json +13 -0
- package/public/sitemap.xml +28 -0
- package/public/sw.js +67 -0
- package/scripts/test-runner.sh +152 -0
- package/src/App.tsx +48 -0
- package/src/api/alerts.ts +19 -0
- package/src/api/analytics.ts +6 -0
- package/src/api/audit.ts +11 -0
- package/src/api/auth.ts +26 -0
- package/src/api/billing.ts +54 -0
- package/src/api/cases.ts +48 -0
- package/src/api/client.ts +50 -0
- package/src/api/config.ts +30 -0
- package/src/api/edd.ts +25 -0
- package/src/api/enforcement.ts +33 -0
- package/src/api/lists.ts +24 -0
- package/src/api/monitoring.ts +82 -0
- package/src/api/pep.ts +30 -0
- package/src/api/risk.ts +26 -0
- package/src/api/screening.ts +37 -0
- package/src/api/team.ts +27 -0
- package/src/api/transactions.ts +37 -0
- package/src/components/admin/TenantCards.tsx +51 -0
- package/src/components/alerts/AlertActions.test.tsx +80 -0
- package/src/components/alerts/AlertActions.tsx +68 -0
- package/src/components/alerts/AlertCard.test.tsx +86 -0
- package/src/components/alerts/AlertCard.tsx +60 -0
- package/src/components/alerts/AlertDetailSidebar.tsx +63 -0
- package/src/components/alerts/AlertFilters.test.tsx +73 -0
- package/src/components/alerts/AlertFilters.tsx +88 -0
- package/src/components/alerts/EntityDetailsCard.test.tsx +61 -0
- package/src/components/alerts/EntityDetailsCard.tsx +37 -0
- package/src/components/alerts/NotesCard.test.tsx +39 -0
- package/src/components/alerts/NotesCard.tsx +28 -0
- package/src/components/auth/AuthDivider.tsx +18 -0
- package/src/components/auth/LoginForm.tsx +62 -0
- package/src/components/auth/LoginLeftPanel.tsx +43 -0
- package/src/components/auth/PasswordStrength.tsx +26 -0
- package/src/components/auth/SignInButtons.tsx +46 -0
- package/src/components/auth/SignupLeftPanel.tsx +35 -0
- package/src/components/auth/countries.ts +17 -0
- package/src/components/charts/AreaChart.tsx +45 -0
- package/src/components/charts/BarChart.tsx +48 -0
- package/src/components/charts/DonutChart.tsx +47 -0
- package/src/components/compliance/CaseActions.tsx +74 -0
- package/src/components/compliance/CaseTimeline.tsx +68 -0
- package/src/components/compliance/MediaResultCard.tsx +87 -0
- package/src/components/compliance/PEPResultCard.tsx +78 -0
- package/src/components/config/MatchingModesCard.test.tsx +75 -0
- package/src/components/config/MatchingModesCard.tsx +40 -0
- package/src/components/config/ScreeningLayersCard.test.tsx +57 -0
- package/src/components/config/ScreeningLayersCard.tsx +36 -0
- package/src/components/config/ThresholdsCard.test.tsx +79 -0
- package/src/components/config/ThresholdsCard.tsx +51 -0
- package/src/components/dashboard/ActivityFeed.tsx +93 -0
- package/src/components/dashboard/DashboardGreeting.tsx +23 -0
- package/src/components/dashboard/DashboardSkeleton.tsx +35 -0
- package/src/components/dashboard/QuickActions.tsx +52 -0
- package/src/components/dashboard/TopEntitiesTable.tsx +63 -0
- package/src/components/data/ComplianceMetrics.test.tsx +32 -0
- package/src/components/data/ComplianceMetrics.tsx +40 -0
- package/src/components/data/ConfidenceScore.test.tsx +62 -0
- package/src/components/data/ConfidenceScore.tsx +27 -0
- package/src/components/data/StatCard.test.tsx +72 -0
- package/src/components/data/StatCard.tsx +63 -0
- package/src/components/data/StatusBadge.test.tsx +63 -0
- package/src/components/data/StatusBadge.tsx +39 -0
- package/src/components/layout/AppShell.tsx +48 -0
- package/src/components/layout/Breadcrumbs.tsx +48 -0
- package/src/components/layout/CommandPalette.tsx +81 -0
- package/src/components/layout/DashboardLayout.tsx +19 -0
- package/src/components/layout/MobileHeader.tsx +30 -0
- package/src/components/layout/NavGroup.test.tsx +63 -0
- package/src/components/layout/NavGroup.tsx +64 -0
- package/src/components/layout/NotificationBell.tsx +89 -0
- package/src/components/layout/PageHeader.test.tsx +61 -0
- package/src/components/layout/PageHeader.tsx +19 -0
- package/src/components/layout/ProtectedRoute.tsx +31 -0
- package/src/components/layout/PublicLayout.tsx +17 -0
- package/src/components/layout/Sidebar.test.tsx +67 -0
- package/src/components/layout/Sidebar.tsx +92 -0
- package/src/components/layout/Toolbar.test.tsx +47 -0
- package/src/components/layout/Toolbar.tsx +68 -0
- package/src/components/layout/navItems.ts +94 -0
- package/src/components/lists/ListCard.tsx +78 -0
- package/src/components/lists/ListMarketplaceCard.tsx +77 -0
- package/src/components/screening/CircularConfidence.tsx +38 -0
- package/src/components/screening/LimitReachedBanner.tsx +31 -0
- package/src/components/screening/ListSelector.tsx +64 -0
- package/src/components/screening/MatchDetailHeader.tsx +64 -0
- package/src/components/screening/MatchEntityInfo.tsx +56 -0
- package/src/components/screening/MatchEvidenceBars.tsx +65 -0
- package/src/components/screening/MatchMetadata.tsx +95 -0
- package/src/components/screening/ScreenResults.tsx +49 -0
- package/src/components/screening/ScreeningForm.test.tsx +83 -0
- package/src/components/screening/ScreeningForm.tsx +100 -0
- package/src/components/screening/ScreeningLayersList.test.tsx +33 -0
- package/src/components/screening/ScreeningLayersList.tsx +46 -0
- package/src/components/screening/ScreeningProgress.tsx +91 -0
- package/src/components/screening/ScreeningQuotaBanner.tsx +64 -0
- package/src/components/screening/ScreeningResultCard.tsx +47 -0
- package/src/components/screening/ScreeningResultRow.tsx +45 -0
- package/src/components/screening/ScreeningResults.test.tsx +37 -0
- package/src/components/screening/ScreeningResults.tsx +33 -0
- package/src/components/screening/ShareResults.tsx +103 -0
- package/src/components/screening/ThresholdSlider.tsx +23 -0
- package/src/components/transactions/WebhookCTA.tsx +63 -0
- package/src/components/ui/Avatar.test.tsx +47 -0
- package/src/components/ui/Avatar.tsx +35 -0
- package/src/components/ui/Badge.test.tsx +49 -0
- package/src/components/ui/Badge.tsx +33 -0
- package/src/components/ui/Button.test.tsx +56 -0
- package/src/components/ui/Button.tsx +46 -0
- package/src/components/ui/Card.test.tsx +61 -0
- package/src/components/ui/Card.tsx +29 -0
- package/src/components/ui/ConfirmModal.tsx +67 -0
- package/src/components/ui/Divider.test.tsx +24 -0
- package/src/components/ui/Divider.tsx +5 -0
- package/src/components/ui/EmptyState.test.tsx +49 -0
- package/src/components/ui/EmptyState.tsx +22 -0
- package/src/components/ui/ErrorBoundary.tsx +44 -0
- package/src/components/ui/ExportMenu.tsx +71 -0
- package/src/components/ui/LanguageSwitcher.tsx +37 -0
- package/src/components/ui/LoadingSpinner.test.tsx +41 -0
- package/src/components/ui/LoadingSpinner.tsx +21 -0
- package/src/components/ui/MetricCard.tsx +63 -0
- package/src/components/ui/ScoreRing.tsx +51 -0
- package/src/components/ui/SearchField.test.tsx +55 -0
- package/src/components/ui/SearchField.tsx +31 -0
- package/src/components/ui/SeverityBadge.tsx +57 -0
- package/src/components/ui/ThemeToggle.tsx +37 -0
- package/src/components/ui/Toast.test.tsx +79 -0
- package/src/components/ui/Toast.tsx +75 -0
- package/src/components/ui/Toggle.test.tsx +85 -0
- package/src/components/ui/Toggle.tsx +46 -0
- package/src/context/AuthContext.tsx +71 -0
- package/src/data/pepProfiles.ts +76 -0
- package/src/data/pepProfilesExtra.ts +58 -0
- package/src/hooks/useAlerts.ts +36 -0
- package/src/hooks/useAnalytics.ts +23 -0
- package/src/hooks/useApi.test.ts +79 -0
- package/src/hooks/useApi.ts +33 -0
- package/src/hooks/useAudit.ts +28 -0
- package/src/hooks/useBilling.ts +38 -0
- package/src/hooks/useConfig.ts +35 -0
- package/src/hooks/useDebounce.test.ts +84 -0
- package/src/hooks/useDebounce.ts +15 -0
- package/src/hooks/useDirection.ts +15 -0
- package/src/hooks/useLists.ts +34 -0
- package/src/hooks/useMediaQuery.test.ts +97 -0
- package/src/hooks/useMediaQuery.ts +28 -0
- package/src/hooks/useScreening.ts +33 -0
- package/src/hooks/useSidebar.ts +18 -0
- package/src/hooks/useUsage.ts +27 -0
- package/src/i18n/config.ts +33 -0
- package/src/i18n/locales/ar/admin.json +19 -0
- package/src/i18n/locales/ar/alerts.json +52 -0
- package/src/i18n/locales/ar/analytics.json +9 -0
- package/src/i18n/locales/ar/audit.json +12 -0
- package/src/i18n/locales/ar/auth.json +60 -0
- package/src/i18n/locales/ar/batch.json +5 -0
- package/src/i18n/locales/ar/billing.json +41 -0
- package/src/i18n/locales/ar/common.json +65 -0
- package/src/i18n/locales/ar/compliance.json +83 -0
- package/src/i18n/locales/ar/config.json +13 -0
- package/src/i18n/locales/ar/dashboard.json +19 -0
- package/src/i18n/locales/ar/errors.json +9 -0
- package/src/i18n/locales/ar/index.ts +29 -0
- package/src/i18n/locales/ar/legal.json +10 -0
- package/src/i18n/locales/ar/lists.json +6 -0
- package/src/i18n/locales/ar/marketing.json +110 -0
- package/src/i18n/locales/ar/monitoring.json +15 -0
- package/src/i18n/locales/ar/nav.json +35 -0
- package/src/i18n/locales/ar/onboarding.json +25 -0
- package/src/i18n/locales/ar/platform.json +23 -0
- package/src/i18n/locales/ar/screening.json +26 -0
- package/src/i18n/locales/ar/team.json +11 -0
- package/src/i18n/locales/en/admin.json +21 -0
- package/src/i18n/locales/en/alerts.json +52 -0
- package/src/i18n/locales/en/analytics.json +9 -0
- package/src/i18n/locales/en/audit.json +12 -0
- package/src/i18n/locales/en/auth.json +60 -0
- package/src/i18n/locales/en/batch.json +5 -0
- package/src/i18n/locales/en/billing.json +87 -0
- package/src/i18n/locales/en/common.json +65 -0
- package/src/i18n/locales/en/compliance.json +83 -0
- package/src/i18n/locales/en/config.json +29 -0
- package/src/i18n/locales/en/dashboard.json +23 -0
- package/src/i18n/locales/en/errors.json +9 -0
- package/src/i18n/locales/en/index.ts +29 -0
- package/src/i18n/locales/en/legal.json +114 -0
- package/src/i18n/locales/en/lists.json +6 -0
- package/src/i18n/locales/en/marketing.json +174 -0
- package/src/i18n/locales/en/monitoring.json +15 -0
- package/src/i18n/locales/en/nav.json +35 -0
- package/src/i18n/locales/en/onboarding.json +31 -0
- package/src/i18n/locales/en/platform.json +25 -0
- package/src/i18n/locales/en/screening.json +26 -0
- package/src/i18n/locales/en/team.json +12 -0
- package/src/i18n/locales/he/admin.json +19 -0
- package/src/i18n/locales/he/alerts.json +52 -0
- package/src/i18n/locales/he/analytics.json +9 -0
- package/src/i18n/locales/he/audit.json +12 -0
- package/src/i18n/locales/he/auth.json +60 -0
- package/src/i18n/locales/he/batch.json +5 -0
- package/src/i18n/locales/he/billing.json +41 -0
- package/src/i18n/locales/he/common.json +65 -0
- package/src/i18n/locales/he/compliance.json +83 -0
- package/src/i18n/locales/he/config.json +13 -0
- package/src/i18n/locales/he/dashboard.json +19 -0
- package/src/i18n/locales/he/errors.json +9 -0
- package/src/i18n/locales/he/index.ts +29 -0
- package/src/i18n/locales/he/legal.json +10 -0
- package/src/i18n/locales/he/lists.json +6 -0
- package/src/i18n/locales/he/marketing.json +110 -0
- package/src/i18n/locales/he/monitoring.json +15 -0
- package/src/i18n/locales/he/nav.json +35 -0
- package/src/i18n/locales/he/onboarding.json +25 -0
- package/src/i18n/locales/he/platform.json +23 -0
- package/src/i18n/locales/he/screening.json +26 -0
- package/src/i18n/locales/he/team.json +11 -0
- package/src/index.css +112 -0
- package/src/main.tsx +15 -0
- package/src/pages/APIKeys.tsx +120 -0
- package/src/pages/AddMonitorModal.tsx +30 -0
- package/src/pages/AdverseMedia.test.tsx +18 -0
- package/src/pages/AdverseMedia.tsx +89 -0
- package/src/pages/AlertDetailPage.tsx +64 -0
- package/src/pages/AlertQueue.test.tsx +48 -0
- package/src/pages/AlertQueue.tsx +63 -0
- package/src/pages/Analytics.tsx +50 -0
- package/src/pages/AuditTrail.tsx +64 -0
- package/src/pages/BatchJobs.tsx +79 -0
- package/src/pages/CaseDetail.tsx +72 -0
- package/src/pages/CaseManagement.test.tsx +31 -0
- package/src/pages/CaseManagement.tsx +92 -0
- package/src/pages/Configuration.test.tsx +96 -0
- package/src/pages/Configuration.tsx +123 -0
- package/src/pages/CryptoScreening.tsx +109 -0
- package/src/pages/Dashboard.test.tsx +51 -0
- package/src/pages/Dashboard.tsx +66 -0
- package/src/pages/EDDWorkflow.test.tsx +29 -0
- package/src/pages/EDDWorkflow.tsx +73 -0
- package/src/pages/ForgotPassword.test.tsx +49 -0
- package/src/pages/ForgotPassword.tsx +67 -0
- package/src/pages/ListsMarketplace.tsx +102 -0
- package/src/pages/Login.test.tsx +100 -0
- package/src/pages/Login.tsx +57 -0
- package/src/pages/MFASetup.tsx +114 -0
- package/src/pages/MonitorProfileCard.tsx +27 -0
- package/src/pages/Monitoring.tsx +68 -0
- package/src/pages/Onboarding.test.tsx +36 -0
- package/src/pages/Onboarding.tsx +60 -0
- package/src/pages/PEPScreening.test.tsx +15 -0
- package/src/pages/PEPScreening.tsx +100 -0
- package/src/pages/ResetPassword.tsx +81 -0
- package/src/pages/RiskAssessment.test.tsx +15 -0
- package/src/pages/RiskAssessment.tsx +108 -0
- package/src/pages/SanctionsLists.tsx +74 -0
- package/src/pages/ScreenEntity.test.tsx +82 -0
- package/src/pages/ScreenEntity.tsx +76 -0
- package/src/pages/Signup.test.tsx +98 -0
- package/src/pages/Signup.tsx +92 -0
- package/src/pages/TaskHistory.tsx +183 -0
- package/src/pages/Team.test.tsx +15 -0
- package/src/pages/Team.tsx +140 -0
- package/src/pages/TransactionMonitoring.test.tsx +18 -0
- package/src/pages/TransactionMonitoring.tsx +118 -0
- package/src/pages/TxnScreening.tsx +125 -0
- package/src/pages/UBOChain.test.tsx +35 -0
- package/src/pages/UBOChain.tsx +65 -0
- package/src/pages/Webhooks.tsx +137 -0
- package/src/pages/admin/DataSources.tsx +230 -0
- package/src/pages/admin/Operations.tsx +103 -0
- package/src/pages/admin/ScheduledTasks.tsx +155 -0
- package/src/pages/admin/SystemHealth.tsx +58 -0
- package/src/pages/admin/TenantDetail.tsx +62 -0
- package/src/pages/admin/Tenants.test.tsx +20 -0
- package/src/pages/admin/Tenants.tsx +63 -0
- package/src/pages/admin/opsRunners.ts +81 -0
- package/src/pages/admin/opsTerminal.tsx +63 -0
- package/src/pages/billing/ActiveSubscriptions.tsx +41 -0
- package/src/pages/billing/AddProductModal.tsx +99 -0
- package/src/pages/billing/BillingPage.test.tsx +30 -0
- package/src/pages/billing/BillingPage.tsx +67 -0
- package/src/pages/billing/CurrentPlan.tsx +50 -0
- package/src/pages/billing/InvoiceList.tsx +104 -0
- package/src/pages/billing/InvoiceRow.tsx +37 -0
- package/src/pages/billing/LemonSqueezySetup.tsx +51 -0
- package/src/pages/billing/PaymentAlert.tsx +33 -0
- package/src/pages/billing/PlanComparison.tsx +53 -0
- package/src/pages/billing/ProductUsage.tsx +43 -0
- package/src/pages/billing/PromoCodeInput.tsx +58 -0
- package/src/pages/billing/SeatManager.tsx +80 -0
- package/src/pages/billing/SeatRow.tsx +32 -0
- package/src/pages/billing/SubscriptionCard.tsx +73 -0
- package/src/pages/billing/UpgradeModal.tsx +53 -0
- package/src/pages/billing/UsageHistory.tsx +37 -0
- package/src/pages/billing/UsageMeter.tsx +26 -0
- package/src/pages/billing/UsageOverview.tsx +38 -0
- package/src/pages/legal/PrivacyPolicy.tsx +25 -0
- package/src/pages/legal/TermsOfService.tsx +25 -0
- package/src/pages/marketing/BundleCallout.tsx +24 -0
- package/src/pages/marketing/CTASection.tsx +48 -0
- package/src/pages/marketing/CaseStudy.tsx +37 -0
- package/src/pages/marketing/ComparisonTable.tsx +66 -0
- package/src/pages/marketing/CompetitiveEdge.tsx +55 -0
- package/src/pages/marketing/DataCoverage.tsx +54 -0
- package/src/pages/marketing/DataRain.tsx +30 -0
- package/src/pages/marketing/EnterpriseCTA.tsx +26 -0
- package/src/pages/marketing/FAQItem.tsx +27 -0
- package/src/pages/marketing/FAQSchema.tsx +43 -0
- package/src/pages/marketing/FAQSection.tsx +19 -0
- package/src/pages/marketing/FeatureDetail.tsx +19 -0
- package/src/pages/marketing/FeaturesGrid.tsx +60 -0
- package/src/pages/marketing/FeaturesSpotlight.tsx +68 -0
- package/src/pages/marketing/FooterSection.tsx +92 -0
- package/src/pages/marketing/GradientOrbs.tsx +26 -0
- package/src/pages/marketing/HeroSearch.tsx +113 -0
- package/src/pages/marketing/HeroSection.tsx +72 -0
- package/src/pages/marketing/LandingPage.tsx +45 -0
- package/src/pages/marketing/LogoCloud.tsx +31 -0
- package/src/pages/marketing/LogoMarquee.tsx +45 -0
- package/src/pages/marketing/MarketingNav.tsx +80 -0
- package/src/pages/marketing/MatchingLayers.tsx +78 -0
- package/src/pages/marketing/MobileMenu.tsx +45 -0
- package/src/pages/marketing/PricingCard.tsx +57 -0
- package/src/pages/marketing/PricingFeatureRow.tsx +16 -0
- package/src/pages/marketing/PricingSection.tsx +103 -0
- package/src/pages/marketing/PricingToggle.tsx +40 -0
- package/src/pages/marketing/ProductPricingCards.tsx +23 -0
- package/src/pages/marketing/ProductShowcase.tsx +81 -0
- package/src/pages/marketing/ProductTabs.tsx +45 -0
- package/src/pages/marketing/QuoteRotator.tsx +56 -0
- package/src/pages/marketing/StatsBar.tsx +81 -0
- package/src/pages/marketing/StatsSection.tsx +44 -0
- package/src/pages/marketing/TestimonialCard.tsx +38 -0
- package/src/pages/marketing/TestimonialsSection.tsx +32 -0
- package/src/pages/marketing/animations.tsx +56 -0
- package/src/pages/onboarding/CountryStep.tsx +38 -0
- package/src/pages/onboarding/ListsStep.tsx +44 -0
- package/src/pages/onboarding/StepIndicator.tsx +34 -0
- package/src/pages/onboarding/ThresholdStep.tsx +49 -0
- package/src/pages/platform/APIKeys.tsx +102 -0
- package/src/pages/platform/Overview.tsx +58 -0
- package/src/pages/platform/Users.tsx +110 -0
- package/src/pages/reporting/ComplianceReport.tsx +99 -0
- package/src/pages/reporting/SARForm.tsx +99 -0
- package/src/routes/appRoutes.tsx +60 -0
- package/src/routes/compliance.tsx +28 -0
- package/src/routes/lazyCompliance.ts +34 -0
- package/src/routes/lazyPages.ts +35 -0
- package/src/routes/lazyPlatform.ts +5 -0
- package/src/routes/platform.tsx +15 -0
- package/src/styles/effects.css +76 -0
- package/src/test/setup.ts +25 -0
- package/src/test/utils.tsx +49 -0
- package/src/types/alert.ts +31 -0
- package/src/types/analytics.ts +16 -0
- package/src/types/audit.ts +11 -0
- package/src/types/billing.ts +60 -0
- package/src/types/common.ts +15 -0
- package/src/types/config.ts +19 -0
- package/src/types/entity.ts +34 -0
- package/src/types/index.ts +8 -0
- package/src/types/list.ts +15 -0
- package/src/types/screening.ts +32 -0
- package/src/vite-env.d.ts +1 -0
- package/tailwind.config.js +65 -0
- package/tsconfig.json +22 -0
- package/vite.config.ts +11 -0
- package/vitest.config.ts +19 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { Product, ProductType } from '../../types/billing'
|
|
4
|
+
import { getProducts, getSubscriptions, createCheckout } from '../../api/billing'
|
|
5
|
+
import { X } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
interface AddProductModalProps {
|
|
8
|
+
isOpen: boolean
|
|
9
|
+
onClose: () => void
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function AddProductModal({ isOpen, onClose }: AddProductModalProps) {
|
|
13
|
+
const { t } = useTranslation('billing')
|
|
14
|
+
const [products, setProducts] = useState<Product[]>([])
|
|
15
|
+
const [subscriptions, setSubscriptions] = useState<any[]>([])
|
|
16
|
+
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null)
|
|
17
|
+
const [selectedPlanId, setSelectedPlanId] = useState('')
|
|
18
|
+
const [loading, setLoading] = useState(true)
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
if (isOpen) {
|
|
22
|
+
Promise.all([getProducts(), getSubscriptions()]).then(([p, s]) => {
|
|
23
|
+
setProducts(p)
|
|
24
|
+
setSubscriptions(s)
|
|
25
|
+
setLoading(false)
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
}, [isOpen])
|
|
29
|
+
|
|
30
|
+
const availableProducts = products.filter(p => !subscriptions.find(s => s.product === p.type))
|
|
31
|
+
|
|
32
|
+
const handleCheckout = async () => {
|
|
33
|
+
if (!selectedProduct || !selectedPlanId) return
|
|
34
|
+
const result = await createCheckout(selectedProduct.type, selectedPlanId)
|
|
35
|
+
window.location.href = result.checkoutUrl
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!isOpen) return null
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-md" role="dialog" aria-modal="true" aria-labelledby="add-product-title" onKeyDown={(e) => { if (e.key === 'Escape') onClose() }}>
|
|
42
|
+
<div className="bg-apple-bg rounded-apple-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto border border-white/10">
|
|
43
|
+
<div className="flex items-center justify-between p-lg border-b border-white/10">
|
|
44
|
+
<h2 id="add-product-title" className="sf-headline text-white">{t('add_product')}</h2>
|
|
45
|
+
<button onClick={onClose} aria-label={t('close')} className="p-md hover:bg-white/10 rounded-apple-md cursor-pointer">
|
|
46
|
+
<X className="w-5 h-5 text-white" />
|
|
47
|
+
</button>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
{loading ? (
|
|
51
|
+
<div className="p-lg text-center">{t('loading')}</div>
|
|
52
|
+
) : availableProducts.length === 0 ? (
|
|
53
|
+
<div className="p-lg text-center sf-body text-apple-label-secondary">{t('invoices.all_subscribed')}</div>
|
|
54
|
+
) : (
|
|
55
|
+
<div className="p-lg space-y-lg">
|
|
56
|
+
<div className="grid grid-cols-1 gap-md">
|
|
57
|
+
{availableProducts.map(product => (
|
|
58
|
+
<button
|
|
59
|
+
key={product.type}
|
|
60
|
+
onClick={() => { setSelectedProduct(product); setSelectedPlanId(product.plans[0].id) }}
|
|
61
|
+
aria-pressed={selectedProduct?.type === product.type}
|
|
62
|
+
className={`p-md rounded-apple-lg border transition-colors text-left cursor-pointer ${selectedProduct?.type === product.type ? 'border-apple-blue bg-apple-blue/10' : 'border-white/10 hover:border-white/20'}`}
|
|
63
|
+
>
|
|
64
|
+
<h3 className="sf-body font-medium text-white">{product.name}</h3>
|
|
65
|
+
<p className="sf-caption text-apple-label-secondary">{product.tagline}</p>
|
|
66
|
+
</button>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
{selectedProduct && (
|
|
71
|
+
<div className="space-y-md">
|
|
72
|
+
<h3 className="sf-body font-medium text-white">{t('invoices.select_plan')}</h3>
|
|
73
|
+
<div className="grid grid-cols-1 gap-sm">
|
|
74
|
+
{selectedProduct.plans.map(plan => (
|
|
75
|
+
<button
|
|
76
|
+
key={plan.id}
|
|
77
|
+
onClick={() => setSelectedPlanId(plan.id)}
|
|
78
|
+
aria-pressed={selectedPlanId === plan.id}
|
|
79
|
+
className={`p-sm rounded-apple-md border transition-colors text-left sf-caption cursor-pointer ${selectedPlanId === plan.id ? 'border-apple-blue bg-apple-blue/10' : 'border-white/10 hover:border-white/20'}`}
|
|
80
|
+
>
|
|
81
|
+
<span className="text-white">{plan.name}</span> - <span className="text-apple-label-secondary">${plan.monthlyPrice}{t('pricing.per_month')}</span>
|
|
82
|
+
</button>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
<button
|
|
87
|
+
onClick={handleCheckout}
|
|
88
|
+
className="w-full px-lg py-md bg-apple-blue hover:bg-apple-blue/80 text-white rounded-apple-md sf-body font-medium transition-colors cursor-pointer"
|
|
89
|
+
>
|
|
90
|
+
{t('invoices.checkout')}
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
)
|
|
99
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
3
|
+
import BillingPage from './BillingPage'
|
|
4
|
+
|
|
5
|
+
vi.mock('../../api/billing', () => ({
|
|
6
|
+
getSubscriptions: vi.fn(() => Promise.resolve([])),
|
|
7
|
+
getInvoices: vi.fn(() => Promise.resolve([])),
|
|
8
|
+
getSeats: vi.fn(() => Promise.resolve([])),
|
|
9
|
+
getUsage: vi.fn(() => Promise.resolve(null)),
|
|
10
|
+
getProducts: vi.fn(() => Promise.resolve([])),
|
|
11
|
+
}))
|
|
12
|
+
|
|
13
|
+
describe('BillingPage', () => {
|
|
14
|
+
it('renders page title', () => {
|
|
15
|
+
render(<BillingPage />)
|
|
16
|
+
expect(screen.getByText('Billing & Subscriptions')).toBeInTheDocument()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('renders section headings', () => {
|
|
20
|
+
render(<BillingPage />)
|
|
21
|
+
expect(screen.getByText('Active Subscriptions')).toBeInTheDocument()
|
|
22
|
+
expect(screen.getByText('Dashboard Seats')).toBeInTheDocument()
|
|
23
|
+
expect(screen.getByText('Promo Code')).toBeInTheDocument()
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('renders add product button', () => {
|
|
27
|
+
render(<BillingPage />)
|
|
28
|
+
expect(screen.getByText('Add Product')).toBeInTheDocument()
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { Plus } from 'lucide-react'
|
|
4
|
+
import { ActiveSubscriptions } from './ActiveSubscriptions'
|
|
5
|
+
import { ProductUsage } from './ProductUsage'
|
|
6
|
+
import { SeatManager } from './SeatManager'
|
|
7
|
+
import { PromoCodeInput } from './PromoCodeInput'
|
|
8
|
+
import { AddProductModal } from './AddProductModal'
|
|
9
|
+
import UsageHistory from './UsageHistory'
|
|
10
|
+
import InvoiceList from './InvoiceList'
|
|
11
|
+
|
|
12
|
+
export default function BillingPage() {
|
|
13
|
+
const { t } = useTranslation('billing')
|
|
14
|
+
const [modalOpen, setModalOpen] = useState(false)
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="min-h-screen bg-apple-bg/50 p-md md:p-lg">
|
|
18
|
+
<div className="max-w-6xl mx-auto space-y-xl">
|
|
19
|
+
<div>
|
|
20
|
+
<h1 className="sf-title text-white">{t('title')}</h1>
|
|
21
|
+
<p className="sf-body text-apple-label-secondary mt-xs">{t('subtitle')}</p>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div className="flex justify-end">
|
|
25
|
+
<button
|
|
26
|
+
onClick={() => setModalOpen(true)}
|
|
27
|
+
className="flex items-center gap-xs px-lg py-md bg-apple-blue hover:bg-apple-blue/80 text-white rounded-apple-md sf-body font-medium transition-colors"
|
|
28
|
+
>
|
|
29
|
+
<Plus className="w-4 h-4" />
|
|
30
|
+
{t('add_product')}
|
|
31
|
+
</button>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<section className="space-y-md">
|
|
35
|
+
<h2 className="sf-body font-medium text-white">{t('sections.active_subscriptions')}</h2>
|
|
36
|
+
<ActiveSubscriptions />
|
|
37
|
+
</section>
|
|
38
|
+
|
|
39
|
+
<section className="space-y-md">
|
|
40
|
+
<h2 className="sf-body font-medium text-white">{t('sections.dashboard_seats')}</h2>
|
|
41
|
+
<div className="rounded-apple-lg border border-white/10 bg-white/5 p-lg">
|
|
42
|
+
<SeatManager />
|
|
43
|
+
</div>
|
|
44
|
+
</section>
|
|
45
|
+
|
|
46
|
+
<section className="space-y-md">
|
|
47
|
+
<h2 className="sf-body font-medium text-white">{t('sections.promo_code')}</h2>
|
|
48
|
+
<div className="rounded-apple-lg border border-white/10 bg-white/5 p-lg max-w-md">
|
|
49
|
+
<PromoCodeInput />
|
|
50
|
+
</div>
|
|
51
|
+
</section>
|
|
52
|
+
|
|
53
|
+
<section className="space-y-md">
|
|
54
|
+
<h2 className="sf-body font-medium text-white">{t('sections.usage_history')}</h2>
|
|
55
|
+
<UsageHistory />
|
|
56
|
+
</section>
|
|
57
|
+
|
|
58
|
+
<section className="space-y-md">
|
|
59
|
+
<h2 className="sf-body font-medium text-white">{t('sections.invoices')}</h2>
|
|
60
|
+
<InvoiceList />
|
|
61
|
+
</section>
|
|
62
|
+
|
|
63
|
+
<AddProductModal isOpen={modalOpen} onClose={() => setModalOpen(false)} />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { ExternalLink } from 'lucide-react'
|
|
4
|
+
import { Subscription } from '../../types/billing'
|
|
5
|
+
import { getSubscriptions } from '../../api/billing'
|
|
6
|
+
|
|
7
|
+
export default function CurrentPlan() {
|
|
8
|
+
const { t } = useTranslation('billing')
|
|
9
|
+
const [subscription, setSubscription] = useState<Subscription | null>(null)
|
|
10
|
+
const [loading, setLoading] = useState(true)
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
getSubscriptions()
|
|
14
|
+
.then(subs => setSubscription(Array.isArray(subs) ? subs[0] ?? null : null))
|
|
15
|
+
.catch(() => setSubscription(null))
|
|
16
|
+
.finally(() => setLoading(false))
|
|
17
|
+
}, [])
|
|
18
|
+
|
|
19
|
+
if (loading || !subscription) return <div className="animate-pulse h-40 bg-white/5 rounded-xl" />
|
|
20
|
+
|
|
21
|
+
const renewalDate = new Date(subscription.currentPeriodEnd).toLocaleDateString()
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="bg-white/[0.02] rounded-xl border border-white/[0.06] p-6">
|
|
25
|
+
<h3 className="text-lg font-semibold text-white mb-4">{t('subscriptions.current_plan')}</h3>
|
|
26
|
+
<div className="space-y-4">
|
|
27
|
+
<div>
|
|
28
|
+
<p className="text-3xl font-bold text-white">{subscription.plan.name}</p>
|
|
29
|
+
<p className="text-sm text-zinc-400 mt-1">
|
|
30
|
+
<span className="inline-block px-3 py-1 bg-green-900/30 text-green-300 rounded-full text-xs font-semibold">
|
|
31
|
+
{subscription.status === 'trialing' ? t('subscriptions.trialing') : t('subscriptions.active')}
|
|
32
|
+
</span>
|
|
33
|
+
</p>
|
|
34
|
+
</div>
|
|
35
|
+
<div className="text-sm text-zinc-400">
|
|
36
|
+
<p>{t('subscriptions.renews_on', { date: renewalDate })}</p>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="flex flex-col sm:flex-row gap-3 pt-4">
|
|
39
|
+
<button type="button" className="flex-1 px-4 py-2 bg-apple-blue hover:bg-apple-blue/80 text-white text-sm font-medium rounded-lg transition-colors flex items-center justify-center gap-2 cursor-pointer">
|
|
40
|
+
<ExternalLink size={16} />
|
|
41
|
+
{t('subscriptions.manage')}
|
|
42
|
+
</button>
|
|
43
|
+
<button type="button" className="flex-1 px-4 py-2 border border-white/10 text-white text-sm font-medium rounded-lg hover:bg-white/5 transition-colors cursor-pointer">
|
|
44
|
+
{t('subscriptions.change_plan')}
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { Invoice } from '../../types/billing'
|
|
4
|
+
import { getInvoices } from '../../api/billing'
|
|
5
|
+
import { Download } from 'lucide-react'
|
|
6
|
+
|
|
7
|
+
export default function InvoiceList() {
|
|
8
|
+
const { t } = useTranslation('billing')
|
|
9
|
+
const [invoices, setInvoices] = useState<Invoice[]>([])
|
|
10
|
+
const [loading, setLoading] = useState(true)
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
getInvoices()
|
|
14
|
+
.then(i => setInvoices(i))
|
|
15
|
+
.catch(() => setInvoices([]))
|
|
16
|
+
.finally(() => setLoading(false))
|
|
17
|
+
}, [])
|
|
18
|
+
|
|
19
|
+
if (loading) return <div className="sf-body text-apple-label-secondary">{t('invoices.loading')}</div>
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className="rounded-apple-lg border border-white/10 bg-white/5 overflow-hidden">
|
|
23
|
+
<div className="px-lg py-md border-b border-white/10">
|
|
24
|
+
<h3 className="sf-body font-medium text-white">{t('invoices.recent')}</h3>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
{/* Desktop table */}
|
|
28
|
+
<div className="hidden md:block overflow-x-auto">
|
|
29
|
+
<table className="w-full">
|
|
30
|
+
<thead>
|
|
31
|
+
<tr className="border-b border-white/10 bg-white/5">
|
|
32
|
+
<th className="px-lg py-md text-left text-xs font-medium text-apple-label-secondary uppercase">{t('invoices.date')}</th>
|
|
33
|
+
<th className="px-lg py-md text-left text-xs font-medium text-apple-label-secondary uppercase">{t('invoices.product')}</th>
|
|
34
|
+
<th className="px-lg py-md text-left text-xs font-medium text-apple-label-secondary uppercase">{t('invoices.amount')}</th>
|
|
35
|
+
<th className="px-lg py-md text-left text-xs font-medium text-apple-label-secondary uppercase">{t('invoices.status')}</th>
|
|
36
|
+
<th className="px-lg py-md text-right text-xs font-medium text-apple-label-secondary uppercase">{t('invoices.action')}</th>
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody className="divide-y divide-white/10">
|
|
40
|
+
{invoices.map(inv => (
|
|
41
|
+
<tr key={inv.id} className="hover:bg-white/5 transition-colors">
|
|
42
|
+
<td className="px-lg py-md sf-caption text-apple-label-secondary">{new Date(inv.date).toLocaleDateString()}</td>
|
|
43
|
+
<td className="px-lg py-md sf-caption text-white capitalize">{inv.product}</td>
|
|
44
|
+
<td className="px-lg py-md sf-body text-white">${(inv.amountCents / 100).toFixed(2)}</td>
|
|
45
|
+
<td className="px-lg py-md">
|
|
46
|
+
<InvoiceStatusBadge status={inv.status} />
|
|
47
|
+
</td>
|
|
48
|
+
<td className="px-lg py-md text-right">
|
|
49
|
+
<a href={inv.url} target="_blank" rel="noopener noreferrer"
|
|
50
|
+
aria-label={`${t('invoices.download')} ${inv.product}`}
|
|
51
|
+
className="inline-flex p-md hover:bg-white/10 rounded-apple-md transition-colors">
|
|
52
|
+
<Download className="w-4 h-4 text-apple-blue" />
|
|
53
|
+
</a>
|
|
54
|
+
</td>
|
|
55
|
+
</tr>
|
|
56
|
+
))}
|
|
57
|
+
</tbody>
|
|
58
|
+
</table>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* Mobile cards */}
|
|
62
|
+
<div className="md:hidden divide-y divide-white/10">
|
|
63
|
+
{invoices.map(inv => (
|
|
64
|
+
<div key={inv.id} className="p-lg">
|
|
65
|
+
<div className="flex items-center justify-between mb-xs">
|
|
66
|
+
<span className="sf-body text-white capitalize font-medium">{inv.product}</span>
|
|
67
|
+
<InvoiceStatusBadge status={inv.status} />
|
|
68
|
+
</div>
|
|
69
|
+
<div className="flex items-center justify-between">
|
|
70
|
+
<div>
|
|
71
|
+
<span className="sf-body text-white font-semibold">${(inv.amountCents / 100).toFixed(2)}</span>
|
|
72
|
+
<span className="sf-caption text-apple-label-secondary ml-sm">
|
|
73
|
+
{new Date(inv.date).toLocaleDateString()}
|
|
74
|
+
</span>
|
|
75
|
+
</div>
|
|
76
|
+
<a href={inv.url} target="_blank" rel="noopener noreferrer"
|
|
77
|
+
aria-label={`${t('invoices.download')} ${inv.product}`}
|
|
78
|
+
className="p-sm hover:bg-white/10 rounded-apple-md transition-colors">
|
|
79
|
+
<Download className="w-4 h-4 text-apple-blue" />
|
|
80
|
+
</a>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{invoices.length === 0 && (
|
|
87
|
+
<p className="text-center py-lg sf-caption text-apple-label-secondary">
|
|
88
|
+
No invoices yet
|
|
89
|
+
</p>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function InvoiceStatusBadge({ status }: { status: string }) {
|
|
96
|
+
const cls = status === 'paid'
|
|
97
|
+
? 'bg-green-600/20 text-green-400'
|
|
98
|
+
: 'bg-yellow-600/20 text-yellow-400'
|
|
99
|
+
return (
|
|
100
|
+
<span className={`text-xs px-md py-xs rounded-apple-md font-medium ${cls}`}>
|
|
101
|
+
{status}
|
|
102
|
+
</span>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Download } from 'lucide-react'
|
|
3
|
+
import { Invoice } from '../../types/billing'
|
|
4
|
+
|
|
5
|
+
interface InvoiceRowProps {
|
|
6
|
+
invoice: Invoice
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export default function InvoiceRow({ invoice }: InvoiceRowProps) {
|
|
10
|
+
const date = new Date(invoice.date).toLocaleDateString()
|
|
11
|
+
const amount = (invoice.amountCents / 100).toFixed(2)
|
|
12
|
+
|
|
13
|
+
const statusColor = {
|
|
14
|
+
paid: 'text-green-400 bg-green-900/20',
|
|
15
|
+
open: 'text-amber-400 bg-amber-900/20',
|
|
16
|
+
failed: 'text-red-400 bg-red-900/20'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<tr className="border-b border-white/[0.06] hover:bg-white/[0.02]">
|
|
21
|
+
<td className="px-4 py-3 text-sm text-white">{date}</td>
|
|
22
|
+
<td className="px-4 py-3 text-sm font-medium text-white">
|
|
23
|
+
${amount} {invoice.currency}
|
|
24
|
+
</td>
|
|
25
|
+
<td className="px-4 py-3">
|
|
26
|
+
<span className={`text-xs font-semibold px-3 py-1 rounded-full ${statusColor[invoice.status]}`}>
|
|
27
|
+
{invoice.status.charAt(0).toUpperCase() + invoice.status.slice(1)}
|
|
28
|
+
</span>
|
|
29
|
+
</td>
|
|
30
|
+
<td className="px-4 py-3 text-right">
|
|
31
|
+
<button type="button" className="text-blue-400 hover:text-blue-300 transition-colors">
|
|
32
|
+
<Download size={16} />
|
|
33
|
+
</button>
|
|
34
|
+
</td>
|
|
35
|
+
</tr>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Check } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
export function LemonSqueezySetup() {
|
|
5
|
+
const webhookUrl = 'https://api.amliq.finance/webhooks/lemonsqueezy'
|
|
6
|
+
const lastWebhook = '2026-03-26T14:32:00Z'
|
|
7
|
+
|
|
8
|
+
const connectedProducts = ['api', 'dashboard', 'sdk', 'iframe', 'dataset']
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="space-y-lg">
|
|
12
|
+
<div className="rounded-apple-lg border border-white/10 bg-white/5 p-lg">
|
|
13
|
+
<h3 className="sf-body font-medium text-white mb-md">LemonSqueezy Integration Status</h3>
|
|
14
|
+
|
|
15
|
+
<div className="space-y-md">
|
|
16
|
+
<div>
|
|
17
|
+
<label className="sf-caption text-apple-label-secondary">Webhook URL</label>
|
|
18
|
+
<div className="mt-xs flex gap-xs">
|
|
19
|
+
<input
|
|
20
|
+
type="text"
|
|
21
|
+
value={webhookUrl}
|
|
22
|
+
readOnly
|
|
23
|
+
className="flex-1 px-md py-sm bg-white/5 border border-white/10 rounded-apple-md sf-caption text-apple-label-secondary"
|
|
24
|
+
/>
|
|
25
|
+
<button className="px-lg py-sm bg-white/10 hover:bg-white/20 text-white rounded-apple-md sf-caption transition-colors">
|
|
26
|
+
Copy
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div>
|
|
32
|
+
<label className="sf-caption text-apple-label-secondary">Last Webhook Received</label>
|
|
33
|
+
<p className="mt-xs sf-body text-white">{new Date(lastWebhook).toLocaleString()}</p>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
|
|
38
|
+
<div className="rounded-apple-lg border border-white/10 bg-white/5 p-lg">
|
|
39
|
+
<h3 className="sf-body font-medium text-white mb-md">Connected Products</h3>
|
|
40
|
+
<div className="space-y-xs">
|
|
41
|
+
{connectedProducts.map(product => (
|
|
42
|
+
<div key={product} className="flex items-center gap-md p-sm bg-white/5 rounded-apple-md">
|
|
43
|
+
<Check className="w-4 h-4 text-green-400" />
|
|
44
|
+
<span className="sf-caption text-white capitalize">{product}</span>
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { AlertCircle, X } from 'lucide-react'
|
|
4
|
+
|
|
5
|
+
interface PaymentAlertProps {
|
|
6
|
+
visible: boolean
|
|
7
|
+
onDismiss: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function PaymentAlert({ visible, onDismiss }: PaymentAlertProps) {
|
|
11
|
+
const { t } = useTranslation('billing')
|
|
12
|
+
if (!visible) return null
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div role="alert" className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 flex items-start justify-between gap-4">
|
|
16
|
+
<div className="flex items-start gap-3">
|
|
17
|
+
<AlertCircle size={20} className="text-red-600 dark:text-red-400 flex-shrink-0 mt-0.5" />
|
|
18
|
+
<div>
|
|
19
|
+
<h3 className="font-semibold text-red-900 dark:text-red-200">{t('payment.failed_title')}</h3>
|
|
20
|
+
<p className="text-sm text-red-800 dark:text-red-300 mt-1">
|
|
21
|
+
{t('payment.failed_message')}
|
|
22
|
+
</p>
|
|
23
|
+
<button aria-label={t('payment.update_method')} className="text-sm font-semibold text-red-700 dark:text-red-300 hover:text-red-900 dark:hover:text-red-100 mt-2 cursor-pointer">
|
|
24
|
+
{t('payment.update_method')}
|
|
25
|
+
</button>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
<button onClick={onDismiss} aria-label={t('payment.dismiss')} className="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-300 cursor-pointer">
|
|
29
|
+
<X size={20} />
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { Plan } from '../../types/billing'
|
|
4
|
+
import { getProducts } from '../../api/billing'
|
|
5
|
+
import PricingCard from '../marketing/PricingCard'
|
|
6
|
+
|
|
7
|
+
export default function PlanComparison() {
|
|
8
|
+
const { t } = useTranslation('billing')
|
|
9
|
+
const [plans, setPlans] = useState<Plan[]>([])
|
|
10
|
+
const [loading, setLoading] = useState(true)
|
|
11
|
+
const [annual, setAnnual] = useState(false)
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
getProducts().then(products => {
|
|
15
|
+
const allPlans = products.flatMap(p => p.plans)
|
|
16
|
+
setPlans(allPlans)
|
|
17
|
+
}).finally(() => setLoading(false))
|
|
18
|
+
}, [])
|
|
19
|
+
|
|
20
|
+
if (loading) return <div className="animate-pulse h-96 bg-white/5 rounded-xl" />
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="bg-white/[0.02] rounded-xl border border-white/[0.06] p-6">
|
|
24
|
+
<h3 className="text-lg font-semibold text-white mb-2">{t('plan_comparison.title')}</h3>
|
|
25
|
+
<p className="text-sm text-zinc-400 mb-6">{t('plan_comparison.subtitle')}</p>
|
|
26
|
+
|
|
27
|
+
<div className="flex justify-center gap-2 mb-8">
|
|
28
|
+
<button
|
|
29
|
+
type="button"
|
|
30
|
+
onClick={() => setAnnual(false)}
|
|
31
|
+
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
32
|
+
!annual ? 'bg-white text-black' : 'bg-white/5 text-white'
|
|
33
|
+
}`}
|
|
34
|
+
>
|
|
35
|
+
{t('plan_comparison.monthly')}
|
|
36
|
+
</button>
|
|
37
|
+
<button
|
|
38
|
+
type="button"
|
|
39
|
+
onClick={() => setAnnual(true)}
|
|
40
|
+
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
41
|
+
annual ? 'bg-white text-black' : 'bg-white/5 text-white'
|
|
42
|
+
}`}
|
|
43
|
+
>
|
|
44
|
+
{t('plan_comparison.annual')}
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
49
|
+
{plans.map(plan => <PricingCard key={plan.id} plan={plan} annual={annual} />)}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { ProductType } from '../../types/billing'
|
|
4
|
+
import { getUsage } from '../../api/billing'
|
|
5
|
+
|
|
6
|
+
interface ProductUsageProps {
|
|
7
|
+
product: ProductType
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ProductUsage({ product }: ProductUsageProps) {
|
|
11
|
+
const { t } = useTranslation('billing')
|
|
12
|
+
const [usage, setUsage] = useState<any>(null)
|
|
13
|
+
const [loading, setLoading] = useState(true)
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
getUsage(product)
|
|
17
|
+
.then(data => setUsage(data))
|
|
18
|
+
.catch(() => setUsage(null))
|
|
19
|
+
.finally(() => setLoading(false))
|
|
20
|
+
}, [product])
|
|
21
|
+
|
|
22
|
+
if (loading) return <div className="sf-caption text-apple-label-secondary">{t('usage.loading')}</div>
|
|
23
|
+
if (!usage) return <div className="sf-caption text-apple-label-secondary">{t('usage.no_data')}</div>
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="space-y-md">
|
|
27
|
+
{(usage.metrics ?? []).map((metric: any) => (
|
|
28
|
+
<div key={metric.name}>
|
|
29
|
+
<div className="flex justify-between mb-xs">
|
|
30
|
+
<span className="sf-body text-white">{metric.name}</span>
|
|
31
|
+
<span className="sf-caption text-apple-label-secondary">{metric.current}/{metric.limit} {metric.unit}</span>
|
|
32
|
+
</div>
|
|
33
|
+
<div className="w-full h-2 bg-white/10 rounded-apple-md overflow-hidden">
|
|
34
|
+
<div
|
|
35
|
+
className="h-full bg-gradient-to-r from-apple-blue to-blue-600 transition-all"
|
|
36
|
+
style={{ width: `${Math.min((metric.current / metric.limit) * 100, 100)}%` }}
|
|
37
|
+
/>
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
))}
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
|
+
import { useTranslation } from 'react-i18next'
|
|
3
|
+
import { applyPromoCode } from '../../api/billing'
|
|
4
|
+
import { Check } from 'lucide-react'
|
|
5
|
+
|
|
6
|
+
export function PromoCodeInput() {
|
|
7
|
+
const { t } = useTranslation('billing')
|
|
8
|
+
const [code, setCode] = useState('')
|
|
9
|
+
const [discount, setDiscount] = useState<{ percent: number; message: string } | null>(null)
|
|
10
|
+
const [loading, setLoading] = useState(false)
|
|
11
|
+
|
|
12
|
+
const handleApply = async () => {
|
|
13
|
+
if (!code) return
|
|
14
|
+
setLoading(true)
|
|
15
|
+
try {
|
|
16
|
+
const result = await applyPromoCode(code)
|
|
17
|
+
setDiscount({ percent: result.discountPercent, message: result.message })
|
|
18
|
+
} finally {
|
|
19
|
+
setLoading(false)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div className="space-y-md">
|
|
25
|
+
<div className="flex gap-xs">
|
|
26
|
+
<input
|
|
27
|
+
type="text"
|
|
28
|
+
value={code}
|
|
29
|
+
onChange={e => setCode(e.target.value.toUpperCase())}
|
|
30
|
+
placeholder={t('promo.placeholder')}
|
|
31
|
+
aria-label={t('promo.placeholder')}
|
|
32
|
+
className="flex-1 px-md py-sm bg-white/5 border border-white/10 rounded-apple-md sf-body text-white placeholder:text-apple-label-secondary focus:outline-none focus:ring-2 focus:ring-apple-blue focus:border-apple-blue"
|
|
33
|
+
/>
|
|
34
|
+
<button
|
|
35
|
+
onClick={handleApply}
|
|
36
|
+
disabled={!code || loading}
|
|
37
|
+
className="px-lg py-sm bg-apple-blue hover:bg-apple-blue/80 disabled:opacity-50 text-white rounded-apple-md sf-body font-medium transition-colors cursor-pointer"
|
|
38
|
+
>
|
|
39
|
+
{t('promo.apply')}
|
|
40
|
+
</button>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
{discount && (
|
|
44
|
+
<div className={`p-md rounded-apple-md flex items-start gap-xs ${discount.percent === 100 ? 'bg-green-600/20' : 'bg-blue-600/20'}`}>
|
|
45
|
+
<Check className={`w-4 h-4 flex-shrink-0 mt-xs ${discount.percent === 100 ? 'text-green-400' : 'text-blue-400'}`} />
|
|
46
|
+
<div>
|
|
47
|
+
<p className={`sf-body font-medium ${discount.percent === 100 ? 'text-green-300' : 'text-blue-300'}`}>
|
|
48
|
+
{t('promo.discount', { percent: discount.percent })}
|
|
49
|
+
</p>
|
|
50
|
+
<p className={`sf-caption ${discount.percent === 100 ? 'text-green-400' : 'text-blue-400'}`}>
|
|
51
|
+
{discount.message}
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
)
|
|
58
|
+
}
|