feat(F020): frontend infrastructure foundation
- src/lib/constants.ts: centralized route paths, labels, thresholds - src/lib/utils.ts: formatting, color helpers, math utils - src/hooks/: useProperties, useMatches, useFutureSignals, useNeeds - src/components/scores/MatchScoreRing - src/components/data-quality/DataQualityBar - src/components/future-signals/SignalTypeBadge - src/components/cards/SourceTypeBadge - src/provider/AuthProvider (placeholder, swappable) - main.tsx: AuthProvider + centralized stale time Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
import { Chip } from '@mui/material'
|
||||
import { ShieldCheck, Globe, Sparkles } from 'lucide-react'
|
||||
import type { ResultType } from '../../domain/enums'
|
||||
import { RESULT_TYPE_LABELS } from '../../lib/constants'
|
||||
|
||||
interface SourceTypeBadgeProps {
|
||||
type: ResultType
|
||||
size?: 'small' | 'medium'
|
||||
}
|
||||
|
||||
const CONFIG: Record<string, { bg: string; color: string; Icon: React.ElementType }> = {
|
||||
VERIFIED_PORTFOLIO: { bg: 'rgba(30,58,95,0.1)', color: '#1e3a5f', Icon: ShieldCheck },
|
||||
EXTERNAL_MARKET: { bg: 'rgba(217,119,6,0.1)', color: '#b45309', Icon: Globe },
|
||||
FUTURE_AVAILABILITY: { bg: 'rgba(124,58,237,0.1)', color: '#6d28d9', Icon: Sparkles },
|
||||
}
|
||||
|
||||
export function SourceTypeBadge({ type, size = 'small' }: SourceTypeBadgeProps) {
|
||||
const { bg, color, Icon } = CONFIG[type] ?? { bg: '#f1f5f9', color: '#475569', Icon: Globe }
|
||||
return (
|
||||
<Chip
|
||||
icon={<Icon size={11} color={color} />}
|
||||
label={RESULT_TYPE_LABELS[type] ?? type}
|
||||
size={size}
|
||||
sx={{ bgcolor: bg, color, fontWeight: 600, border: 'none', '& .MuiChip-icon': { ml: 0.5 } }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { SourceTypeBadge } from './SourceTypeBadge'
|
||||
@@ -0,0 +1,101 @@
|
||||
import { Box, LinearProgress, Tooltip, Typography, Chip } from '@mui/material'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import { dataQualityColor, dataQualityHex, formatPercent } from '../../lib/utils'
|
||||
import { FRESHNESS_LABELS } from '../../lib/constants'
|
||||
import type { DataQuality } from '../../domain/property'
|
||||
|
||||
interface DataQualityBarProps {
|
||||
quality: DataQuality
|
||||
compact?: boolean
|
||||
showWarnings?: boolean
|
||||
}
|
||||
|
||||
export function DataQualityBar({ quality, compact = false, showWarnings = true }: DataQualityBarProps) {
|
||||
const color = dataQualityColor(quality.score)
|
||||
const hex = dataQualityHex(quality.score)
|
||||
const pct = Math.round(quality.score * 100)
|
||||
|
||||
const tooltipContent = (
|
||||
<Box sx={{ p: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ fontWeight: 600, display: 'block', mb: 0.5 }}>
|
||||
Datenqualität {formatPercent(quality.score)}
|
||||
</Typography>
|
||||
{quality.missingCriticalFields.length > 0 && (
|
||||
<Box sx={{ mb: 0.5 }}>
|
||||
<Typography variant="caption" sx={{ color: '#fca5a5', display: 'block' }}>Fehlende Pflichtfelder:</Typography>
|
||||
{quality.missingCriticalFields.map(f => (
|
||||
<Typography key={f} variant="caption" sx={{ display: 'block', pl: 1 }}>• {f}</Typography>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
{quality.warnings.length > 0 && (
|
||||
<Box>
|
||||
<Typography variant="caption" sx={{ color: '#fcd34d', display: 'block' }}>Warnungen:</Typography>
|
||||
{quality.warnings.map((w, i) => (
|
||||
<Typography key={i} variant="caption" sx={{ display: 'block', pl: 1 }}>• {w}</Typography>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Typography variant="caption" sx={{ color: '#94a3b8', display: 'block', mt: 0.5 }}>
|
||||
Aktualität: {FRESHNESS_LABELS[quality.freshness]}
|
||||
{quality.lastVerifiedAt ? ` · Geprüft: ${quality.lastVerifiedAt}` : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<Tooltip title={tooltipContent} arrow placement="top">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, cursor: 'default' }}>
|
||||
<Box sx={{ width: 56 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={pct}
|
||||
color={color}
|
||||
sx={{ height: 5, borderRadius: 3 }}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: hex, fontWeight: 600, fontSize: '0.7rem', whiteSpace: 'nowrap' }}>
|
||||
{pct}%
|
||||
</Typography>
|
||||
{quality.missingCriticalFields.length > 0 && showWarnings && (
|
||||
<AlertTriangle size={11} color="#d97706" />
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Tooltip title={tooltipContent} arrow placement="top">
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, cursor: 'default' }}>
|
||||
<Box sx={{ flex: 1, minWidth: 80 }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={pct}
|
||||
color={color}
|
||||
sx={{ height: 6, borderRadius: 3 }}
|
||||
/>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ color: hex, fontWeight: 600, whiteSpace: 'nowrap' }}>
|
||||
{pct}%
|
||||
</Typography>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{showWarnings && quality.missingCriticalFields.length > 0 && (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.25, mt: 0.5 }}>
|
||||
{quality.missingCriticalFields.slice(0, 2).map(f => (
|
||||
<Chip key={f} label={f} size="small" color="error" variant="outlined"
|
||||
sx={{ height: 16, fontSize: '0.625rem' }} />
|
||||
))}
|
||||
{quality.missingCriticalFields.length > 2 && (
|
||||
<Typography variant="caption" sx={{ color: 'error.main' }}>
|
||||
+{quality.missingCriticalFields.length - 2}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { DataQualityBar } from './DataQualityBar'
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Chip } from '@mui/material'
|
||||
import type { SignalType } from '../../domain/enums'
|
||||
import { SIGNAL_TYPE_LABELS } from '../../lib/constants'
|
||||
|
||||
interface SignalTypeBadgeProps {
|
||||
type: SignalType
|
||||
size?: 'small' | 'medium'
|
||||
}
|
||||
|
||||
const COLOR_MAP: Record<string, { bg: string; color: string }> = {
|
||||
EXPANSION: { bg: 'rgba(26,122,74,0.12)', color: '#1a7a4a' },
|
||||
POSSIBLE_MOVE_OUT: { bg: 'rgba(217,119,6,0.12)', color: '#d97706' },
|
||||
CONSTRUCTION_PROJECT: { bg: 'rgba(37,99,235,0.12)', color: '#1d4ed8' },
|
||||
RESTRUCTURING: { bg: 'rgba(234,88,12,0.12)', color: '#c2410c' },
|
||||
PROJECT_DEVELOPMENT: { bg: 'rgba(124,58,237,0.12)', color: '#6d28d9' },
|
||||
SPACE_CONSOLIDATION: { bg: 'rgba(100,116,139,0.12)',color: '#475569' },
|
||||
}
|
||||
|
||||
export function SignalTypeBadge({ type, size = 'small' }: SignalTypeBadgeProps) {
|
||||
const { bg, color } = COLOR_MAP[type] ?? { bg: '#f1f5f9', color: '#475569' }
|
||||
return (
|
||||
<Chip
|
||||
label={SIGNAL_TYPE_LABELS[type] ?? type}
|
||||
size={size}
|
||||
sx={{ bgcolor: bg, color, fontWeight: 600, border: 'none' }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { SignalTypeBadge } from './SignalTypeBadge'
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Box, Typography } from '@mui/material'
|
||||
import { matchScoreColor } from '../../lib/utils'
|
||||
import type { MatchStrength } from '../../domain/enums'
|
||||
import { MATCH_STRENGTH_LABELS } from '../../lib/constants'
|
||||
|
||||
interface MatchScoreRingProps {
|
||||
score: number
|
||||
strength: MatchStrength
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
showLabel?: boolean
|
||||
}
|
||||
|
||||
const SIZE_MAP = {
|
||||
sm: { ring: 48, font: '0.875rem', label: '0.625rem' },
|
||||
md: { ring: 64, font: '1.125rem', label: '0.75rem' },
|
||||
lg: { ring: 80, font: '1.5rem', label: '0.8125rem' },
|
||||
}
|
||||
|
||||
const COLOR_MAP = {
|
||||
success: '#1a7a4a',
|
||||
warning: '#d97706',
|
||||
error: '#c0392b',
|
||||
}
|
||||
|
||||
export function MatchScoreRing({ score, strength, size = 'md', showLabel = true }: MatchScoreRingProps) {
|
||||
const dim = SIZE_MAP[size]
|
||||
const colorKey = matchScoreColor(score)
|
||||
const color = COLOR_MAP[colorKey]
|
||||
const bgColor = colorKey === 'success' ? 'rgba(26,122,74,0.08)' : colorKey === 'warning' ? 'rgba(217,119,6,0.08)' : 'rgba(192,57,43,0.08)'
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 0.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: dim.ring,
|
||||
height: dim.ring,
|
||||
borderRadius: '50%',
|
||||
border: `3px solid ${color}`,
|
||||
bgcolor: bgColor,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: dim.font, fontWeight: 700, color, lineHeight: 1 }}>
|
||||
{score}
|
||||
</Typography>
|
||||
</Box>
|
||||
{showLabel && (
|
||||
<Typography sx={{ fontSize: dim.label, fontWeight: 600, color, textTransform: 'uppercase', letterSpacing: '0.06em' }}>
|
||||
{MATCH_STRENGTH_LABELS[strength]}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { MatchScoreRing } from './MatchScoreRing'
|
||||
Reference in New Issue
Block a user