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:
@@ -4,6 +4,7 @@ import {
|
||||
Briefcase,
|
||||
Building2,
|
||||
CheckCircle2,
|
||||
ExternalLink,
|
||||
FileCheck,
|
||||
FileText,
|
||||
Newspaper,
|
||||
@@ -42,6 +43,16 @@ const SIGNAL_TYPE_LABELS: Record<string, string> = {
|
||||
SPACE_CONSOLIDATION: 'Flächenkonsolidierung',
|
||||
}
|
||||
|
||||
// Human-readable explanation of WHY a signal type is relevant to a space search
|
||||
const SIGNAL_RELEVANCE_BRIDGE: Record<string, string> = {
|
||||
EXPANSION: 'Neuer Flächenbedarf entsteht — Fläche wird gesucht',
|
||||
POSSIBLE_MOVE_OUT: 'Fläche könnte frei werden — frühzeitig beobachten',
|
||||
CONSTRUCTION_PROJECT:'Neubau geplant — zukünftige Verfügbarkeit möglich',
|
||||
RESTRUCTURING: 'Flächenveränderung durch Restrukturierung möglich',
|
||||
PROJECT_DEVELOPMENT: 'Projektentwicklung — neue Flächen in Planung',
|
||||
SPACE_CONSOLIDATION: 'Konsolidierung — Teilflächen könnten verfügbar werden',
|
||||
}
|
||||
|
||||
const CREDIBILITY_META: Record<string, { label: string; color: string }> = {
|
||||
HIGH: { label: 'Hohe Quellenqualität', color: '#1a7a4a' },
|
||||
MEDIUM: { label: 'Mittlere Quellenqualität', color: '#d97706' },
|
||||
@@ -54,6 +65,7 @@ function SignalHero({ vm }: { vm: MatchCardViewModel }) {
|
||||
const tier = getScoreTier(vm.matchScore)
|
||||
const theme = SCORE_THEME[tier]
|
||||
const signalLabel = vm.signalType ? (SIGNAL_TYPE_LABELS[vm.signalType] ?? vm.signalType) : 'Signal'
|
||||
const relevanceBridge = vm.signalType ? (SIGNAL_RELEVANCE_BRIDGE[vm.signalType] ?? null) : null
|
||||
const initials = (vm.title ?? '?')
|
||||
.split(/\s+/)
|
||||
.slice(0, 2)
|
||||
@@ -66,7 +78,7 @@ function SignalHero({ vm }: { vm: MatchCardViewModel }) {
|
||||
{/* Gradient hero background */}
|
||||
<Box
|
||||
sx={{
|
||||
height: 130,
|
||||
height: 140,
|
||||
background: 'linear-gradient(135deg, #1a0a3a 0%, #3b0a6e 50%, #6d28d9 100%)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -75,6 +87,7 @@ function SignalHero({ vm }: { vm: MatchCardViewModel }) {
|
||||
gap: 0.5,
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
px: 7,
|
||||
}}
|
||||
>
|
||||
{/* Subtle pattern overlay */}
|
||||
@@ -86,25 +99,32 @@ function SignalHero({ vm }: { vm: MatchCardViewModel }) {
|
||||
|
||||
{/* Company initials */}
|
||||
<Box sx={{
|
||||
width: 44, height: 44, borderRadius: '50%',
|
||||
width: 40, height: 40, borderRadius: '50%',
|
||||
bgcolor: 'rgba(255,255,255,0.15)',
|
||||
border: '1.5px solid rgba(255,255,255,0.3)',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
zIndex: 1,
|
||||
zIndex: 1, flexShrink: 0,
|
||||
}}>
|
||||
<Typography sx={{ color: 'white', fontWeight: 800, fontSize: '1rem', letterSpacing: 1 }}>
|
||||
<Typography sx={{ color: 'white', fontWeight: 800, fontSize: '0.9rem', letterSpacing: 1 }}>
|
||||
{initials}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Signal type label */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, zIndex: 1 }}>
|
||||
<Zap size={11} color="#c4b5fd" />
|
||||
<Typography sx={{ color: '#c4b5fd', fontSize: '0.68rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.8 }}>
|
||||
<Zap size={10} color="#c4b5fd" />
|
||||
<Typography sx={{ color: '#c4b5fd', fontSize: '0.63rem', fontWeight: 600, textTransform: 'uppercase', letterSpacing: 0.8 }}>
|
||||
{signalLabel}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Relevance bridge — why this appears in results */}
|
||||
{relevanceBridge && (
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.85)', fontSize: '0.67rem', textAlign: 'center', lineHeight: 1.3, zIndex: 1 }}>
|
||||
{relevanceBridge}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Verified badge */}
|
||||
{vm.signalIsVerified && (
|
||||
<Box sx={{ position: 'absolute', bottom: 8, right: 8, display: 'flex', alignItems: 'center', gap: 0.4, bgcolor: 'rgba(26,122,74,0.9)', borderRadius: 1, px: 0.75, py: 0.25 }}>
|
||||
@@ -153,10 +173,24 @@ function SignalMetaStrip({ vm }: { vm: MatchCardViewModel }) {
|
||||
{/* Source + credibility row */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.75, flexWrap: 'wrap' }}>
|
||||
{sourceMeta && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4, bgcolor: '#ede9fe', color: '#6d28d9', borderRadius: 1, px: 0.75, py: 0.25 }}>
|
||||
{sourceMeta.icon}
|
||||
<Typography sx={{ fontSize: '0.68rem', fontWeight: 600 }}>{sourceMeta.label}</Typography>
|
||||
</Box>
|
||||
vm.signalSourceUrl ? (
|
||||
<Box
|
||||
component="a"
|
||||
href={vm.signalSourceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 0.4, bgcolor: '#ede9fe', color: '#6d28d9', borderRadius: 1, px: 0.75, py: 0.25, textDecoration: 'none', '&:hover': { bgcolor: '#ddd6fe' } }}
|
||||
>
|
||||
{sourceMeta.icon}
|
||||
<Typography sx={{ fontSize: '0.68rem', fontWeight: 600 }}>{sourceMeta.label}</Typography>
|
||||
<ExternalLink size={9} style={{ opacity: 0.7 }} />
|
||||
</Box>
|
||||
) : (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.4, bgcolor: '#ede9fe', color: '#6d28d9', borderRadius: 1, px: 0.75, py: 0.25 }}>
|
||||
{sourceMeta.icon}
|
||||
<Typography sx={{ fontSize: '0.68rem', fontWeight: 600 }}>{sourceMeta.label}</Typography>
|
||||
</Box>
|
||||
)
|
||||
)}
|
||||
{credMeta && (
|
||||
<Tooltip title={credMeta.label} arrow>
|
||||
|
||||
@@ -51,6 +51,7 @@ export interface MatchCardViewModel {
|
||||
|
||||
// Schattenmarkt / FUTURE_AVAILABILITY signal fields
|
||||
signalSourceType?: 'PRESS' | 'CONSTRUCTION_PERMIT' | 'JOB_POSTING' | 'COMPANY_REPORT' | 'MARKET_DATA' | 'MANUAL'
|
||||
signalSourceUrl?: string
|
||||
signalSourceCredibility?: 'LOW' | 'MEDIUM' | 'HIGH'
|
||||
signalProbability?: number
|
||||
signalMarketIndicator?: string
|
||||
|
||||
Reference in New Issue
Block a user