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
+87
View File
@@ -0,0 +1,87 @@
import { useState } from 'react'
import { Box, IconButton, Tooltip, Typography } from '@mui/material'
import { MapPin, Map } from 'lucide-react'
interface Props {
imageUrl?: string
lat?: number
lng?: number
address?: string
cityLabel?: string
height?: number
}
// TODO: replace placeholder with Google Maps Static API:
// https://maps.googleapis.com/maps/api/staticmap?center={lat},{lng}&zoom=15&size=800x400&key=YOUR_KEY
function buildMapsUrl(lat?: number, lng?: number, address?: string): string {
if (lat != null && lng != null) {
return `https://www.google.com/maps/search/?api=1&query=${lat},${lng}`
}
if (address) {
return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(address)}`
}
return 'https://www.google.com/maps'
}
export function LocationPreview({ imageUrl, lat, lng, address, cityLabel, height = 180 }: Props) {
const [imgError, setImgError] = useState(false)
const mapsUrl = buildMapsUrl(lat, lng, address)
const showImage = !!imageUrl && !imgError
return (
<Box sx={{ position: 'relative', width: '100%', height, overflow: 'hidden', flexShrink: 0 }}>
{showImage ? (
<img
src={imageUrl}
alt={cityLabel ?? 'Standort'}
onError={() => setImgError(true)}
style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
/>
) : (
<Box
sx={{
width: '100%',
height: '100%',
background: 'linear-gradient(160deg,#dce8f2 0%,#b8cfe0 100%)',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: 0.5,
}}
>
<MapPin size={28} color="#7ba3bf" />
{cityLabel && (
<Typography variant="body2" sx={{ color: '#5a87a3', fontWeight: 600, letterSpacing: 0.5 }}>
{cityLabel}
</Typography>
)}
</Box>
)}
{/* Google Maps button */}
<Tooltip title="In Google Maps öffnen" placement="top">
<IconButton
component="a"
href={mapsUrl}
target="_blank"
rel="noopener noreferrer"
size="small"
sx={{
position: 'absolute',
bottom: 8,
right: 8,
bgcolor: 'rgba(255,255,255,0.88)',
backdropFilter: 'blur(4px)',
width: 28,
height: 28,
'&:hover': { bgcolor: 'white' },
}}
>
<Map size={14} color="#1e3a5f" />
</IconButton>
</Tooltip>
</Box>
)
}
+46
View File
@@ -0,0 +1,46 @@
import { Box, IconButton, Tooltip } from '@mui/material'
import { LayoutGrid, LayoutList } from 'lucide-react'
interface Props {
view: 'list' | 'grid'
onChange: (v: 'list' | 'grid') => void
}
export function ViewToggle({ view, onChange }: Props) {
return (
<Box sx={{ display: 'flex', gap: 0.25, border: '1px solid #e2e8f0', borderRadius: 1, overflow: 'hidden' }}>
<Tooltip title="Listenansicht">
<IconButton
size="small"
onClick={() => onChange('list')}
sx={{
borderRadius: 0,
width: 32,
height: 32,
bgcolor: view === 'list' ? '#1e3a5f' : 'transparent',
color: view === 'list' ? 'white' : '#64748b',
'&:hover': { bgcolor: view === 'list' ? '#162d4a' : '#f1f5f9' },
}}
>
<LayoutList size={15} />
</IconButton>
</Tooltip>
<Tooltip title="Kartenansicht">
<IconButton
size="small"
onClick={() => onChange('grid')}
sx={{
borderRadius: 0,
width: 32,
height: 32,
bgcolor: view === 'grid' ? '#1e3a5f' : 'transparent',
color: view === 'grid' ? 'white' : '#64748b',
'&:hover': { bgcolor: view === 'grid' ? '#162d4a' : '#f1f5f9' },
}}
>
<LayoutGrid size={15} />
</IconButton>
</Tooltip>
</Box>
)
}
+4
View File
@@ -0,0 +1,4 @@
export { ViewToggle } from './ViewToggle'
export { LocationPreview } from './LocationPreview'
export { getScoreTier, SCORE_THEME } from './scoreTheme'
export type { ScoreTier } from './scoreTheme'
+37
View File
@@ -0,0 +1,37 @@
export type ScoreTier = 'gold' | 'silver' | 'bronze'
export function getScoreTier(score: number): ScoreTier {
if (score >= 90) return 'gold'
if (score >= 75) return 'silver'
return 'bronze'
}
export const SCORE_THEME = {
gold: {
gradient: 'linear-gradient(135deg,#f8e642 0%,#d4920e 100%)',
border: '#c9900c',
glow: 'rgba(212,146,14,0.35)',
text: '#7a4f00',
label: 'Top Match',
cardBorder: '#fbbf24',
cardBg: 'linear-gradient(160deg,#fffbeb,#fef3c7)',
},
silver: {
gradient: 'linear-gradient(135deg,#f1f5f9 0%,#cbd5e1 100%)',
border: '#94a3b8',
glow: 'rgba(148,163,184,0.30)',
text: '#334155',
label: 'Starkes Match',
cardBorder: '#cbd5e1',
cardBg: 'linear-gradient(160deg,#f8fafc,#f1f5f9)',
},
bronze: {
gradient: 'linear-gradient(135deg,#fde8c8 0%,#d4956a 100%)',
border: '#c07a46',
glow: 'rgba(192,122,70,0.25)',
text: '#7c3d0c',
label: 'Gutes Match',
cardBorder: '#f5d0a9',
cardBg: 'linear-gradient(160deg,#fdf6f0,#fef3e8)',
},
} as const