Initial commit
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { Component, type ReactNode } from 'react'
|
||||
import { Box, Button, Typography } from '@mui/material'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
fallback?: ReactNode
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean
|
||||
error?: Error
|
||||
}
|
||||
|
||||
export class AppErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({ hasError: false, error: undefined })
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) return this.props.fallback
|
||||
return (
|
||||
<Box className="flex flex-col items-center justify-center min-h-64 gap-4 p-8 text-center">
|
||||
<AlertTriangle size={40} className="text-red-500" />
|
||||
<Typography variant="h6" color="error">Unerwarteter Fehler</Typography>
|
||||
<Typography variant="body2" color="text.secondary" className="max-w-md">
|
||||
{this.state.error?.message ?? 'Ein unbekannter Fehler ist aufgetreten.'}
|
||||
</Typography>
|
||||
<Button variant="outlined" onClick={this.handleReset}>Neu laden</Button>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Box, Button, Typography } from '@mui/material'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface EmptyStateProps {
|
||||
icon?: ReactNode
|
||||
title: string
|
||||
description?: string
|
||||
action?: {
|
||||
label: string
|
||||
onClick: () => void
|
||||
}
|
||||
}
|
||||
|
||||
export function EmptyState({ icon, title, description, action }: EmptyStateProps) {
|
||||
return (
|
||||
<Box className="flex flex-col items-center justify-center gap-3 py-16 px-8 text-center">
|
||||
{icon && <Box className="text-slate-400 mb-2">{icon}</Box>}
|
||||
<Typography variant="h6" color="text.primary" fontWeight={500}>{title}</Typography>
|
||||
{description && (
|
||||
<Typography variant="body2" color="text.secondary" className="max-w-sm">{description}</Typography>
|
||||
)}
|
||||
{action && (
|
||||
<Button variant="outlined" size="small" onClick={action.onClick} className="mt-2">
|
||||
{action.label}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Box, Button, Typography } from '@mui/material'
|
||||
import { AlertCircle } from 'lucide-react'
|
||||
|
||||
interface ErrorStateProps {
|
||||
message?: string
|
||||
onRetry?: () => void
|
||||
}
|
||||
|
||||
export function ErrorState({ message = 'Daten konnten nicht geladen werden.', onRetry }: ErrorStateProps) {
|
||||
return (
|
||||
<Box className="flex flex-col items-center justify-center gap-3 py-16 px-8 text-center">
|
||||
<AlertCircle size={36} className="text-red-400" />
|
||||
<Typography variant="body1" color="text.secondary">{message}</Typography>
|
||||
{onRetry && (
|
||||
<Button variant="outlined" size="small" onClick={onRetry}>Erneut versuchen</Button>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Box, Skeleton } from '@mui/material'
|
||||
|
||||
interface LoadingPageProps {
|
||||
rows?: number
|
||||
}
|
||||
|
||||
export function LoadingPage({ rows = 5 }: LoadingPageProps) {
|
||||
return (
|
||||
<Box className="flex flex-col gap-4 p-6 w-full">
|
||||
<Skeleton variant="rectangular" height={48} className="rounded" />
|
||||
<Box className="flex gap-4">
|
||||
{[1, 2, 3, 4].map(i => (
|
||||
<Skeleton key={i} variant="rectangular" height={80} className="flex-1 rounded" />
|
||||
))}
|
||||
</Box>
|
||||
{Array.from({ length: rows }).map((_, i) => (
|
||||
<Skeleton key={i} variant="rectangular" height={64} className="rounded" />
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { Box } from '@mui/material'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface PageContainerProps {
|
||||
children: ReactNode
|
||||
maxWidth?: string | number
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PageContainer({ children, maxWidth = 1440, className = '' }: PageContainerProps) {
|
||||
return (
|
||||
<Box
|
||||
className={`w-full mx-auto px-6 py-6 ${className}`}
|
||||
sx={{ maxWidth }}
|
||||
>
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Box, Divider, Typography } from '@mui/material'
|
||||
import type { ReactNode } from 'react'
|
||||
|
||||
interface SectionContainerProps {
|
||||
title?: string
|
||||
subtitle?: string
|
||||
action?: ReactNode
|
||||
children: ReactNode
|
||||
className?: string
|
||||
divider?: boolean
|
||||
}
|
||||
|
||||
export function SectionContainer({ title, subtitle, action, children, className = '', divider = false }: SectionContainerProps) {
|
||||
return (
|
||||
<Box className={`flex flex-col gap-3 ${className}`}>
|
||||
{(title || action) && (
|
||||
<Box className="flex items-center justify-between gap-2">
|
||||
<Box>
|
||||
{title && <Typography variant="subtitle1" fontWeight={600} color="text.primary">{title}</Typography>}
|
||||
{subtitle && <Typography variant="caption" color="text.secondary">{subtitle}</Typography>}
|
||||
</Box>
|
||||
{action}
|
||||
</Box>
|
||||
)}
|
||||
{divider && <Divider />}
|
||||
{children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export { AppErrorBoundary } from './AppErrorBoundary'
|
||||
export { LoadingPage } from './LoadingPage'
|
||||
export { EmptyState } from './EmptyState'
|
||||
export { ErrorState } from './ErrorState'
|
||||
export { PageContainer } from './PageContainer'
|
||||
export { SectionContainer } from './SectionContainer'
|
||||
Reference in New Issue
Block a user