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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { ViewToggle } from './ViewToggle'
|
||||
export { LocationPreview } from './LocationPreview'
|
||||
export { getScoreTier, SCORE_THEME } from './scoreTheme'
|
||||
export type { ScoreTier } from './scoreTheme'
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user