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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
}
|
||||
|
||||
@@ -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" />
|
||||
))}
|
||||
</>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user