feat: full responsive layout — mobile sidebar, stacked panels, fullscreen drawers
- AppShell: permanent sidebar on desktop, temporary drawer + hamburger on mobile (<md) - TopBar: hamburger icon on mobile, AI-Assistent als Icon-Button auf kleinen Screens - MatchDetail: Sidebar stacks below content on mobile, key facts wrap, px responsive - Properties: Drawer fullscreen (100vw) on mobile, 650px on desktop - ActiveInquiriesTab: mobile shows list → tap opens detail with back button - LatentInquiriesTab: mobile sequential navigation (list → detail → own properties), tablet 2-col, desktop 3-col - PublicNeedList: fullWidth prop for mobile layout - InquiryDetailPanel: property card stacks below chat on mobile Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Box, IconButton, Tooltip, Typography } from '@mui/material'
|
||||
import { LayoutGrid, List as ListIcon, Inbox } from 'lucide-react'
|
||||
import { Box, IconButton, Tooltip, Typography, useMediaQuery, useTheme } from '@mui/material'
|
||||
import { ArrowLeft, LayoutGrid, List as ListIcon, Inbox } from 'lucide-react'
|
||||
import { useActiveInquiries } from '../../hooks/useInquiries'
|
||||
import { EmptyState } from '../ui'
|
||||
import { InquiryList } from './InquiryList'
|
||||
@@ -21,6 +21,8 @@ export function ActiveInquiriesTab() {
|
||||
const { data: inquiries = [], isLoading } = useActiveInquiries()
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null)
|
||||
const [view, setView] = useState<ViewMode>(loadViewMode())
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
@@ -28,12 +30,12 @@ export function ActiveInquiriesTab() {
|
||||
}
|
||||
}, [view])
|
||||
|
||||
// Auto-select first inquiry when data loads
|
||||
// Auto-select first inquiry on desktop only
|
||||
useEffect(() => {
|
||||
if (!selectedId && inquiries.length > 0) {
|
||||
if (!isMobile && !selectedId && inquiries.length > 0) {
|
||||
setSelectedId(inquiries[0].id)
|
||||
}
|
||||
}, [inquiries, selectedId])
|
||||
}, [inquiries, selectedId, isMobile])
|
||||
|
||||
if (!isLoading && inquiries.length === 0) {
|
||||
return (
|
||||
@@ -45,15 +47,31 @@ export function ActiveInquiriesTab() {
|
||||
)
|
||||
}
|
||||
|
||||
// Mobile: show detail panel when an inquiry is selected
|
||||
if (isMobile && selectedId) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
<Box sx={{ px: 1.5, py: 1, borderBottom: '1px solid #e2e8f0', bgcolor: 'white', flexShrink: 0 }}>
|
||||
<IconButton size="small" onClick={() => setSelectedId(null)}>
|
||||
<ArrowLeft size={18} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, overflow: 'hidden' }}>
|
||||
<InquiryDetailPanel inquiryId={selectedId} />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', height: '100%', overflow: 'hidden' }}>
|
||||
{/* Left column: list/grid */}
|
||||
<Box
|
||||
sx={{
|
||||
width: view === 'list' ? 380 : 720,
|
||||
minWidth: view === 'list' ? 380 : 480,
|
||||
width: isMobile ? '100%' : (view === 'list' ? 380 : 720),
|
||||
minWidth: isMobile ? 'unset' : (view === 'list' ? 380 : 480),
|
||||
flexShrink: 0,
|
||||
borderRight: '1px solid #e2e8f0',
|
||||
borderRight: isMobile ? 'none' : '1px solid #e2e8f0',
|
||||
bgcolor: 'white',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
@@ -75,44 +93,18 @@ export function ActiveInquiriesTab() {
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: '#0f172a' }}>
|
||||
Anfragen
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
px: 1,
|
||||
py: 0.125,
|
||||
borderRadius: 1,
|
||||
bgcolor: '#1e3a5f',
|
||||
color: 'white',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.7rem',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ px: 1, py: 0.125, borderRadius: 1, bgcolor: '#1e3a5f', color: 'white', fontWeight: 600, fontSize: '0.7rem' }}>
|
||||
{inquiries.length}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box sx={{ display: 'flex', gap: 0.5 }}>
|
||||
<Tooltip title="Listenansicht">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setView('list')}
|
||||
sx={{
|
||||
bgcolor: view === 'list' ? '#e0e7ff' : 'transparent',
|
||||
color: view === 'list' ? '#1e3a5f' : '#64748b',
|
||||
'&:hover': { bgcolor: view === 'list' ? '#c7d2fe' : '#f1f5f9' },
|
||||
}}
|
||||
>
|
||||
<IconButton size="small" onClick={() => setView('list')} sx={{ bgcolor: view === 'list' ? '#e0e7ff' : 'transparent', color: view === 'list' ? '#1e3a5f' : '#64748b' }}>
|
||||
<ListIcon size={16} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Kartenansicht">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => setView('grid')}
|
||||
sx={{
|
||||
bgcolor: view === 'grid' ? '#e0e7ff' : 'transparent',
|
||||
color: view === 'grid' ? '#1e3a5f' : '#64748b',
|
||||
'&:hover': { bgcolor: view === 'grid' ? '#c7d2fe' : '#f1f5f9' },
|
||||
}}
|
||||
>
|
||||
<IconButton size="small" onClick={() => setView('grid')} sx={{ bgcolor: view === 'grid' ? '#e0e7ff' : 'transparent', color: view === 'grid' ? '#1e3a5f' : '#64748b' }}>
|
||||
<LayoutGrid size={16} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -120,32 +112,26 @@ export function ActiveInquiriesTab() {
|
||||
</Box>
|
||||
|
||||
{view === 'list' ? (
|
||||
<InquiryList
|
||||
inquiries={inquiries}
|
||||
selectedId={selectedId}
|
||||
onSelect={setSelectedId}
|
||||
/>
|
||||
<InquiryList inquiries={inquiries} selectedId={selectedId} onSelect={setSelectedId} />
|
||||
) : (
|
||||
<InquiryCardGrid
|
||||
inquiries={inquiries}
|
||||
selectedId={selectedId}
|
||||
onSelect={setSelectedId}
|
||||
/>
|
||||
<InquiryCardGrid inquiries={inquiries} selectedId={selectedId} onSelect={setSelectedId} />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Right column: detail */}
|
||||
<Box sx={{ flex: 1, overflow: 'hidden', bgcolor: '#f8fafc' }}>
|
||||
{selectedId ? (
|
||||
<InquiryDetailPanel inquiryId={selectedId} />
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={<Inbox size={40} />}
|
||||
title="Anfrage auswählen"
|
||||
description="Wählen Sie eine Anfrage aus der Liste, um Details anzuzeigen und zu antworten."
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
{/* Right column: detail (desktop only) */}
|
||||
{!isMobile && (
|
||||
<Box sx={{ flex: 1, overflow: 'hidden', bgcolor: '#f8fafc' }}>
|
||||
{selectedId ? (
|
||||
<InquiryDetailPanel inquiryId={selectedId} />
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={<Inbox size={40} />}
|
||||
title="Anfrage auswählen"
|
||||
description="Wählen Sie eine Anfrage aus der Liste, um Details anzuzeigen und zu antworten."
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -109,9 +109,9 @@ export function InquiryDetailPanel({ inquiryId }: InquiryDetailPanelProps) {
|
||||
</Select>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
||||
<Box sx={{ flex: 1, display: 'flex', flexDirection: { xs: 'column', md: 'row' }, overflow: 'hidden' }}>
|
||||
<InquiryChat inquiry={inquiry} />
|
||||
<Box sx={{ width: 300, flexShrink: 0 }}>
|
||||
<Box sx={{ width: { xs: '100%', md: 300 }, flexShrink: 0, borderTop: { xs: '1px solid #e2e8f0', md: 'none' }, maxHeight: { xs: 280, md: 'none' }, overflowY: 'auto' }}>
|
||||
<RelatedPropertyCardPanel propertyId={inquiry.propertyId} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -1,41 +1,117 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Box } from '@mui/material'
|
||||
import { Sparkles } from 'lucide-react'
|
||||
import { Box, IconButton, Typography, useMediaQuery, useTheme } from '@mui/material'
|
||||
import { ArrowLeft, Sparkles } from 'lucide-react'
|
||||
import { usePublicNeeds, useLatentNeedById } from '../../hooks/useLatentNeeds'
|
||||
import { EmptyState } from '../ui'
|
||||
import { PublicNeedList } from './PublicNeedList'
|
||||
import { PublicNeedDetail } from './PublicNeedDetail'
|
||||
import { OwnPropertyMatchList } from './OwnPropertyMatchList'
|
||||
|
||||
type MobileView = 'list' | 'detail' | 'properties'
|
||||
|
||||
export function LatentInquiriesTab() {
|
||||
const { data: needs = [] } = usePublicNeeds()
|
||||
const [selectedNeedId, setSelectedNeedId] = useState<string | null>(null)
|
||||
const { data: selectedNeed } = useLatentNeedById(selectedNeedId)
|
||||
const theme = useTheme()
|
||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'))
|
||||
const isTablet = useMediaQuery(theme.breakpoints.between('md', 'lg'))
|
||||
const [mobileView, setMobileView] = useState<MobileView>('list')
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedNeedId && needs.length > 0) {
|
||||
if (!isMobile && !selectedNeedId && needs.length > 0) {
|
||||
setSelectedNeedId(needs[0].id)
|
||||
}
|
||||
}, [needs, selectedNeedId])
|
||||
}, [needs, selectedNeedId, isMobile])
|
||||
|
||||
const handleSelectNeed = (id: string) => {
|
||||
setSelectedNeedId(id)
|
||||
if (isMobile) setMobileView('detail')
|
||||
}
|
||||
|
||||
// Mobile: sequential single-panel navigation
|
||||
if (isMobile) {
|
||||
if (mobileView === 'detail' && selectedNeed) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
<Box sx={{ px: 1.5, py: 1, borderBottom: '1px solid #e2e8f0', bgcolor: 'white', flexShrink: 0, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton size="small" onClick={() => setMobileView('list')}>
|
||||
<ArrowLeft size={18} />
|
||||
</IconButton>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, fontSize: '0.85rem' }} noWrap>
|
||||
{selectedNeed.title}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
|
||||
<PublicNeedDetail need={selectedNeed} />
|
||||
<Box sx={{ px: 2, py: 1.5, borderTop: '1px solid #e2e8f0', flexShrink: 0 }}>
|
||||
<Box
|
||||
onClick={() => setMobileView('properties')}
|
||||
sx={{ p: 1.25, bgcolor: '#1e3a5f', color: 'white', borderRadius: 1, textAlign: 'center', cursor: 'pointer', fontSize: '0.85rem', fontWeight: 600 }}
|
||||
>
|
||||
Eigene Objekte anzeigen →
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
if (mobileView === 'properties' && selectedNeed) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
|
||||
<Box sx={{ px: 1.5, py: 1, borderBottom: '1px solid #e2e8f0', bgcolor: 'white', flexShrink: 0, display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<IconButton size="small" onClick={() => setMobileView('detail')}>
|
||||
<ArrowLeft size={18} />
|
||||
</IconButton>
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, fontSize: '0.85rem' }}>
|
||||
Eigene Objekte
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box sx={{ flex: 1, overflow: 'hidden' }}>
|
||||
<OwnPropertyMatchList need={selectedNeed} />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
// list view
|
||||
return (
|
||||
<Box sx={{ height: '100%', overflow: 'hidden' }}>
|
||||
<PublicNeedList selectedNeedId={selectedNeedId} onSelect={handleSelectNeed} fullWidth />
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Tablet: hide own-property column, show it as button in detail
|
||||
if (isTablet) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', height: '100%', overflow: 'hidden' }}>
|
||||
<PublicNeedList selectedNeedId={selectedNeedId} onSelect={setSelectedNeedId} />
|
||||
<Box sx={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
||||
{selectedNeed ? (
|
||||
<PublicNeedDetail need={selectedNeed} />
|
||||
) : (
|
||||
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<EmptyState icon={<Sparkles size={40} />} title="Bedarf auswählen" description="Wählen Sie einen latenten Bedarf, um Details zu sehen." />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// Desktop: full 3-column layout
|
||||
return (
|
||||
<Box sx={{ display: 'flex', height: '100%', overflow: 'hidden' }}>
|
||||
<PublicNeedList selectedNeedId={selectedNeedId} onSelect={setSelectedNeedId} />
|
||||
|
||||
<Box sx={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
|
||||
{selectedNeed ? (
|
||||
<PublicNeedDetail need={selectedNeed} />
|
||||
) : (
|
||||
<Box sx={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<EmptyState
|
||||
icon={<Sparkles size={40} />}
|
||||
title="Bedarf auswählen"
|
||||
description="Wählen Sie einen latenten Bedarf, um Details und passende Objekte zu sehen."
|
||||
/>
|
||||
<EmptyState icon={<Sparkles size={40} />} title="Bedarf auswählen" description="Wählen Sie einen latenten Bedarf, um Details und passende Objekte zu sehen." />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{selectedNeed && <OwnPropertyMatchList need={selectedNeed} />}
|
||||
</Box>
|
||||
)
|
||||
|
||||
@@ -7,18 +7,19 @@ import { EmptyState } from '../ui'
|
||||
interface PublicNeedListProps {
|
||||
selectedNeedId: string | null
|
||||
onSelect: (id: string) => void
|
||||
fullWidth?: boolean
|
||||
}
|
||||
|
||||
export function PublicNeedList({ selectedNeedId, onSelect }: PublicNeedListProps) {
|
||||
export function PublicNeedList({ selectedNeedId, onSelect, fullWidth }: PublicNeedListProps) {
|
||||
const { data: needs = [], isLoading, error } = usePublicNeeds()
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: 280,
|
||||
minWidth: 280,
|
||||
width: fullWidth ? '100%' : 280,
|
||||
minWidth: fullWidth ? 'unset' : 280,
|
||||
flexShrink: 0,
|
||||
borderRight: '1px solid #e2e8f0',
|
||||
borderRight: fullWidth ? 'none' : '1px solid #e2e8f0',
|
||||
bgcolor: 'white',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
|
||||
Reference in New Issue
Block a user