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
42 changes: 42 additions & 0 deletions apps/site/src/app/event-code-of-conduct/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type { Metadata } from "next";
import {
cocSections,
cocLastUpdated,
cocDescription,
} from "@/data/event-code-of-conduct";
import { LegalAccordion } from "@/components/legal-accordion";

export const metadata: Metadata = {
title: "Event Code of Conduct | Prisma",
description:
"All attendees, speakers, sponsors, and volunteers at Prisma events are required to agree to this code of conduct.",
};

export default function EventCodeOfConductPage() {
return (
<main className="flex-1 w-full z-1 -mt-24 pt-24 relative legal-hero-gradient">
{/* Hero */}
<div className="text-center py-16">
<h1 className="text-5xl font-bold font-sans-display text-foreground-neutral mb-6">
Event Code of Conduct
</h1>
<p className="max-w-[640px] mx-auto text-lg text-foreground-neutral-weak text-left mb-6">
{cocDescription}
</p>
<p className="text-lg text-foreground-neutral-weak">
<b>Last updated:</b> {cocLastUpdated}
</p>
</div>

{/* Separator */}
<div className="max-w-[1248px] mx-auto px-2.5 md:px-6">
<hr className="border-stroke-neutral" />
</div>

{/* Content */}
<div className="mx-auto w-full max-w-[1248px] px-2.5 md:px-6 grid gap-4 grid-rows-[auto_1fr] md:grid-cols-[150px_1fr] lg:grid-cols-[1fr_640px_1fr] print:grid-cols-[100%]">
<LegalAccordion sections={cocSections} />
</div>
</main>
);
}
14 changes: 14 additions & 0 deletions apps/site/src/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,20 @@
--color-fd-primary: var(--color-stroke-ppg);
}

.legal-hero-gradient::before {
content: "";
position: absolute;
inset-inline: 0;
top: 0;
height: 600px;
z-index: -1;
background: linear-gradient(180deg, #EDEEF9 0%, transparent 100%);
}

.dark .legal-hero-gradient::before {
background: linear-gradient(180deg, #171937 0%, transparent 100%);
}

body {
background: var(--color-background-default);
}
Expand Down
35 changes: 35 additions & 0 deletions apps/site/src/app/partners/tos/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Metadata } from "next";
import { partnersTosSections, partnersTosLastUpdated } from "@/data/partners-tos";
import { LegalAccordion } from "@/components/legal-accordion";

export const metadata: Metadata = {
title: "Partner Network Terms of Service | Prisma",
description:
"Terms of Service for the Prisma Partner Network covering affiliates, technology partners, and resellers.",
};

export default function PartnersTosPage() {
return (
<main className="flex-1 w-full z-1 -mt-24 pt-24 relative legal-hero-gradient">
{/* Hero */}
<div className="text-center py-16">
<h1 className="text-5xl font-bold font-sans-display text-foreground-neutral mb-6">
Terms of Service Prisma Partner Network
</h1>
<p className="text-lg text-foreground-neutral-weak">
<b>Last updated:</b> {partnersTosLastUpdated}
</p>
</div>

{/* Separator */}
<div className="max-w-[1248px] mx-auto px-2.5 md:px-6">
<hr className="border-stroke-neutral" />
</div>

{/* Content */}
<div className="mx-auto w-full max-w-[1248px] px-2.5 md:px-6 grid gap-4 grid-rows-[auto_1fr] md:grid-cols-[150px_1fr] lg:grid-cols-[1fr_640px_1fr] print:grid-cols-[100%]">
<LegalAccordion sections={partnersTosSections} />
</div>
</main>
);
}
35 changes: 35 additions & 0 deletions apps/site/src/app/privacy/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Metadata } from "next";
import { privacySections, privacyLastUpdated } from "@/data/privacy";
import { LegalAccordion } from "@/components/legal-accordion";

export const metadata: Metadata = {
title: "Privacy Policy | Prisma",
description:
"Read the Prisma Privacy Policy covering how we collect, use, and protect your data.",
};

export default function PrivacyPage() {
return (
<main className="flex-1 w-full z-1 -mt-24 pt-24 relative legal-hero-gradient">
{/* Hero */}
<div className="text-center py-16">
<h1 className="text-5xl font-bold font-sans-display text-foreground-neutral mb-6">
Privacy Policy
</h1>
<p className="text-lg text-foreground-neutral-weak">
<b>Last updated:</b> {privacyLastUpdated}
</p>
</div>

{/* Separator */}
<div className="max-w-[1248px] mx-auto px-2.5 md:px-6">
<hr className="border-stroke-neutral" />
</div>

{/* Content */}
<div className="mx-auto w-full max-w-[1248px] px-2.5 md:px-6 grid gap-4 grid-rows-[auto_1fr] md:grid-cols-[150px_1fr] lg:grid-cols-[1fr_640px_1fr] print:grid-cols-[100%]">
<LegalAccordion sections={privacySections} />
</div>
</main>
);
}
35 changes: 35 additions & 0 deletions apps/site/src/app/sla/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Metadata } from "next";
import { slaSections, slaLastUpdated } from "@/data/sla";
import { LegalAccordion } from "@/components/legal-accordion";

export const metadata: Metadata = {
title: "Service Level Agreement | Prisma",
description:
"Read the Prisma Service Level Agreement covering uptime commitments and service credits.",
};

export default function SlaPage() {
return (
<main className="flex-1 w-full z-1 -mt-24 pt-24 relative legal-hero-gradient">
{/* Hero */}
<div className="text-center py-16">
<h1 className="text-5xl font-bold font-sans-display text-foreground-neutral mb-6">
Prisma Service Level Agreement
</h1>
<p className="text-lg text-foreground-neutral-weak">
<b>Last updated:</b> {slaLastUpdated}
</p>
</div>

{/* Separator */}
<div className="max-w-[1248px] mx-auto px-2.5 md:px-6">
<hr className="border-stroke-neutral" />
</div>

{/* Content */}
<div className="mx-auto w-full max-w-[1248px] px-2.5 md:px-6 grid gap-4 grid-rows-[auto_1fr] md:grid-cols-[150px_1fr] lg:grid-cols-[1fr_640px_1fr] print:grid-cols-[100%]">
<LegalAccordion sections={slaSections} />
</div>
</main>
);
}
35 changes: 35 additions & 0 deletions apps/site/src/app/terms/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Metadata } from "next";
import { termsSections, termsLastUpdated } from "@/data/terms";
import { LegalAccordion } from "@/components/legal-accordion";

export const metadata: Metadata = {
title: "Terms of Service | Prisma",
description:
"Read the Prisma Terms of Service governing your use of Prisma products and services.",
};

export default function TermsPage() {
return (
<main className="flex-1 w-full z-1 -mt-24 pt-24 relative legal-hero-gradient">
{/* Hero */}
<div className="text-center py-16">
<h1 className="text-5xl font-bold font-sans-display text-foreground-neutral mb-6">
Terms of Service
</h1>
<p className="text-lg text-foreground-neutral-weak">
<b>Last updated:</b> {termsLastUpdated}
</p>
</div>

{/* Separator */}
<div className="max-w-[1248px] mx-auto px-2.5 md:px-6">
<hr className="border-stroke-neutral" />
</div>

{/* Content */}
<div className="mx-auto w-full max-w-[1248px] px-2.5 md:px-6 grid gap-4 grid-rows-[auto_1fr] md:grid-cols-[150px_1fr] lg:grid-cols-[1fr_640px_1fr] print:grid-cols-[100%]">
<LegalAccordion sections={termsSections} />
</div>
</main>
);
}
139 changes: 139 additions & 0 deletions apps/site/src/components/legal-accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"use client";

import { useState, useEffect } from "react";
import type { ReactNode } from "react";
import { cn } from "@/lib/cn";

type Section = {
title: string;
content: ReactNode;
};

function AccordionItem({
section,
isOpen,
onToggle,
index,
}: {
section: Section;
isOpen: boolean;
onToggle: () => void;
index: number;
}) {
const anchorId = section.title.trim().toLowerCase().replace(/\s+/g, "-");
const contentId = `${anchorId}-content-${index}`;

return (
<div className="scroll-mt-16 md:scroll-mt-24 border-t border-stroke-neutral" id={anchorId}>
<button
type="button"
className="flex w-full items-center justify-between py-3 text-left cursor-pointer"
onClick={onToggle}
aria-expanded={isOpen}
aria-controls={contentId}
>
<span className="text-lg font-bold leading-[25px] text-foreground-neutral">
{section.title}
</span>
<i
className={cn(
"fa-regular text-foreground-neutral-weaker text-lg",
isOpen ? "fa-chevron-up" : "fa-chevron-down",
)}
/>
</button>
<div
id={contentId}
hidden={!isOpen}
className="pb-4 text-foreground-neutral-weak text-left [&_p]:my-4 [&_a]:underline [&_a]:transition-colors [&_a]:duration-150 hover:[&_a]:text-foreground-neutral [&_ul]:list-revert [&_ul]:m-revert [&_ul]:p-revert [&_ol]:list-revert [&_ol]:m-revert [&_ol]:p-revert [&_li]:my-2 print:text-foreground-neutral"
>
{section.content}
</div>
</div>
);
}

export function LegalAccordion({
sections,
defaultExpand = false,
}: {
sections: Section[];
defaultExpand?: boolean;
}) {
const allIndices = sections.map((_, i) => i);
const [openItems, setOpenItems] = useState<Set<number>>(
() => new Set(defaultExpand ? allIndices : []),
);

const isAllExpanded = openItems.size === sections.length;

const toggleAll = () => {
setOpenItems(new Set(isAllExpanded ? [] : allIndices));
};

const toggleItem = (idx: number) => {
setOpenItems((prev) => {
const next = new Set(prev);
if (next.has(idx)) next.delete(idx);
else next.add(idx);
return next;
});
};

const [isPrinting, setIsPrinting] = useState(false);

const printPage = () => {
setOpenItems(new Set(allIndices));
setIsPrinting(true);
};

useEffect(() => {
if (isPrinting && openItems.size === sections.length) {
window.print();
setIsPrinting(false);
}
}, [isPrinting, openItems, sections.length]);

return (
<>
{/* Controls — sticky sidebar on desktop, horizontal row on mobile */}
<div className="flex items-start flex-row-reverse gap-4 justify-between pt-14 pb-14 md:justify-center md:sticky md:top-[120px] md:flex-col md:pt-14 md:pb-24 md:self-start print:hidden">
<button
type="button"
className="text-foreground-orm hover:text-foreground-orm-strong transition-all duration-300 cursor-pointer"
onClick={toggleAll}
>
<span className="text-lg leading-6 font-semibold underline">
{isAllExpanded ? "Collapse" : "Expand"} all
</span>
<i
className={`fa-regular fa-${isAllExpanded ? "minus" : "plus"} ml-2 text-base`}
/>
</button>
<button
type="button"
className="text-foreground-orm hover:text-foreground-orm-strong transition-all duration-300 cursor-pointer"
onClick={printPage}
>
<span className="text-lg leading-6 font-semibold underline">
Print
</span>
<i className="fa-regular fa-print ml-2 text-base" />
</button>
</div>

{/* Accordion content */}
<div className="w-full pb-24 md:pt-10">
{sections.map((section, idx) => (
<AccordionItem
key={idx}
section={section}
isOpen={openItems.has(idx)}
onToggle={() => toggleItem(idx)}
index={idx}
/>
))}
</div>
</>
);
}
Loading
Loading