Workflow-App/Workflow/components/CopyButton.tsx
sjcoetzeesj-coder 11d0178ab1 feat: commit Workflow app source; move CIPC credentials to env
Adds the full Next.js app (app/, components/, lib/, scripts/, assets/,
docs/, Dockerfile, compose, configs) to version control.

Security:
- CIPC BizPortal customer-code logins (Login 1 + Login 3) now read
  CIPC_LOGIN1_CODE / CIPC_LOGIN1_PASSWORD / CIPC_LOGIN3_CODE /
  CIPC_LOGIN3_PASSWORD from .env.local instead of source.
- BizPortal personal login in apply-bizportal route reads
  BIZPORTAL_ID_NO / BIZPORTAL_PASSWORD.
- Added .env.example as the committed template.

.gitignore excludes .env*, node_modules, .next, logs, sqlite, Playwright
storage_state.json (live CIPC cookies), scratch dirs, client PDFs and
debug screenshots.
2026-10-18 15:21:45 +02:00

143 lines
3.8 KiB
TypeScript

// components/CopyButton.tsx
// Tiny client component that copies a string to the clipboard with a
// transient "Copied" confirmation. Used on read-only detail pages
// (e.g. SARS eFiling credentials) where selecting individual
// characters by hand is fiddly. Falls back to the legacy textarea
// trick when navigator.clipboard is unavailable (e.g. insecure
// context) so it still works on http://localhost.
"use client";
import { useState } from "react";
interface Props {
/** The string to copy when the user clicks. */
value: string;
/** Accessible label for the button (also shown as the tooltip
* in the idle state). Defaults to "Copy". */
label?: string;
/** Optional extra classes appended to the button. */
className?: string;
}
type State = "idle" | "copied" | "error";
export default function CopyButton({
value,
label = "Copy",
className = "",
}: Props) {
const [state, setState] = useState<State>("idle");
async function handleClick() {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(value);
} else {
// Legacy fallback for browsers that block the async Clipboard
// API (e.g. served over plain http://). Mirrors the pattern
// used in ShareLinkButton so behaviour is consistent.
const ta = document.createElement("textarea");
ta.value = value;
ta.setAttribute("readonly", "");
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
document.execCommand("copy");
document.body.removeChild(ta);
}
setState("copied");
} catch {
setState("error");
}
// Revert after a short delay so the icon change is noticeable
// without sticking around and confusing later clicks.
window.setTimeout(() => setState("idle"), 1800);
}
const title =
state === "copied"
? "Copied!"
: state === "error"
? "Copy failed — try selecting manually"
: label;
return (
<button
type="button"
onClick={handleClick}
title={title}
aria-label={title}
aria-live="polite"
className={
"inline-flex items-center justify-center w-7 h-7 rounded-md text-muted hover:text-fg hover:bg-surface-warm active:bg-surface-warm transition-colors " +
className
}
>
{state === "copied" ? (
<IconCheck className="w-4 h-4 text-success" />
) : state === "error" ? (
<IconAlert className="w-4 h-4 text-danger" />
) : (
<IconCopy className="w-4 h-4" />
)}
</button>
);
}
function IconCopy({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<rect x="9" y="9" width="11" height="11" rx="2" />
<path d="M5 15V5a2 2 0 0 1 2-2h10" />
</svg>
);
}
function IconCheck({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M5 12.5l4.5 4.5L19 7" />
</svg>
);
}
function IconAlert({ className }: { className?: string }) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={1.8}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
aria-hidden="true"
>
<path d="M12 9v4" />
<path d="M12 17h.01" />
<path
d="M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0Z"
/>
</svg>
);
}