-
Notifications
You must be signed in to change notification settings - Fork 908
feat: DR-7746 legal pages (terms, SLA, privacy, code of conduct, partner TOS) #7712
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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> | ||
| ); | ||
| } |
| 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> | ||
| )} | ||
| </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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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 🤖 Prompt for AI Agents |
||
|
|
||
| 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"> | ||
| </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> | ||
| </> | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accessibility: Add
aria-controlsand content panelidfor screen reader support.The button has
aria-expanded(good!), but it's missingaria-controlsto indicate which element it controls. Additionally, the content panel needs anidthat 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
hiddeninstead of conditional rendering keeps the element in the DOM foraria-controlsto reference while still hiding it visually and from assistive tech.🤖 Prompt for AI Agents