feat: Schattenmarkt UX — separate section, relevance bridge, source links

Signals are now in their own feed section with an explanation banner
so users understand they are not confirmed properties. Each signal card
shows WHY it appears in their search (e.g. 'Fläche könnte frei werden').
Source pills are now clickable links. FutureAvailabilityContextPanel
redesigned with full AI summary, source button, and relevance bridge.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Sutter
2026-05-18 21:59:08 +02:00
parent b5cdb0353b
commit 9cf3e72033
5 changed files with 281 additions and 70 deletions
+67 -15
View File
@@ -1,4 +1,5 @@
import { Box, Chip, Divider, Typography } from '@mui/material'
import { Alert, Box, Chip, Divider, Typography } from '@mui/material'
import { Zap } from 'lucide-react'
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
import { UnifiedResultCard } from './UnifiedResultCard'
import { SCORE_THEME } from '../shared/scoreTheme'
@@ -72,12 +73,67 @@ function TierHeader({ label, sublabel, tierKey, count, isFirst }: TierHeaderProp
)
}
function SchattenmarktSectionHeader({ count }: { count: number }) {
return (
<Box sx={{ mt: 4, mb: 0 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mb: 1.5 }}>
<Box sx={{ width: 4, height: 28, borderRadius: 2, background: 'linear-gradient(180deg, #7c3aed, #4c1d95)', flexShrink: 0 }} />
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Zap size={15} color="#7c3aed" />
<Typography variant="subtitle1" sx={{ fontWeight: 700, lineHeight: 1.2, color: '#4c1d95' }}>
Schattenmarkt
</Typography>
<Chip
label={count}
size="small"
sx={{ fontWeight: 700, fontSize: '0.75rem', bgcolor: '#ede9fe', color: '#6d28d9', border: '1px solid #c4b5fd' }}
/>
</Box>
<Divider sx={{ flex: 1 }} />
</Box>
<Alert
severity="info"
icon={<Zap size={16} />}
sx={{
mb: 2,
bgcolor: '#faf5ff',
border: '1px solid #e9d5ff',
color: '#4c1d95',
'& .MuiAlert-icon': { color: '#7c3aed' },
}}
>
<strong>Was sind Schattenmarkt-Signale?</strong> Die KI hat aus öffentlichen Quellen (Stelleninseraten, Pressemeldungen, Baubewilligungen etc.) Hinweise gefunden, dass in Ihrer Zielregion in absehbarer Zeit Flächen frei werden könnten. Dies sind <strong>keine bestätigten Angebote</strong> aber frühzeitige Hinweise, die Sie im Auge behalten sollten. Klicken Sie auf Details für Quellen und die vollständige KI-Analyse.
</Alert>
</Box>
)
}
export function UnifiedResultFeed({ results, view = 'list' }: Props) {
const tiers = TIER_CONFIG.map(t => ({ ...t, items: results.filter(t.filter) }))
const realResults = results.filter(r => r.resultType !== 'FUTURE_AVAILABILITY')
const signalResults = results.filter(r => r.resultType === 'FUTURE_AVAILABILITY')
const tiers = TIER_CONFIG.map(t => ({ ...t, items: realResults.filter(t.filter) }))
.filter(t => t.items.length > 0)
function renderCards(items: UnifiedMatchResult[]) {
return view === 'grid' ? (
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 2 }}>
{items.map(result => (
<UnifiedResultCard key={result.matchId} result={result} view="grid" />
))}
</Box>
) : (
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
{items.map(result => (
<UnifiedResultCard key={result.matchId} result={result} view="list" />
))}
</Box>
)
}
return (
<Box>
{/* Real properties — scored tiers */}
{tiers.map((tier, idx) => (
<Box key={tier.key}>
<TierHeader
@@ -87,21 +143,17 @@ export function UnifiedResultFeed({ results, view = 'list' }: Props) {
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>
)}
{renderCards(tier.items)}
</Box>
))}
{/* Schattenmarkt signals — dedicated section */}
{signalResults.length > 0 && (
<Box>
<SchattenmarktSectionHeader count={signalResults.length} />
{renderCards(signalResults)}
</Box>
)}
</Box>
)
}