From a75818b983361705355fed5d3ea61eb779793589 Mon Sep 17 00:00:00 2001 From: Benjamin Sutter Date: Mon, 18 May 2026 21:24:00 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20full=20responsive=20layout=20=E2=80=94?= =?UTF-8?q?=20mobile=20sidebar,=20stacked=20panels,=20fullscreen=20drawers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AppShell: permanent sidebar on desktop, temporary drawer + hamburger on mobile ( --- .../anfragencenter/ActiveInquiriesTab.tsx | 104 ++++++++---------- .../anfragencenter/InquiryDetailPanel.tsx | 4 +- .../anfragencenter/LatentInquiriesTab.tsx | 98 +++++++++++++++-- .../anfragencenter/PublicNeedList.tsx | 9 +- src/components/layout/AppShell.tsx | 87 +++++++++++---- src/pages/demand/MatchDetail.tsx | 17 +-- src/pages/supply/Properties.tsx | 6 +- 7 files changed, 220 insertions(+), 105 deletions(-) diff --git a/src/components/anfragencenter/ActiveInquiriesTab.tsx b/src/components/anfragencenter/ActiveInquiriesTab.tsx index 7f6e9fd..dab929c 100644 --- a/src/components/anfragencenter/ActiveInquiriesTab.tsx +++ b/src/components/anfragencenter/ActiveInquiriesTab.tsx @@ -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(null) const [view, setView] = useState(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 ( + + + setSelectedId(null)}> + + + + + + + + ) + } + return ( {/* Left column: list/grid */} Anfragen - + {inquiries.length} - setView('list')} - sx={{ - bgcolor: view === 'list' ? '#e0e7ff' : 'transparent', - color: view === 'list' ? '#1e3a5f' : '#64748b', - '&:hover': { bgcolor: view === 'list' ? '#c7d2fe' : '#f1f5f9' }, - }} - > + setView('list')} sx={{ bgcolor: view === 'list' ? '#e0e7ff' : 'transparent', color: view === 'list' ? '#1e3a5f' : '#64748b' }}> - setView('grid')} - sx={{ - bgcolor: view === 'grid' ? '#e0e7ff' : 'transparent', - color: view === 'grid' ? '#1e3a5f' : '#64748b', - '&:hover': { bgcolor: view === 'grid' ? '#c7d2fe' : '#f1f5f9' }, - }} - > + setView('grid')} sx={{ bgcolor: view === 'grid' ? '#e0e7ff' : 'transparent', color: view === 'grid' ? '#1e3a5f' : '#64748b' }}> @@ -120,32 +112,26 @@ export function ActiveInquiriesTab() { {view === 'list' ? ( - + ) : ( - + )} - {/* Right column: detail */} - - {selectedId ? ( - - ) : ( - } - title="Anfrage auswählen" - description="Wählen Sie eine Anfrage aus der Liste, um Details anzuzeigen und zu antworten." - /> - )} - + {/* Right column: detail (desktop only) */} + {!isMobile && ( + + {selectedId ? ( + + ) : ( + } + title="Anfrage auswählen" + description="Wählen Sie eine Anfrage aus der Liste, um Details anzuzeigen und zu antworten." + /> + )} + + )} ) } diff --git a/src/components/anfragencenter/InquiryDetailPanel.tsx b/src/components/anfragencenter/InquiryDetailPanel.tsx index 8d70cc1..8157ce8 100644 --- a/src/components/anfragencenter/InquiryDetailPanel.tsx +++ b/src/components/anfragencenter/InquiryDetailPanel.tsx @@ -109,9 +109,9 @@ export function InquiryDetailPanel({ inquiryId }: InquiryDetailPanelProps) { - + - + diff --git a/src/components/anfragencenter/LatentInquiriesTab.tsx b/src/components/anfragencenter/LatentInquiriesTab.tsx index 0120305..c2e03f4 100644 --- a/src/components/anfragencenter/LatentInquiriesTab.tsx +++ b/src/components/anfragencenter/LatentInquiriesTab.tsx @@ -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(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('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 ( + + + setMobileView('list')}> + + + + {selectedNeed.title} + + + + + + setMobileView('properties')} + sx={{ p: 1.25, bgcolor: '#1e3a5f', color: 'white', borderRadius: 1, textAlign: 'center', cursor: 'pointer', fontSize: '0.85rem', fontWeight: 600 }} + > + Eigene Objekte anzeigen → + + + + + ) + } + if (mobileView === 'properties' && selectedNeed) { + return ( + + + setMobileView('detail')}> + + + + Eigene Objekte + + + + + + + ) + } + // list view + return ( + + + + ) + } + + // Tablet: hide own-property column, show it as button in detail + if (isTablet) { + return ( + + + + {selectedNeed ? ( + + ) : ( + + } title="Bedarf auswählen" description="Wählen Sie einen latenten Bedarf, um Details zu sehen." /> + + )} + + + ) + } + + // Desktop: full 3-column layout return ( - {selectedNeed ? ( ) : ( - } - title="Bedarf auswählen" - description="Wählen Sie einen latenten Bedarf, um Details und passende Objekte zu sehen." - /> + } title="Bedarf auswählen" description="Wählen Sie einen latenten Bedarf, um Details und passende Objekte zu sehen." /> )} - {selectedNeed && } ) diff --git a/src/components/anfragencenter/PublicNeedList.tsx b/src/components/anfragencenter/PublicNeedList.tsx index 0d1dfc7..9103435 100644 --- a/src/components/anfragencenter/PublicNeedList.tsx +++ b/src/components/anfragencenter/PublicNeedList.tsx @@ -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 ( void onToggle: () => void + onClose?: () => void userName: string orgName: string } @@ -179,6 +184,7 @@ function Sidebar({ allowedWorkspaces, onWorkspaceClick, onToggle, + onClose, userName, orgName, }: SidebarProps) { @@ -260,7 +266,7 @@ function Sidebar({ const tabContent = ( onWorkspaceClick(ws)} + onClick={() => { onWorkspaceClick(ws); onClose?.() }} sx={{ display: 'flex', alignItems: 'center', @@ -313,6 +319,7 @@ function Sidebar({ onClose?.()} > {({ isActive }) => ( void + isMobile?: boolean } -function TopBar({ activeWorkspace, pathname }: TopBarProps) { +function TopBar({ activeWorkspace, pathname, onMenuClick, isMobile }: TopBarProps) { const config = WORKSPACE_CONFIG[activeWorkspace] const pageName = getPageNameFromPath(pathname) const openAssistant = useAssistantStore(s => s.open) @@ -452,11 +461,16 @@ function TopBar({ activeWorkspace, pathname }: TopBarProps) { display: 'flex', alignItems: 'center', justifyContent: 'space-between', - px: 3, + px: { xs: 1.5, sm: 3 }, }} > {/* Left side */} - + + {isMobile && ( + + + + )} {pageName} {/* Right side */} - - + + + + + + + @@ -505,6 +524,12 @@ export function AppShell() { const { currentUser } = useSessionStore() const navigate = useNavigate() const location = useLocation() + const theme = useTheme() + const isMobile = useMediaQuery(theme.breakpoints.down('md')) + const [mobileOpen, setMobileOpen] = useState(false) + + // Close mobile menu on route change + useEffect(() => { setMobileOpen(false) }, [location.pathname]) // Sync active workspace with URL useEffect(() => { @@ -523,20 +548,44 @@ export function AppShell() { const orgName = currentUser?.organizationName ?? '' const allowedWorkspaces = currentUser?.allowedWorkspaces ?? WORKSPACE_ORDER + const sidebarContent = ( + setMobileOpen(false) : toggleSidebar} + onClose={isMobile ? () => setMobileOpen(false) : undefined} + userName={userName} + orgName={orgName} + /> + ) + return ( - + {/* Desktop: permanent sidebar */} + {!isMobile && sidebarContent} + + {/* Mobile: temporary drawer */} + {isMobile && ( + setMobileOpen(false)} + variant="temporary" + ModalProps={{ keepMounted: true }} + slotProps={{ paper: { sx: { width: 240, bgcolor: 'transparent', boxShadow: 'none' } } }} + > + {sidebarContent} + + )} - + setMobileOpen(true)} + /> diff --git a/src/pages/demand/MatchDetail.tsx b/src/pages/demand/MatchDetail.tsx index 6e1c360..77c0991 100644 --- a/src/pages/demand/MatchDetail.tsx +++ b/src/pages/demand/MatchDetail.tsx @@ -207,8 +207,8 @@ export default function MatchDetail() { {/* Property header — white section below hero */} - - + + {/* Left: title + address + chips */} @@ -264,12 +264,13 @@ export default function MatchDetail() { {/* Key facts strip */} - + {keyFacts.map((fact, i) => ( @@ -284,7 +285,7 @@ export default function MatchDetail() { {/* Main content */} - + {/* Main column */} @@ -340,7 +341,7 @@ export default function MatchDetail() { {/* Sidebar */} - + (null) const [filters, setFilters] = useState({}) const [view, setView] = useState<'list' | 'grid'>(() => @@ -155,7 +157,7 @@ export default function Properties() { anchor="right" open={!!selectedId} onClose={() => setSelectedId(null)} - slotProps={{ paper: { sx: { width: 650, boxShadow: '-4px 0 24px rgba(0,0,0,0.10)' } } }} + slotProps={{ paper: { sx: { width: isMobile ? '100vw' : 650, boxShadow: '-4px 0 24px rgba(0,0,0,0.10)' } } }} > {selectedId && ( setSelectedId(null)} />