feat: F029 intelligence card system — premium AI-native CRE cards

Gold/Silver/Bronze FIFA-style score badges overlaid on location imagery.
List/grid view toggle on Results and Properties pages (persisted in
localStorage). IntelligenceMatchCard for match results grid view;
PropertyIntelligenceCard for property grid view with warm brown theme.
LocationPreview component with Google Maps link. 10 mock properties
enriched with Unsplash building images.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-18 13:25:27 +02:00
parent 9e7a09f8a2
commit c75d3c8121
13 changed files with 581 additions and 26 deletions
@@ -0,0 +1,141 @@
import { Box, Button, Chip, LinearProgress, Typography } from '@mui/material'
import { LocationPreview } from '../shared/LocationPreview'
import { getAssetTypeColor, getAssetTypeLabel, getAvailabilityLabel } from './propertyHelpers'
import type { Property } from '../../domain/property'
interface Props {
property: Property
onSelect: (id: string) => void
}
function availabilityBadgeColor(status: string): string {
if (status === 'AVAILABLE_NOW') return '#1a7a4a'
if (status === 'AVAILABLE_SOON') return '#d97706'
return '#64748b'
}
export function PropertyIntelligenceCard({ property: p, onSelect }: Props) {
const confPct = Math.round(p.confidenceScore * 100)
const confColor = p.confidenceScore >= 0.75 ? '#1a7a4a' : p.confidenceScore >= 0.55 ? '#d97706' : '#c0392b'
return (
<Box
sx={{
borderRadius: 2,
overflow: 'hidden',
border: '1px solid #e8d5c4',
background: 'linear-gradient(160deg,#fdf8f3,#faf0e6)',
boxShadow: '0 2px 12px rgba(0,0,0,0.06)',
display: 'flex',
flexDirection: 'column',
transition: 'box-shadow 0.15s, transform 0.1s',
cursor: 'pointer',
'&:hover': {
boxShadow: '0 6px 24px rgba(0,0,0,0.10)',
transform: 'translateY(-1px)',
},
}}
onClick={() => onSelect(p.id)}
>
{/* Image zone */}
<Box sx={{ position: 'relative' }}>
<LocationPreview
imageUrl={p.images?.[0]}
lat={p.location.coordinates?.lat}
lng={p.location.coordinates?.lng}
address={`${p.address.street} ${p.address.houseNumber}, ${p.address.city}`}
cityLabel={p.location.city}
height={175}
/>
{/* Availability badge */}
<Box sx={{ position: 'absolute', top: 10, left: 10 }}>
<Chip
label={getAvailabilityLabel(p.availabilityStatus)}
size="small"
sx={{
bgcolor: availabilityBadgeColor(p.availabilityStatus),
color: 'white',
fontWeight: 700,
fontSize: 10,
height: 20,
}}
/>
</Box>
{/* Asset type chip */}
<Box sx={{ position: 'absolute', top: 10, right: 44 }}>
<Chip
label={getAssetTypeLabel(p.assetType)}
size="small"
sx={{
bgcolor: getAssetTypeColor(p.assetType),
color: 'white',
fontWeight: 600,
fontSize: 10,
height: 20,
}}
/>
</Box>
</Box>
{/* Card body */}
<Box sx={{ p: 2, display: 'flex', flexDirection: 'column', gap: 0, flex: 1 }}>
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.3 }} noWrap>
{p.title}
</Typography>
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 0.25, mb: 1.25 }}>
{p.location.city}{p.location.district ? ` · ${p.location.district}` : ''}
</Typography>
{/* Key specs */}
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 1.25 }}>
<Chip label={`${p.areaSqm}`} size="small" sx={{ fontSize: 11, bgcolor: '#f1f5f9', height: 22 }} />
<Chip label={`CHF ${p.rentPricePerSqm}/m²`} size="small" sx={{ fontSize: 11, bgcolor: '#f1f5f9', height: 22 }} />
{p.contractDurationMonths && (
<Chip label={`${p.contractDurationMonths}M Vertrag`} size="small" sx={{ fontSize: 11, bgcolor: '#f1f5f9', height: 22 }} />
)}
</Box>
{/* Soft factors */}
{p.softFactors && (
<Box sx={{ display: 'flex', gap: 0.5, flexWrap: 'wrap', mb: 1.25 }}>
{p.softFactors.prestige != null && (
<Chip label={`Prestige ${p.softFactors.prestige}`} size="small"
sx={{ fontSize: 10, bgcolor: 'rgba(30,58,95,0.08)', color: '#1e3a5f', height: 20 }} />
)}
{p.softFactors.accessibility != null && (
<Chip label={`ÖV ${p.softFactors.publicTransportMinutes ?? p.softFactors.accessibility}min`} size="small"
sx={{ fontSize: 10, bgcolor: 'rgba(30,58,95,0.08)', color: '#1e3a5f', height: 20 }} />
)}
</Box>
)}
{/* Confidence */}
<Box sx={{ mt: 'auto', pt: 1 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', mb: 0.5 }}>
<Typography variant="caption" color="text.secondary">Datenkonfidenz</Typography>
<Typography variant="caption" sx={{ fontWeight: 700, color: confColor }}>{confPct}%</Typography>
</Box>
<LinearProgress
variant="determinate"
value={confPct}
sx={{
height: 5, borderRadius: 3, bgcolor: '#e2e8f0',
'& .MuiLinearProgress-bar': { bgcolor: confColor },
}}
/>
</Box>
<Button
size="small"
variant="text"
onClick={e => { e.stopPropagation(); onSelect(p.id) }}
sx={{ mt: 1.25, alignSelf: 'flex-end', textTransform: 'none', fontSize: '0.75rem', color: '#1e3a5f', px: 0 }}
>
Details anzeigen
</Button>
</Box>
</Box>
)
}
+1
View File
@@ -15,3 +15,4 @@ export { PropertyFilterBar } from './PropertyFilterBar'
export type { PropertyTableFilters } from './PropertyFilterBar'
export { PropertyDetailView } from './PropertyDetailView'
export { PropertyDetailSkeleton } from './PropertyDetailSkeleton'
export { PropertyIntelligenceCard } from './PropertyIntelligenceCard'