Refactored UI endpoint usage
This commit is contained in:
@@ -38,8 +38,7 @@ export const AccountActivationModal: React.FC<AccountActivationModalProps> = ({
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(initialError || null);
|
||||
const [syncAttemptCount, setSyncAttemptCount] = useState(0);
|
||||
|
||||
// Activation process dismisses all other modals so that it is displayed without overlapping.
|
||||
// By extension, any active session expiration modal is dismissed and its processes cancelled.
|
||||
// Clear any existing session prompt and close background dialogs
|
||||
useEffect(() => {
|
||||
cancelSessionExpiration();
|
||||
dismissSubordinateModals();
|
||||
|
||||
@@ -21,14 +21,14 @@ export const SessionExpiredModal: React.FC<SessionExpiredModalProps> = ({
|
||||
const totalDurationMs = countdownSeconds * 1000;
|
||||
const isClearingRef = useRef(false);
|
||||
|
||||
// When session expiration modal is displayed, dismiss all subordinate modals
|
||||
// Close background modals when session expires
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
dismissSubordinateModals();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Listen for cancellation event (e.g., when activation lockout takes priority)
|
||||
// Reset state if session expiration is cancelled
|
||||
useEffect(() => {
|
||||
const handleCancelled = () => {
|
||||
setTimeLeftMs(countdownSeconds * 1000);
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '../../auth/AuthProvider';
|
||||
import { BrandLogo } from '../ui/BrandLogo';
|
||||
import { HelpTooltip } from '../ui/HelpTooltip';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUsers } from '../../services/api';
|
||||
import { getUserEmail, registerUserEmail } from '../../lib/formatters';
|
||||
@@ -27,31 +28,37 @@ const navItems = [
|
||||
name: 'Executive Dashboard',
|
||||
path: '/dashboard',
|
||||
icon: LayoutDashboard,
|
||||
help: 'Strategic macro metrics, portfolio value, disbursed funding, ISO 31000 risk heatmap, and projects requiring immediate intervention.',
|
||||
},
|
||||
{
|
||||
name: 'Governance & Structure',
|
||||
path: '/governance',
|
||||
icon: Building2,
|
||||
help: 'Master records for legal entities, holding structures, investment portfolios, and thematic programmes funded under statutory mandates.',
|
||||
},
|
||||
{
|
||||
name: 'Projects & Milestones',
|
||||
path: '/projects',
|
||||
icon: FolderKanban,
|
||||
help: 'Project Work Breakdown Structure (WBS), contractual deliverables, scheduled milestones, evidence verification, and execution tracking.',
|
||||
},
|
||||
{
|
||||
name: 'Grant & Financials',
|
||||
path: '/grants',
|
||||
icon: Coins,
|
||||
help: 'Financial management including statutory awards, multi-year budget line allocations, tranches, and scheduled payment disbursements.',
|
||||
},
|
||||
{
|
||||
name: 'M&E, Evidence & Risks',
|
||||
path: '/monitoring',
|
||||
icon: ShieldAlert,
|
||||
help: 'Monitoring & Evaluation (M&E) framework, indicator tracking, ISO 31000 risk registers, mitigation plans, and compliance audits.',
|
||||
},
|
||||
{
|
||||
name: 'Settings & Communications',
|
||||
path: '/settings',
|
||||
icon: Settings,
|
||||
help: 'User account provisioning, statutory sync, tenant environment parameters, and communication invite templates.',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -109,7 +116,7 @@ export const AppLayout: React.FC = () => {
|
||||
sidebarOpen ? 'w-64' : 'w-20'
|
||||
} shrink-0 bg-[#0b101b]/95 text-slate-200 border-r border-slate-800/80 transition-all duration-300 flex flex-col z-30 shadow-2xl backdrop-blur-md`}
|
||||
>
|
||||
{/* Brand Header — Clickable link navigating to auth-aware Home page */}
|
||||
{/* Brand Header */}
|
||||
<div className="h-16 px-4 flex items-center justify-between border-b border-slate-800/80 bg-[#0d121f]/90">
|
||||
<Link
|
||||
to="/"
|
||||
@@ -141,25 +148,36 @@ export const AppLayout: React.FC = () => {
|
||||
{navItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-3.5 py-2.5 rounded-xl text-xs font-medium transition-all ${
|
||||
isActive
|
||||
? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-md shadow-blue-600/25 font-semibold'
|
||||
: 'text-slate-400 hover:text-slate-100 hover:bg-slate-800/60'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon className="w-4 h-4 shrink-0" />
|
||||
{sidebarOpen && <span className="truncate">{item.name}</span>}
|
||||
</NavLink>
|
||||
<div key={item.path} className="flex items-center justify-between group">
|
||||
<NavLink
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
`flex items-center gap-3 px-3.5 py-2.5 rounded-xl text-xs font-medium transition-all flex-1 min-w-0 ${
|
||||
isActive
|
||||
? 'bg-gradient-to-r from-blue-600 to-indigo-600 text-white shadow-md shadow-blue-600/25 font-semibold'
|
||||
: 'text-slate-400 hover:text-slate-100 hover:bg-slate-800/60'
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon className="w-4 h-4 shrink-0" />
|
||||
{sidebarOpen && <span className="truncate">{item.name}</span>}
|
||||
</NavLink>
|
||||
{sidebarOpen && (
|
||||
<div className="pl-1">
|
||||
<HelpTooltip
|
||||
title={item.name}
|
||||
content={item.help}
|
||||
size="sm"
|
||||
placement="right"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* User Card with Token-derived info & Identity Server Logout */}
|
||||
{/* User Card */}
|
||||
<div className="p-3 border-t border-slate-800/80 bg-[#0d121f]/90">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="h-9 w-9 shrink-0 rounded-full bg-blue-600/20 border border-blue-500/40 flex items-center justify-center font-bold text-xs text-blue-400">
|
||||
@@ -192,7 +210,7 @@ export const AppLayout: React.FC = () => {
|
||||
{/* Top Header */}
|
||||
<header className="h-16 bg-[#0d121f]/90 border-b border-slate-800/80 backdrop-blur-md px-4 sm:px-6 md:px-8 flex items-center justify-between shrink-0 z-20">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Top Bar Brand Icon & Direct Portal Link */}
|
||||
{/* Brand Icon */}
|
||||
<Link
|
||||
to="/"
|
||||
id="header-brand-logo-link"
|
||||
@@ -207,7 +225,7 @@ export const AppLayout: React.FC = () => {
|
||||
|
||||
<span className="h-4 w-px bg-slate-800 hidden md:block" />
|
||||
|
||||
{/* Clickable Home Link */}
|
||||
{/* Home Navigation */}
|
||||
<Link
|
||||
to="/"
|
||||
id="header-home-link"
|
||||
@@ -229,17 +247,31 @@ export const AppLayout: React.FC = () => {
|
||||
<Database className="w-3.5 h-3.5 text-blue-400 shrink-0" />
|
||||
<span className="truncate">{tenantName}</span>
|
||||
</span>
|
||||
<HelpTooltip
|
||||
title="Tenant Context"
|
||||
content="Identifies the multi-tenant organization partition and database boundary within the national PFM environment."
|
||||
size="sm"
|
||||
placement="bottom"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ChevronRight className="w-4 h-4 text-slate-600 hidden sm:block" />
|
||||
|
||||
<span className="text-xs font-medium text-emerald-400 bg-emerald-500/10 px-2.5 py-0.5 rounded-full border border-emerald-500/30 flex items-center gap-1.5 shadow-xs">
|
||||
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />
|
||||
Authenticated Session
|
||||
</span>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs font-medium text-emerald-400 bg-emerald-500/10 px-2.5 py-0.5 rounded-full border border-emerald-500/30 flex items-center gap-1.5 shadow-xs">
|
||||
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" />
|
||||
Authenticated Session
|
||||
</span>
|
||||
<HelpTooltip
|
||||
title="Security Session Status"
|
||||
content="Current session is verified against OIDC Identity Server with active cryptographic token and claims."
|
||||
size="sm"
|
||||
placement="bottom"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* User Name & Email derived from token, plus Identity Server signout */}
|
||||
{/* User Profile and Controls */}
|
||||
<div className="relative flex items-center gap-3">
|
||||
<div className="text-right hidden sm:block">
|
||||
<p className="text-xs font-semibold text-white truncate max-w-[200px]" title={tokenUserName}>
|
||||
@@ -292,7 +324,7 @@ export const AppLayout: React.FC = () => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Viewport with Landing Page ambient dot grid and glow */}
|
||||
{/* Main Content Area */}
|
||||
<main className="flex-1 overflow-y-auto p-6 md:p-8 bg-[#090d16] relative text-slate-100">
|
||||
<div
|
||||
className="absolute inset-0 pointer-events-none opacity-15"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,12 +17,7 @@ const sizeMap = {
|
||||
};
|
||||
|
||||
/**
|
||||
* Modern, stylized SVG brand emblem representing Post Fund Management (PFM).
|
||||
* Features an isometric kinetic nexus with three interlocking governance facets:
|
||||
* 1. Top Canopy: Governance, Strategy & Portfolios (Radiant Indigo / Violet)
|
||||
* 2. Left Vault: Capital, Grants & Disbursement Assurance (Electric Sapphire)
|
||||
* 3. Right Wing: Milestones, Evidence & Impact Delivery (Cyan to Emerald)
|
||||
* 4. Center Core: Luminous Floating Diamond Milestone Star
|
||||
* SVG brand logo component for Post Fund Management (PFM).
|
||||
*/
|
||||
export const BrandLogo: React.FC<BrandLogoProps> = ({
|
||||
size = 'md',
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import React, { useState, useRef, useEffect, ReactNode } from 'react';
|
||||
import { Info, X } from 'lucide-react';
|
||||
|
||||
export interface HelpTooltipProps {
|
||||
/** Concise context-sensitive explanation of the section, control, metric, or dialog */
|
||||
content: ReactNode;
|
||||
/** Optional title for the tooltip popover */
|
||||
title?: string;
|
||||
/** Custom alignment: top, bottom, left, right (defaults to top) */
|
||||
placement?: 'top' | 'bottom' | 'left' | 'right';
|
||||
/** Size of the (i) icon: sm (13px), md (15px), lg (18px) */
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
/** Additional styling classes for the container or trigger button */
|
||||
className?: string;
|
||||
/** Accessible label or ARIA label */
|
||||
ariaLabel?: string;
|
||||
}
|
||||
|
||||
export const HelpTooltip: React.FC<HelpTooltipProps> = ({
|
||||
content,
|
||||
title,
|
||||
placement = 'top',
|
||||
size = 'md',
|
||||
className = '',
|
||||
ariaLabel = 'Information and guidance',
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Close on Escape or click outside
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
const handleOutsideClick = (e: MouseEvent | TouchEvent) => {
|
||||
if (containerRef.current && !containerRef.current.contains(e.target as Node)) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleOutsideClick);
|
||||
document.addEventListener('touchstart', handleOutsideClick);
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleOutsideClick);
|
||||
document.removeEventListener('touchstart', handleOutsideClick);
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [isOpen]);
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'w-3.5 h-3.5 text-[11px]',
|
||||
md: 'w-4 h-4 text-xs',
|
||||
lg: 'w-5 h-5 text-sm',
|
||||
}[size];
|
||||
|
||||
const iconSizes = {
|
||||
sm: 'w-3 h-3',
|
||||
md: 'w-3.5 h-3.5',
|
||||
lg: 'w-4 h-4',
|
||||
}[size];
|
||||
|
||||
const placementClasses = {
|
||||
top: 'bottom-full left-1/2 -translate-x-1/2 mb-2',
|
||||
bottom: 'top-full left-1/2 -translate-x-1/2 mt-2',
|
||||
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
|
||||
right: 'left-full top-1/2 -translate-y-1/2 ml-2',
|
||||
}[placement];
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={`relative inline-flex items-center align-middle ${className}`}>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={ariaLabel}
|
||||
aria-expanded={isOpen}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen((prev) => !prev);
|
||||
}}
|
||||
className={`inline-flex items-center justify-center rounded-full text-slate-400 hover:text-blue-400 bg-slate-800/80 hover:bg-slate-700/80 border border-slate-700/60 hover:border-blue-500/50 transition-all cursor-pointer shadow-xs focus:outline-none focus:ring-1 focus:ring-blue-500/60 ${sizeClasses} p-0.5`}
|
||||
title="Click for context guidance"
|
||||
>
|
||||
<Info className={`${iconSizes} shrink-0`} />
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div
|
||||
role="tooltip"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className={`absolute ${placementClasses} z-50 w-72 max-w-[calc(100vw-32px)] p-3 rounded-xl bg-[#0c121e] border border-blue-500/30 text-slate-200 shadow-2xl backdrop-blur-md animate-in fade-in zoom-in-95 duration-150`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2 border-b border-slate-800/80 pb-1.5 mb-2">
|
||||
<div className="flex items-center gap-1.5 text-blue-400">
|
||||
<Info className="w-3.5 h-3.5 shrink-0" />
|
||||
<span className="font-semibold text-xs tracking-tight text-white">
|
||||
{title || 'Guidance & Information'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setIsOpen(false);
|
||||
}}
|
||||
className="rounded-md p-0.5 text-slate-400 hover:text-white hover:bg-slate-800 transition cursor-pointer"
|
||||
aria-label="Close tooltip"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="text-[11px] leading-relaxed text-slate-300 font-normal space-y-1">
|
||||
{content}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,11 +1,13 @@
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import { X } from 'lucide-react';
|
||||
import { HelpTooltip } from './HelpTooltip';
|
||||
|
||||
interface ModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
helpText?: ReactNode;
|
||||
children: ReactNode;
|
||||
maxWidth?: 'sm' | 'md' | 'lg' | 'xl' | '2xl';
|
||||
}
|
||||
@@ -15,6 +17,7 @@ export const Modal: React.FC<ModalProps> = ({
|
||||
onClose,
|
||||
title,
|
||||
subtitle,
|
||||
helpText,
|
||||
children,
|
||||
maxWidth = 'lg',
|
||||
}) => {
|
||||
@@ -56,7 +59,17 @@ export const Modal: React.FC<ModalProps> = ({
|
||||
>
|
||||
<div className="flex items-start justify-between p-5 border-b border-slate-800/80 bg-[#0d121f]/90">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white tracking-tight">{title}</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-white tracking-tight">{title}</h3>
|
||||
{helpText && (
|
||||
<HelpTooltip
|
||||
title={`${title} Guidance`}
|
||||
content={helpText}
|
||||
size="sm"
|
||||
placement="bottom"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{subtitle && <p className="text-xs text-slate-400 mt-0.5">{subtitle}</p>}
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { AlertCircle, AlertTriangle, X } from 'lucide-react';
|
||||
import { parseFriendlyError } from '../../utils/apiErrorParser';
|
||||
|
||||
interface ProblemDetailsAlertProps {
|
||||
error: unknown;
|
||||
onDismiss?: () => void;
|
||||
title?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const ProblemDetailsAlert: React.FC<ProblemDetailsAlertProps> = ({
|
||||
error,
|
||||
onDismiss,
|
||||
title,
|
||||
className = '',
|
||||
}) => {
|
||||
if (!error) return null;
|
||||
|
||||
const parsed = parseFriendlyError(error, title || 'Action Could Not Be Completed');
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className={`rounded-xl border border-rose-500/40 bg-[#160d14] p-4 text-gray-200 shadow-lg shadow-rose-950/20 ${className}`}
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertCircle className="w-5 h-5 text-rose-400 shrink-0 mt-0.5" />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h4 className="text-sm font-semibold text-white tracking-tight">
|
||||
{parsed.title}
|
||||
</h4>
|
||||
{parsed.categoryLabel && (
|
||||
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded border bg-rose-950/60 text-rose-300 border-rose-800/60">
|
||||
{parsed.categoryLabel}
|
||||
</span>
|
||||
)}
|
||||
{parsed.statusCode && (
|
||||
<span className="text-[10px] font-mono font-medium px-1.5 py-0.5 rounded bg-slate-800 text-slate-300 border border-slate-700">
|
||||
HTTP {parsed.statusCode}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Single error or fallback message displayed directly */}
|
||||
{(!parsed.errorsList || parsed.errorsList.length <= 1) && parsed.message && (
|
||||
<p className="text-xs mt-1.5 text-rose-200/90 leading-relaxed whitespace-pre-line">
|
||||
{parsed.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Multiple errors from ProblemDetails displayed directly */}
|
||||
{parsed.errorsList && parsed.errorsList.length > 1 && (
|
||||
<ul className="mt-2 space-y-1 bg-black/30 rounded-lg p-2.5 border border-rose-900/40">
|
||||
{parsed.errorsList.map((err, idx) => (
|
||||
<li key={idx} className="text-xs text-rose-200/90 flex items-start gap-2">
|
||||
<span className="text-rose-400 mt-0.5">•</span>
|
||||
<span className="leading-snug">{err}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{parsed.technicalDetails && (
|
||||
<p className="text-[10px] font-mono text-slate-500 mt-2">
|
||||
Ref: {parsed.technicalDetails}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{onDismiss && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onDismiss}
|
||||
className="text-slate-400 hover:text-white p-1 rounded-lg hover:bg-slate-800/60 transition-colors shrink-0 cursor-pointer"
|
||||
aria-label="Dismiss error"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -2,6 +2,7 @@ import React, { useState, useMemo } from 'react';
|
||||
import { Risk, Project } from '../../types';
|
||||
import { getUserEmail } from '../../lib/formatters';
|
||||
import { AlertTriangle, ShieldAlert, CheckCircle2, ChevronRight, X, Info } from 'lucide-react';
|
||||
import { HelpTooltip } from './HelpTooltip';
|
||||
|
||||
interface RiskHeatmapMatrixProps {
|
||||
risks: Risk[];
|
||||
@@ -168,7 +169,15 @@ export const RiskHeatmapMatrix: React.FC<RiskHeatmapMatrixProps> = ({
|
||||
{/* Header & Controls */}
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div>
|
||||
{title && <h3 className="text-sm font-semibold text-white">{title}</h3>}
|
||||
<div className="flex items-center gap-2">
|
||||
{title && <h3 className="text-sm font-semibold text-white">{title}</h3>}
|
||||
<HelpTooltip
|
||||
title="Risk Heatmap Guidance"
|
||||
content="Visualizes project risk exposure based on ISO 31000 standards. Severity score = Likelihood (1–5) × Impact (1–5). Click any populated cell to drill into registered risks."
|
||||
size="sm"
|
||||
placement="bottom"
|
||||
/>
|
||||
</div>
|
||||
{subtitle && <p className="text-xs text-slate-400 mt-0.5">{subtitle}</p>}
|
||||
</div>
|
||||
|
||||
@@ -200,6 +209,12 @@ export const RiskHeatmapMatrix: React.FC<RiskHeatmapMatrixProps> = ({
|
||||
3×3 Summary
|
||||
</button>
|
||||
</div>
|
||||
<HelpTooltip
|
||||
title="Matrix Resolution Switcher"
|
||||
content="Toggle between 5×5 detailed granular risk distribution and 3×3 executive aggregated categories (Low, Medium, High)."
|
||||
size="sm"
|
||||
placement="left"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface ToastItem {
|
||||
categoryLabel?: string;
|
||||
technicalDetails?: string;
|
||||
fieldErrors?: Array<{ field: string; message: string }>;
|
||||
errorsList?: string[];
|
||||
}
|
||||
|
||||
export interface ToastOptions {
|
||||
@@ -34,6 +35,7 @@ export interface ToastOptions {
|
||||
categoryLabel?: string;
|
||||
technicalDetails?: string;
|
||||
fieldErrors?: Array<{ field: string; message: string }>;
|
||||
errorsList?: string[];
|
||||
}
|
||||
|
||||
interface ToastContextType {
|
||||
@@ -153,11 +155,21 @@ const ToastCard: React.FC<{
|
||||
)}
|
||||
</div>
|
||||
|
||||
{toast.message && (
|
||||
{/* Display errors directly if multiple, or single message directly */}
|
||||
{toast.errorsList && toast.errorsList.length > 1 ? (
|
||||
<ul className="mt-1.5 space-y-1">
|
||||
{toast.errorsList.map((err, idx) => (
|
||||
<li key={idx} className="text-xs text-rose-200/90 flex items-start gap-1.5">
|
||||
<span className="text-rose-400 mt-0.5">•</span>
|
||||
<span className="leading-snug break-words">{err}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : toast.message ? (
|
||||
<p className="text-xs mt-1.5 text-slate-300 leading-relaxed whitespace-pre-line break-words">
|
||||
{toast.message}
|
||||
</p>
|
||||
)}
|
||||
) : null}
|
||||
|
||||
{/* Optional collapsible technical reference */}
|
||||
{toast.technicalDetails && (
|
||||
@@ -255,6 +267,7 @@ export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) =
|
||||
categoryLabel: options?.categoryLabel,
|
||||
technicalDetails: options?.technicalDetails,
|
||||
fieldErrors: options?.fieldErrors,
|
||||
errorsList: options?.errorsList,
|
||||
},
|
||||
]);
|
||||
};
|
||||
@@ -302,6 +315,7 @@ export const ToastProvider: React.FC<{ children: ReactNode }> = ({ children }) =
|
||||
categoryLabel: options?.categoryLabel || parsed.categoryLabel,
|
||||
technicalDetails: parsed.technicalDetails || options?.technicalDetails,
|
||||
fieldErrors: parsed.fieldErrors || options?.fieldErrors,
|
||||
errorsList: parsed.errorsList || options?.errorsList,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user