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,49 @@
|
|
|
1
|
+
import { Card } from '../ui/Card'
|
|
2
|
+
import { ScreeningResultRow } from './ScreeningResultRow'
|
|
3
|
+
import { ShareResults } from './ShareResults'
|
|
4
|
+
import type { ScreenResponse } from '../../types'
|
|
5
|
+
|
|
6
|
+
export type { ScreenResponse }
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
data: ScreenResponse
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ScreenResults({ data }: Props) {
|
|
13
|
+
const matches = data.matches ?? []
|
|
14
|
+
const totalMatches = data.total_matches ?? matches.length
|
|
15
|
+
|
|
16
|
+
if (totalMatches === 0) {
|
|
17
|
+
return (
|
|
18
|
+
<Card className="text-center py-xxl">
|
|
19
|
+
<div className="text-apple-green text-3xl mb-md">✓</div>
|
|
20
|
+
<p className="sf-headline text-apple-green">No Matches Found</p>
|
|
21
|
+
<p className="sf-caption mt-sm">
|
|
22
|
+
No sanctions matches for “{data.query}”
|
|
23
|
+
</p>
|
|
24
|
+
<div className="mt-lg flex justify-center">
|
|
25
|
+
<ShareResults data={data} />
|
|
26
|
+
</div>
|
|
27
|
+
</Card>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div className="space-y-md">
|
|
33
|
+
<div className="flex items-center justify-between">
|
|
34
|
+
<p className="sf-body">
|
|
35
|
+
<span className="sf-headline">{totalMatches}</span> matches
|
|
36
|
+
</p>
|
|
37
|
+
<div className="flex items-center gap-md">
|
|
38
|
+
<span className="sf-caption text-white/50">
|
|
39
|
+
{data.processing_time_ms ?? 0}ms
|
|
40
|
+
</span>
|
|
41
|
+
<ShareResults data={data} />
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
{matches.map((m, i) => (
|
|
45
|
+
<ScreeningResultRow key={m.entity_id} match={m} index={i} />
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
4
|
+
import { ScreeningForm } from './ScreeningForm'
|
|
5
|
+
|
|
6
|
+
describe('ScreeningForm', () => {
|
|
7
|
+
it('renders entity type selection', () => {
|
|
8
|
+
render(<ScreeningForm onSubmit={vi.fn()} />)
|
|
9
|
+
expect(screen.getByRole('button', { name: 'Individual' })).toBeInTheDocument()
|
|
10
|
+
expect(screen.getByRole('button', { name: 'Company' })).toBeInTheDocument()
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('renders individual fields by default', () => {
|
|
14
|
+
render(<ScreeningForm onSubmit={vi.fn()} />)
|
|
15
|
+
expect(screen.getByPlaceholderText('First Name')).toBeInTheDocument()
|
|
16
|
+
expect(screen.getByPlaceholderText('Last Name')).toBeInTheDocument()
|
|
17
|
+
expect(screen.getByPlaceholderText('Nationality')).toBeInTheDocument()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('switches to company fields when company type selected', async () => {
|
|
21
|
+
render(<ScreeningForm onSubmit={vi.fn()} />)
|
|
22
|
+
const companyTab = screen.getByRole('button', { name: 'Company' })
|
|
23
|
+
await userEvent.click(companyTab)
|
|
24
|
+
expect(screen.getByPlaceholderText('Company Name')).toBeInTheDocument()
|
|
25
|
+
expect(screen.queryByPlaceholderText('First Name')).not.toBeInTheDocument()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('submits individual form data', async () => {
|
|
29
|
+
const handler = vi.fn()
|
|
30
|
+
render(<ScreeningForm onSubmit={handler} />)
|
|
31
|
+
|
|
32
|
+
await userEvent.type(screen.getByPlaceholderText('First Name'), 'John')
|
|
33
|
+
await userEvent.type(screen.getByPlaceholderText('Last Name'), 'Doe')
|
|
34
|
+
await userEvent.type(screen.getByPlaceholderText('Nationality'), 'US')
|
|
35
|
+
|
|
36
|
+
await userEvent.click(screen.getByRole('button', { name: /screen entity/i }))
|
|
37
|
+
|
|
38
|
+
expect(handler).toHaveBeenCalledWith(
|
|
39
|
+
expect.objectContaining({
|
|
40
|
+
firstName: 'John',
|
|
41
|
+
lastName: 'Doe',
|
|
42
|
+
nationality: 'US',
|
|
43
|
+
}),
|
|
44
|
+
'individual'
|
|
45
|
+
)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('submits company form data', async () => {
|
|
49
|
+
const handler = vi.fn()
|
|
50
|
+
render(<ScreeningForm onSubmit={handler} />)
|
|
51
|
+
|
|
52
|
+
const companyTab = screen.getByRole('button', { name: 'Company' })
|
|
53
|
+
await userEvent.click(companyTab)
|
|
54
|
+
|
|
55
|
+
await userEvent.type(screen.getByPlaceholderText('Company Name'), 'Acme Corp')
|
|
56
|
+
await userEvent.click(screen.getByRole('button', { name: /screen entity/i }))
|
|
57
|
+
|
|
58
|
+
expect(handler).toHaveBeenCalledWith(
|
|
59
|
+
expect.objectContaining({
|
|
60
|
+
companyName: 'Acme Corp',
|
|
61
|
+
}),
|
|
62
|
+
'company'
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('disables submit button when loading', () => {
|
|
67
|
+
render(<ScreeningForm onSubmit={vi.fn()} loading={true} />)
|
|
68
|
+
expect(screen.getByRole('button', { name: /screening/i })).toBeDisabled()
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('shows loading text when loading', () => {
|
|
72
|
+
render(<ScreeningForm onSubmit={vi.fn()} loading={true} />)
|
|
73
|
+
expect(screen.getByText('Screening...')).toBeInTheDocument()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('prevents form submission with Enter key', async () => {
|
|
77
|
+
const handler = vi.fn()
|
|
78
|
+
render(<ScreeningForm onSubmit={handler} />)
|
|
79
|
+
const form = screen.getByPlaceholderText('First Name').closest('form')
|
|
80
|
+
await userEvent.type(form!, '{Enter}')
|
|
81
|
+
expect(handler).not.toHaveBeenCalled()
|
|
82
|
+
})
|
|
83
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Search } from 'lucide-react';
|
|
4
|
+
import type { EntityType } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface FormData {
|
|
7
|
+
firstName: string;
|
|
8
|
+
lastName: string;
|
|
9
|
+
companyName: string;
|
|
10
|
+
dob: string;
|
|
11
|
+
nationality: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ScreeningFormProps {
|
|
15
|
+
onSubmit: (data: FormData, type: EntityType) => void;
|
|
16
|
+
loading?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ScreeningForm({ onSubmit, loading }: ScreeningFormProps) {
|
|
20
|
+
const { t } = useTranslation('screening');
|
|
21
|
+
const [entityType, setEntityType] = useState<EntityType>('individual');
|
|
22
|
+
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
23
|
+
const [data, setData] = useState<FormData>({
|
|
24
|
+
firstName: '', lastName: '', companyName: '', dob: '', nationality: '',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
const tabClass = (active: boolean) =>
|
|
28
|
+
`flex-1 py-md text-center text-[14px] font-semibold rounded-apple-md transition-all cursor-pointer ${active
|
|
29
|
+
? 'bg-gradient-to-r from-apple-blue to-[#00D4AA] text-white shadow-[0_0_16px_rgba(10,132,255,0.3)]'
|
|
30
|
+
: 'text-apple-label-secondary hover:text-white hover:bg-white/[0.04]'}`;
|
|
31
|
+
|
|
32
|
+
const validate = (): boolean => {
|
|
33
|
+
const errs: Record<string, string> = {};
|
|
34
|
+
if (entityType === 'individual') {
|
|
35
|
+
if (!data.firstName.trim()) errs.firstName = 'First name is required';
|
|
36
|
+
if (!data.lastName.trim()) errs.lastName = 'Last name is required';
|
|
37
|
+
} else {
|
|
38
|
+
if (!data.companyName.trim()) errs.companyName = 'Company name is required';
|
|
39
|
+
}
|
|
40
|
+
setErrors(errs);
|
|
41
|
+
return Object.keys(errs).length === 0;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
if (validate()) onSubmit(data, entityType);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const inputCls = (field: string) =>
|
|
50
|
+
`input-field w-full ${errors[field] ? 'border-apple-red' : ''}`;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div className="card-vibrancy p-xl">
|
|
54
|
+
<div className="flex gap-sm p-xs bg-white/[0.03] rounded-apple-md mb-xl border border-white/[0.04]">
|
|
55
|
+
<button type="button" className={tabClass(entityType === 'individual')}
|
|
56
|
+
onClick={() => setEntityType('individual')}>{t('form.individual')}</button>
|
|
57
|
+
<button type="button" className={tabClass(entityType === 'company')}
|
|
58
|
+
onClick={() => setEntityType('company')}>{t('form.company')}</button>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
<form onSubmit={handleSubmit} className="space-y-lg">
|
|
62
|
+
{entityType === 'individual' ? (
|
|
63
|
+
<>
|
|
64
|
+
<div className="grid grid-cols-1 sm:grid-cols-2 gap-lg">
|
|
65
|
+
<div>
|
|
66
|
+
<input type="text" required aria-label={t('form.first_name')}
|
|
67
|
+
placeholder={t('form.first_name')} className={inputCls('firstName')}
|
|
68
|
+
value={data.firstName} onChange={(e) => setData({ ...data, firstName: e.target.value })} />
|
|
69
|
+
{errors.firstName && <p className="text-apple-red text-xs mt-1">{errors.firstName}</p>}
|
|
70
|
+
</div>
|
|
71
|
+
<div>
|
|
72
|
+
<input type="text" required aria-label={t('form.last_name')}
|
|
73
|
+
placeholder={t('form.last_name')} className={inputCls('lastName')}
|
|
74
|
+
value={data.lastName} onChange={(e) => setData({ ...data, lastName: e.target.value })} />
|
|
75
|
+
{errors.lastName && <p className="text-apple-red text-xs mt-1">{errors.lastName}</p>}
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<input type="date" aria-label={t('form.dob')} className="input-field w-full"
|
|
79
|
+
value={data.dob} onChange={(e) => setData({ ...data, dob: e.target.value })} />
|
|
80
|
+
<input type="text" aria-label={t('form.nationality')} placeholder={t('form.nationality')}
|
|
81
|
+
className="input-field w-full" value={data.nationality}
|
|
82
|
+
onChange={(e) => setData({ ...data, nationality: e.target.value })} />
|
|
83
|
+
</>
|
|
84
|
+
) : (
|
|
85
|
+
<div>
|
|
86
|
+
<input type="text" required aria-label={t('form.company_name')}
|
|
87
|
+
placeholder={t('form.company_name')} className={inputCls('companyName')}
|
|
88
|
+
value={data.companyName} onChange={(e) => setData({ ...data, companyName: e.target.value })} />
|
|
89
|
+
{errors.companyName && <p className="text-apple-red text-xs mt-1">{errors.companyName}</p>}
|
|
90
|
+
</div>
|
|
91
|
+
)}
|
|
92
|
+
<button type="submit" disabled={loading}
|
|
93
|
+
className="button-primary w-full flex items-center justify-center gap-sm disabled:opacity-50 text-[15px] font-bold tracking-wide">
|
|
94
|
+
<Search className="w-5 h-5" />
|
|
95
|
+
{loading ? t('form.submitting') : t('form.submit')}
|
|
96
|
+
</button>
|
|
97
|
+
</form>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { describe, it, expect } from 'vitest'
|
|
4
|
+
import { ScreeningLayersList } from './ScreeningLayersList'
|
|
5
|
+
|
|
6
|
+
describe('ScreeningLayersList', () => {
|
|
7
|
+
it('renders title', () => {
|
|
8
|
+
render(<ScreeningLayersList />)
|
|
9
|
+
expect(screen.getByText(/screening layers/i)).toBeInTheDocument()
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('renders all four layer toggles', () => {
|
|
13
|
+
render(<ScreeningLayersList />)
|
|
14
|
+
expect(screen.getByText(/ofac sdn/i)).toBeInTheDocument()
|
|
15
|
+
expect(screen.getByText(/eu sanctions/i)).toBeInTheDocument()
|
|
16
|
+
expect(screen.getByText(/un consolidated/i)).toBeInTheDocument()
|
|
17
|
+
expect(screen.getByText(/custom lists/i)).toBeInTheDocument()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('all toggles are checked by default', () => {
|
|
21
|
+
const { container } = render(<ScreeningLayersList />)
|
|
22
|
+
const greenToggles = container.querySelectorAll('div[class*="bg-apple-green"]')
|
|
23
|
+
expect(greenToggles.length).toBe(4)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('toggles a layer off when clicked', async () => {
|
|
27
|
+
const { container } = render(<ScreeningLayersList />)
|
|
28
|
+
const toggles = container.querySelectorAll('div[class*="rounded-full"]')
|
|
29
|
+
await userEvent.click(toggles[0])
|
|
30
|
+
const greenToggles = container.querySelectorAll('div[class*="bg-apple-green"]')
|
|
31
|
+
expect(greenToggles.length).toBe(3)
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Shield, Globe, Flag, Database } from 'lucide-react';
|
|
4
|
+
import { Card } from '../ui/Card';
|
|
5
|
+
import { Toggle } from '../ui/Toggle';
|
|
6
|
+
|
|
7
|
+
const layerConfig = [
|
|
8
|
+
{ key: 'ofac', icon: Shield, color: 'text-apple-red', glow: 'shadow-[0_0_12px_rgba(255,69,58,0.15)]', label: 'layers.ofac_sdn' },
|
|
9
|
+
{ key: 'eu', icon: Globe, color: 'text-apple-blue', glow: 'shadow-[0_0_12px_rgba(10,132,255,0.15)]', label: 'layers.eu_sanctions' },
|
|
10
|
+
{ key: 'un', icon: Flag, color: 'text-apple-orange', glow: 'shadow-[0_0_12px_rgba(255,159,10,0.15)]', label: 'layers.un_consolidated' },
|
|
11
|
+
{ key: 'custom', icon: Database, color: 'text-apple-green', glow: 'shadow-[0_0_12px_rgba(48,209,88,0.15)]', label: 'layers.custom_lists' },
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
type LayerKey = (typeof layerConfig)[number]['key'];
|
|
15
|
+
|
|
16
|
+
export function ScreeningLayersList() {
|
|
17
|
+
const { t } = useTranslation('screening');
|
|
18
|
+
const [layers, setLayers] = React.useState<Record<LayerKey, boolean>>({
|
|
19
|
+
ofac: true, eu: true, un: true, custom: true,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Card>
|
|
24
|
+
<h3 className="sf-headline mb-lg">{t('layers.title')}</h3>
|
|
25
|
+
<div className="space-y-sm">
|
|
26
|
+
{layerConfig.map(({ key, icon: Icon, color, glow, label }) => (
|
|
27
|
+
<div key={key}
|
|
28
|
+
className={`flex items-center gap-md p-md rounded-apple-md bg-white/[0.03]
|
|
29
|
+
border border-white/[0.04] transition-all hover:bg-white/[0.06]
|
|
30
|
+
${layers[key] ? glow : ''}`}>
|
|
31
|
+
<div className={`w-8 h-8 rounded-full bg-white/[0.06] flex items-center justify-center flex-shrink-0`}>
|
|
32
|
+
<Icon className={`w-4 h-4 ${color}`} />
|
|
33
|
+
</div>
|
|
34
|
+
<div className="flex-1">
|
|
35
|
+
<Toggle
|
|
36
|
+
checked={layers[key]}
|
|
37
|
+
onChange={(v) => setLayers({ ...layers, [key]: v })}
|
|
38
|
+
label={t(label)}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
))}
|
|
43
|
+
</div>
|
|
44
|
+
</Card>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react'
|
|
2
|
+
import { Shield, Search, Fingerprint, Volume2, Hash, Sparkles, Network, Check } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
const LAYERS = [
|
|
5
|
+
{ icon: Search, label: 'Exact Match', desc: 'Unicode normalized comparison' },
|
|
6
|
+
{ icon: Fingerprint, label: 'Fuzzy Match', desc: 'Jaro-Winkler similarity' },
|
|
7
|
+
{ icon: Volume2, label: 'Phonetic', desc: 'Soundex + Double Metaphone' },
|
|
8
|
+
{ icon: Hash, label: 'Token Match', desc: 'Jaccard coefficient' },
|
|
9
|
+
{ icon: Sparkles, label: 'AI Embeddings', desc: 'pgvector semantic search' },
|
|
10
|
+
{ icon: Network, label: 'Graph', desc: 'Relationship traversal' },
|
|
11
|
+
] as const
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
query: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function ScreeningProgress({ query }: Props) {
|
|
18
|
+
const [activeLayer, setActiveLayer] = useState(0)
|
|
19
|
+
const [completed, setCompleted] = useState<Set<number>>(new Set())
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
const interval = setInterval(() => {
|
|
23
|
+
setActiveLayer(prev => {
|
|
24
|
+
const next = prev + 1
|
|
25
|
+
if (next >= LAYERS.length) {
|
|
26
|
+
return 0
|
|
27
|
+
}
|
|
28
|
+
setCompleted(c => new Set([...c, prev]))
|
|
29
|
+
return next
|
|
30
|
+
})
|
|
31
|
+
}, 120)
|
|
32
|
+
return () => clearInterval(interval)
|
|
33
|
+
}, [])
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="max-w-2xl mx-auto">
|
|
37
|
+
<div className="text-center mb-xl">
|
|
38
|
+
<div className="relative inline-flex items-center justify-center w-20 h-20 mb-md">
|
|
39
|
+
<div className="absolute inset-0 rounded-full bg-apple-blue/20 blur-2xl animate-pulse" />
|
|
40
|
+
<div className="absolute inset-0 rounded-full border-2 border-apple-blue/30 animate-ping" />
|
|
41
|
+
<Shield className="relative w-10 h-10 text-apple-blue" />
|
|
42
|
+
</div>
|
|
43
|
+
<h3 className="sf-headline text-white mb-xs">Screening in Progress</h3>
|
|
44
|
+
<p className="sf-caption text-apple-label-secondary">
|
|
45
|
+
Analyzing <span className="text-apple-blue font-medium">{query}</span> across 2.17M entities
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="space-y-sm">
|
|
50
|
+
{LAYERS.map((layer, i) => {
|
|
51
|
+
const isActive = i === activeLayer
|
|
52
|
+
const isDone = completed.has(i)
|
|
53
|
+
const Icon = layer.icon
|
|
54
|
+
return (
|
|
55
|
+
<div key={i}
|
|
56
|
+
className={`flex items-center gap-md p-md rounded-apple-md transition-all duration-300
|
|
57
|
+
${isActive ? 'bg-apple-blue/10 border border-apple-blue/30 scale-[1.02]' : ''}
|
|
58
|
+
${isDone && !isActive ? 'bg-white/[0.02] border border-white/[0.04]' : ''}
|
|
59
|
+
${!isActive && !isDone ? 'bg-white/[0.01] border border-white/[0.02]' : ''}
|
|
60
|
+
`}>
|
|
61
|
+
<div className={`flex items-center justify-center w-9 h-9 rounded-apple-md transition-all
|
|
62
|
+
${isActive ? 'bg-apple-blue text-white' : ''}
|
|
63
|
+
${isDone && !isActive ? 'bg-apple-green/15 text-apple-green' : ''}
|
|
64
|
+
${!isActive && !isDone ? 'bg-white/5 text-apple-label-tertiary' : ''}
|
|
65
|
+
`}>
|
|
66
|
+
{isDone && !isActive ? <Check className="w-4 h-4" /> : <Icon className={`w-4 h-4 ${isActive ? 'animate-pulse' : ''}`} />}
|
|
67
|
+
</div>
|
|
68
|
+
<div className="flex-1">
|
|
69
|
+
<p className={`text-sm font-medium transition-colors
|
|
70
|
+
${isActive ? 'text-white' : 'text-apple-label-secondary'}`}>
|
|
71
|
+
{layer.label}
|
|
72
|
+
</p>
|
|
73
|
+
<p className="text-xs text-apple-label-tertiary">{layer.desc}</p>
|
|
74
|
+
</div>
|
|
75
|
+
{isActive && (
|
|
76
|
+
<div className="flex gap-xs">
|
|
77
|
+
<span className="w-1.5 h-1.5 rounded-full bg-apple-blue animate-pulse" />
|
|
78
|
+
<span className="w-1.5 h-1.5 rounded-full bg-apple-blue animate-pulse" style={{ animationDelay: '0.2s' }} />
|
|
79
|
+
<span className="w-1.5 h-1.5 rounded-full bg-apple-blue animate-pulse" style={{ animationDelay: '0.4s' }} />
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
{isDone && !isActive && (
|
|
83
|
+
<span className="text-xs text-apple-green font-medium">Done</span>
|
|
84
|
+
)}
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
})}
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
)
|
|
91
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router-dom'
|
|
3
|
+
import { ArrowUpCircle } from 'lucide-react'
|
|
4
|
+
import { screeningApi, ScreeningQuota } from '../../api/screening'
|
|
5
|
+
|
|
6
|
+
export function ScreeningQuotaBanner() {
|
|
7
|
+
const navigate = useNavigate()
|
|
8
|
+
const [quota, setQuota] = useState<ScreeningQuota | null>(null)
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
screeningApi.getQuota().then(setQuota).catch(() => null)
|
|
12
|
+
}, [])
|
|
13
|
+
|
|
14
|
+
if (!quota) return null
|
|
15
|
+
|
|
16
|
+
// Unlimited plan or no enforcer
|
|
17
|
+
if (quota.limit < 0) return null
|
|
18
|
+
|
|
19
|
+
const pct = quota.limit > 0 ? (quota.used / quota.limit) * 100 : 0
|
|
20
|
+
const isLow = pct >= 80
|
|
21
|
+
const isExhausted = quota.remaining <= 0
|
|
22
|
+
|
|
23
|
+
const barColor = isExhausted
|
|
24
|
+
? 'bg-apple-red'
|
|
25
|
+
: isLow
|
|
26
|
+
? 'bg-amber-500'
|
|
27
|
+
: 'bg-apple-green'
|
|
28
|
+
|
|
29
|
+
const borderColor = isExhausted
|
|
30
|
+
? 'border-apple-red/20'
|
|
31
|
+
: isLow
|
|
32
|
+
? 'border-amber-500/20'
|
|
33
|
+
: 'border-white/[0.06]'
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className={`rounded-apple-md border ${borderColor} bg-white/[0.02] px-lg py-md`}>
|
|
37
|
+
<div className="flex items-center justify-between mb-xs">
|
|
38
|
+
<span className="text-xs text-apple-label-secondary">
|
|
39
|
+
{quota.used.toLocaleString()} / {quota.limit.toLocaleString()} screenings used
|
|
40
|
+
</span>
|
|
41
|
+
{isExhausted ? (
|
|
42
|
+
<button onClick={() => navigate('/billing')}
|
|
43
|
+
className="text-xs text-apple-blue hover:underline cursor-pointer font-medium">
|
|
44
|
+
Upgrade Plan
|
|
45
|
+
</button>
|
|
46
|
+
) : isLow ? (
|
|
47
|
+
<span className="text-xs text-amber-400">
|
|
48
|
+
{quota.remaining.toLocaleString()} remaining
|
|
49
|
+
</span>
|
|
50
|
+
) : (
|
|
51
|
+
<span className="text-xs text-apple-label-tertiary">
|
|
52
|
+
{quota.remaining.toLocaleString()} remaining
|
|
53
|
+
</span>
|
|
54
|
+
)}
|
|
55
|
+
</div>
|
|
56
|
+
<div className="w-full h-1.5 bg-white/10 rounded-full overflow-hidden">
|
|
57
|
+
<div
|
|
58
|
+
className={`h-full ${barColor} rounded-full transition-all duration-500`}
|
|
59
|
+
style={{ width: `${Math.min(pct, 100)}%` }}
|
|
60
|
+
/>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
)
|
|
64
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { CircularConfidence } from './CircularConfidence';
|
|
4
|
+
|
|
5
|
+
interface Result {
|
|
6
|
+
name: string;
|
|
7
|
+
confidence: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
result: Result;
|
|
12
|
+
index: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ScreeningResultCard({ result, index }: Props) {
|
|
16
|
+
const { t } = useTranslation('screening');
|
|
17
|
+
const delay = `${index * 80}ms`;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div
|
|
21
|
+
className="card-vibrancy p-lg flex items-center gap-xl animate-fade-in"
|
|
22
|
+
style={{ animationDelay: delay }}
|
|
23
|
+
>
|
|
24
|
+
<CircularConfidence score={result.confidence} />
|
|
25
|
+
<div className="flex-1 min-w-0">
|
|
26
|
+
<h4 className="sf-headline truncate">{result.name}</h4>
|
|
27
|
+
<p className="sf-caption">{t('results.match_found')}</p>
|
|
28
|
+
<ConfidenceBar score={result.confidence} />
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ConfidenceBar({ score }: { score: number }) {
|
|
35
|
+
const color = score >= 80 ? 'from-apple-red to-apple-orange'
|
|
36
|
+
: score >= 60 ? 'from-apple-orange to-apple-yellow'
|
|
37
|
+
: 'from-apple-green to-apple-blue';
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="mt-md h-1.5 w-full bg-white/5 rounded-full overflow-hidden">
|
|
41
|
+
<div
|
|
42
|
+
className={`h-full bg-gradient-to-r ${color} rounded-full transition-all duration-500`}
|
|
43
|
+
style={{ width: `${score}%` }}
|
|
44
|
+
/>
|
|
45
|
+
</div>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Card } from '../ui/Card'
|
|
2
|
+
import { MatchDetailHeader } from './MatchDetailHeader'
|
|
3
|
+
import { MatchEntityInfo } from './MatchEntityInfo'
|
|
4
|
+
import { MatchEvidenceBars } from './MatchEvidenceBars'
|
|
5
|
+
import { MatchMetadata } from './MatchMetadata'
|
|
6
|
+
import type { ScreenMatch } from '../../types'
|
|
7
|
+
|
|
8
|
+
export type { ScreenMatch }
|
|
9
|
+
|
|
10
|
+
interface Props {
|
|
11
|
+
match: ScreenMatch
|
|
12
|
+
index?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ScreeningResultRow({ match, index = 0 }: Props) {
|
|
16
|
+
const delay = `${index * 60}ms`
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="animate-fade-in" style={{ animationDelay: delay }}>
|
|
20
|
+
<Card>
|
|
21
|
+
<div className="space-y-lg">
|
|
22
|
+
<MatchDetailHeader match={match} />
|
|
23
|
+
|
|
24
|
+
<div className="border-t border-white/[0.06] pt-md">
|
|
25
|
+
<MatchEntityInfo match={match} />
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div className="border-t border-white/[0.06] pt-md">
|
|
29
|
+
<MatchEvidenceBars layers={match.layers} />
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{match.explanation && (
|
|
33
|
+
<p className="sf-caption text-white/50 italic">
|
|
34
|
+
{match.explanation}
|
|
35
|
+
</p>
|
|
36
|
+
)}
|
|
37
|
+
|
|
38
|
+
<div className="border-t border-white/[0.06] pt-md">
|
|
39
|
+
<MatchMetadata metadata={match.metadata} />
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</Card>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react'
|
|
2
|
+
import { describe, it, expect } from 'vitest'
|
|
3
|
+
import { ScreeningResults } from './ScreeningResults'
|
|
4
|
+
|
|
5
|
+
const mockResults = [
|
|
6
|
+
{ name: 'John Doe', confidence: 0.95 },
|
|
7
|
+
{ name: 'Jane Smith', confidence: 0.72 },
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
describe('ScreeningResults', () => {
|
|
11
|
+
it('renders results title', () => {
|
|
12
|
+
render(<ScreeningResults results={mockResults} />)
|
|
13
|
+
expect(screen.getByText(/screening results/i)).toBeInTheDocument()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('renders all result names', () => {
|
|
17
|
+
render(<ScreeningResults results={mockResults} />)
|
|
18
|
+
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
|
19
|
+
expect(screen.getByText('Jane Smith')).toBeInTheDocument()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('renders match description for each result', () => {
|
|
23
|
+
render(<ScreeningResults results={mockResults} />)
|
|
24
|
+
const matches = screen.getAllByText(/match found/i)
|
|
25
|
+
expect(matches).toHaveLength(2)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('renders empty list for no results', () => {
|
|
29
|
+
const { container } = render(<ScreeningResults results={[]} />)
|
|
30
|
+
expect(container.querySelectorAll('[class*="space-y-md"] > div')).toHaveLength(0)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('renders one result correctly', () => {
|
|
34
|
+
render(<ScreeningResults results={[{ name: 'Solo', confidence: 0.5 }]} />)
|
|
35
|
+
expect(screen.getByText('Solo')).toBeInTheDocument()
|
|
36
|
+
})
|
|
37
|
+
})
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ScreeningResultCard } from './ScreeningResultCard';
|
|
4
|
+
|
|
5
|
+
interface Result {
|
|
6
|
+
name: string;
|
|
7
|
+
confidence: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface ResultsProps {
|
|
11
|
+
results: Result[];
|
|
12
|
+
processingTime?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ScreeningResults({ results, processingTime }: ResultsProps) {
|
|
16
|
+
const { t } = useTranslation('screening');
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="mt-xxl">
|
|
20
|
+
<div className="flex items-center justify-between mb-lg">
|
|
21
|
+
<h2 className="sf-headline text-xl">{t('results.title')}</h2>
|
|
22
|
+
{processingTime !== undefined && (
|
|
23
|
+
<span className="badge-blue">{processingTime}ms</span>
|
|
24
|
+
)}
|
|
25
|
+
</div>
|
|
26
|
+
<div className="space-y-md">
|
|
27
|
+
{results.map((result, idx) => (
|
|
28
|
+
<ScreeningResultCard key={idx} result={result} index={idx} />
|
|
29
|
+
))}
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
);
|
|
33
|
+
}
|