Files
hope-events/frontend/src/components/shared/PrivacyNoticeSection.tsx
T
joshuaandClaude Opus 5.5 d265047c84 POPIA toolkit part 2: privacy notice editor, operators, consent and access logs
- Admin -> Privacy (POPIA): edit the privacy notice (built-in text as the
  start, placeholders for org/IO/operators), drafts, preview, numbered
  versions; earlier versions readable on /legal/privacy?version=N
- "Significant change" versions are accepted once after login
  (NoticePrompt); sign-up records the version accepted
- Operator register (seeded with the services the app uses), listed in
  the notice via {operators}
- Consent log (news opt-ins, notice acceptances, parental consent) and
  access log (report exports, registers, certificates, group lists, form
  answers, consent/medical lists, backups, impersonation)
- Information Officer on the contact page
- Unit tests (privacyNotice.test.js), e2e (privacy-notice.spec.ts); docs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012mt6TCv85T9VKRq3cV7rXq
2026-10-23 19:11:20 +02:00

60 lines
2.2 KiB
TypeScript

"use client";
import React from "react";
import { CookieSettingsButton } from "@/components/shared/CookieSettingsButton";
export type NoticeSection = { id: string; title: string; body: string };
export type PublicNotice = {
version: number;
builtIn: boolean;
current: boolean;
publishedAt: string | null;
effectiveDate: string;
changeSummary: string | null;
sections: NoticeSection[];
versions: { version: number; publishedAt: string; changeSummary: string | null; requiresAcceptance: boolean }[];
};
// Email addresses, web addresses and bare "www." names in notice text become links.
const LINK_RE = /([\w.+-]+@[\w-]+\.[\w.]+|https?:\/\/[^\s)]+|www\.[\w-]+\.[\w.]+)/g;
function linkify(line: string) {
const parts = line.split(LINK_RE);
return parts.map((part, i) => {
if (i % 2 === 0) return part;
const href = part.includes("@") && !part.startsWith("http") ? `mailto:${part}` : part.startsWith("http") ? part : `https://${part}`;
return (
<a key={i} href={href} className="text-brand-600 hover:underline break-all" {...(href.startsWith("http") ? { target: "_blank", rel: "noopener noreferrer" } : {})}>
{part}
</a>
);
});
}
/** One section of the privacy notice: one paragraph per line, "• " lines as bullets. */
export function PrivacyNoticeSection({ section, first = false }: { section: NoticeSection; first?: boolean }) {
const lines = section.body.split("\n").filter(l => l.trim() !== "");
return (
<section id={section.id} className="mb-10 scroll-mt-24">
{first ? (
<h2 className="text-2xl font-semibold text-brand-600 mb-4">{section.title}</h2>
) : (
<h3 className="text-xl font-semibold text-brand-600 mb-3">{section.title}</h3>
)}
{section.id === "cookies" && (
<CookieSettingsButton className="mb-3 px-3 py-1.5 text-sm rounded-lg border border-brand-300 text-brand-700 hover:bg-brand-50">
Change my cookie choice
</CookieSettingsButton>
)}
{lines.map((line, i) =>
line.startsWith("• ") ? (
<p key={i} className="mb-1.5 pl-5 -indent-3">{"• "}{linkify(line.slice(2))}</p>
) : (
<p key={i} className="mb-2">{linkify(line)}</p>
),
)}
</section>
);
}