Skip to content
Open
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
35 changes: 35 additions & 0 deletions apps/web/src/components/book-founder-call.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useAnalytics } from "@/hooks/use-posthog";
import {
BOOK_CALL_URL,
ENTERPRISE_EVENTS,
type EnterpriseCtaLocation,
type EnterpriseSurface,
} from "@/lib/enterprise";

export function BookFounderCall({
location,
page,
}: {
location: EnterpriseCtaLocation;
page: EnterpriseSurface;
}) {
const { track } = useAnalytics();

return (
<a
href={BOOK_CALL_URL}
target="_blank"
rel="noopener noreferrer"
onClick={() =>
track(ENTERPRISE_EVENTS.ctaClicked, {
cta: "book_call",
location,
page,
})
}
className="inline-flex h-11 items-center justify-center rounded-full bg-[#181613] px-6 text-sm font-medium text-white transition-all hover:scale-[102%] hover:bg-[#4f4940] active:scale-[98%]"
>
Book a call with the founder
</a>
);
}
2 changes: 1 addition & 1 deletion apps/web/src/components/enterprise-callout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Link } from "@tanstack/react-router";

const heading = "Rolling Anarlog out to a team?";
const body =
"Workspace admin, SSO, and a self-hosted server for regulated environments — shaped with early partners.";
"Forward the enterprise page to IT: encryption, retention, training, and subprocessors, then a founder-led pilot.";
const ctaClassName =
"inline-flex h-11 shrink-0 items-center justify-center rounded-full bg-[#181613] px-6 text-sm font-medium text-white transition-all hover:scale-[102%] hover:bg-[#4f4940] active:scale-[98%]";

Expand Down
59 changes: 59 additions & 0 deletions apps/web/src/components/enterprise-cta-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Link } from "@tanstack/react-router";
import type { ReactNode } from "react";

import { cn } from "@anlg/utils";

import { useAnalytics } from "@/hooks/use-posthog";
import {
ENTERPRISE_EVENTS,
type EnterpriseCta,
type EnterpriseCtaLocation,
type EnterpriseSurface,
} from "@/lib/enterprise";

export function EnterpriseCtaLink({
to,
href,
cta,
location,
page,
children,
className,
}: {
to?: "/security/" | "/privacy/" | "/terms/" | "/pricing/" | "/enterprise/";
href?: string;
cta: EnterpriseCta;
location: EnterpriseCtaLocation;
page: EnterpriseSurface;
children: ReactNode;
className?: string;
}) {
const { track } = useAnalytics();
const classes = cn([
"text-sm text-[#756b5d] underline decoration-[#d9cdb8] underline-offset-4 transition-colors hover:text-[#181613]",
className,
]);
const onClick = () =>
track(ENTERPRISE_EVENTS.ctaClicked, { cta, location, page });

if (to) {
return (
<Link to={to} onClick={onClick} className={classes}>
{children}
</Link>
);
}

return (
<a
href={href}
onClick={onClick}
className={classes}
{...(href?.startsWith("http")
? { target: "_blank", rel: "noopener noreferrer" }
: {})}
>
{children}
</a>
);
}
21 changes: 21 additions & 0 deletions apps/web/src/components/pilot-path.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { pilotSteps } from "@/lib/trust-center";

export function PilotPath() {
return (
<ol className="mx-auto mt-8 flex max-w-2xl flex-col gap-6 text-left">
{pilotSteps.map((step, index) => (
<li key={step.title} className="flex gap-4">
<span className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-full bg-[#181613] text-xs font-medium text-white">
{index + 1}
</span>
<div>
<h3 className="text-base font-medium text-[#181613]">
{step.title}
</h3>
<p className="mt-1 text-sm leading-6 text-[#4f4940]">{step.body}</p>
</div>
</li>
))}
</ol>
);
}
18 changes: 18 additions & 0 deletions apps/web/src/components/security-review-list.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { securityReviewAnswers } from "@/lib/trust-center";

export function SecurityReviewList({ detail = false }: { detail?: boolean }) {
return (
<dl className="mx-auto mt-8 max-w-2xl divide-y divide-[#eadfce] text-left">
{securityReviewAnswers.map((item) => (
<div key={item.question} className="py-5 first:pt-0 last:pb-0">
<dt className="text-base font-medium text-[#181613]">
{item.question}
</dt>
<dd className="mt-2 text-sm leading-6 text-[#4f4940]">
{detail ? item.detail : item.summary}
</dd>
</div>
))}
</dl>
);
}
1 change: 1 addition & 0 deletions apps/web/src/components/site-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const footerGroups = [
{
title: "Legal",
links: [
{ label: "Security", to: "/security/" },
{ label: "Privacy", to: "/privacy/" },
{ label: "Terms", to: "/terms/" },
],
Expand Down
31 changes: 31 additions & 0 deletions apps/web/src/lib/enterprise.ts
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
export const BOOK_CALL_URL = "https://cal.com/team/fastrepl/hi";
export const SECURITY_REPORT_EMAIL = "founders@fastrepl.com";
export const SECURITY_ADVISORY_URL =
"https://github.com/fastrepl/anarlog/security/advisories/new";
export const PROCUREMENT_EMAIL = "founders@anarlog.so";

export const ENTERPRISE_EVENTS = {
pageViewed: "enterprise_page_viewed",
securityPageViewed: "security_page_viewed",
ctaClicked: "enterprise_cta_clicked",
} as const;

export type EnterpriseCta =
| "book_call"
| "trust_center"
| "enterprise"
| "privacy"
| "terms"
| "pricing"
| "docs"
| "dpa"
| "security_report";

export type EnterpriseCtaLocation =
| "hero"
| "security_review"
| "pilot"
| "talk"
| "footer"
| "packet";

export type EnterpriseSurface = "enterprise" | "security";
78 changes: 78 additions & 0 deletions apps/web/src/lib/trust-center.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import assert from "node:assert/strict";
import test from "node:test";

import { ENTERPRISE_EVENTS } from "./enterprise.ts";
import {
architectureLayers,
certificationStatus,
contractualDocs,
pilotSteps,
proofStatus,
securityReviewAnswers,
shipsToday,
shipsWithPartners,
subprocessors,
} from "./trust-center.ts";

test("answers the security-review questions buyers forward to IT", () => {
const questions = securityReviewAnswers.map((item) => item.question);

assert.deepEqual(questions, [
"How is meeting content encrypted?",
"Where does data live, and which jurisdiction applies?",
"How long do you keep data?",
"Do you train models on our meetings?",
"Who are your subprocessors?",
"Does a bot join our calls?",
]);

for (const item of securityReviewAnswers) {
assert.ok(item.summary.length > 40);
assert.ok(item.detail.length > item.summary.length);
}
});

test("lists processors without claiming they can read Cloud Sync content", () => {
const names = subprocessors.map((item) => item.name).join(" ");

assert.match(names, /SQLite Cloud/);
assert.match(names, /Stripe/);
assert.match(names, /Deepgram/);
assert.match(names, /OpenRouter/);
assert.match(names, /Nango/);

const sync = subprocessors.find((item) => item.name === "SQLite Cloud");
assert.match(sync?.receives ?? "", /encrypted/i);
});

test("does not claim certifications that are not complete", () => {
assert.deepEqual(certificationStatus.claimed, []);
assert.match(certificationStatus.planned, /does not claim/);
assert.doesNotMatch(certificationStatus.planned, /we are SOC 2/i);
assert.doesNotMatch(certificationStatus.planned, /HIPAA compliant/i);
});

test("keeps the DPA as a request, not a published legal invention", () => {
const dpa = contractualDocs.find((doc) => doc.label.includes("Processing"));
assert.ok(dpa?.href.startsWith("mailto:"));
assert.match(dpa?.note ?? "", /on request/i);
});

test("describes a founder-led rollout without inventing customer proof", () => {
assert.equal(pilotSteps.length, 3);
assert.equal(pilotSteps[0]?.title, "Security review");
assert.equal(pilotSteps[1]?.title, "Scoped pilot");
assert.equal(pilotSteps[2]?.title, "Rollout");
assert.match(proofStatus.body, /does not invent/);
assert.ok(shipsToday.length >= 4);
assert.ok(shipsWithPartners.length >= 2);
assert.equal(architectureLayers.length, 4);
});

test("uses stable funnel event names", () => {
assert.deepEqual(ENTERPRISE_EVENTS, {
pageViewed: "enterprise_page_viewed",
securityPageViewed: "security_page_viewed",
ctaClicked: "enterprise_cta_clicked",
});
});
Loading
Loading