wip 10
This commit is contained in:
@@ -10,23 +10,20 @@ type ButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
type LinkProps = BaseButtonProps & AnchorHTMLAttributes<HTMLAnchorElement> & { href: string };
|
||||
|
||||
const getVariantClasses = (variant: 'default' | 'secondary' = 'default'): string => {
|
||||
const baseClass = 'transition duration-100 ease-in font-medium w-full h-full px-2 py-1 text-white';
|
||||
|
||||
const baseClass =
|
||||
'transition duration-100 ease-in font-medium w-full h-full px-2 py-1 text-white';
|
||||
|
||||
if (variant === 'secondary') {
|
||||
return `${baseClass} bg-black shadow-md hover:text-black hover:bg-white`;
|
||||
}
|
||||
|
||||
|
||||
return `${baseClass} bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100`;
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ variant = 'default', children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={`${getVariantClasses(variant)} ${className}`.trim()}
|
||||
{...props}
|
||||
>
|
||||
<button ref={ref} className={`${getVariantClasses(variant)} ${className}`.trim()} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
@@ -38,11 +35,7 @@ Button.displayName = 'Button';
|
||||
export const ButtonLink = forwardRef<HTMLAnchorElement, LinkProps>(
|
||||
({ variant = 'default', children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<a
|
||||
ref={ref}
|
||||
className={`${getVariantClasses(variant)} ${className}`.trim()}
|
||||
{...props}
|
||||
>
|
||||
<a ref={ref} className={`${getVariantClasses(variant)} ${className}`.trim()} {...props}>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -44,36 +44,39 @@ export default function HamburgerMenu() {
|
||||
className="absolute -top-2 z-50 flex size-7 cursor-pointer opacity-0 lg:hidden"
|
||||
id="mobile-nav-checkbox"
|
||||
checked={isOpen}
|
||||
onChange={(e) => setIsOpen(e.target.checked)}
|
||||
onChange={e => setIsOpen(e.target.checked)}
|
||||
/>
|
||||
|
||||
|
||||
{/* Hamburger icon lines with CSS animations - hidden on desktop */}
|
||||
<span
|
||||
className="transition-background z-40 mt-0.5 h-0.5 w-7 bg-black transition-opacity transition-transform duration-500 lg:hidden dark:bg-white"
|
||||
className="z-40 mt-0.5 h-0.5 w-7 bg-black transition-opacity duration-500 lg:hidden dark:bg-white"
|
||||
style={{
|
||||
transformOrigin: '5px 0px',
|
||||
transition: 'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
||||
transition:
|
||||
'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
||||
transform: isOpen ? 'rotate(45deg) translate(2px, -2px)' : 'none',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="transition-background z-40 mt-1 h-0.5 w-7 bg-black transition-opacity transition-transform duration-500 lg:hidden dark:bg-white"
|
||||
className="z-40 mt-1 h-0.5 w-7 bg-black transition-opacity duration-500 lg:hidden dark:bg-white"
|
||||
style={{
|
||||
transformOrigin: '0% 100%',
|
||||
transition: 'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
||||
transition:
|
||||
'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
||||
opacity: isOpen ? 0 : 1,
|
||||
transform: isOpen ? 'rotate(0deg) scale(0.2, 0.2)' : 'none',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="transition-background z-40 mt-1 h-0.5 w-7 bg-black transition-opacity transition-transform duration-500 lg:hidden dark:bg-white"
|
||||
className="z-40 mt-1 h-0.5 w-7 bg-black transition-opacity duration-500 lg:hidden dark:bg-white"
|
||||
style={{
|
||||
transformOrigin: '0% 0%',
|
||||
transition: 'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
||||
transition:
|
||||
'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
||||
transform: isOpen ? 'rotate(-45deg) translate(0, 6px)' : 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
{/* Navigation menu with slide animation */}
|
||||
<div
|
||||
id="menu"
|
||||
@@ -102,7 +105,7 @@ export default function HamburgerMenu() {
|
||||
</p>
|
||||
</div>
|
||||
<nav>
|
||||
{navItems.map((item) => (
|
||||
{navItems.map(item => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
@@ -117,14 +120,16 @@ export default function HamburgerMenu() {
|
||||
<span className="mx-4 text-sm font-normal">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
|
||||
{/* Admin section - only visible for admins */}
|
||||
{isAdmin && (
|
||||
<div className={`my-2 flex flex-col gap-4 border-l-4 p-2 pl-6 transition-colors duration-200 ${
|
||||
hasPrefix(location.pathname, '/admin')
|
||||
? 'border-purple-500 dark:text-white'
|
||||
: 'border-transparent text-gray-400'
|
||||
}`}>
|
||||
<div
|
||||
className={`my-2 flex flex-col gap-4 border-l-4 p-2 pl-6 transition-colors duration-200 ${
|
||||
hasPrefix(location.pathname, '/admin')
|
||||
? 'border-purple-500 dark:text-white'
|
||||
: 'border-transparent text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{/* Admin header - always shown */}
|
||||
<Link
|
||||
to="/admin"
|
||||
@@ -138,10 +143,10 @@ export default function HamburgerMenu() {
|
||||
<Settings size={20} />
|
||||
<span className="mx-4 text-sm font-normal">Admin</span>
|
||||
</Link>
|
||||
|
||||
|
||||
{hasPrefix(location.pathname, '/admin') && (
|
||||
<div className="flex flex-col gap-4">
|
||||
{adminSubItems.map((item) => (
|
||||
{adminSubItems.map(item => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
@@ -164,7 +169,8 @@ export default function HamburgerMenu() {
|
||||
<a
|
||||
className="absolute bottom-0 flex w-full flex-col items-center justify-center gap-2 p-6 text-black dark:text-white"
|
||||
target="_blank"
|
||||
href="https://gitea.va.reichard.io/evan/AnthoLume" rel="noreferrer"
|
||||
href="https://gitea.va.reichard.io/evan/AnthoLume"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<span className="text-xs">v1.0.0</span>
|
||||
</a>
|
||||
|
||||
@@ -41,7 +41,8 @@ export default function Layout() {
|
||||
{ path: '/search', title: 'Search' },
|
||||
{ path: '/settings', title: 'Settings' },
|
||||
];
|
||||
const currentPageTitle = navItems.find(item => location.pathname === item.path)?.title || 'Documents';
|
||||
const currentPageTitle =
|
||||
navItems.find(item => location.pathname === item.path)?.title || 'Documents';
|
||||
|
||||
// Show loading while checking authentication status
|
||||
if (isCheckingAuth) {
|
||||
@@ -61,12 +62,13 @@ export default function Layout() {
|
||||
<HamburgerMenu />
|
||||
|
||||
{/* Header Title */}
|
||||
<h1 className="px-6 text-xl font-bold lg:ml-44 dark:text-white">
|
||||
{currentPageTitle}
|
||||
</h1>
|
||||
<h1 className="px-6 text-xl font-bold lg:ml-44 dark:text-white">{currentPageTitle}</h1>
|
||||
|
||||
{/* User Dropdown */}
|
||||
<div className="relative flex w-full items-center justify-end space-x-4 p-4" ref={dropdownRef}>
|
||||
<div
|
||||
className="relative flex w-full items-center justify-end space-x-4 p-4"
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<button
|
||||
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
|
||||
className="relative block text-gray-800 dark:text-gray-200"
|
||||
@@ -86,7 +88,7 @@ export default function Layout() {
|
||||
<Link
|
||||
to="/settings"
|
||||
onClick={() => setIsUserDropdownOpen(false)}
|
||||
className="text-md block px-4 py-2 text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
className="block px-4 py-2 text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem"
|
||||
>
|
||||
<span className="flex flex-col">
|
||||
@@ -95,7 +97,7 @@ export default function Layout() {
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="text-md block w-full px-4 py-2 text-left text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
className="block w-full px-4 py-2 text-left text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
role="menuitem"
|
||||
>
|
||||
<span className="flex flex-col">
|
||||
@@ -109,10 +111,13 @@ export default function Layout() {
|
||||
|
||||
<button
|
||||
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
|
||||
className="text-md flex cursor-pointer items-center gap-2 py-4 text-gray-500 dark:text-white"
|
||||
className="flex cursor-pointer items-center gap-2 py-4 text-gray-500 dark:text-white"
|
||||
>
|
||||
<span>{userData?.username || 'User'}</span>
|
||||
<span className="text-gray-800 transition-transform duration-200 dark:text-gray-200" style={{ transform: isUserDropdownOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}>
|
||||
<span
|
||||
className="text-gray-800 transition-transform duration-200 dark:text-gray-200"
|
||||
style={{ transform: isUserDropdownOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}
|
||||
>
|
||||
<ChevronDown size={20} />
|
||||
</span>
|
||||
</button>
|
||||
@@ -120,11 +125,18 @@ export default function Layout() {
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="relative overflow-hidden" style={{ height: 'calc(100dvh - 4rem - env(safe-area-inset-top))' }}>
|
||||
<div id="container" className="h-dvh overflow-auto px-4 md:px-6 lg:ml-48" style={{ paddingBottom: 'calc(5em + env(safe-area-inset-bottom) * 2)' }}>
|
||||
<main
|
||||
className="relative overflow-hidden"
|
||||
style={{ height: 'calc(100dvh - 4rem - env(safe-area-inset-top))' }}
|
||||
>
|
||||
<div
|
||||
id="container"
|
||||
className="h-dvh overflow-auto px-4 md:px-6 lg:ml-48"
|
||||
style={{ paddingBottom: 'calc(5em + env(safe-area-inset-bottom) * 2)' }}
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,10 +124,10 @@ Card placeholder with optional elements:
|
||||
<SkeletonCard showAvatar />
|
||||
|
||||
// Custom configuration
|
||||
<SkeletonCard
|
||||
showAvatar
|
||||
showTitle
|
||||
showText
|
||||
<SkeletonCard
|
||||
showAvatar
|
||||
showTitle
|
||||
showText
|
||||
textLines={4}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
@@ -183,12 +183,7 @@ function DocumentList() {
|
||||
return <SkeletonTable rows={10} columns={5} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Table
|
||||
columns={columns}
|
||||
data={data?.documents || []}
|
||||
/>
|
||||
);
|
||||
return <Table columns={columns} data={data?.documents || []} />;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export function Skeleton({
|
||||
animation = 'pulse',
|
||||
}: SkeletonProps) {
|
||||
const baseClasses = 'bg-gray-200 dark:bg-gray-600';
|
||||
|
||||
|
||||
const variantClasses = {
|
||||
default: 'rounded',
|
||||
text: 'rounded-md h-4',
|
||||
@@ -32,17 +32,13 @@ export function Skeleton({
|
||||
|
||||
const style = {
|
||||
width: width !== undefined ? (typeof width === 'number' ? `${width}px` : width) : undefined,
|
||||
height: height !== undefined ? (typeof height === 'number' ? `${height}px` : height) : undefined,
|
||||
height:
|
||||
height !== undefined ? (typeof height === 'number' ? `${height}px` : height) : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
baseClasses,
|
||||
variantClasses[variant],
|
||||
animationClasses[animation],
|
||||
className
|
||||
)}
|
||||
className={cn(baseClasses, variantClasses[variant], animationClasses[animation], className)}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
@@ -61,10 +57,7 @@ export function SkeletonText({ lines = 3, className = '', lineClassName = '' }:
|
||||
<Skeleton
|
||||
key={i}
|
||||
variant="text"
|
||||
className={cn(
|
||||
lineClassName,
|
||||
i === lines - 1 && lines > 1 ? 'w-3/4' : 'w-full'
|
||||
)}
|
||||
className={cn(lineClassName, i === lines - 1 && lines > 1 ? 'w-3/4' : 'w-full')}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -85,14 +78,7 @@ export function SkeletonAvatar({ size = 'md', className = '' }: SkeletonAvatarPr
|
||||
|
||||
const pixelSize = typeof size === 'number' ? size : sizeMap[size];
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
variant="circular"
|
||||
width={pixelSize}
|
||||
height={pixelSize}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
return <Skeleton variant="circular" width={pixelSize} height={pixelSize} className={className} />;
|
||||
}
|
||||
|
||||
interface SkeletonCardProps {
|
||||
@@ -111,7 +97,12 @@ export function SkeletonCard({
|
||||
textLines = 3,
|
||||
}: SkeletonCardProps) {
|
||||
return (
|
||||
<div className={cn('bg-white dark:bg-gray-700 rounded-lg p-4 border dark:border-gray-600', className)}>
|
||||
<div
|
||||
className={cn(
|
||||
'bg-white dark:bg-gray-700 rounded-lg p-4 border dark:border-gray-600',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{showAvatar && (
|
||||
<div className="mb-4 flex items-start gap-4">
|
||||
<SkeletonAvatar />
|
||||
@@ -121,12 +112,8 @@ export function SkeletonCard({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{showTitle && (
|
||||
<Skeleton variant="text" className="mb-4 h-6 w-1/2" />
|
||||
)}
|
||||
{showText && (
|
||||
<SkeletonText lines={textLines} />
|
||||
)}
|
||||
{showTitle && <Skeleton variant="text" className="mb-4 h-6 w-1/2" />}
|
||||
{showText && <SkeletonText lines={textLines} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -163,7 +150,10 @@ export function SkeletonTable({
|
||||
<tr key={rowIndex} className="border-b last:border-0 dark:border-gray-600">
|
||||
{Array.from({ length: columns }).map((_, colIndex) => (
|
||||
<td key={colIndex} className="p-3">
|
||||
<Skeleton variant="text" className={colIndex === columns - 1 ? 'w-1/2' : 'w-full'} />
|
||||
<Skeleton
|
||||
variant="text"
|
||||
className={colIndex === columns - 1 ? 'w-1/2' : 'w-full'}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
@@ -220,11 +210,12 @@ export function InlineLoader({ size = 'md', className = '' }: InlineLoaderProps)
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center', className)}>
|
||||
<div className={`${sizeMap[size]} animate-spin rounded-full border-gray-200 border-t-blue-500 dark:border-gray-600`} />
|
||||
<div
|
||||
className={`${sizeMap[size]} animate-spin rounded-full border-gray-200 border-t-blue-500 dark:border-gray-600`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Re-export SkeletonTable for backward compatibility
|
||||
export { SkeletonTable as SkeletonTableExport };
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { cn } from '../utils/cn';
|
||||
export interface Column<T> {
|
||||
key: keyof T;
|
||||
header: string;
|
||||
render?: (value: any, row: T, index: number) => React.ReactNode;
|
||||
render?: (value: any, _row: T, _index: number) => React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,47 @@ export interface TableProps<T> {
|
||||
rowKey?: keyof T | ((row: T) => string);
|
||||
}
|
||||
|
||||
// Skeleton table component for loading state
|
||||
function SkeletonTable({
|
||||
rows = 5,
|
||||
columns = 4,
|
||||
className = '',
|
||||
}: {
|
||||
rows?: number;
|
||||
columns?: number;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn('bg-white dark:bg-gray-700 rounded-lg overflow-hidden', className)}>
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-gray-600">
|
||||
{Array.from({ length: columns }).map((_, i) => (
|
||||
<th key={i} className="p-3">
|
||||
<Skeleton variant="text" className="h-5 w-3/4" />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: rows }).map((_, rowIndex) => (
|
||||
<tr key={rowIndex} className="border-b last:border-0 dark:border-gray-600">
|
||||
{Array.from({ length: columns }).map((_, colIndex) => (
|
||||
<td key={colIndex} className="p-3">
|
||||
<Skeleton
|
||||
variant="text"
|
||||
className={colIndex === columns - 1 ? 'w-1/2' : 'w-full'}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Table<T extends Record<string, any>>({
|
||||
columns,
|
||||
data,
|
||||
@@ -24,50 +65,18 @@ export function Table<T extends Record<string, any>>({
|
||||
emptyMessage = 'No Results',
|
||||
rowKey,
|
||||
}: TableProps<T>) {
|
||||
const getRowKey = (row: T, index: number): string => {
|
||||
const getRowKey = (_row: T, index: number): string => {
|
||||
if (typeof rowKey === 'function') {
|
||||
return rowKey(row);
|
||||
return rowKey(_row);
|
||||
}
|
||||
if (rowKey) {
|
||||
return String(row[rowKey] ?? index);
|
||||
return String(_row[rowKey] ?? index);
|
||||
}
|
||||
return `row-${index}`;
|
||||
};
|
||||
|
||||
// Skeleton table component for loading state
|
||||
function SkeletonTable({ rows = 5, columns = 4, className = '' }: { rows?: number; columns?: number; className?: string }) {
|
||||
return (
|
||||
<div className={cn('bg-white dark:bg-gray-700 rounded-lg overflow-hidden', className)}>
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-gray-600">
|
||||
{Array.from({ length: columns }).map((_, i) => (
|
||||
<th key={i} className="p-3">
|
||||
<Skeleton variant="text" className="h-5 w-3/4" />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: rows }).map((_, rowIndex) => (
|
||||
<tr key={rowIndex} className="border-b last:border-0 dark:border-gray-600">
|
||||
{Array.from({ length: columns }).map((_, colIndex) => (
|
||||
<td key={colIndex} className="p-3">
|
||||
<Skeleton variant="text" className={colIndex === columns - 1 ? 'w-1/2' : 'w-full'} />
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<SkeletonTable rows={5} columns={columns.length} />
|
||||
);
|
||||
return <SkeletonTable rows={5} columns={columns.length} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -76,7 +85,7 @@ export function Table<T extends Record<string, any>>({
|
||||
<table className="min-w-full bg-white dark:bg-gray-700">
|
||||
<thead>
|
||||
<tr className="border-b dark:border-gray-600">
|
||||
{columns.map((column) => (
|
||||
{columns.map(column => (
|
||||
<th
|
||||
key={String(column.key)}
|
||||
className={`p-3 text-left text-gray-500 dark:text-white ${column.className || ''}`}
|
||||
@@ -98,18 +107,13 @@ export function Table<T extends Record<string, any>>({
|
||||
</tr>
|
||||
) : (
|
||||
data.map((row, index) => (
|
||||
<tr
|
||||
key={getRowKey(row, index)}
|
||||
className="border-b dark:border-gray-600"
|
||||
>
|
||||
{columns.map((column) => (
|
||||
<tr key={getRowKey(row, index)} className="border-b dark:border-gray-600">
|
||||
{columns.map(column => (
|
||||
<td
|
||||
key={`${getRowKey(row, index)}-${String(column.key)}`}
|
||||
className={`p-3 text-gray-700 dark:text-gray-300 ${column.className || ''}`}
|
||||
>
|
||||
{column.render
|
||||
? column.render(row[column.key], row, index)
|
||||
: row[column.key]}
|
||||
{column.render ? column.render(row[column.key], row, index) : row[column.key]}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
|
||||
@@ -11,9 +11,10 @@ export interface ToastProps {
|
||||
onClose?: (id: string) => void;
|
||||
}
|
||||
|
||||
const getToastStyles = (type: ToastType) => {
|
||||
const baseStyles = 'flex items-center gap-3 p-4 rounded-lg shadow-lg border-l-4 transition-all duration-300';
|
||||
|
||||
const getToastStyles = (_type: ToastType) => {
|
||||
const baseStyles =
|
||||
'flex items-center gap-3 p-4 rounded-lg shadow-lg border-l-4 transition-all duration-300';
|
||||
|
||||
const typeStyles = {
|
||||
info: 'bg-blue-50 dark:bg-blue-900/30 border-blue-500 dark:border-blue-400',
|
||||
warning: 'bg-yellow-50 dark:bg-yellow-900/30 border-yellow-500 dark:border-yellow-400',
|
||||
@@ -69,15 +70,11 @@ export function Toast({ id, type, message, duration = 5000, onClose }: ToastProp
|
||||
return (
|
||||
<div
|
||||
className={`${baseStyles} ${typeStyles[type]} ${
|
||||
isAnimatingOut
|
||||
? 'translate-x-full opacity-0'
|
||||
: 'animate-slideInRight opacity-100'
|
||||
isAnimatingOut ? 'translate-x-full opacity-0' : 'animate-slideInRight opacity-100'
|
||||
}`}
|
||||
>
|
||||
{icons[type]}
|
||||
<p className={`flex-1 text-sm font-medium ${textStyles[type]}`}>
|
||||
{message}
|
||||
</p>
|
||||
<p className={`flex-1 text-sm font-medium ${textStyles[type]}`}>{message}</p>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className={`ml-2 opacity-70 transition-opacity hover:opacity-100 ${textStyles[type]}`}
|
||||
|
||||
@@ -16,33 +16,50 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
const [toasts, setToasts] = useState<(ToastProps & { id: string })[]>([]);
|
||||
|
||||
const removeToast = useCallback((id: string) => {
|
||||
setToasts((prev) => prev.filter((toast) => toast.id !== id));
|
||||
setToasts(prev => prev.filter(toast => toast.id !== id));
|
||||
}, []);
|
||||
|
||||
const showToast = useCallback((message: string, type: ToastType = 'info', duration?: number): string => {
|
||||
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
setToasts((prev) => [...prev, { id, type, message, duration, onClose: removeToast }]);
|
||||
return id;
|
||||
}, [removeToast]);
|
||||
const showToast = useCallback(
|
||||
(message: string, _type: ToastType = 'info', _duration?: number): string => {
|
||||
const id = `toast-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
setToasts(prev => [
|
||||
...prev,
|
||||
{ id, type: _type, message, duration: _duration, onClose: removeToast },
|
||||
]);
|
||||
return id;
|
||||
},
|
||||
[removeToast]
|
||||
);
|
||||
|
||||
const showInfo = useCallback((message: string, duration?: number) => {
|
||||
return showToast(message, 'info', duration);
|
||||
}, [showToast]);
|
||||
const showInfo = useCallback(
|
||||
(message: string, _duration?: number) => {
|
||||
return showToast(message, 'info', _duration);
|
||||
},
|
||||
[showToast]
|
||||
);
|
||||
|
||||
const showWarning = useCallback((message: string, duration?: number) => {
|
||||
return showToast(message, 'warning', duration);
|
||||
}, [showToast]);
|
||||
const showWarning = useCallback(
|
||||
(message: string, _duration?: number) => {
|
||||
return showToast(message, 'warning', _duration);
|
||||
},
|
||||
[showToast]
|
||||
);
|
||||
|
||||
const showError = useCallback((message: string, duration?: number) => {
|
||||
return showToast(message, 'error', duration);
|
||||
}, [showToast]);
|
||||
const showError = useCallback(
|
||||
(message: string, _duration?: number) => {
|
||||
return showToast(message, 'error', _duration);
|
||||
},
|
||||
[showToast]
|
||||
);
|
||||
|
||||
const clearToasts = useCallback(() => {
|
||||
setToasts([]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ToastContext.Provider value={{ showToast, showInfo, showWarning, showError, removeToast, clearToasts }}>
|
||||
<ToastContext.Provider
|
||||
value={{ showToast, showInfo, showWarning, showError, removeToast, clearToasts }}
|
||||
>
|
||||
{children}
|
||||
<ToastContainer toasts={toasts} />
|
||||
</ToastContext.Provider>
|
||||
@@ -61,7 +78,7 @@ function ToastContainer({ toasts }: ToastContainerProps) {
|
||||
return (
|
||||
<div className="pointer-events-none fixed bottom-4 right-4 z-50 flex w-full max-w-sm flex-col gap-2">
|
||||
<div className="pointer-events-auto">
|
||||
{toasts.map((toast) => (
|
||||
{toasts.map(toast => (
|
||||
<Toast key={toast.id} {...toast} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -4,13 +4,13 @@ export { ToastProvider, useToasts } from './ToastContext';
|
||||
export type { ToastType, ToastProps } from './Toast';
|
||||
|
||||
// Skeleton components
|
||||
export {
|
||||
Skeleton,
|
||||
SkeletonText,
|
||||
SkeletonAvatar,
|
||||
SkeletonCard,
|
||||
SkeletonTable,
|
||||
export {
|
||||
Skeleton,
|
||||
SkeletonText,
|
||||
SkeletonAvatar,
|
||||
SkeletonCard,
|
||||
SkeletonTable,
|
||||
SkeletonButton,
|
||||
PageLoader,
|
||||
InlineLoader
|
||||
InlineLoader,
|
||||
} from './Skeleton';
|
||||
|
||||
Reference in New Issue
Block a user