feat(demand): Ergebnis-Grid — Spalten-Umschalter, sauberere Karten, mehr Abstand
- 3/5/10-Spalten-Umschalter im Ergebnis-Header (nur Grid-Ansicht, in localStorage gemerkt) analog zu "Meine Objekte" - Karten-Bild auf aspectRatio 15/8 statt fester Höhe → keine Verzerrung bei beliebiger Spaltenzahl - Orts-Overlay vom Bild entfernt → Match-Score überdeckt das Ort-Label nicht mehr - Sidebar bleibt bis < lg (1200px) ausgeklappt → Navigation + Account auf Laptops sichtbar - Größere vertikale Abstände im Ergebnis-Feed Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -8,9 +8,11 @@ interface Props {
|
||||
futureCount: number
|
||||
view?: 'list' | 'grid'
|
||||
onViewChange?: (v: 'list' | 'grid') => void
|
||||
columns?: 3 | 5 | 10
|
||||
onColumnsChange?: (n: 3 | 5 | 10) => void
|
||||
}
|
||||
|
||||
export function ResultFeedHeader({ total, platformCount, maisonWorkCount, futureCount, view = 'list', onViewChange }: Props) {
|
||||
export function ResultFeedHeader({ total, platformCount, maisonWorkCount, futureCount, view = 'list', onViewChange, columns = 3, onColumnsChange }: Props) {
|
||||
return (
|
||||
<Box sx={{ bgcolor: 'white', borderBottom: '1px solid #e2e8f0', px: 3, py: 2, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Box>
|
||||
@@ -21,9 +23,30 @@ export function ResultFeedHeader({ total, platformCount, maisonWorkCount, future
|
||||
{platformCount} Plattform · {maisonWorkCount} Maison Work · {futureCount} Future Availability
|
||||
</Typography>
|
||||
</Box>
|
||||
{onViewChange && (
|
||||
<ViewToggle view={view} onChange={onViewChange} />
|
||||
)}
|
||||
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
|
||||
{view === 'grid' && onColumnsChange && (
|
||||
<Box sx={{ display: 'flex', border: '1px solid #e2e8f0', borderRadius: 1, overflow: 'hidden' }}>
|
||||
{([3, 5, 10] as const).map(n => (
|
||||
<Box
|
||||
key={n}
|
||||
onClick={() => onColumnsChange(n)}
|
||||
sx={{
|
||||
width: 32, height: 32, display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
cursor: 'pointer', fontSize: '0.72rem', fontWeight: 600,
|
||||
bgcolor: columns === n ? '#152642' : 'transparent',
|
||||
color: columns === n ? 'white' : '#64748b',
|
||||
'&:hover': { bgcolor: columns === n ? '#162d4a' : '#f1f5f9' },
|
||||
}}
|
||||
>
|
||||
{n}
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
{onViewChange && (
|
||||
<ViewToggle view={view} onChange={onViewChange} />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,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 { resolveImageLabel } from '../../lib/propertyImageResolver'
|
||||
import { useCompareStore } from '../../stores/compareStore'
|
||||
import { usePipelineStore } from '../../stores/pipelineStore'
|
||||
import type { UnifiedMatchResult } from '../../domain/unifiedResult'
|
||||
@@ -98,17 +97,6 @@ export function UnifiedResultCard({ result, view = 'list' }: Props) {
|
||||
? result.property.location.city
|
||||
: result.signal.locationHint ?? undefined
|
||||
|
||||
const overlayLabels = result.resultType !== 'FUTURE_AVAILABILITY'
|
||||
? resolveImageLabel({
|
||||
assetType: result.property.assetType,
|
||||
city: result.property.location.city,
|
||||
district: result.property.location.district,
|
||||
prestige: result.property.softFactors?.prestige,
|
||||
floorLevel: result.property.floorLevel,
|
||||
propertyId: result.property.id,
|
||||
})
|
||||
: null
|
||||
|
||||
return (
|
||||
<IntelligenceMatchCard
|
||||
vm={vm}
|
||||
@@ -116,8 +104,6 @@ export function UnifiedResultCard({ result, view = 'list' }: Props) {
|
||||
lat={lat}
|
||||
lng={lng}
|
||||
cityLabel={cityLabel}
|
||||
overlayTypeLabel={overlayLabels?.type}
|
||||
overlayLocationLabel={overlayLabels?.location}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { SCORE_THEME } from '../shared/scoreTheme'
|
||||
interface Props {
|
||||
results: UnifiedMatchResult[]
|
||||
view?: 'list' | 'grid'
|
||||
columns?: 3 | 5 | 10
|
||||
}
|
||||
|
||||
const TIER_CONFIG = [
|
||||
@@ -40,7 +41,7 @@ interface TierHeaderProps {
|
||||
function TierHeader({ label, sublabel, tierKey, count, isFirst }: TierHeaderProps) {
|
||||
const theme = SCORE_THEME[tierKey]
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mt: isFirst ? 0 : 3, mb: 1.5 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5, mt: isFirst ? 0 : 4.5, mb: 2.5 }}>
|
||||
<Box
|
||||
sx={{
|
||||
width: 4,
|
||||
@@ -72,7 +73,7 @@ function TierHeader({ label, sublabel, tierKey, count, isFirst }: TierHeaderProp
|
||||
)
|
||||
}
|
||||
|
||||
export function UnifiedResultFeed({ results, view = 'list' }: Props) {
|
||||
export function UnifiedResultFeed({ results, view = 'list', columns = 3 }: Props) {
|
||||
// All results — real properties and Schattenmarkt signals — are sorted into
|
||||
// the same score tiers so they appear in a single unified ranked list.
|
||||
const tiers = TIER_CONFIG.map(t => ({ ...t, items: results.filter(t.filter) }))
|
||||
@@ -80,7 +81,7 @@ export function UnifiedResultFeed({ results, view = 'list' }: Props) {
|
||||
|
||||
function renderCards(items: UnifiedMatchResult[]) {
|
||||
return view === 'grid' ? (
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 2 }}>
|
||||
<Box sx={{ display: 'grid', gridTemplateColumns: `repeat(${columns}, 1fr)`, gap: 3 }}>
|
||||
{items.map(result => (
|
||||
<UnifiedResultCard key={result.matchId} result={result} view="grid" />
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user