refactor: PropertyIntelligenceCard — E&V-style minimal layout
Remove all image overlays, colored badges, metric boxes and confidence bar. New structure: image → location (gray) → title → price large (DM Serif) → single facts line. Single neutral dark tag for asset type. Matches Engel & Völkers card pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { memo } from 'react'
|
||||
import { Box, Chip, LinearProgress, Tooltip, Typography } from '@mui/material'
|
||||
import { MapPin, Maximize2, TrendingUp, Calendar } from 'lucide-react'
|
||||
import { getAssetTypeColor, getAssetTypeLabel, getAvailabilityLabel } from './propertyHelpers'
|
||||
import { Box, Chip, Tooltip, Typography } from '@mui/material'
|
||||
import { getAssetTypeColor, getAssetTypeLabel } from './propertyHelpers'
|
||||
import { DS_TEXT, DS_BORDER } from '../../lib/ds'
|
||||
import type { Property } from '../../domain/property'
|
||||
|
||||
@@ -10,19 +9,24 @@ interface Props {
|
||||
onSelect: (id: string) => void
|
||||
}
|
||||
|
||||
function availabilityColor(status: string) {
|
||||
if (status === 'AVAILABLE_NOW') return { bg: '#dcfce7', text: '#1d6342' }
|
||||
if (status === 'AVAILABLE_SOON') return { bg: '#fef9c3', text: '#a16207' }
|
||||
return { bg: '#f4f3f0', text: '#9a9a9a' }
|
||||
const FIT_OUT_LABELS: Record<string, string> = {
|
||||
SHELL: 'Rohbau', BASIC: 'Grundausbau', FULL: 'Vollausbau', PREMIUM: 'Premium-Ausbau',
|
||||
}
|
||||
|
||||
export const PropertyIntelligenceCard = memo(function PropertyIntelligenceCard({ property: p, onSelect }: Props) {
|
||||
const confPct = Math.round(p.confidenceScore * 100)
|
||||
const annualRent = Math.round(p.areaSqm * p.rentPricePerSqm / 1000)
|
||||
const avail = availabilityColor(p.availabilityStatus)
|
||||
const assetColor = getAssetTypeColor(p.assetType)
|
||||
const hasImage = !!p.images?.[0]
|
||||
|
||||
// single compact facts line below price
|
||||
const facts: string[] = []
|
||||
facts.push(`${p.areaSqm.toLocaleString('de-CH')} m²`)
|
||||
if (p.availabilityDate) {
|
||||
facts.push(`ab ${new Date(p.availabilityDate).toLocaleDateString('de-CH', { month: 'short', year: '2-digit' })}`)
|
||||
}
|
||||
if (p.hardFacts?.fitOut) facts.push(FIT_OUT_LABELS[p.hardFacts.fitOut] ?? p.hardFacts.fitOut)
|
||||
if (p.contractDurationMonths) facts.push(`${p.contractDurationMonths}M Vertrag`)
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={() => onSelect(p.id)}
|
||||
@@ -30,8 +34,7 @@ export const PropertyIntelligenceCard = memo(function PropertyIntelligenceCard({
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
border: `1px solid ${DS_BORDER.default}`,
|
||||
bgcolor: 'white',
|
||||
boxShadow: '0 1px 3px rgba(0,0,0,0.06)',
|
||||
bgcolor: '#ffffff',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
cursor: 'pointer',
|
||||
@@ -39,154 +42,70 @@ export const PropertyIntelligenceCard = memo(function PropertyIntelligenceCard({
|
||||
'&:hover': { borderColor: '#c8c7c4' },
|
||||
}}
|
||||
>
|
||||
{/* ── Image / header ── */}
|
||||
<Box sx={{ position: 'relative', height: 140, flexShrink: 0, bgcolor: '#f4f3f0', overflow: 'hidden' }}>
|
||||
{/* ── Image ── */}
|
||||
<Box sx={{ position: 'relative', height: 180, flexShrink: 0, bgcolor: '#f4f3f0', overflow: 'hidden' }}>
|
||||
{hasImage ? (
|
||||
<Box
|
||||
component="img"
|
||||
src={p.images![0]}
|
||||
alt={p.title}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover' }}
|
||||
sx={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%', height: '100%',
|
||||
background: `linear-gradient(135deg, ${assetColor}22 0%, ${assetColor}44 100%)`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: '2.5rem', opacity: 0.3 }}>🏢</Typography>
|
||||
<Box sx={{
|
||||
width: '100%', height: '100%',
|
||||
background: `linear-gradient(135deg, ${assetColor}18 0%, ${assetColor}30 100%)`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
}}>
|
||||
<Typography sx={{ fontSize: '2.5rem', opacity: 0.2 }}>🏢</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box sx={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(0,0,0,0.45) 0%, transparent 55%)' }} />
|
||||
|
||||
{/* Top badges */}
|
||||
<Box sx={{ position: 'absolute', top: 8, left: 8, display: 'flex', gap: 0.5 }}>
|
||||
<Chip
|
||||
label={getAssetTypeLabel(p.assetType)}
|
||||
size="small"
|
||||
sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700, bgcolor: assetColor, color: 'white', borderRadius: 1 }}
|
||||
/>
|
||||
<Chip
|
||||
label={getAvailabilityLabel(p.availabilityStatus)}
|
||||
size="small"
|
||||
sx={{ height: 20, fontSize: '0.65rem', fontWeight: 700, bgcolor: avail.bg, color: avail.text, borderRadius: 1 }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Bottom: annual rent on image */}
|
||||
<Box sx={{ position: 'absolute', bottom: 8, left: 10, right: 10, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end' }}>
|
||||
<Box>
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.8)', fontSize: '0.65rem', lineHeight: 1 }}>Jahresmiete ca.</Typography>
|
||||
<Typography sx={{
|
||||
color: 'white', lineHeight: 1.2,
|
||||
fontFamily: '"DM Serif Display", serif',
|
||||
fontWeight: 400, fontSize: '1.25rem',
|
||||
}}>
|
||||
CHF {annualRent}k
|
||||
{/* Single small neutral asset-type tag */}
|
||||
<Tooltip title={getAssetTypeLabel(p.assetType)} placement="right">
|
||||
<Box sx={{
|
||||
position: 'absolute', top: 10, left: 10,
|
||||
bgcolor: 'rgba(0,0,0,0.52)', borderRadius: '4px',
|
||||
px: 0.75, py: 0.2,
|
||||
}}>
|
||||
<Typography sx={{ fontSize: '0.65rem', fontWeight: 600, color: 'white', lineHeight: 1.3 }}>
|
||||
{getAssetTypeLabel(p.assetType)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.85)', fontSize: '0.72rem', fontWeight: 600 }}>
|
||||
{p.rentPricePerSqm} /m²/J
|
||||
</Typography>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
</Box>
|
||||
|
||||
{/* ── Card body ── */}
|
||||
<Box sx={{ p: 1.5, display: 'flex', flexDirection: 'column', gap: 1, flex: 1 }}>
|
||||
{/* ── Content ── */}
|
||||
<Box sx={{ px: 1.75, pt: 1.5, pb: 1.75, display: 'flex', flexDirection: 'column', flex: 1 }}>
|
||||
|
||||
{/* Title + location */}
|
||||
<Box>
|
||||
<Typography variant="subtitle2" sx={{ fontWeight: 700, lineHeight: 1.3, color: DS_TEXT.primary, fontSize: '0.875rem' }} noWrap>
|
||||
{p.title}
|
||||
{/* Location */}
|
||||
<Typography sx={{ fontSize: '0.72rem', color: DS_TEXT.muted, mb: 0.25 }}>
|
||||
{p.location.city}{p.location.district ? ` · ${p.location.district}` : ''}
|
||||
</Typography>
|
||||
|
||||
{/* Title */}
|
||||
<Typography sx={{ fontWeight: 600, fontSize: '0.9375rem', color: DS_TEXT.primary, lineHeight: 1.3, mb: 0.75 }} noWrap>
|
||||
{p.title}
|
||||
</Typography>
|
||||
|
||||
{/* Price — hero element */}
|
||||
<Box sx={{ mb: 0.875 }}>
|
||||
<Typography sx={{
|
||||
fontFamily: '"DM Serif Display", serif',
|
||||
fontWeight: 400, fontSize: '1.5rem',
|
||||
color: DS_TEXT.primary, lineHeight: 1.1,
|
||||
}}>
|
||||
CHF {annualRent}k
|
||||
</Typography>
|
||||
<Typography sx={{ fontSize: '0.72rem', color: DS_TEXT.muted, mt: 0.125 }}>
|
||||
{p.rentPricePerSqm} CHF/m²/Jahr
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4, mt: 0.3 }}>
|
||||
<MapPin size={11} color="#9a9a9a" />
|
||||
<Typography variant="caption" sx={{ color: DS_TEXT.muted, fontSize: '0.72rem' }}>
|
||||
{p.location.city}{p.location.district ? ` · ${p.location.district}` : ''}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Key metrics row */}
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Box sx={{ flex: 1, bgcolor: '#f9f8f6', borderRadius: 1, px: 1, py: 0.75, textAlign: 'center', border: `1px solid ${DS_BORDER.muted}` }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.3, mb: 0.1 }}>
|
||||
<Maximize2 size={10} color="#9a9a9a" />
|
||||
<Typography sx={{ fontSize: '0.62rem', color: DS_TEXT.muted, textTransform: 'uppercase', letterSpacing: 0.3 }}>Fläche</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ fontWeight: 700, fontSize: '0.82rem', color: DS_TEXT.primary }}>
|
||||
{p.areaSqm.toLocaleString('de-CH')} m²
|
||||
</Typography>
|
||||
</Box>
|
||||
{p.availabilityDate && (
|
||||
<Box sx={{ flex: 1, bgcolor: '#f9f8f6', borderRadius: 1, px: 1, py: 0.75, textAlign: 'center', border: `1px solid ${DS_BORDER.muted}` }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.3, mb: 0.1 }}>
|
||||
<Calendar size={10} color="#9a9a9a" />
|
||||
<Typography sx={{ fontSize: '0.62rem', color: DS_TEXT.muted, textTransform: 'uppercase', letterSpacing: 0.3 }}>Ab</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ fontWeight: 700, fontSize: '0.82rem', color: DS_TEXT.primary }}>
|
||||
{new Date(p.availabilityDate).toLocaleDateString('de-CH', { month: 'short', year: '2-digit' })}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
{p.softFactors?.prestige != null && (
|
||||
<Box sx={{ flex: 1, bgcolor: '#f9f8f6', borderRadius: 1, px: 1, py: 0.75, textAlign: 'center', border: `1px solid ${DS_BORDER.muted}` }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 0.3, mb: 0.1 }}>
|
||||
<TrendingUp size={10} color="#9a9a9a" />
|
||||
<Typography sx={{ fontSize: '0.62rem', color: DS_TEXT.muted, textTransform: 'uppercase', letterSpacing: 0.3 }}>Prestige</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ fontWeight: 700, fontSize: '0.82rem', color: DS_TEXT.primary }}>
|
||||
{p.softFactors.prestige}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Chips: fit-out + contract */}
|
||||
{p.softFactors && (
|
||||
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
{p.hardFacts?.fitOut && (() => {
|
||||
const FIT_OUT_META: Record<string, { label: string; tip: string }> = {
|
||||
SHELL: { label: 'Rohbau', tip: 'Keine Einbauten — volle Ausbauinvestition durch Mieter.' },
|
||||
BASIC: { label: 'Grundausbau', tip: 'Grundinfrastruktur vorhanden, Ausbau noch nötig.' },
|
||||
FULL: { label: 'Vollausbau', tip: 'Bezugsfertig ausgebaut.' },
|
||||
PREMIUM: { label: 'Premium-Ausbau', tip: 'Hochwertig ausgebaut, sofort bezugsfertig.' },
|
||||
}
|
||||
const meta = FIT_OUT_META[p.hardFacts!.fitOut!] ?? { label: p.hardFacts!.fitOut!, tip: '' }
|
||||
return (
|
||||
<Tooltip title={meta.tip} placement="top" arrow>
|
||||
<Chip label={meta.label} size="small"
|
||||
sx={{ height: 20, fontSize: '0.65rem', bgcolor: '#f4f3f0', color: DS_TEXT.secondary, cursor: 'help', border: `1px solid ${DS_BORDER.default}` }} />
|
||||
</Tooltip>
|
||||
)
|
||||
})()}
|
||||
{p.contractDurationMonths && (
|
||||
<Tooltip title={`Mindest-Vertragslaufzeit: ${p.contractDurationMonths} Monate`} placement="top" arrow>
|
||||
<Chip label={`${p.contractDurationMonths}M Vertrag`} size="small"
|
||||
sx={{ height: 20, fontSize: '0.65rem', bgcolor: '#f4f3f0', color: DS_TEXT.secondary, cursor: 'help', border: `1px solid ${DS_BORDER.default}` }} />
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Confidence bar */}
|
||||
<Box sx={{ mt: 'auto' }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 0.5 }}>
|
||||
<Typography sx={{ fontSize: '0.67rem', color: DS_TEXT.muted, textTransform: 'uppercase', letterSpacing: 0.3 }}>Datenkonfidenz</Typography>
|
||||
<Typography sx={{ fontSize: '0.72rem', fontWeight: 700, color: p.confidenceScore >= 0.75 ? '#b8975a' : DS_TEXT.muted }}>{confPct}%</Typography>
|
||||
</Box>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={confPct}
|
||||
sx={{
|
||||
height: 3, borderRadius: 2, bgcolor: '#f4f3f0',
|
||||
'& .MuiLinearProgress-bar': { bgcolor: '#152642', borderRadius: 2 },
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{/* Key facts — single compact line */}
|
||||
<Typography sx={{ fontSize: '0.8rem', color: DS_TEXT.secondary, lineHeight: 1.4 }}>
|
||||
{facts.join(' · ')}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user