Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/trust/src/app/[id]/components/compliance-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export default function ComplianceHeader({ organization, title }: ComplianceHead
className="object-contain"
/>
) : (
<div className="w-10 h-10 bg-muted rounded-md flex items-center justify-center text-white font-bold">
<div className="w-10 h-10 bg-muted-foreground rounded-md flex items-center justify-center text-white font-bold">
{organization.name.charAt(0)}
</div>
)}
Expand All @@ -32,12 +32,12 @@ export default function ComplianceHeader({ organization, title }: ComplianceHead
</div>

<div className="flex gap-2">
<Link className={buttonVariants({ variant: "outline", className: "text-xs" })} href={`${organization.website}`}>
<Link className={buttonVariants({ variant: "outline", className: "text-xs" })} href={`${organization.website || "https://trycomp.ai"}`}>
<Globe className="w-3 h-3" />
{organization.name}
</Link>
<Link className={buttonVariants({ variant: "outline", className: "text-xs" })} href="https://trycomp.ai">
<div className="w-2 h-2 bg-green-500 rounded-full" />
<div className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
Monitoring with Comp AI
</Link>
</div>
Expand Down
2 changes: 1 addition & 1 deletion apps/trust/src/app/[id]/components/compliance-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface ComplianceItemProps {

export default function ComplianceItem({ text, isCompliant }: ComplianceItemProps) {
return (
<div className="flex items-center justify-between py-1 font-mono">
<div className="flex items-center justify-between py-1">
<span className="text-sm">{text}</span>
{isCompliant ? (
<div className="w-2 h-2 bg-green-500 rounded-full" />
Expand Down
45 changes: 23 additions & 22 deletions apps/trust/src/app/[id]/components/report.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ import type { Organization, Policy, Task } from '@comp/db/types';

interface ComplianceReportProps {
organization: Organization;
policies: Policy[];
controls: Task[];
policies: Pick<Policy, "id" | "name" | "status">[];
controls: Pick<Task, "id" | "title" | "status">[];
}

export default function ComplianceReport({ organization, policies, controls }: ComplianceReportProps) {
return (
<div className="min-h-screen">
<div className="max-w-6xl mx-auto p-6">
<div>
<div className="max-w-6xl mx-auto">
<div className="rounded-lg">
<div>
<ComplianceHeader
Expand All @@ -22,30 +22,31 @@ export default function ComplianceReport({ organization, policies, controls }: C
/>

<ComplianceSummary
text={`${organization.name} is using Comp AI to monitor their compliance against frameworks like SOC 2, ISO 27001, and more.`}
text={`${organization.name} is using Comp AI to monitor their compliance against common cybersecurity frameworks like SOC 2, ISO 27001, and more.`}
/>

<div className="border-b mt-4" />
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<ComplianceSection title="Policies" isLive>
<div className="space-y-2">
{policies.map((policy) => (
<ComplianceItem key={policy.id} text={policy.name} isCompliant={policy.status === "published"} />
))}
</div>
</ComplianceSection>
{policies.length > 0 && (
<ComplianceSection title="Policies" isLive>
<div className="space-y-2">
{policies.map((policy) => (
<ComplianceItem key={policy.id} text={policy.name} isCompliant={policy.status === "published"} />
))}
</div>
</ComplianceSection>
)}

<ComplianceSection title="Controls" isLive>
<div className="space-y-2">
{controls.map((control) => (
<ComplianceItem key={control.id} text={control.title} isCompliant={control.status === "done"} />
))}
</div>
</ComplianceSection>
{controls.length > 0 && (
<ComplianceSection title="Controls" isLive>
<div className="space-y-2">
{controls.map((control) => (
<ComplianceItem key={control.id} text={control.title} isCompliant={control.status === "done"} />
))}
</div>
</ComplianceSection>
)}
</div>

</div>
</div>
</div>
Expand Down
81 changes: 59 additions & 22 deletions apps/trust/src/app/[id]/lib/data.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,85 @@
"use server";

import { db } from "@comp/db";
import { redirect } from "next/navigation";
import { cache } from "react";

export const findOrganization = async (id: string) => {
export const findOrganization = cache(async (id: string) => {
const organization = await db.organization.findFirst({
where: { id: id },
include: {
trust: {
where: {
status: "published",
},
},
},
});

const isPublished = await db.trust.findUnique({
where: { organizationId: organization?.id, status: "published" },
});

if (!organization || !isPublished) {
return null;
if (!organization) {
return redirect("/");
}

return {
...organization,
};
};
});

export const getPublishedPolicies = cache(async (organizationId: string) => {
const organization = await findOrganization(organizationId);

if (!organization) {
return redirect("/");
}

export const getPublishedPolicies = async (organizationId: string) => {
const policies = await db.policy.findMany({
where: { organizationId, status: "published" },
select: {
id: true,
name: true,
status: true,
},
});

return policies;
};

export const getPublishedPolicy = async (
organizationId: string,
policyId: string,
) => {
const policy = await db.policy.findFirst({
where: { organizationId, status: "published", id: policyId },
});
});

return policy;
};
export const getPublishedPolicy = cache(
async (organizationId: string, policyId: string) => {
const organization = await findOrganization(organizationId);

if (!organization) {
return redirect("/");
}

const policy = await db.policy.findFirst({
where: { organizationId, status: "published", id: policyId },
select: {
id: true,
name: true,
status: true,
},
});

return policy;
},
);

export const getPublishedControls = cache(async (organizationId: string) => {
const organization = await findOrganization(organizationId);

if (!organization) {
return redirect("/");
}

export const getPublishedControls = async (organizationId: string) => {
const controls = await db.task.findMany({
where: { organizationId, status: "done" },
select: {
id: true,
title: true,
status: true,
},
});

return controls;
};
});
58 changes: 57 additions & 1 deletion apps/trust/src/app/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { findOrganization, getPublishedControls, getPublishedPolicies } from "./lib/data";
import ComplianceReport from './components/report';
import { Metadata } from "next";

export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const id = (await params).id;
Expand All @@ -13,5 +14,60 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
const policies = await getPublishedPolicies(organization.id);
const controls = await getPublishedControls(organization.id);

return <ComplianceReport organization={organization} policies={policies} controls={controls} />;
return (
<div>
<div className="pb-6 p-6">
<ComplianceReport organization={organization} policies={policies} controls={controls} />
</div>
</div>
)
}

export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>;
}): Promise<Metadata> {
const id = (await params).id;
const organization = await findOrganization(id);

if (!organization) {
return {
title: "Organization not found",
};
}

const title = `${organization.name} - Trust Center`;
const description = `${organization.name} is using Comp AI to monitor their compliance against common cybersecurity frameworks like SOC 2, ISO 27001, and more.`;
const url = `https://trycomp.ai/trust/${organization.id}`;

return {
title,
description,
alternates: {
canonical: url,
},
openGraph: {
type: "website",
url,
title,
description,
siteName: "Comp AI Trust Center",
images: [
{
url: 'https://trycomp.ai/og.png',
width: 1200,
height: 630,
alt: `${organization.name} Trust Center`,
},
],
},
twitter: {
card: "summary_large_image",
site: "@trycompai",
title,
description,
images: ['https://trycomp.ai/og.png'],
},
};
}
22 changes: 18 additions & 4 deletions apps/trust/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,38 @@ import { Toaster } from '@comp/ui/toaster';
import type { Metadata } from 'next';
import { type ReactNode } from 'react';
import "@comp/ui/globals.css";
import localFont from 'next/font/local';
import { GeistMono } from "geist/font/mono";
import { cn } from '@comp/ui/cn';

export const metadata: Metadata = {
title: 'Comp AI - Trust Portal',
description: 'Trust Portal',
}

const font = localFont({
src: "/../../public/fonts/GeneralSans-Variable.ttf",
display: "swap",
variable: "--font-general-sans",
});

export default function RootLayout({
children,
}: {
children: ReactNode
}) {
return (
<html lang="en" className="h-full">
<body>
<div className="flex flex-col container gap-2 h-full">
<html lang="en">
<body
className={cn(
`${GeistMono.variable} ${font.variable}`,
"whitespace-pre-line overscroll-none antialiased",
)}
>
<div className="flex flex-col container min-h-screen">
{children}
<Toaster />
</div>
<Toaster />
</body>
</html>
)
Expand Down
6 changes: 4 additions & 2 deletions apps/trust/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { redirect } from "next/navigation";

export default async function RootPage() {
return redirect("https://trycomp.ai/");
export default function Page() {
return (
redirect("https://trycomp.ai")
);
}
43 changes: 43 additions & 0 deletions apps/trust/src/app/providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"use client";

import { AnalyticsProvider } from "@comp/analytics";
import { GoogleTagManager } from "@next/third-parties/google";
import { ThemeProvider } from "next-themes";
import type { ReactNode } from "react";

type ProviderProps = {
children: ReactNode;
};

export function Providers({ children }: ProviderProps) {
return (
<ThemeProvider
attribute="class"
disableTransitionOnChange
scriptProps={{ "data-cfasync": "false" }}
defaultTheme="dark"
enableSystem={false}
>
<GoogleTagManager
gtmId="GTM-56GW3TVW"
dataLayer={{
user_id: "",
user_email: "",
}}
/>
<GoogleTagManager
gtmId="AW-16886441131"
dataLayer={{
user_id: "",
user_email: "",
}}
/>
<AnalyticsProvider
userId={undefined}
userEmail={undefined}
>
{children}
</AnalyticsProvider>
</ThemeProvider>
);
}
Loading
Loading