ADD new frontend

This commit is contained in:
Urtzi Alfaro
2025-08-28 10:41:04 +02:00
parent 9c247a5f99
commit 0fd273cfce
492 changed files with 114979 additions and 1632 deletions

View File

@@ -0,0 +1,441 @@
import React, { forwardRef } from 'react';
import { clsx } from 'clsx';
import { useNavigate, useLocation } from 'react-router-dom';
import { Button } from '../../ui';
import { Badge } from '../../ui';
import { Breadcrumbs } from '../Breadcrumbs';
import {
ArrowLeft,
RefreshCw,
Download,
Upload,
Settings,
MoreVertical,
Calendar,
Clock,
User
} from 'lucide-react';
export interface ActionButton {
id: string;
label: string;
icon?: React.ComponentType<{ className?: string }>;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
loading?: boolean;
badge?: {
text: string;
variant?: 'default' | 'primary' | 'success' | 'warning' | 'error';
};
tooltip?: string;
external?: boolean;
href?: string;
}
export interface MetadataItem {
id: string;
label: string;
value: string | React.ReactNode;
icon?: React.ComponentType<{ className?: string }>;
tooltip?: string;
copyable?: boolean;
}
export interface PageHeaderProps {
className?: string;
/**
* Page title
*/
title: string;
/**
* Optional subtitle
*/
subtitle?: string;
/**
* Page description
*/
description?: string;
/**
* Action buttons
*/
actions?: ActionButton[];
/**
* Metadata items (last updated, created by, etc.)
*/
metadata?: MetadataItem[];
/**
* Show back button
*/
showBackButton?: boolean;
/**
* Custom back button handler
*/
onBack?: () => void;
/**
* Show breadcrumbs
*/
showBreadcrumbs?: boolean;
/**
* Custom breadcrumb props
*/
breadcrumbProps?: any;
/**
* Status badge
*/
status?: {
text: string;
variant?: 'default' | 'primary' | 'success' | 'warning' | 'error';
};
/**
* Page icon
*/
icon?: React.ComponentType<{ className?: string }>;
/**
* Loading state
*/
loading?: boolean;
/**
* Error state
*/
error?: string;
/**
* Custom refresh handler
*/
onRefresh?: () => void;
/**
* Show refresh button
*/
showRefreshButton?: boolean;
/**
* Sticky header
*/
sticky?: boolean;
/**
* Compact mode (smaller padding and text)
*/
compact?: boolean;
/**
* Center align content
*/
centered?: boolean;
/**
* Custom content in header
*/
children?: React.ReactNode;
}
export interface PageHeaderRef {
scrollIntoView: () => void;
refresh: () => void;
}
/**
* PageHeader - Page-level header with title, actions, and metadata
*
* Features:
* - Page title with optional subtitle and description
* - Action buttons area with various button types and states
* - Metadata display (last updated, created by, etc.) with icons
* - Optional back button with navigation handling
* - Integrated breadcrumb navigation
* - Status badges and page icons
* - Loading and error states
* - Responsive layout with mobile adaptations
* - Sticky positioning support
* - Keyboard shortcuts and accessibility
*/
export const PageHeader = forwardRef<PageHeaderRef, PageHeaderProps>(({
className,
title,
subtitle,
description,
actions = [],
metadata = [],
showBackButton = false,
onBack,
showBreadcrumbs = true,
breadcrumbProps,
status,
icon: PageIcon,
loading = false,
error,
onRefresh,
showRefreshButton = false,
sticky = false,
compact = false,
centered = false,
children,
}, ref) => {
const navigate = useNavigate();
const location = useLocation();
const headerRef = React.useRef<HTMLDivElement>(null);
// Handle back navigation
const handleBack = () => {
if (onBack) {
onBack();
} else {
navigate(-1);
}
};
// Handle refresh
const handleRefresh = () => {
if (onRefresh) {
onRefresh();
} else {
window.location.reload();
}
};
// Scroll into view
const scrollIntoView = React.useCallback(() => {
headerRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, []);
// Expose ref methods
React.useImperativeHandle(ref, () => ({
scrollIntoView,
refresh: handleRefresh,
}), [scrollIntoView, handleRefresh]);
// Render action button
const renderAction = (action: ActionButton) => {
const ActionIcon = action.icon;
const buttonContent = (
<>
{ActionIcon && <ActionIcon className="w-4 h-4" />}
<span className={clsx(
compact ? 'text-sm' : 'text-sm',
ActionIcon && 'ml-2'
)}>
{action.label}
</span>
{action.badge && (
<Badge
variant={action.badge.variant || 'default'}
size="sm"
className="ml-2"
>
{action.badge.text}
</Badge>
)}
</>
);
if (action.href) {
return (
<a
key={action.id}
href={action.href}
target={action.external ? '_blank' : undefined}
rel={action.external ? 'noopener noreferrer' : undefined}
className={clsx(
'inline-flex items-center px-4 py-2 rounded-lg font-medium transition-colors duration-200',
'focus:outline-none focus:ring-2 focus:ring-[var(--color-primary)]/20',
action.disabled && 'pointer-events-none opacity-50'
)}
title={action.tooltip}
>
{buttonContent}
</a>
);
}
return (
<Button
key={action.id}
variant={action.variant || 'outline'}
size={action.size || 'md'}
onClick={action.onClick}
disabled={action.disabled || loading}
loading={action.loading}
title={action.tooltip}
className={compact ? 'px-3 py-1.5' : undefined}
>
{buttonContent}
</Button>
);
};
// Render metadata item
const renderMetadata = (item: MetadataItem) => {
const ItemIcon = item.icon;
const handleCopy = async () => {
if (item.copyable && typeof item.value === 'string') {
try {
await navigator.clipboard.writeText(item.value);
// TODO: Show toast notification
} catch (error) {
console.error('Failed to copy:', error);
}
}
};
return (
<div
key={item.id}
className="flex items-center gap-2 text-sm text-[var(--text-secondary)]"
title={item.tooltip}
>
{ItemIcon && <ItemIcon className="w-4 h-4 text-[var(--text-tertiary)]" />}
<span className="font-medium">{item.label}:</span>
{item.copyable && typeof item.value === 'string' ? (
<button
onClick={handleCopy}
className="hover:text-[var(--text-primary)] transition-colors duration-200 cursor-pointer"
>
{item.value}
</button>
) : (
<span>{item.value}</span>
)}
</div>
);
};
return (
<div
ref={headerRef}
className={clsx(
'bg-[var(--bg-primary)] border-b border-[var(--border-primary)]',
sticky && 'sticky top-[var(--header-height)] z-[var(--z-sticky)]',
compact ? 'py-4' : 'py-6',
className
)}
>
<div className={clsx(
'max-w-full px-4 lg:px-6',
centered && 'mx-auto max-w-4xl'
)}>
{/* Breadcrumbs */}
{showBreadcrumbs && (
<div className="mb-4">
<Breadcrumbs {...breadcrumbProps} />
</div>
)}
{/* Main header content */}
<div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
{/* Left section - Title and metadata */}
<div className="flex-1 min-w-0">
{/* Title row */}
<div className="flex items-center gap-3 mb-2">
{showBackButton && (
<Button
variant="ghost"
size="sm"
onClick={handleBack}
className="p-2 -ml-2"
aria-label="Volver"
>
<ArrowLeft className="w-4 h-4" />
</Button>
)}
{PageIcon && (
<PageIcon className={clsx(
'text-[var(--text-tertiary)]',
compact ? 'w-5 h-5' : 'w-6 h-6'
)} />
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-3 flex-wrap">
<h1 className={clsx(
'font-bold text-[var(--text-primary)] truncate',
compact ? 'text-xl' : 'text-2xl lg:text-3xl'
)}>
{title}
</h1>
{status && (
<Badge
variant={status.variant || 'default'}
size={compact ? 'sm' : 'md'}
>
{status.text}
</Badge>
)}
{loading && (
<RefreshCw className="w-4 h-4 text-[var(--text-tertiary)] animate-spin" />
)}
</div>
{subtitle && (
<p className={clsx(
'text-[var(--text-secondary)] mt-1',
compact ? 'text-sm' : 'text-lg'
)}>
{subtitle}
</p>
)}
</div>
{showRefreshButton && (
<Button
variant="ghost"
size="sm"
onClick={handleRefresh}
disabled={loading}
className="p-2"
aria-label="Actualizar"
>
<RefreshCw className={clsx(
'w-4 h-4',
loading && 'animate-spin'
)} />
</Button>
)}
</div>
{/* Description */}
{description && (
<p className={clsx(
'text-[var(--text-secondary)] mb-3',
compact ? 'text-sm' : 'text-base'
)}>
{description}
</p>
)}
{/* Error */}
{error && (
<div className="mb-3 p-3 bg-[var(--color-error)]/10 border border-[var(--color-error)]/20 rounded-lg">
<p className="text-sm text-[var(--color-error)]">{error}</p>
</div>
)}
{/* Metadata */}
{metadata.length > 0 && (
<div className="flex flex-wrap items-center gap-x-6 gap-y-2">
{metadata.map(renderMetadata)}
</div>
)}
{/* Custom children */}
{children && (
<div className="mt-4">
{children}
</div>
)}
</div>
{/* Right section - Actions */}
{actions.length > 0 && (
<div className="flex items-center gap-2 flex-wrap lg:flex-nowrap">
{actions.map(renderAction)}
</div>
)}
</div>
</div>
</div>
);
});
PageHeader.displayName = 'PageHeader';

View File

@@ -0,0 +1,2 @@
export { PageHeader } from './PageHeader';
export type { PageHeaderProps, PageHeaderRef, ActionButton, MetadataItem } from './PageHeader';