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
+16 -8
View File
@@ -1,21 +1,29 @@
import { Box, Typography } from '@mui/material'
import { ViewToggle } from '../shared'
interface Props {
total: number
verifiedCount: number
externalCount: number
futureCount: number
view?: 'list' | 'grid'
onViewChange?: (v: 'list' | 'grid') => void
}
export function ResultFeedHeader({ total, verifiedCount, externalCount, futureCount }: Props) {
export function ResultFeedHeader({ total, verifiedCount, externalCount, futureCount, view = 'list', onViewChange }: Props) {
return (
<Box sx={{ bgcolor: 'white', borderBottom: '1px solid #e2e8f0', px: 3, py: 2 }}>
<Typography variant="h5" sx={{ fontWeight: 700 }} color="text.primary">
{total} Treffer gefunden
</Typography>
<Typography variant="body2" color="text.secondary">
{verifiedCount} Verified · {externalCount} Extern · {futureCount} Signale
</Typography>
<Box sx={{ bgcolor: 'white', borderBottom: '1px solid #e2e8f0', px: 3, py: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box>
<Typography variant="h5" sx={{ fontWeight: 700 }} color="text.primary">
{total} Empfehlungen
</Typography>
<Typography variant="body2" color="text.secondary">
{verifiedCount} Verifiziert · {externalCount} Extern · {futureCount} Marktsignale
</Typography>
</Box>
{onViewChange && (
<ViewToggle view={view} onChange={onViewChange} />
)}
</Box>
)
}
+32 -4
View File
@@ -1,5 +1,6 @@
import { useNavigate } from 'react-router'
import { MatchCardCompact } from '../match-card/MatchCardCompact'
import { IntelligenceMatchCard } from '../match-card/IntelligenceMatchCard'
import { buildMatchCardViewModel } from '../../features/matching/matchCardAdapter'
import { useCompareStore } from '../../stores/compareStore'
import { useShortlistStore } from '../../stores/shortlistStore'
@@ -8,16 +9,17 @@ import type { MatchCardAction } from '../match-card/MatchCardViewModel'
interface Props {
result: UnifiedMatchResult
view?: 'list' | 'grid'
}
function getResultTitle(result: UnifiedMatchResult): string {
if (result.resultType !== 'FUTURE_AVAILABILITY') {
return (result as any).property?.title ?? result.matchId
return result.property?.title ?? result.matchId
}
return (result as any).signal?.companyName ?? result.matchId
return result.signal?.companyName ?? result.matchId
}
export function UnifiedResultCard({ result }: Props) {
export function UnifiedResultCard({ result, view = 'list' }: Props) {
const navigate = useNavigate()
const { addToCompare, removeFromCompare, isInCompare, isFull } = useCompareStore()
const { openAddDialog } = useShortlistStore()
@@ -52,7 +54,7 @@ export function UnifiedResultCard({ result }: Props) {
},
{
id: 'details',
label: 'Details',
label: 'Details',
actionType: 'OPEN_DETAIL',
variant: 'primary',
onClick: () => navigate(`/demand/results/${result.matchId}`),
@@ -60,5 +62,31 @@ export function UnifiedResultCard({ result }: Props) {
]
const vm = buildMatchCardViewModel(result, actions)
if (view === 'grid') {
const imageUrl = result.resultType !== 'FUTURE_AVAILABILITY'
? result.property.images?.[0]
: undefined
const lat = result.resultType !== 'FUTURE_AVAILABILITY'
? result.property.location.coordinates?.lat
: undefined
const lng = result.resultType !== 'FUTURE_AVAILABILITY'
? result.property.location.coordinates?.lng
: undefined
const cityLabel = result.resultType !== 'FUTURE_AVAILABILITY'
? result.property.location.city
: result.signal.locationHint ?? undefined
return (
<IntelligenceMatchCard
vm={vm}
imageUrl={imageUrl}
lat={lat}
lng={lng}
cityLabel={cityLabel}
/>
)
}
return <MatchCardCompact vm={vm} />
}
+14 -2
View File
@@ -1,15 +1,27 @@
import { Box } from '@mui/material'
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
import { UnifiedResultCard } from './UnifiedResultCard'
interface Props {
results: UnifiedMatchResult[]
view?: 'list' | 'grid'
}
export function UnifiedResultFeed({ results }: Props) {
export function UnifiedResultFeed({ results, view = 'list' }: Props) {
if (view === 'grid') {
return (
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 2 }}>
{results.map(result => (
<UnifiedResultCard key={result.matchId} result={result} view="grid" />
))}
</Box>
)
}
return (
<>
{results.map(result => (
<UnifiedResultCard key={result.matchId} result={result} />
<UnifiedResultCard key={result.matchId} result={result} view="list" />
))}
</>
)