Refactored Import.tsx

This commit is contained in:
Rishi Ghan
2026-04-15 11:53:53 -04:00
parent 2dc38b6c95
commit 4514f578ae
5 changed files with 403 additions and 212 deletions

View File

@@ -36,24 +36,31 @@ const b = 2;
// const a = 1, b = 2; // WRONG
Types and Interfaces
Prefer Interfaces Over Type Aliases
Prefer Type Aliases Over Interfaces
// Good: interface for object shapes
interface User {
id: string;
name: string;
email?: string;
}
// Avoid: type alias for object shapes
// Good: type alias for object shapes
type User = {
id: string;
name: string;
email?: string;
};
// Type aliases OK for unions, intersections, mapped types
// Avoid: interface for object shapes
// interface User {
// id: string;
// name: string;
// }
// Type aliases work for everything: objects, unions, intersections, mapped types
type Status = 'active' | 'inactive';
type Combined = TypeA & TypeB;
type Handler = (event: Event) => void;
// Benefits of types over interfaces:
// 1. Consistent syntax for all type definitions
// 2. Cannot be merged/extended unexpectedly (no declaration merging)
// 3. Better for union types and computed properties
// 4. Works with utility types more naturally
Type Inference

View File

@@ -6,7 +6,6 @@
*/
import { ReactElement, useEffect, useRef, useState } from "react";
import { format } from "date-fns";
import { isEmpty } from "lodash";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useStore } from "../../store";
@@ -14,30 +13,11 @@ import { useShallow } from "zustand/react/shallow";
import axios from "axios";
import { useGetJobResultStatisticsQuery } from "../../graphql/generated";
import { RealTimeImportStats } from "./RealTimeImportStats";
import { PastImportsTable } from "./PastImportsTable";
import { AlertBanner } from "../shared/AlertBanner";
import { useImportSessionStatus } from "../../hooks/useImportSessionStatus";
import { SETTINGS_SERVICE_BASE_URI } from "../../constants/endpoints";
/**
* Represents an issue with a configured directory.
* @interface DirectoryIssue
* @property {string} directory - Path to the directory with issues
* @property {string} issue - Description of the issue
*/
interface DirectoryIssue {
directory: string;
issue: string;
}
/**
* Result of directory status check from the backend.
* @interface DirectoryStatus
* @property {boolean} isValid - Whether all required directories are accessible
* @property {DirectoryIssue[]} issues - List of specific issues found
*/
interface DirectoryStatus {
isValid: boolean;
issues: DirectoryIssue[];
}
import type { DirectoryStatus, DirectoryIssue } from "./import.types";
/**
* Import page component for managing comic library imports.
@@ -59,28 +39,36 @@ export const Import = (): ReactElement => {
importJobQueue: state.importJobQueue,
getSocket: state.getSocket,
disconnectSocket: state.disconnectSocket,
})),
}))
);
// Check if required directories exist
const { data: directoryStatus, isLoading: isCheckingDirectories, isError: isDirectoryCheckError, error: directoryError } = useQuery({
const {
data: directoryStatus,
isLoading: isCheckingDirectories,
isError: isDirectoryCheckError,
error: directoryError,
} = useQuery({
queryKey: ["directoryStatus"],
queryFn: async (): Promise<DirectoryStatus> => {
const response = await axios.get(`${SETTINGS_SERVICE_BASE_URI}/getDirectoryStatus`);
const response = await axios.get(
`${SETTINGS_SERVICE_BASE_URI}/getDirectoryStatus`
);
return response.data;
},
refetchOnWindowFocus: false,
staleTime: 30000, // Cache for 30 seconds
retry: false, // Don't retry on failure - show error immediately
staleTime: 30000,
retry: false,
});
// Use isValid for quick check, issues array for detailed display
// If there's an error fetching directory status, assume directories are invalid
const directoryCheckFailed = isDirectoryCheckError;
const hasAllDirectories = directoryCheckFailed ? false : (directoryStatus?.isValid ?? true);
const hasAllDirectories = directoryCheckFailed
? false
: (directoryStatus?.isValid ?? true);
const directoryIssues = directoryStatus?.issues ?? [];
// Force re-import mutation - re-imports all files regardless of import status
// Force re-import mutation
const { mutate: forceReImport, isPending: isForceReImporting } = useMutation({
mutationFn: async () => {
const sessionId = localStorage.getItem("sessionId") || "";
@@ -97,7 +85,11 @@ export const Import = (): ReactElement => {
},
onError: (error: any) => {
console.error("Failed to start force re-import:", error);
setImportError(error?.response?.data?.message || error?.message || "Failed to start force re-import. Please try again.");
setImportError(
error?.response?.data?.message ||
error?.message ||
"Failed to start force re-import. Please try again."
);
},
});
@@ -107,14 +99,11 @@ export const Import = (): ReactElement => {
const hasActiveSession = importSession.isActive;
const wasComplete = useRef(false);
// React to importSession.isComplete rather than socket events — more reliable
// since it's derived from the actual GraphQL state, not a raw socket event.
// React to importSession.isComplete for state updates
useEffect(() => {
if (importSession.isComplete && !wasComplete.current) {
wasComplete.current = true;
// Small delay so the backend has time to commit job result stats
setTimeout(() => {
// Invalidate the cache to force a fresh fetch of job result statistics
queryClient.invalidateQueries({ queryKey: ["GetJobResultStatistics"] });
refetch();
}, 1500);
@@ -124,21 +113,23 @@ export const Import = (): ReactElement => {
}
}, [importSession.isComplete, refetch, importJobQueue, queryClient]);
// Listen to socket events to update Past Imports table in real-time
// Listen to socket events to update Past Imports table
useEffect(() => {
const socket = getSocket("/");
const handleImportCompleted = () => {
console.log("[Import] IMPORT_SESSION_COMPLETED event - refreshing Past Imports");
// Small delay to ensure backend has committed the job results
console.log(
"[Import] IMPORT_SESSION_COMPLETED event - refreshing Past Imports"
);
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ["GetJobResultStatistics"] });
}, 1500);
};
const handleQueueDrained = () => {
console.log("[Import] LS_IMPORT_QUEUE_DRAINED event - refreshing Past Imports");
// Small delay to ensure backend has committed the job results
console.log(
"[Import] LS_IMPORT_QUEUE_DRAINED event - refreshing Past Imports"
);
setTimeout(() => {
queryClient.invalidateQueries({ queryKey: ["GetJobResultStatistics"] });
}, 1500);
@@ -159,14 +150,15 @@ export const Import = (): ReactElement => {
const handleForceReImport = async () => {
setImportError(null);
// Check for missing directories before starting
if (!hasAllDirectories) {
if (directoryCheckFailed) {
setImportError(
"Cannot start import: Failed to verify directory status. Please check that the backend service is running."
);
} else {
const issueDetails = directoryIssues.map(i => `${i.directory}: ${i.issue}`).join(", ");
const issueDetails = directoryIssues
.map((i) => `${i.directory}: ${i.issue}`)
.join(", ");
setImportError(
`Cannot start import: ${issueDetails || "Required directories are missing"}. Please check your Docker volume configuration.`
);
@@ -174,7 +166,6 @@ export const Import = (): ReactElement => {
return;
}
// Check for active session before starting using definitive status
if (hasActiveSession) {
setImportError(
`Cannot start import: An import session "${importSession.sessionId}" is already active. Please wait for it to complete.`
@@ -182,10 +173,12 @@ export const Import = (): ReactElement => {
return;
}
if (window.confirm(
"This will re-import ALL files in your library folder, even those already imported. " +
"This can help fix Elasticsearch indexing issues. Continue?"
)) {
if (
window.confirm(
"This will re-import ALL files in your library folder, even those already imported. " +
"This can help fix Elasticsearch indexing issues. Continue?"
)
) {
if (importJobQueue.status === "drained") {
localStorage.removeItem("sessionId");
disconnectSocket("/");
@@ -201,6 +194,10 @@ export const Import = (): ReactElement => {
}
};
const canStartImport =
!hasActiveSession &&
(importJobQueue.status === "drained" || importJobQueue.status === undefined);
return (
<div>
<section>
@@ -211,7 +208,6 @@ export const Import = (): ReactElement => {
<h1 className="text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
Import
</h1>
<p className="mt-1.5 text-sm text-gray-500 dark:text-white">
Import comics into the ThreeTwo library.
</p>
@@ -247,97 +243,72 @@ export const Import = (): ReactElement => {
{/* Error Message */}
{importError && (
<div className="my-6 max-w-screen-lg rounded-lg border-s-4 border-red-500 bg-red-50 dark:bg-red-900/20 p-4">
<div className="flex items-start gap-3">
<span className="w-6 h-6 text-red-600 dark:text-red-400 mt-0.5">
<i className="h-6 w-6 icon-[solar--danger-circle-bold]"></i>
</span>
<div className="flex-1">
<p className="font-semibold text-red-800 dark:text-red-300">
Import Error
</p>
<p className="text-sm text-red-700 dark:text-red-400 mt-1">
{importError}
</p>
</div>
<button
onClick={() => setImportError(null)}
className="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200"
>
<span className="w-5 h-5">
<i className="h-5 w-5 icon-[solar--close-circle-bold]"></i>
</span>
</button>
</div>
<div className="my-6 max-w-screen-lg">
<AlertBanner
severity="error"
title="Import Error"
onClose={() => setImportError(null)}
>
{importError}
</AlertBanner>
</div>
)}
{/* Directory Check Error - shown when API call fails */}
{/* Directory Check Error */}
{!isCheckingDirectories && directoryCheckFailed && (
<div className="my-6 max-w-screen-lg rounded-lg border-s-4 border-red-500 bg-red-50 dark:bg-red-900/20 p-4">
<div className="flex items-start gap-3">
<span className="w-6 h-6 text-red-600 dark:text-red-400 mt-0.5">
<i className="h-6 w-6 icon-[solar--danger-circle-bold]"></i>
</span>
<div className="flex-1">
<p className="font-semibold text-red-800 dark:text-red-300">
Failed to Check Directory Status
</p>
<p className="text-sm text-red-700 dark:text-red-400 mt-1">
Unable to verify if required directories exist. Import functionality has been disabled.
</p>
<p className="text-sm text-red-700 dark:text-red-400 mt-2">
Error: {(directoryError as Error)?.message || "Unknown error"}
</p>
</div>
</div>
<div className="my-6 max-w-screen-lg">
<AlertBanner severity="error" title="Failed to Check Directory Status">
<p>
Unable to verify if required directories exist. Import
functionality has been disabled.
</p>
<p className="mt-2">
Error: {(directoryError as Error)?.message || "Unknown error"}
</p>
</AlertBanner>
</div>
)}
{/* Directory Status Warning - shown when directories have issues */}
{!isCheckingDirectories && !directoryCheckFailed && directoryIssues.length > 0 && (
<div className="my-6 max-w-screen-lg rounded-lg border-s-4 border-amber-500 bg-amber-50 dark:bg-amber-900/20 p-4">
<div className="flex items-start gap-3">
<span className="w-6 h-6 text-amber-600 dark:text-amber-400 mt-0.5">
<i className="h-6 w-6 icon-[solar--folder-error-bold]"></i>
</span>
<div className="flex-1">
<p className="font-semibold text-amber-800 dark:text-amber-300">
Directory Configuration Issues
{/* Directory Status Warning */}
{!isCheckingDirectories &&
!directoryCheckFailed &&
directoryIssues.length > 0 && (
<div className="my-6 max-w-screen-lg">
<AlertBanner
severity="warning"
title="Directory Configuration Issues"
iconClass="icon-[solar--folder-error-bold]"
>
<p>
The following issues were detected with your directory
configuration:
</p>
<p className="text-sm text-amber-700 dark:text-amber-400 mt-1">
The following issues were detected with your directory configuration:
<DirectoryIssuesList issues={directoryIssues} />
<p className="mt-2">
Please ensure these directories are mounted correctly in your
Docker configuration.
</p>
<ul className="list-disc list-inside text-sm text-amber-700 dark:text-amber-400 mt-2">
{directoryIssues.map((item) => (
<li key={item.directory}>
<code className="bg-amber-100 dark:bg-amber-900/50 px-1 rounded">{item.directory}</code>
<span className="ml-1"> {item.issue}</span>
</li>
))}
</ul>
<p className="text-sm text-amber-700 dark:text-amber-400 mt-2">
Please ensure these directories are mounted correctly in your Docker configuration.
</p>
</div>
</AlertBanner>
</div>
</div>
)}
)}
{/* Force Re-Import Button - always shown when no import is running */}
{!hasActiveSession &&
(importJobQueue.status === "drained" || importJobQueue.status === undefined) && (
{/* Force Re-Import Button */}
{canStartImport && (
<div className="my-6 max-w-screen-lg">
<button
className="flex space-x-1 sm:mt-0 sm:flex-row sm:items-center rounded-lg border border-orange-400 dark:border-orange-200 bg-orange-200 px-5 py-3 text-gray-700 hover:bg-transparent hover:text-orange-600 focus:outline-none focus:ring active:text-orange-500 disabled:opacity-50 disabled:cursor-not-allowed"
onClick={handleForceReImport}
disabled={isForceReImporting || hasActiveSession || !hasAllDirectories}
title={!hasAllDirectories
? "Cannot import: Required directories are missing"
: "Re-import all files to fix Elasticsearch indexing issues"}
title={
!hasAllDirectories
? "Cannot import: Required directories are missing"
: "Re-import all files to fix Elasticsearch indexing issues"
}
>
<span className="text-md font-medium">
{isForceReImporting ? "Starting Re-Import..." : "Force Re-Import All Files"}
{isForceReImporting
? "Starting Re-Import..."
: "Force Re-Import All Files"}
</span>
<span className="w-6 h-6">
<i className="h-6 w-6 icon-[solar--refresh-bold-duotone]"></i>
@@ -346,87 +317,9 @@ export const Import = (): ReactElement => {
</div>
)}
{/* Import activity is now shown in the RealTimeImportStats component above */}
{/* Past Imports Table */}
{!isLoading && !isEmpty(data?.getJobResultStatistics) && (
<div className="max-w-screen-lg">
<span className="flex items-center mt-6">
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
Past Imports
</span>
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
</span>
<div className="overflow-x-auto w-fit mt-4 rounded-lg border border-gray-200">
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-200 text-md">
<thead className="ltr:text-left rtl:text-right">
<tr>
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
#
</th>
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
Time Started
</th>
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
Session Id
</th>
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
Imported
</th>
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
Failed
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{data?.getJobResultStatistics.map((jobResult: any, index: number) => {
return (
<tr key={index}>
<td className="whitespace-nowrap px-4 py-2 text-gray-700 dark:text-slate-300 font-medium">
{index + 1}
</td>
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
{jobResult.earliestTimestamp && !isNaN(parseInt(jobResult.earliestTimestamp))
? format(
new Date(parseInt(jobResult.earliestTimestamp)),
"EEEE, hh:mma, do LLLL y",
)
: "N/A"}
</td>
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
<span className="tag is-warning">
{jobResult.sessionId}
</span>
</td>
<td className="whitespace-nowrap px-4 py-2 text-gray-700 dark:text-slate-300">
<span className="inline-flex items-center justify-center rounded-full bg-emerald-100 px-2 py-0.5 text-emerald-700">
<span className="h-5 w-6">
<i className="icon-[solar--check-circle-line-duotone] h-5 w-5"></i>
</span>
<p className="whitespace-nowrap text-sm">
{jobResult.completedJobs}
</p>
</span>
</td>
<td className="whitespace-nowrap px-4 py-2 text-gray-700 dark:text-slate-300">
<span className="inline-flex items-center justify-center rounded-full bg-red-100 px-2 py-0.5 text-red-700">
<span className="h-5 w-6">
<i className="icon-[solar--close-circle-line-duotone] h-5 w-5"></i>
</span>
<p className="whitespace-nowrap text-sm">
{jobResult.failedJobs}
</p>
</span>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
<PastImportsTable data={data!.getJobResultStatistics as any} />
)}
</div>
</section>
@@ -434,4 +327,20 @@ export const Import = (): ReactElement => {
);
};
/**
* Helper component to render directory issues list.
*/
const DirectoryIssuesList = ({ issues }: { issues: DirectoryIssue[] }): ReactElement => (
<ul className="list-disc list-inside mt-2">
{issues.map((item) => (
<li key={item.directory}>
<code className="bg-amber-100 dark:bg-amber-900/50 px-1 rounded">
{item.directory}
</code>
<span className="ml-1"> {item.issue}</span>
</li>
))}
</ul>
);
export default Import;

View File

@@ -0,0 +1,103 @@
/**
* @fileoverview Table component displaying historical import sessions.
* @module components/Import/PastImportsTable
*/
import { ReactElement } from "react";
import { format } from "date-fns";
import type { JobResultStatistics } from "./import.types";
/**
* Props for the PastImportsTable component.
*/
export type PastImportsTableProps = {
/** Array of job result statistics from past imports */
data: JobResultStatistics[];
};
/**
* Displays a table of past import sessions with their statistics.
*
* @param props - Component props
* @returns Table element showing import history
*/
export const PastImportsTable = ({ data }: PastImportsTableProps): ReactElement => {
return (
<div className="max-w-screen-lg">
<span className="flex items-center mt-6">
<span className="text-xl text-slate-500 dark:text-slate-200 pr-5">
Past Imports
</span>
<span className="h-px flex-1 bg-slate-200 dark:bg-slate-400"></span>
</span>
<div className="overflow-x-auto w-fit mt-4 rounded-lg border border-gray-200">
<table className="min-w-full divide-y-2 divide-gray-200 dark:divide-gray-200 text-md">
<thead className="ltr:text-left rtl:text-right">
<tr>
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
#
</th>
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
Time Started
</th>
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
Session Id
</th>
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
Imported
</th>
<th className="whitespace-nowrap px-4 py-2 font-medium text-gray-900 dark:text-slate-200">
Failed
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{data.map((jobResult, index) => (
<tr key={jobResult.sessionId || index}>
<td className="whitespace-nowrap px-4 py-2 text-gray-700 dark:text-slate-300 font-medium">
{index + 1}
</td>
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
{jobResult.earliestTimestamp &&
!isNaN(parseInt(jobResult.earliestTimestamp))
? format(
new Date(parseInt(jobResult.earliestTimestamp)),
"EEEE, hh:mma, do LLLL y"
)
: "N/A"}
</td>
<td className="whitespace-nowrap px-2 py-2 text-gray-700 dark:text-slate-300">
<span className="tag is-warning">{jobResult.sessionId}</span>
</td>
<td className="whitespace-nowrap px-4 py-2 text-gray-700 dark:text-slate-300">
<span className="inline-flex items-center justify-center rounded-full bg-emerald-100 px-2 py-0.5 text-emerald-700">
<span className="h-5 w-6">
<i className="icon-[solar--check-circle-line-duotone] h-5 w-5"></i>
</span>
<p className="whitespace-nowrap text-sm">
{jobResult.completedJobs}
</p>
</span>
</td>
<td className="whitespace-nowrap px-4 py-2 text-gray-700 dark:text-slate-300">
<span className="inline-flex items-center justify-center rounded-full bg-red-100 px-2 py-0.5 text-red-700">
<span className="h-5 w-6">
<i className="icon-[solar--close-circle-line-duotone] h-5 w-5"></i>
</span>
<p className="whitespace-nowrap text-sm">
{jobResult.failedJobs}
</p>
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
};
export default PastImportsTable;

View File

@@ -0,0 +1,43 @@
/**
* @fileoverview Type definitions for the Import module.
* @module components/Import/import.types
*/
/**
* Represents an issue with a configured directory.
*/
export type DirectoryIssue = {
/** Path to the directory with issues */
directory: string;
/** Description of the issue */
issue: string;
};
/**
* Result of directory status check from the backend.
*/
export type DirectoryStatus = {
/** Whether all required directories are accessible */
isValid: boolean;
/** List of specific issues found */
issues: DirectoryIssue[];
};
/**
* Statistics for a completed import job session.
*/
export type JobResultStatistics = {
/** Unique session identifier */
sessionId: string;
/** Timestamp of the earliest job in the session (as string for GraphQL compatibility) */
earliestTimestamp: string;
/** Number of successfully completed jobs */
completedJobs: number;
/** Number of failed jobs */
failedJobs: number;
};
/**
* Status of the import job queue.
*/
export type ImportQueueStatus = "running" | "drained" | undefined;

View File

@@ -0,0 +1,129 @@
/**
* @fileoverview Reusable alert banner component for displaying status messages.
* @module components/shared/AlertBanner
*/
import { ReactElement, ReactNode } from "react";
/**
* Alert severity levels that determine styling.
*/
export type AlertSeverity = "error" | "warning" | "info" | "success";
/**
* Props for the AlertBanner component.
*/
export type AlertBannerProps = {
/** Alert severity level */
severity: AlertSeverity;
/** Alert title/heading */
title: string;
/** Alert content - can be string or JSX */
children: ReactNode;
/** Optional close handler - shows close button when provided */
onClose?: () => void;
/** Optional custom icon class (defaults based on severity) */
iconClass?: string;
/** Optional additional CSS classes */
className?: string;
};
const severityConfig: Record<
AlertSeverity,
{
border: string;
bg: string;
titleColor: string;
textColor: string;
iconColor: string;
defaultIcon: string;
}
> = {
error: {
border: "border-red-500",
bg: "bg-red-50 dark:bg-red-900/20",
titleColor: "text-red-800 dark:text-red-300",
textColor: "text-red-700 dark:text-red-400",
iconColor: "text-red-600 dark:text-red-400",
defaultIcon: "icon-[solar--danger-circle-bold]",
},
warning: {
border: "border-amber-500",
bg: "bg-amber-50 dark:bg-amber-900/20",
titleColor: "text-amber-800 dark:text-amber-300",
textColor: "text-amber-700 dark:text-amber-400",
iconColor: "text-amber-600 dark:text-amber-400",
defaultIcon: "icon-[solar--folder-error-bold]",
},
info: {
border: "border-blue-500",
bg: "bg-blue-50 dark:bg-blue-900/20",
titleColor: "text-blue-800 dark:text-blue-300",
textColor: "text-blue-700 dark:text-blue-400",
iconColor: "text-blue-600 dark:text-blue-400",
defaultIcon: "icon-[solar--info-circle-bold]",
},
success: {
border: "border-emerald-500",
bg: "bg-emerald-50 dark:bg-emerald-900/20",
titleColor: "text-emerald-800 dark:text-emerald-300",
textColor: "text-emerald-700 dark:text-emerald-400",
iconColor: "text-emerald-600 dark:text-emerald-400",
defaultIcon: "icon-[solar--check-circle-bold]",
},
};
/**
* Reusable alert banner component for displaying status messages.
*
* @param props - Component props
* @returns Alert banner element
*
* @example
* ```tsx
* <AlertBanner severity="error" title="Import Error" onClose={() => setError(null)}>
* Failed to import files. Please try again.
* </AlertBanner>
* ```
*/
export const AlertBanner = ({
severity,
title,
children,
onClose,
iconClass,
className = "",
}: AlertBannerProps): ReactElement => {
const config = severityConfig[severity];
const icon = iconClass || config.defaultIcon;
return (
<div
className={`rounded-lg border-s-4 ${config.border} ${config.bg} p-4 ${className}`}
role="alert"
>
<div className="flex items-start gap-3">
<span className={`w-6 h-6 ${config.iconColor} mt-0.5`}>
<i className={`h-6 w-6 ${icon}`}></i>
</span>
<div className="flex-1">
<p className={`font-semibold ${config.titleColor}`}>{title}</p>
<div className={`text-sm ${config.textColor} mt-1`}>{children}</div>
</div>
{onClose && (
<button
onClick={onClose}
className={`${config.iconColor} hover:opacity-70`}
aria-label="Close alert"
>
<span className="w-5 h-5">
<i className="h-5 w-5 icon-[solar--close-circle-bold]"></i>
</span>
</button>
)}
</div>
</div>
);
};
export default AlertBanner;