2026-07-30 14:38:05 +00:00
|
|
|
// components/ResolutionExplorer.tsx
|
|
|
|
|
// Client Component. 4-section menu for company registration,
|
|
|
|
|
// director resignation, director appointment, and resolutions.
|
|
|
|
|
// Only Company Registration is active; others are placeholders.
|
|
|
|
|
|
|
|
|
|
"use client";
|
|
|
|
|
|
|
|
|
|
import { useState, useRef, useEffect, useCallback } from "react";
|
|
|
|
|
import type { ComponentProps } from "react";
|
|
|
|
|
import type {
|
|
|
|
|
PBList,
|
|
|
|
|
YsterbaardResolution,
|
|
|
|
|
ResolutionStatus,
|
|
|
|
|
} from "@/lib/types";
|
|
|
|
|
import StatusBadge from "@/components/StatusBadge";
|
|
|
|
|
import { useRouter } from "next/navigation";
|
|
|
|
|
|
|
|
|
|
type Section = "company_registration" | "director_resignation" | "director_appointment" | "resolution_appointment";
|
|
|
|
|
|
|
|
|
|
const SECTIONS: { id: Section; label: string }[] = [
|
|
|
|
|
{ id: "company_registration", label: "1 — Company registration" },
|
|
|
|
|
{ id: "director_resignation", label: "2 — Director resignation" },
|
|
|
|
|
{ id: "director_appointment", label: "3 — Director appointment acceptance" },
|
|
|
|
|
{ id: "resolution_appointment", label: "4 — Resolution appointment and resignation" },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
type BadgeVariant = ComponentProps<typeof StatusBadge>["variant"];
|
|
|
|
|
|
|
|
|
|
function statusVariant(s: ResolutionStatus): BadgeVariant {
|
|
|
|
|
switch (s) {
|
|
|
|
|
case "draft": return "muted";
|
|
|
|
|
case "pending": return "warn";
|
|
|
|
|
case "completed": return "success";
|
|
|
|
|
case "archived": return "muted";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatDate(iso: string | null): string {
|
|
|
|
|
if (!iso) return "—";
|
|
|
|
|
return new Date(iso).toLocaleDateString("en-ZA", {
|
|
|
|
|
day: "2-digit", month: "short", year: "numeric",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
resolutions: PBList<YsterbaardResolution>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface RegSearchItem {
|
|
|
|
|
id: string;
|
|
|
|
|
reference_name: string;
|
|
|
|
|
status: string;
|
|
|
|
|
proposed_name_1: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default function ResolutionExplorer({ resolutions }: Props) {
|
|
|
|
|
const router = useRouter();
|
|
|
|
|
const [activeSection, setActiveSection] = useState<Section>("company_registration");
|
|
|
|
|
const [showNewDialog, setShowNewDialog] = useState(false);
|
|
|
|
|
const [newRegName, setNewRegName] = useState("");
|
|
|
|
|
const [newRegLoading, setNewRegLoading] = useState(false);
|
|
|
|
|
const [generatedLink, setGeneratedLink] = useState<string | null>(null);
|
|
|
|
|
const [dialogError, setDialogError] = useState<string | null>(null);
|
2026-08-11 16:33:03 +00:00
|
|
|
const [dialogExisting, setDialogExisting] = useState<{
|
|
|
|
|
id: string;
|
|
|
|
|
reference_name: string;
|
|
|
|
|
status: string;
|
|
|
|
|
proposed_name_1?: string;
|
|
|
|
|
} | null>(null);
|
2026-07-30 14:38:05 +00:00
|
|
|
const [linkCopied, setLinkCopied] = useState(false);
|
|
|
|
|
|
|
|
|
|
// Search state
|
|
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
|
const [searchResults, setSearchResults] = useState<RegSearchItem[]>([]);
|
|
|
|
|
const [searchLoading, setSearchLoading] = useState(false);
|
|
|
|
|
const [searchFocused, setSearchFocused] = useState(false);
|
|
|
|
|
const searchRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
const searchTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
|
|
|
|
|
|
const filteredResolutions = resolutions.items
|
|
|
|
|
.filter((r) => r.type === activeSection)
|
|
|
|
|
.sort((a, b) => new Date(b.created).getTime() - new Date(a.created).getTime());
|
|
|
|
|
|
|
|
|
|
// Close search dropdown when clicking outside
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
const handler = (e: MouseEvent) => {
|
|
|
|
|
if (searchRef.current && !searchRef.current.contains(e.target as Node)) {
|
|
|
|
|
setSearchFocused(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
document.addEventListener("mousedown", handler);
|
|
|
|
|
return () => document.removeEventListener("mousedown", handler);
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
// Search registrations as user types
|
|
|
|
|
const doSearch = useCallback(async (q: string) => {
|
|
|
|
|
if (q.trim().length === 0) { setSearchResults([]); setSearchLoading(false); return; }
|
|
|
|
|
setSearchLoading(true);
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch(`/api/register/search?q=${encodeURIComponent(q.trim())}&fields=id,reference_name,status,proposed_name_1`);
|
|
|
|
|
if (!res.ok) throw new Error();
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
setSearchResults(data.items || []);
|
|
|
|
|
} catch { setSearchResults([]); }
|
|
|
|
|
finally { setSearchLoading(false); }
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
const q = e.target.value;
|
|
|
|
|
setSearchQuery(q);
|
|
|
|
|
if (searchTimeout.current) clearTimeout(searchTimeout.current);
|
|
|
|
|
if (!q.trim()) { setSearchResults([]); setSearchLoading(false); return; }
|
|
|
|
|
setSearchLoading(true);
|
|
|
|
|
searchTimeout.current = setTimeout(() => doSearch(q), 300);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSelectExisting = (reg: RegSearchItem) => {
|
|
|
|
|
setSearchFocused(false);
|
|
|
|
|
setShowNewDialog(false);
|
|
|
|
|
router.push(`/register/${reg.id}?prefill=1`);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleCreateRegistration = async () => {
|
|
|
|
|
if (!newRegName.trim()) return;
|
|
|
|
|
setNewRegLoading(true);
|
|
|
|
|
setDialogError(null);
|
2026-08-11 16:33:03 +00:00
|
|
|
setDialogExisting(null);
|
2026-07-30 14:38:05 +00:00
|
|
|
try {
|
|
|
|
|
const res = await fetch("/api/register/create", {
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ reference_name: newRegName.trim() }),
|
|
|
|
|
});
|
2026-08-11 16:33:03 +00:00
|
|
|
const data = await res.json().catch(() => ({}));
|
|
|
|
|
if (res.status === 409 && data.duplicate) {
|
|
|
|
|
setDialogExisting(data.existing);
|
|
|
|
|
setDialogError(`"${newRegName.trim()}" is already in use by another registration.`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
2026-07-30 14:38:05 +00:00
|
|
|
setGeneratedLink(`${window.location.origin}/register/${data.id}`);
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
setDialogError(err instanceof Error ? err.message : "Failed to create link");
|
|
|
|
|
} finally {
|
|
|
|
|
setNewRegLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const openNewDialog = () => {
|
2026-08-11 16:33:03 +00:00
|
|
|
setNewRegName(""); setGeneratedLink(null); setDialogError(null); setDialogExisting(null);
|
2026-07-30 14:38:05 +00:00
|
|
|
setSearchQuery(""); setSearchResults([]);
|
|
|
|
|
setShowNewDialog(true);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="container-x py-8 space-y-6">
|
|
|
|
|
{/* 4 Section Buttons */}
|
|
|
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
|
|
|
|
{SECTIONS.map((sec) => {
|
|
|
|
|
const isActive = activeSection === sec.id;
|
|
|
|
|
const isDisabled = sec.id !== "company_registration";
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={sec.id}
|
|
|
|
|
onClick={() => { if (!isDisabled) setActiveSection(sec.id); }}
|
|
|
|
|
className={
|
|
|
|
|
"px-4 py-5 rounded-lg border text-sm font-semibold text-center transition-all " +
|
|
|
|
|
(isDisabled
|
|
|
|
|
? "bg-surface border-border text-muted cursor-not-allowed opacity-50"
|
|
|
|
|
: isActive
|
|
|
|
|
? "bg-accent border-accent text-accent-on shadow-sm"
|
|
|
|
|
: "bg-surface border-border text-fg hover:border-accent hover:text-accent")
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{sec.label}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* ── Company Registration ── */}
|
|
|
|
|
{activeSection === "company_registration" && (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
{/* Header + New button */}
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<p className="text-sm font-semibold text-fg">
|
|
|
|
|
{filteredResolutions.length === 0 ? "No registrations yet" : `${filteredResolutions.length} registration${filteredResolutions.length === 1 ? "" : "s"}`}
|
|
|
|
|
</p>
|
|
|
|
|
<button
|
|
|
|
|
onClick={openNewDialog}
|
|
|
|
|
className="px-4 py-2 bg-accent text-accent-on rounded font-semibold text-sm hover:opacity-90 transition-opacity"
|
|
|
|
|
>
|
|
|
|
|
+ New registration
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Registration search bar */}
|
|
|
|
|
<div ref={searchRef} className="relative">
|
|
|
|
|
<label className="block text-xs font-semibold text-muted uppercase tracking-wider mb-1.5">
|
|
|
|
|
Search submitted registrations
|
|
|
|
|
</label>
|
2026-07-30 16:24:23 +00:00
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<div className="relative flex-1">
|
|
|
|
|
<input
|
|
|
|
|
className="w-full bg-surface border border-border rounded px-3 py-2.5 pr-10 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent transition-colors"
|
|
|
|
|
placeholder="Search by reference or company name..."
|
|
|
|
|
value={searchQuery}
|
|
|
|
|
onChange={handleSearchChange}
|
|
|
|
|
onFocus={() => setSearchFocused(true)}
|
|
|
|
|
/>
|
|
|
|
|
{searchLoading && (
|
|
|
|
|
<div className="absolute right-3 top-1/2 -translate-y-1/2">
|
|
|
|
|
<svg className="w-4 h-4 animate-spin text-muted" fill="none" viewBox="0 0 24 24">
|
|
|
|
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
|
|
|
|
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
|
|
|
|
|
</svg>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<a
|
|
|
|
|
href="/registrations"
|
|
|
|
|
className="flex-shrink-0 px-4 py-2 bg-surface border border-border rounded text-sm font-semibold text-fg hover:border-accent hover:text-accent no-underline transition-colors text-center"
|
|
|
|
|
>
|
|
|
|
|
View all →
|
|
|
|
|
</a>
|
2026-07-30 14:38:05 +00:00
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Search results dropdown */}
|
|
|
|
|
{searchFocused && searchResults.length > 0 && (
|
|
|
|
|
<div className="absolute z-10 top-full left-0 right-0 mt-1 bg-surface border border-border rounded-lg shadow-xl overflow-hidden max-h-72 overflow-y-auto">
|
|
|
|
|
{searchResults.map((reg) => (
|
|
|
|
|
<button
|
|
|
|
|
key={reg.id}
|
|
|
|
|
onClick={() => handleSelectExisting(reg)}
|
|
|
|
|
className="w-full text-left px-4 py-3 hover:bg-surface-warm transition-colors border-b border-border last:border-b-0"
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-start justify-between gap-2">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="text-sm font-semibold text-fg truncate">{reg.reference_name || reg.id}</p>
|
|
|
|
|
<p className="text-xs text-muted truncate">{reg.proposed_name_1 || "No company name"}</p>
|
|
|
|
|
</div>
|
|
|
|
|
<span className={`flex-shrink-0 inline-flex px-2 py-0.5 rounded text-xs font-semibold ${
|
|
|
|
|
reg.status === "submitted"
|
|
|
|
|
? "bg-accent/10 text-accent"
|
|
|
|
|
: "bg-surface border border-border text-muted"
|
|
|
|
|
}`}>
|
|
|
|
|
{reg.status === "submitted" ? "Submitted" : "Draft"}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{searchFocused && !searchLoading && searchQuery.trim() && searchResults.length === 0 && (
|
|
|
|
|
<div className="absolute z-10 top-full left-0 right-0 mt-1 bg-surface border border-border rounded-lg shadow-xl px-4 py-3 text-sm text-muted">
|
|
|
|
|
No registrations found for “{searchQuery}”
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Resolutions list (draft/pending resolutions for company_registration) */}
|
|
|
|
|
{filteredResolutions.length > 0 && (
|
|
|
|
|
<div className="bg-surface border border-border rounded-lg overflow-hidden">
|
|
|
|
|
<table className="w-full text-sm">
|
|
|
|
|
<thead>
|
|
|
|
|
<tr className="border-b border-border bg-surface-warm">
|
|
|
|
|
<th className="text-left px-5 py-3 text-xs font-semibold text-muted uppercase tracking-wide">Title</th>
|
|
|
|
|
<th className="text-left px-5 py-3 text-xs font-semibold text-muted uppercase tracking-wide hidden md:table-cell">Client</th>
|
|
|
|
|
<th className="text-left px-5 py-3 text-xs font-semibold text-muted uppercase tracking-wide hidden lg:table-cell">ID / Ref</th>
|
|
|
|
|
<th className="text-left px-5 py-3 text-xs font-semibold text-muted uppercase tracking-wide">Status</th>
|
|
|
|
|
<th className="text-left px-5 py-3 text-xs font-semibold text-muted uppercase tracking-wide hidden sm:table-cell">Date</th>
|
|
|
|
|
</tr>
|
|
|
|
|
</thead>
|
|
|
|
|
<tbody className="divide-y divide-border-soft">
|
|
|
|
|
{filteredResolutions.map((r) => (
|
|
|
|
|
<tr key={r.id} className="hover:bg-surface-warm transition-colors">
|
|
|
|
|
<td className="px-5 py-4"><span className="font-semibold text-fg">{r.title ?? "—"}</span></td>
|
|
|
|
|
<td className="px-5 py-4 hidden md:table-cell"><span className="text-fg-2">{r.client_name ?? "—"}</span></td>
|
|
|
|
|
<td className="px-5 py-4 hidden lg:table-cell"><span className="font-mono text-xs text-muted">{r.client_id_no ?? "—"}</span></td>
|
|
|
|
|
<td className="px-5 py-4"><StatusBadge variant={statusVariant(r.status)}>{r.status}</StatusBadge></td>
|
|
|
|
|
<td className="px-5 py-4 hidden sm:table-cell"><span className="text-muted tabular-nums text-xs">{formatDate(r.resolution_date ?? r.created)}</span></td>
|
|
|
|
|
</tr>
|
|
|
|
|
))}
|
|
|
|
|
</tbody>
|
|
|
|
|
</table>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* ── Other sections (placeholder) ── */}
|
|
|
|
|
{activeSection !== "company_registration" && (
|
|
|
|
|
<div className="bg-surface border border-border rounded-lg p-12 text-center text-muted text-sm">
|
|
|
|
|
This section is under development.
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* New Registration Dialog */}
|
|
|
|
|
{showNewDialog && (
|
|
|
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
|
|
|
<div className="bg-surface border border-border rounded-xl shadow-2xl w-full max-w-md mx-4 overflow-hidden">
|
|
|
|
|
<div className="border-b border-border bg-surface-warm px-6 py-4">
|
|
|
|
|
<h2 className="text-fg font-semibold text-lg">New registration</h2>
|
|
|
|
|
<p className="text-muted text-sm mt-0.5">Give this registration a reference name so you can find it later.</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="p-6 space-y-4">
|
|
|
|
|
{!generatedLink ? (
|
|
|
|
|
<>
|
|
|
|
|
<div>
|
|
|
|
|
<label className="block text-sm font-semibold text-fg mb-1.5">Reference name</label>
|
|
|
|
|
<input
|
|
|
|
|
className="w-full bg-surface border border-border rounded px-3 py-2.5 text-sm text-fg placeholder-muted focus:outline-none focus:border-accent transition-colors"
|
|
|
|
|
placeholder="e.g. Company A — Smith family"
|
|
|
|
|
value={newRegName}
|
|
|
|
|
onChange={(e) => setNewRegName(e.target.value)}
|
|
|
|
|
onKeyDown={(e) => { if (e.key === "Enter") handleCreateRegistration(); }}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
{dialogError && <p className="text-danger text-sm">{dialogError}</p>}
|
2026-08-11 16:33:03 +00:00
|
|
|
{dialogExisting && (
|
|
|
|
|
<div className="border border-warning/40 bg-warning/5 rounded-lg p-3 space-y-2">
|
|
|
|
|
<p className="text-xs font-semibold text-warning uppercase tracking-wide">Already used by</p>
|
|
|
|
|
<div className="flex items-center justify-between gap-2">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<p className="text-sm font-semibold text-fg truncate">
|
|
|
|
|
{dialogExisting.proposed_name_1 || dialogExisting.reference_name}
|
|
|
|
|
</p>
|
|
|
|
|
<p className="text-xs text-muted">
|
|
|
|
|
Status: <span className="font-mono">{dialogExisting.status}</span>
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<a
|
|
|
|
|
href={`/registrations/${dialogExisting.id}/step2`}
|
|
|
|
|
className="flex-shrink-0 px-3 py-1.5 bg-warning text-warning-on rounded text-xs font-semibold no-underline hover:opacity-90 transition-opacity whitespace-nowrap"
|
|
|
|
|
>
|
|
|
|
|
View →
|
|
|
|
|
</a>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-xs text-muted">
|
|
|
|
|
Use the search box above to find this registration, or cancel it before creating a new one with the same name.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
2026-07-30 14:38:05 +00:00
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
<p className="text-sm text-fg font-semibold">Share this link with your client:</p>
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<input readOnly className="flex-1 bg-surface border border-border rounded px-3 py-2 text-sm text-fg font-mono" value={generatedLink} />
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
const el = document.createElement("textarea");
|
|
|
|
|
el.value = generatedLink ?? "";
|
|
|
|
|
el.style.position = "fixed";
|
|
|
|
|
el.style.opacity = "0";
|
|
|
|
|
document.body.appendChild(el);
|
|
|
|
|
el.focus();
|
|
|
|
|
el.select();
|
|
|
|
|
const ok = document.execCommand("copy");
|
|
|
|
|
document.body.removeChild(el);
|
|
|
|
|
if (ok) { setLinkCopied(true); setTimeout(() => setLinkCopied(false), 2000); }
|
|
|
|
|
}}
|
|
|
|
|
className="px-4 py-2 bg-accent text-accent-on rounded font-semibold text-sm flex-shrink-0"
|
|
|
|
|
>
|
|
|
|
|
{linkCopied ? "✓ Copied" : "Copy"}
|
|
|
|
|
</button>
|
|
|
|
|
{typeof navigator !== "undefined" && navigator.share ? (
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
navigator.share({
|
|
|
|
|
title: "Company Registration",
|
|
|
|
|
text: "Please complete your company registration here:",
|
|
|
|
|
url: generatedLink ?? "",
|
|
|
|
|
}).catch(() => {});
|
|
|
|
|
}}
|
|
|
|
|
className="px-4 py-2 bg-surface border border-border text-fg rounded font-semibold text-sm flex-shrink-0 hover:bg-surface-warm transition-colors"
|
|
|
|
|
>
|
|
|
|
|
Share
|
|
|
|
|
</button>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-xs text-muted">
|
|
|
|
|
Send this link to your client. They will fill in the director, company and shareholder details. You will be notified on Telegram when they submit.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="border-t border-border px-6 py-4 flex justify-end gap-3">
|
|
|
|
|
{!generatedLink ? (
|
|
|
|
|
<>
|
|
|
|
|
<button onClick={() => setShowNewDialog(false)} className="px-4 py-2 text-sm font-semibold text-muted hover:text-fg transition-colors">Cancel</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={handleCreateRegistration}
|
|
|
|
|
disabled={!newRegName.trim() || newRegLoading}
|
|
|
|
|
className="px-4 py-2 bg-accent text-accent-on rounded font-semibold text-sm disabled:opacity-40 hover:opacity-90 transition-opacity"
|
|
|
|
|
>
|
|
|
|
|
{newRegLoading ? "Creating..." : "Create link"}
|
|
|
|
|
</button>
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<button onClick={() => setShowNewDialog(false)} className="px-4 py-2 bg-accent text-accent-on rounded font-semibold text-sm hover:opacity-90 transition-opacity">
|
|
|
|
|
Done
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|