From 4c9d53cabafc49756c1b3c2227272d08492c82a2 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Tue, 17 Mar 2026 10:23:13 -0400
Subject: [PATCH] fix: bring back vendor research
[dev] [Marfuen] mariano/vendor-research
---
.../components/VendorDetailTabs.tsx | 77 ++++----
.../[vendorId]/components/VendorHeader.tsx | 160 ---------------
.../components/VendorPageClient.test.tsx | 183 ------------------
.../components/VendorPageClient.tsx | 112 -----------
.../components/VendorResearchSection.tsx | 132 +++++++++++++
...VendorRiskAssessmentCertificationsCard.tsx | 68 +++----
.../VendorRiskAssessmentView.tsx | 82 ++++++--
bun.lock | 18 +-
package.json | 2 +-
9 files changed, 284 insertions(+), 550 deletions(-)
delete mode 100644 apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorHeader.tsx
delete mode 100644 apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorPageClient.test.tsx
delete mode 100644 apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorPageClient.tsx
create mode 100644 apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorResearchSection.tsx
diff --git a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorDetailTabs.tsx b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorDetailTabs.tsx
index 56fed47b10..c848ec4a31 100644
--- a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorDetailTabs.tsx
+++ b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorDetailTabs.tsx
@@ -11,6 +11,7 @@ import { useTaskItems, useTaskItemActions } from '@/hooks/use-task-items';
import { useVendor, useVendorActions, type VendorResponse } from '@/hooks/use-vendors';
import { usePermissions } from '@/hooks/use-permissions';
import { SecondaryFields } from './secondary-fields/secondary-fields';
+import { VendorResearchBadges, VendorResearchLinks } from './VendorResearchSection';
import { VendorInherentRiskChart } from './VendorInherentRiskChart';
import { VendorResidualRiskChart } from './VendorResidualRiskChart';
import type { Member, User, Vendor } from '@db';
@@ -259,7 +260,7 @@ export function VendorDetailTabs({
/>
-
+
{isEditingTitle ? (
)}
-
+ {!isViewingTask && (
+
+ )}
+
+ {!isViewingTask && (
+ isEditingDescription ? (
+
diff --git a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorHeader.tsx b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorHeader.tsx
deleted file mode 100644
index 63d823ac51..0000000000
--- a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorHeader.tsx
+++ /dev/null
@@ -1,160 +0,0 @@
-'use client';
-
-import {
- HIPAA,
- ISO27001,
- ISO42001,
- SOC2Type1,
- SOC2Type2,
-} from '@/app/(app)/[orgId]/trust/portal-settings/components/logos';
-import { filterCertifications } from '@/components/vendor-risk-assessment/filter-certifications';
-import { parseVendorRiskAssessmentDescription } from '@/components/vendor-risk-assessment/parse-vendor-risk-assessment-description';
-import type { VendorRiskAssessmentCertification } from '@/components/vendor-risk-assessment/vendor-risk-assessment-types';
-import { cn } from '@/lib/utils';
-import type { Vendor } from '@db';
-import type { Prisma } from '@prisma/client';
-import { Button } from '@trycompai/design-system';
-import { Launch } from '@trycompai/design-system/icons';
-import Link from 'next/link';
-import { useMemo } from 'react';
-
-// Vendor with risk assessment data merged from GlobalVendors
-type VendorWithRiskAssessment = Vendor & {
- riskAssessmentData?: Prisma.InputJsonValue | null;
- riskAssessmentVersion?: string | null;
- riskAssessmentUpdatedAt?: Date | null;
-};
-
-interface VendorHeaderProps {
- vendor: VendorWithRiskAssessment;
-}
-
-/**
- * Get the compliance icon component for a certification type
- */
-function getCertificationIcon(cert: VendorRiskAssessmentCertification) {
- const typeLower = cert.type.toLowerCase().trim();
-
- // ISO 27001
- if (typeLower.includes('iso') && typeLower.includes('27001')) {
- return ISO27001;
- }
-
- // ISO 42001
- if (typeLower.includes('iso') && typeLower.includes('42001')) {
- return ISO42001;
- }
-
- // SOC 2 Type 1
- if (
- typeLower.includes('soc') &&
- (typeLower.includes('type 1') || typeLower.includes('type i')) &&
- !typeLower.includes('type 2') &&
- !typeLower.includes('type ii')
- ) {
- return SOC2Type1;
- }
-
- // SOC 2 Type 2
- if (
- typeLower.includes('soc') &&
- (typeLower.includes('type 2') || typeLower.includes('type ii'))
- ) {
- return SOC2Type2;
- }
-
- // HIPAA
- if (typeLower === 'hipaa' || typeLower === 'hipa') {
- return HIPAA;
- }
-
- return null;
-}
-
-export function VendorHeader({ vendor }: VendorHeaderProps) {
- // Parse risk assessment data to get certifications and links
- // Note: This should come from GlobalVendors, but we're reading from vendor for now
- // TODO: Update to fetch from GlobalVendors via vendor.website lookup
- const { certifications, links } = useMemo(() => {
- if (!vendor.riskAssessmentData) return { certifications: [], links: [] };
- const data = parseVendorRiskAssessmentDescription(
- typeof vendor.riskAssessmentData === 'string'
- ? vendor.riskAssessmentData
- : JSON.stringify(vendor.riskAssessmentData),
- );
- return {
- certifications: filterCertifications(data?.certifications),
- links: data?.links ?? [],
- };
- }, [vendor.riskAssessmentData]);
-
- return (
-
-
-
{vendor.name}
- {certifications.filter((cert) => cert.status === 'verified').length > 0 && (
-
- {certifications
- .filter((cert) => {
- // Only show verified certifications
- return cert.status === 'verified';
- })
- .map((cert, index) => {
- const IconComponent = getCertificationIcon(cert);
-
- if (!IconComponent) return null;
-
- const iconContent = (
-
-
-
- );
-
- if (cert.url) {
- return (
-
- {iconContent}
-
- );
- }
-
- return
{iconContent}
;
- })}
-
- )}
-
- {vendor.description &&
{vendor.description}
}
- {links.length > 0 && (
-
- {links.map((link, index) => {
- return (
- }
- onClick={() => window.open(link.url, '_blank', 'noopener,noreferrer')}
- >
- {link.label}
-
- );
- })}
-
- )}
-
- );
-}
diff --git a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorPageClient.test.tsx b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorPageClient.test.tsx
deleted file mode 100644
index 1c35ffbab9..0000000000
--- a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorPageClient.test.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-import { render, screen } from '@testing-library/react';
-import { beforeEach, describe, expect, it, vi } from 'vitest';
-import {
- setMockPermissions,
- mockHasPermission,
- ADMIN_PERMISSIONS,
- AUDITOR_PERMISSIONS,
-} from '@/test-utils/mocks/permissions';
-
-// Mock usePermissions
-vi.mock('@/hooks/use-permissions', () => ({
- usePermissions: () => ({
- permissions: {},
- hasPermission: mockHasPermission,
- }),
-}));
-
-// Mock useVendor
-vi.mock('@/hooks/use-vendors', () => ({
- useVendor: () => ({
- vendor: null,
- mutate: vi.fn(),
- }),
-}));
-
-// Mock child components
-vi.mock('./VendorHeader', () => ({
- VendorHeader: ({ vendor }: any) => (
- {vendor.name}
- ),
-}));
-
-vi.mock('./secondary-fields/secondary-fields', () => ({
- SecondaryFields: () => ,
-}));
-
-let inherentChartProps: any = null;
-let residualChartProps: any = null;
-
-vi.mock('./VendorInherentRiskChart', () => ({
- VendorInherentRiskChart: (props: any) => {
- inherentChartProps = props;
- return ;
- },
-}));
-
-vi.mock('./VendorResidualRiskChart', () => ({
- VendorResidualRiskChart: (props: any) => {
- residualChartProps = props;
- return ;
- },
-}));
-
-vi.mock('@/components/task-items/TaskItems', () => ({
- TaskItems: () => ,
-}));
-
-vi.mock('@/components/comments/Comments', () => ({
- Comments: () => ,
-}));
-
-import { VendorPageClient } from './VendorPageClient';
-
-const mockVendor: any = {
- id: 'vendor-1',
- name: 'Test Vendor',
- description: 'A test vendor',
- category: 'cloud',
- status: 'assessed',
- inherentProbability: 'possible',
- inherentImpact: 'moderate',
- residualProbability: 'unlikely',
- residualImpact: 'minor',
- website: null,
- isSubProcessor: false,
- logoUrl: null,
- showOnTrustPortal: false,
- trustPortalOrder: null,
- complianceBadges: null,
- organizationId: 'org-1',
- assigneeId: null,
- assignee: null,
- createdAt: new Date('2024-01-01'),
- updatedAt: new Date('2024-01-01'),
- riskAssessmentData: null,
- riskAssessmentVersion: null,
- riskAssessmentUpdatedAt: null,
-};
-
-const mockAssignees: any[] = [];
-
-describe('VendorPageClient', () => {
- beforeEach(() => {
- setMockPermissions({});
- inherentChartProps = null;
- residualChartProps = null;
- });
-
- it('renders vendor header, risk charts, task items, and comments with no permissions', () => {
- setMockPermissions({});
-
- render(
- ,
- );
-
- // Component renders all sections regardless of permissions
- // Permission gating happens inside child components (VendorInherentRiskChart, VendorResidualRiskChart)
- expect(screen.getByTestId('vendor-header')).toBeInTheDocument();
- expect(screen.getByTestId('secondary-fields')).toBeInTheDocument();
- expect(screen.getByTestId('inherent-risk-chart')).toBeInTheDocument();
- expect(screen.getByTestId('residual-risk-chart')).toBeInTheDocument();
- expect(screen.getByTestId('task-items')).toBeInTheDocument();
- expect(screen.getByTestId('comments')).toBeInTheDocument();
- });
-
- it('renders all sections with admin permissions', () => {
- setMockPermissions(ADMIN_PERMISSIONS);
-
- render(
- ,
- );
-
- expect(screen.getByTestId('vendor-header')).toBeInTheDocument();
- expect(screen.getByTestId('secondary-fields')).toBeInTheDocument();
- expect(screen.getByTestId('inherent-risk-chart')).toBeInTheDocument();
- expect(screen.getByTestId('residual-risk-chart')).toBeInTheDocument();
- expect(screen.getByTestId('task-items')).toBeInTheDocument();
- expect(screen.getByTestId('comments')).toBeInTheDocument();
- });
-
- it('hides header, secondary fields, risk charts, and comments when isViewingTask is true', () => {
- setMockPermissions(ADMIN_PERMISSIONS);
-
- render(
- ,
- );
-
- expect(screen.queryByTestId('vendor-header')).not.toBeInTheDocument();
- expect(screen.queryByTestId('secondary-fields')).not.toBeInTheDocument();
- expect(screen.queryByTestId('inherent-risk-chart')).not.toBeInTheDocument();
- expect(screen.queryByTestId('residual-risk-chart')).not.toBeInTheDocument();
- expect(screen.queryByTestId('comments')).not.toBeInTheDocument();
- // TaskItems should still be visible
- expect(screen.getByTestId('task-items')).toBeInTheDocument();
- });
-
- it('renders for auditor role with limited permissions', () => {
- setMockPermissions(AUDITOR_PERMISSIONS);
-
- render(
- ,
- );
-
- // VendorPageClient itself renders all sections; gating is in child components
- expect(screen.getByTestId('vendor-header')).toBeInTheDocument();
- expect(screen.getByTestId('inherent-risk-chart')).toBeInTheDocument();
- expect(screen.getByTestId('residual-risk-chart')).toBeInTheDocument();
- });
-});
diff --git a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorPageClient.tsx b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorPageClient.tsx
deleted file mode 100644
index 3fc4ffb30c..0000000000
--- a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorPageClient.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-'use client';
-
-import { Comments } from '@/components/comments/Comments';
-import { TaskItems } from '@/components/task-items/TaskItems';
-import { useVendor, type VendorResponse } from '@/hooks/use-vendors';
-import type { Member, User, Vendor } from '@db';
-import { CommentEntityType } from '@db';
-import type { Prisma } from '@prisma/client';
-import { useMemo } from 'react';
-import { usePermissions } from '@/hooks/use-permissions';
-import { SecondaryFields } from './secondary-fields/secondary-fields';
-import { VendorHeader } from './VendorHeader';
-import { VendorInherentRiskChart } from './VendorInherentRiskChart';
-import { VendorResidualRiskChart } from './VendorResidualRiskChart';
-
-// Vendor with risk assessment data merged from GlobalVendors
-type VendorWithRiskAssessment = Vendor & {
- assignee: { user: User | null } | null;
- riskAssessmentData?: Prisma.InputJsonValue | null;
- riskAssessmentVersion?: string | null;
- riskAssessmentUpdatedAt?: Date | null;
-};
-
-/**
- * Normalize API response to match Prisma types
- * API returns dates as strings, Prisma returns Date objects
- */
-function normalizeVendor(apiVendor: VendorResponse): VendorWithRiskAssessment {
- return {
- ...apiVendor,
- createdAt: new Date(apiVendor.createdAt),
- updatedAt: new Date(apiVendor.updatedAt),
- riskAssessmentUpdatedAt: apiVendor.riskAssessmentUpdatedAt
- ? new Date(apiVendor.riskAssessmentUpdatedAt)
- : null,
- assignee: apiVendor.assignee
- ? {
- ...apiVendor.assignee,
- user: apiVendor.assignee.user as User | null,
- }
- : null,
- } as unknown as VendorWithRiskAssessment;
-}
-
-interface VendorPageClientProps {
- vendorId: string;
- orgId: string;
- initialVendor: VendorWithRiskAssessment;
- assignees: (Member & { user: User })[];
- isViewingTask: boolean;
-}
-
-/**
- * Client component for vendor detail page content
- * Uses SWR for real-time updates and caching
- *
- * Benefits:
- * - Instant initial render (uses server-fetched data)
- * - Real-time updates via polling (5s interval)
- * - Mutations trigger automatic refresh via mutate()
- */
-export function VendorPageClient({
- vendorId,
- orgId,
- initialVendor,
- assignees,
- isViewingTask,
-}: VendorPageClientProps) {
- // Use SWR for real-time updates with polling
- const { vendor: swrVendor, mutate: refreshVendor } = useVendor(vendorId);
- const { hasPermission } = usePermissions();
-
- // Normalize and memoize the vendor data
- // Use SWR data when available, fall back to initial data
- const vendor = useMemo(() => {
- if (swrVendor) {
- return normalizeVendor(swrVendor);
- }
- return initialVendor;
- }, [swrVendor, initialVendor]);
-
- return (
- <>
- {!isViewingTask && }
-
- {!isViewingTask && (
- <>
-
-
-
-
-
- >
- )}
-
- {!isViewingTask && (
-
- )}
-
- >
- );
-}
-
-/**
- * Export the vendor mutate function for use by mutation components
- * Call this after updating vendor data to trigger SWR revalidation
- */
-export { useVendor } from '@/hooks/use-vendors';
diff --git a/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorResearchSection.tsx b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorResearchSection.tsx
new file mode 100644
index 0000000000..2f1387970f
--- /dev/null
+++ b/apps/app/src/app/(app)/[orgId]/vendors/[vendorId]/components/VendorResearchSection.tsx
@@ -0,0 +1,132 @@
+'use client';
+
+import {
+ HIPAA,
+ ISO27001,
+ ISO42001,
+ SOC2Type1,
+ SOC2Type2,
+} from '@/app/(app)/[orgId]/trust/portal-settings/components/logos';
+import { filterCertifications } from '@/components/vendor-risk-assessment/filter-certifications';
+import { parseVendorRiskAssessmentDescription } from '@/components/vendor-risk-assessment/parse-vendor-risk-assessment-description';
+import type { VendorRiskAssessmentCertification } from '@/components/vendor-risk-assessment/vendor-risk-assessment-types';
+import { cn } from '@/lib/utils';
+import type { Prisma } from '@prisma/client';
+import { Button } from '@trycompai/design-system';
+import { Launch } from '@trycompai/design-system/icons';
+import Link from 'next/link';
+import { useMemo } from 'react';
+
+function getCertificationIcon(cert: VendorRiskAssessmentCertification) {
+ const typeLower = cert.type.toLowerCase().trim();
+
+ if (typeLower.includes('iso') && typeLower.includes('27001')) return ISO27001;
+ if (typeLower.includes('iso') && typeLower.includes('42001')) return ISO42001;
+ if (
+ typeLower.includes('soc') &&
+ (typeLower.includes('type 1') || typeLower.includes('type i')) &&
+ !typeLower.includes('type 2') &&
+ !typeLower.includes('type ii')
+ ) return SOC2Type1;
+ if (
+ typeLower.includes('soc') &&
+ (typeLower.includes('type 2') || typeLower.includes('type ii'))
+ ) return SOC2Type2;
+ if (typeLower === 'hipaa' || typeLower === 'hipa') return HIPAA;
+
+ return null;
+}
+
+function useVendorResearchData(riskAssessmentData?: Prisma.InputJsonValue | null) {
+ return useMemo(() => {
+ if (!riskAssessmentData) return { certifications: [], links: [] };
+ const data = parseVendorRiskAssessmentDescription(
+ typeof riskAssessmentData === 'string'
+ ? riskAssessmentData
+ : JSON.stringify(riskAssessmentData),
+ );
+ return {
+ certifications: filterCertifications(data?.certifications).filter(
+ (cert) => cert.status === 'verified',
+ ),
+ links: data?.links ?? [],
+ };
+ }, [riskAssessmentData]);
+}
+
+interface VendorResearchProps {
+ riskAssessmentData?: Prisma.InputJsonValue | null;
+}
+
+export function VendorResearchBadges({ riskAssessmentData }: VendorResearchProps) {
+ const { certifications } = useVendorResearchData(riskAssessmentData);
+
+ if (certifications.length === 0) return null;
+
+ return (
+
+ {certifications.map((cert, index) => {
+ const IconComponent = getCertificationIcon(cert);
+ if (!IconComponent) return null;
+
+ const iconContent = (
+
+
+
+ );
+
+ if (cert.url) {
+ return (
+
+ {iconContent}
+
+ );
+ }
+
+ return
{iconContent}
;
+ })}
+
+ );
+}
+
+export function VendorResearchLinks({ riskAssessmentData }: VendorResearchProps) {
+ const { links } = useVendorResearchData(riskAssessmentData);
+
+ if (links.length === 0) return null;
+
+ return (
+
+ {links.map((link, index) => (
+
+ }
+ >
+ {link.label}
+
+
+ ))}
+
+ );
+}
diff --git a/apps/app/src/components/vendor-risk-assessment/VendorRiskAssessmentCertificationsCard.tsx b/apps/app/src/components/vendor-risk-assessment/VendorRiskAssessmentCertificationsCard.tsx
index 9689a3b4b5..d959c8f495 100644
--- a/apps/app/src/components/vendor-risk-assessment/VendorRiskAssessmentCertificationsCard.tsx
+++ b/apps/app/src/components/vendor-risk-assessment/VendorRiskAssessmentCertificationsCard.tsx
@@ -1,10 +1,7 @@
'use client';
-import { Badge } from '@trycompai/ui/badge';
-import { Button } from '@trycompai/ui/button';
-import { Card, CardContent, CardHeader, CardTitle } from '@trycompai/ui/card';
-import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@trycompai/ui/collapsible';
-import { Clock, ExternalLink, Shield, ShieldCheck, XCircle, ChevronDown, ChevronUp } from 'lucide-react';
+import { Badge, Button, Card, CardContent, CardHeader, CardTitle, Collapsible, CollapsibleContent, CollapsibleTrigger } from '@trycompai/design-system';
+import { CheckmarkFilled, ChevronDown, ChevronUp, CloseOutline, Launch, Security, Time } from '@trycompai/design-system/icons';
import { useMemo, useState } from 'react';
import { filterCertifications } from './filter-certifications';
import type { VendorRiskAssessmentCertification } from './vendor-risk-assessment-types';
@@ -12,18 +9,18 @@ import type { VendorRiskAssessmentCertification } from './vendor-risk-assessment
function CertificationRow({ cert }: { cert: VendorRiskAssessmentCertification }) {
const statusIcon =
cert.status === 'verified' ? (
-
+
) : cert.status === 'expired' ? (
-
+
) : cert.status === 'not_certified' ? (
-
+
) : (
-
+
);
const statusBadge =
cert.status === 'verified' ? (
- verified
+ verified
) : cert.status === 'expired' ? (
expired
) : cert.status === 'not_certified' ? (
@@ -46,7 +43,7 @@ function CertificationRow({ cert }: { cert: VendorRiskAssessmentCertification })
onClick={() => window.open(cert.url ?? undefined, '_blank', 'noopener,noreferrer')}
aria-label="Open certification link"
>
-
+
) : null}
@@ -79,17 +76,19 @@ export function VendorRiskAssessmentCertificationsCard({
return (
-
-
-
- Certifications
+
+
+
+
+ Certifications
+
+ {filteredCerts.length > 0 ? (
+
{filteredVerifiedCount} verified
+ ) : null}
- {filteredCerts.length > 0 ? (
- {filteredVerifiedCount} verified
- ) : null}
-
+
{filteredCerts.length === 0 ? (
No certifications found.
) : (
@@ -113,23 +112,20 @@ export function VendorRiskAssessmentCertificationsCard({
{rest.length > 0 ? (
-
-
+
+ {open ? (
+ <>
+ Show less
+
+ >
+ ) : (
+ <>
+ Show {rest.length} more
+
+ >
+ )}
) : null}
diff --git a/apps/app/src/components/vendor-risk-assessment/VendorRiskAssessmentView.tsx b/apps/app/src/components/vendor-risk-assessment/VendorRiskAssessmentView.tsx
index 395dce9050..56dfdf2602 100644
--- a/apps/app/src/components/vendor-risk-assessment/VendorRiskAssessmentView.tsx
+++ b/apps/app/src/components/vendor-risk-assessment/VendorRiskAssessmentView.tsx
@@ -1,9 +1,11 @@
'use client';
-import { Card, CardContent, CardHeader, CardTitle } from '@trycompai/ui/card';
-import { Shield } from 'lucide-react';
+import { Button, Card, CardContent, CardHeader, CardTitle } from '@trycompai/design-system';
+import { Launch, Security } from '@trycompai/design-system/icons';
import { useMemo } from 'react';
import { parseVendorRiskAssessmentDescription } from './parse-vendor-risk-assessment-description';
+import { filterCertifications } from './filter-certifications';
+import { VendorRiskAssessmentCertificationsCard } from './VendorRiskAssessmentCertificationsCard';
import { VendorRiskAssessmentTimelineCard } from './VendorRiskAssessmentTimelineCard';
import { SecurityAssessmentContent } from './SecurityAssessmentContent';
@@ -21,38 +23,76 @@ export function VendorRiskAssessmentView({ source }: { source: VendorRiskAssessm
return parseVendorRiskAssessmentDescription(source.description);
}, [source.description]);
+ const certifications = data?.certifications ?? [];
+ const filteredCerts = useMemo(() => filterCertifications(certifications), [certifications]);
+ const verifiedCount = useMemo(
+ () => filteredCerts.filter((c) => c.status === 'verified').length,
+ [filteredCerts],
+ );
const links = data?.links ?? [];
const news = data?.news ?? [];
return (
-
+
+
+
+
+
+ Security Assessment
+
+
+
+
+ {data?.securityAssessment ? (
+
+ ) : (
+
+ No automated security assessment found.
+
+ )}
+
+
+
+ {certifications.length > 0 && (
+
+ )}
+
+ {links.length > 0 && (
-
-
- Security Assessment
+
+ Links
- {data?.securityAssessment ? (
-
- ) : (
-
- No automated security assessment found.
-
- )}
+
+ {links.map((link, index) => (
+ }
+ onClick={() => window.open(link.url, '_blank', 'noopener,noreferrer')}
+ >
+ {link.label}
+
+ ))}
+
+ )}
-
-
+
);
}
diff --git a/bun.lock b/bun.lock
index fd32e62666..402c2b6e55 100644
--- a/bun.lock
+++ b/bun.lock
@@ -5,7 +5,7 @@
"": {
"name": "comp",
"dependencies": {
- "@trycompai/design-system": "^1.0.43",
+ "@trycompai/design-system": "1.0.45",
"@types/cheerio": "^1.0.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@upstash/vector": "^1.2.2",
@@ -2326,7 +2326,7 @@
"@trycompai/db": ["@trycompai/db@workspace:packages/db"],
- "@trycompai/design-system": ["@trycompai/design-system@1.0.43", "", { "dependencies": { "@base-ui/react": "^1.0.0", "@carbon/icons-react": "^11.72.0", "@fontsource-variable/plus-jakarta-sans": "^5.2.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "next-themes": "^0.4.6", "react-day-picker": "^9.13.0", "react-resizable-panels": "^4.2.0", "recharts": "2.15.4", "shadcn": "^3.6.2", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", "tailwindcss": "^4.0.0" } }, "sha512-iPJrwtCJDbFEQyjZJDNQh+4J86H7Bt5WReM+Yaf0IBdB+2z5pF3FsQrMkkew9GvQSztIxXiWMiHoDVOWAVG9ow=="],
+ "@trycompai/design-system": ["@trycompai/design-system@1.0.45", "", { "dependencies": { "@base-ui/react": "^1.0.0", "@carbon/icons-react": "^11.72.0", "@fontsource-variable/plus-jakarta-sans": "^5.2.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "next-themes": "^0.4.6", "react-day-picker": "^9.13.0", "react-resizable-panels": "^4.2.0", "recharts": "2.15.4", "shadcn": "^3.6.2", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", "tailwindcss": "^4.0.0" } }, "sha512-sBPNClQIh1RZOrHMsgFJEDamt0GI23BGsxbh4xZlwbQJxXZsniXQWh0F3cV/M2YLXKSoDnCWAj1QdcJ1V5tCfQ=="],
"@trycompai/device-agent": ["@trycompai/device-agent@workspace:packages/device-agent"],
@@ -6710,6 +6710,8 @@
"@trycompai/email/resend": ["resend@4.8.0", "", { "dependencies": { "@react-email/render": "1.1.2" } }, "sha512-R8eBOFQDO6dzRTDmaMEdpqrkmgSjPpVXt4nGfWsZdYOet0kqra0xgbvTES6HmCriZEXbmGk3e0DiGIaLFTFSHA=="],
+ "@trycompai/portal/@trycompai/design-system": ["@trycompai/design-system@1.0.43", "", { "dependencies": { "@base-ui/react": "^1.0.0", "@carbon/icons-react": "^11.72.0", "@fontsource-variable/plus-jakarta-sans": "^5.2.8", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", "date-fns": "^4.1.0", "embla-carousel-react": "^8.6.0", "input-otp": "^1.4.2", "next-themes": "^0.4.6", "react-day-picker": "^9.13.0", "react-resizable-panels": "^4.2.0", "recharts": "2.15.4", "shadcn": "^3.6.2", "sonner": "^2.0.7", "tailwind-merge": "^3.4.0", "tw-animate-css": "^1.4.0", "vaul": "^1.1.2" }, "peerDependencies": { "react": "^19.0.0", "react-dom": "^19.0.0", "tailwindcss": "^4.0.0" } }, "sha512-iPJrwtCJDbFEQyjZJDNQh+4J86H7Bt5WReM+Yaf0IBdB+2z5pF3FsQrMkkew9GvQSztIxXiWMiHoDVOWAVG9ow=="],
+
"@trycompai/portal/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="],
"@trycompai/ui/lucide-react": ["lucide-react@0.554.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-St+z29uthEJVx0Is7ellNkgTEhaeSoA42I7JjOCBCrc5X6LYMGSv0P/2uS5HDLTExP5tpiqRD2PyUEOS6s9UXA=="],
@@ -8238,6 +8240,16 @@
"@trycompai/email/resend/@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="],
+ "@trycompai/portal/@trycompai/design-system/react-day-picker": ["react-day-picker@9.13.0", "", { "dependencies": { "@date-fns/tz": "^1.4.1", "date-fns": "^4.1.0", "date-fns-jalali": "^4.1.0-0" }, "peerDependencies": { "react": ">=16.8.0" } }, "sha512-euzj5Hlq+lOHqI53NiuNhCP8HWgsPf/bBAVijR50hNaY1XwjKjShAnIe8jm8RD2W9IJUvihDIZ+KrmqfFzNhFQ=="],
+
+ "@trycompai/portal/@trycompai/design-system/react-resizable-panels": ["react-resizable-panels@4.4.0", "", { "peerDependencies": { "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-vGH1rIhyDOL4RSWYTx3eatjDohDFIRxJCAXUOaeL9HyamptUnUezqndjMtBo9hQeaq1CIP0NBbc7ZV3lBtlgxA=="],
+
+ "@trycompai/portal/@trycompai/design-system/recharts": ["recharts@2.15.4", "", { "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw=="],
+
+ "@trycompai/portal/@trycompai/design-system/tailwind-merge": ["tailwind-merge@3.4.0", "", {}, "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g=="],
+
+ "@trycompai/portal/@trycompai/design-system/vaul": ["vaul@1.1.2", "", { "dependencies": { "@radix-ui/react-dialog": "^1.1.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA=="],
+
"@trycompai/ui/shiki/@shikijs/core": ["@shikijs/core@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-f2ED7HYV4JEk827mtMDwe/yQ25pRiXZmtHjWF8uzZKuKiEsJR7Ce1nuQ+HhV9FzDcbIo4ObBCD9GPTzNuy9S1g=="],
"@trycompai/ui/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.20.0", "", { "dependencies": { "@shikijs/types": "3.20.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-OFx8fHAZuk7I42Z9YAdZ95To6jDePQ9Rnfbw9uSRTSbBhYBp1kEOKv/3jOimcj3VRUKusDYM6DswLauwfhboLg=="],
@@ -9054,6 +9066,8 @@
"@trycompai/email/resend/@react-email/render/prettier": ["prettier@3.7.4", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA=="],
+ "@trycompai/portal/@trycompai/design-system/recharts/eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="],
+
"@types/cheerio/cheerio/htmlparser2/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"app-builder-lib/hosted-git-info/lru-cache/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
diff --git a/package.json b/package.json
index 2d2595c6a3..424be138ad 100644
--- a/package.json
+++ b/package.json
@@ -88,7 +88,7 @@
"packages/*"
],
"dependencies": {
- "@trycompai/design-system": "^1.0.43",
+ "@trycompai/design-system": "1.0.45",
"@types/cheerio": "^1.0.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@upstash/vector": "^1.2.2",