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>
);
}
123 changes: 123 additions & 0 deletions apps/site/src/components/legal-accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"use client";

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

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

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

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}
>
<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>
{isOpen && (
<div 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>
)}
Comment on lines +25 to +45
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Accessibility: Add aria-controls and content panel id for screen reader support.

The button has aria-expanded (good!), but it's missing aria-controls to indicate which element it controls. Additionally, the content panel needs an id that matches. This helps screen reader users understand the relationship between the toggle and the content.

♿ Proposed fix
 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`;

   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>
-      {isOpen && (
-        <div className="pb-4 text-foreground-neutral-weak ...">
+      <div
+        id={contentId}
+        className="pb-4 text-foreground-neutral-weak ..."
+        hidden={!isOpen}
+      >
           {section.content}
-        </div>
-      )}
+      </div>
     </div>
   );
 }

Using hidden instead of conditional rendering keeps the element in the DOM for aria-controls to reference while still hiding it visually and from assistive tech.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/terms/_components/terms-accordion.tsx` around lines 25 -
45, The toggle button (onToggle, aria-expanded={isOpen}) needs an aria-controls
that points to the content panel id and the content panel needs a matching id so
screen readers can link them; create a stable panelId (e.g., derived from
section.id or a generated string) and set aria-controls={panelId} on the button
and id={panelId} on the content div, render the panel always (do not remove it
from the DOM) and hide it when closed using a CSS utility like hidden or
aria-hidden to preserve the relationship, and ensure you update aria-hidden to
reflect isOpen.

</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 printPage = () => {
setOpenItems(new Set(allIndices));
setTimeout(() => window.print(), 50);
};
Comment on lines +77 to +80
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

The 50ms timeout before printing is a race condition.

React's state update and subsequent re-render aren't guaranteed to complete within 50ms, especially on slower devices. This could result in printing with sections still collapsed.

🔧 Proposed fix using useEffect
+import { useState, useEffect } from "react";
+
 export function TermsAccordion({ sections }: { sections: Section[] }) {
   const [expandAll, setExpandAll] = useState(false);
   const [selected, setSelected] = useState(0);
+  const [isPrinting, setIsPrinting] = useState(false);
+
+  useEffect(() => {
+    if (isPrinting && expandAll) {
+      window.print();
+      setIsPrinting(false);
+    }
+  }, [isPrinting, expandAll]);

   const printPage = () => {
     setExpandAll(true);
-    setTimeout(() => window.print(), 50);
+    setIsPrinting(true);
   };

This ensures window.print() is called only after React has committed the state change where expandAll is true.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/site/src/app/terms/_components/terms-accordion.tsx` around lines 68 -
71, The printPage function currently sets expandAll then uses a 50ms timeout
which is a race; instead update printPage to only call setExpandAll(true) and
move the window.print() call into a useEffect that watches expandAll so printing
occurs after React commits the state change; locate printPage and the expandAll
state (setExpandAll, expandAll) in terms-accordion.tsx and implement a useEffect
that triggers window.print() when expandAll becomes true, then reset expandAll
if needed.


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)}
/>
))}
</div>
</>
);
}
Loading
Loading