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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.turbo/
node_modules/
packages/*/node_modules/
apps/*/node_modules/
Expand Down
72 changes: 72 additions & 0 deletions ee/apps/landing/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,78 @@ body {
text-rendering: optimizeSpeed;
}

/* Legal page prose — styles MDX-rendered markdown for privacy/terms pages */
.legal-prose h1 {
font-size: 2.25rem;
font-weight: 500;
line-height: 1.05;
letter-spacing: -0.025em;
margin-bottom: 1rem;
}
@media (min-width: 768px) {
.legal-prose h1 { font-size: 3rem; }
}
@media (min-width: 1024px) {
.legal-prose h1 { font-size: 3.75rem; }
}
.legal-prose h1 + p:first-of-type {
color: #6b7280;
margin-bottom: 3rem;
}
.legal-prose h2 {
font-size: 1.25rem;
font-weight: 600;
letter-spacing: -0.025em;
color: #011627;
margin-top: 2.5rem;
margin-bottom: 0.75rem;
}
.legal-prose h3 {
font-size: 1rem;
font-weight: 600;
color: #011627;
margin-top: 1.5rem;
margin-bottom: 0.5rem;
}
.legal-prose p {
font-size: 15px;
line-height: 1.7;
color: #374151;
margin-bottom: 1rem;
}
.legal-prose ul,
.legal-prose ol {
font-size: 15px;
line-height: 1.7;
color: #374151;
padding-left: 1.25rem;
margin-bottom: 1rem;
list-style-type: disc;
}
.legal-prose ol { list-style-type: decimal; }
.legal-prose li { margin-bottom: 0.375rem; }
.legal-prose li strong { color: #011627; }
.legal-prose a {
color: #011627;
text-decoration: underline;
text-underline-offset: 2px;
}
.legal-prose a:hover { opacity: 0.7; }
.legal-prose blockquote {
border-left: 3px solid #cbd5e1;
padding-left: 1rem;
margin: 1rem 0;
color: #4b5563;
font-size: 15px;
line-height: 1.7;
}
.legal-prose strong { font-weight: 600; }
.legal-prose hr {
border: 0;
border-top: 1px solid rgba(148, 163, 184, 0.2);
margin: 2.5rem 0;
}

.content-max-width {
max-width: 64rem; /* matches Tailwind max-w-5xl used by the nav */
margin-left: auto;
Expand Down
181 changes: 3 additions & 178 deletions ee/apps/landing/app/privacy/page.tsx
Original file line number Diff line number Diff line change
@@ -1,185 +1,10 @@
import fs from "fs";
import path from "path";
import { LandingBackground } from "../../components/landing-background";
import { LegalPage } from "../../components/legal-page";
import { SiteFooter } from "../../components/site-footer";
import { SiteNav } from "../../components/site-nav";
import { getGithubData } from "../../lib/github";

export const metadata = {
title: "OpenWork — Privacy Policy",
description:
"Privacy policy for Different AI, doing business as OpenWorkLabs."
description: "Privacy policy for Different AI, doing business as OpenWork."
};

/** Parse the plain-text privacy policy into renderable blocks. */
function parsePolicy(raw: string) {
const lines = raw.split("\n");
const title = lines[0]?.trim() ?? "";

// Extract "Updated at YYYY-MM-DD"
const dateLine = lines.find((l) => l.startsWith("Updated at"));
const lastUpdated = dateLine?.replace("Updated at ", "").trim() ?? "";

// Everything after the date line is body content.
const dateIdx = lines.indexOf(dateLine ?? "");
const bodyLines = lines.slice(dateIdx + 1);

// Split into blocks separated by blank lines.
const blocks: string[][] = [];
let current: string[] = [];
for (const line of bodyLines) {
if (line.trim() === "") {
if (current.length > 0) {
blocks.push(current);
current = [];
}
} else {
current.push(line);
}
}
if (current.length > 0) blocks.push(current);

// Classify each block as a heading, subheading, list, or paragraph.
type Block =
| { type: "heading"; text: string }
| { type: "subheading"; text: string }
| { type: "paragraph"; text: string }
| { type: "list"; items: string[] };

const classified: Block[] = [];

for (const block of blocks) {
const joined = block.join(" ").trim();
const allList = block.every((l) => l.trimStart().startsWith("-"));

if (allList) {
const items = block.map((l) => l.replace(/^\s*-/, "").trim());
// Single-item short lists are subheadings (e.g. "Cookies", "Local Storage")
if (items.length === 1 && items[0].length < 50) {
classified.push({ type: "subheading", text: items[0] });
} else {
classified.push({ type: "list", items });
}
} else if (
block.length === 1 &&
joined.length < 120 &&
!joined.startsWith("-") &&
!joined.endsWith(".")
) {
// Short single-line blocks that don't end with a period are headings.
classified.push({ type: "heading", text: joined });
} else {
classified.push({ type: "paragraph", text: joined });
}
}

return { title, lastUpdated, blocks: classified };
}

/** Turn email addresses into clickable links. */
function formatText(text: string) {
const emailRegex = /([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g;
const parts = text.split(emailRegex);
if (parts.length === 1) return text;
return parts.map((part, i) =>
emailRegex.test(part) ? (
<a
key={i}
href={`mailto:${part}`}
className="text-[#011627] underline hover:opacity-70"
>
{part}
</a>
) : (
part
)
);
}

export default async function PrivacyPage() {
const github = await getGithubData();
const callUrl = process.env.NEXT_PUBLIC_CAL_URL || "/enterprise#book";

const raw = fs.readFileSync(
path.join(process.cwd(), "app/privacy/privacy-policy.txt"),
"utf-8"
);
const policy = parsePolicy(raw);

return (
<div className="relative min-h-screen overflow-hidden text-[#011627]">
<LandingBackground />

<div className="relative z-10 flex min-h-screen flex-col items-center pb-3 pt-1 md:pb-4 md:pt-2">
<div className="w-full">
<SiteNav
stars={github.stars}
callUrl={callUrl}
downloadHref={github.downloads.macos}
/>
</div>

<main className="mx-auto flex w-full max-w-6xl flex-col gap-16 px-6 pb-24 md:gap-20 md:px-8 md:pb-28">
<LegalPage title={policy.title} lastUpdated={policy.lastUpdated}>
{policy.blocks.map((block, i) => {
switch (block.type) {
case "heading":
return (
<h2
key={i}
className="mb-3 mt-10 text-xl font-semibold tracking-tight text-[#011627] first:mt-0"
>
{block.text}
</h2>
);
case "subheading":
return (
<h3
key={i}
className="mb-2 mt-6 text-base font-semibold text-[#011627]"
>
{block.text}
</h3>
);
case "list":
return (
<ul
key={i}
className="mb-4 list-disc space-y-1.5 pl-5 text-[15px] leading-relaxed text-gray-700"
>
{block.items.map((item, j) => {
const colonIdx = item.indexOf(":");
if (colonIdx > 0 && colonIdx < 40) {
return (
<li key={j}>
<strong className="font-semibold text-[#011627]">
{item.slice(0, colonIdx)}
</strong>
:{formatText(item.slice(colonIdx + 1))}
</li>
);
}
return <li key={j}>{formatText(item)}</li>;
})}
</ul>
);
case "paragraph":
return (
<p
key={i}
className="mb-4 text-[15px] leading-relaxed text-gray-700"
>
{formatText(block.text)}
</p>
);
}
})}
</LegalPage>

<SiteFooter />
</main>
</div>
</div>
);
export default function PrivacyPage() {
return <LegalPage file="privacy/privacy-policy.md" />;
}
Loading
Loading