feat: Steuerlast-Kriterium, ImmoScout-Layout, Gold/Silver/Bronze-Trennung

- Flächensuche: Flexibilität durch Steuerlast (taxEnvironment) ersetzt
- MatchDetail: Bild als Hero-Banner oben, Karte eingebettet im Inhalt darunter
- Ergebnisliste: Gold/Silber/Bronze-Abschnitte mit farbigen Trennlinien
- ScoreBreakdown: KI-Steuerlast-Link zur kantonalen Steuerrechner-Seite
- Beispieldaten: alle Objekte mit passenden Bildern versehen
- locationIntelligence: taxCalculatorUrl pro Kanton ergänzt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-18 14:14:10 +02:00
parent 5b917bd619
commit 05ac0083b2
13 changed files with 495 additions and 91 deletions
+95 -16
View File
@@ -1,28 +1,107 @@
import { Box } from '@mui/material'
import { Box, Chip, Divider, Typography } from '@mui/material'
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
import { UnifiedResultCard } from './UnifiedResultCard'
import { SCORE_THEME } from '../shared/scoreTheme'
interface Props {
results: UnifiedMatchResult[]
view?: 'list' | 'grid'
}
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>
)
}
const TIER_CONFIG = [
{
key: 'gold' as const,
label: 'Top Matches',
sublabel: '90100 Punkte',
filter: (r: UnifiedMatchResult) => r.matchScore >= 90,
},
{
key: 'silver' as const,
label: 'Starke Matches',
sublabel: '8089 Punkte',
filter: (r: UnifiedMatchResult) => r.matchScore >= 80 && r.matchScore < 90,
},
{
key: 'bronze' as const,
label: 'Weitere Treffer',
sublabel: 'unter 80 Punkte',
filter: (r: UnifiedMatchResult) => r.matchScore < 80,
},
]
interface TierHeaderProps {
label: string
sublabel: string
tierKey: 'gold' | 'silver' | 'bronze'
count: number
isFirst: boolean
}
function TierHeader({ label, sublabel, tierKey, count, isFirst }: TierHeaderProps) {
const theme = SCORE_THEME[tierKey]
return (
<>
{results.map(result => (
<UnifiedResultCard key={result.matchId} result={result} view="list" />
))}
</>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mt: isFirst ? 0 : 3, mb: 1.5 }}>
<Box
sx={{
width: 4,
height: 28,
borderRadius: 2,
background: theme.gradient,
flexShrink: 0,
}}
/>
<Box>
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.2, color: theme.text === '#7a4f00' ? '#92400e' : theme.text === '#334155' ? '#1e293b' : '#7c3d12' }}>
{label}
</Typography>
<Typography variant="caption" color="text.secondary">{sublabel}</Typography>
</Box>
<Chip
label={count}
size="small"
sx={{
fontWeight: 700,
fontSize: '0.75rem',
background: theme.gradient,
color: theme.text,
border: `1px solid ${theme.border}`,
}}
/>
<Divider sx={{ flex: 1 }} />
</Box>
)
}
export function UnifiedResultFeed({ results, view = 'list' }: Props) {
const tiers = TIER_CONFIG.map(t => ({ ...t, items: results.filter(t.filter) }))
.filter(t => t.items.length > 0)
return (
<Box>
{tiers.map((tier, idx) => (
<Box key={tier.key}>
<TierHeader
label={tier.label}
sublabel={tier.sublabel}
tierKey={tier.key}
count={tier.items.length}
isFirst={idx === 0}
/>
{view === 'grid' ? (
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 2 }}>
{tier.items.map(result => (
<UnifiedResultCard key={result.matchId} result={result} view="grid" />
))}
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
{tier.items.map(result => (
<UnifiedResultCard key={result.matchId} result={result} view="list" />
))}
</Box>
)}
</Box>
))}
</Box>
)
}