Nawigacja mobilna - dolny pasek i menu w szufladzie
Deploy Polska Lokalnie / deploy (push) Successful in 17s

Gorne menu na telefonie mialo poziomy pasek przewijania, a najwazniejsze
skroty nie mialy sie gdzie zmiescic.

- dolny pasek (<= 760 px): Start, Szukaj, Dodaj, Ulubione, Konto
- pasek dzialow (<= 1100 px) przenosi sie do szuflady pod przyciskiem menu
- w naglowku na telefonie zostaja wiadomosci, dzwonek i menu
- kafelki narzedzi schodza do dwoch i jednej kolumny
- karty hero wracaja do przeplywu, zamiast nachodzic na liste korzysci
- spis tresci stron prawnych nie rozpycha juz strony przez tabele

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-24 07:11:14 +02:00
parent c9c78b6db5
commit 09d4b2f32b
3 changed files with 561 additions and 49 deletions
+193 -18
View File
@@ -32,6 +32,17 @@ const plLogoDark = new URL('./assets/pl_logo_dark.png', import.meta.url).href;
// Podswietlenie aktywnej pozycji menu wyznacza NavLink na podstawie adresu.
const navLinkClass = ({ isActive }: { isActive: boolean }) => (isActive ? 'active' : '');
// Glowne menu serwisu. Jedno zrodlo dla paska na desktopie i dla szuflady na telefonie -
// dzieki temu nowy dzial nie zniknie z ktorejs wersji nawigacji.
const MAIN_NAV: { to: string; label: string; icon: string }[] = [
{ to: ROUTES.buy, label: 'Kupuję', icon: 'search' },
{ to: ROUTES.rent, label: 'Wynajmuję', icon: 'key' },
{ to: ROUTES.sell, label: 'Sprzedaję', icon: 'tag' },
{ to: ROUTES.valuation, label: 'Wycena mieszkania', icon: 'calculator' },
{ to: ROUTES.services, label: 'Firmy i usługi', icon: 'hammer' },
{ to: ROUTES.guides, label: 'Poradniki', icon: 'document' },
];
type MessageThreadListing = {
listingId: number;
title: string;
@@ -3319,6 +3330,7 @@ function App() {
</Routes>
</main>
<SiteFooter />
<MobileNav />
<CookieBanner />
</div>
);
@@ -23607,6 +23619,9 @@ function Header({
const notificationPopoverRef = useRef<HTMLDivElement | null>(null);
const [isAccountMenuOpen, setIsAccountMenuOpen] = useState(false);
const accountMenuRef = useRef<HTMLDivElement | null>(null);
// Szuflada z pelnym menu - na telefonach i tabletach poziomy pasek dzialow sie nie miesci.
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const location = useLocation();
// Dane powiadomień z centralnego providera. W dzwonku pokazujemy nieprzeczytane (znikają po kliknięciu).
const displayNotifications = toDisplayNotifications(serverNotifications);
@@ -23668,6 +23683,33 @@ function Header({
};
}, [isAccountMenuOpen]);
// Szuflada zamyka sie po przejsciu na inna trase i klawiszem Escape; przy otwartej
// blokujemy przewijanie strony pod spodem.
useEffect(() => {
setIsMobileMenuOpen(false);
}, [location.pathname, location.search]);
useEffect(() => {
if (!isMobileMenuOpen) {
return;
}
const previousOverflow = document.body.style.overflow;
document.body.style.overflow = 'hidden';
const onEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsMobileMenuOpen(false);
}
};
window.addEventListener('keydown', onEscape);
return () => {
document.body.style.overflow = previousOverflow;
window.removeEventListener('keydown', onEscape);
};
}, [isMobileMenuOpen]);
// Pozycje menu sa linkami; zamkniecie popovera to efekt uboczny kliknięcia, nie nawigacja.
const closeAccountMenu = () => setIsAccountMenuOpen(false);
@@ -23688,24 +23730,11 @@ function Header({
</div>
<nav className="nav" aria-label="Główna nawigacja">
<NavLink to={ROUTES.buy} className={navLinkClass}>
Kupuję
</NavLink>
<NavLink to={ROUTES.rent} className={navLinkClass}>
Wynajmuję
</NavLink>
<NavLink to={ROUTES.sell} className={navLinkClass}>
Sprzedaję
</NavLink>
<NavLink to={ROUTES.valuation} className={navLinkClass}>
Wycena mieszkania
</NavLink>
<NavLink to={ROUTES.services} className={navLinkClass}>
Firmy i usługi
</NavLink>
<NavLink to={ROUTES.guides} className={navLinkClass}>
Poradniki
</NavLink>
{MAIN_NAV.map((item) => (
<NavLink key={item.to} to={item.to} className={navLinkClass}>
{item.label}
</NavLink>
))}
</nav>
<div className="header-actions">
@@ -23837,11 +23866,157 @@ function Header({
<span>Dodaj ogłoszenie za darmo</span>
<small>0 zł • Bez prowizji</small>
</Link>
<button
className="header-burger"
type="button"
aria-label="Menu serwisu"
aria-expanded={isMobileMenuOpen}
onClick={() => setIsMobileMenuOpen(true)}
>
<Icon name="rows" />
</button>
</div>
{isMobileMenuOpen && (
<div className="mobile-menu-overlay" onClick={() => setIsMobileMenuOpen(false)}>
<nav
className="mobile-menu"
aria-label="Menu serwisu"
onClick={(event) => event.stopPropagation()}
>
<div className="mobile-menu-head">
<strong>Menu</strong>
<button type="button" aria-label="Zamknij" onClick={() => setIsMobileMenuOpen(false)}>×</button>
</div>
<div className="mobile-menu-group">
{MAIN_NAV.map((item) => (
<NavLink key={item.to} to={item.to} className={navLinkClass}>
<Icon name={item.icon} />
<span>{item.label}</span>
</NavLink>
))}
</div>
<div className="mobile-menu-group">
<h3>Narzędzia</h3>
<NavLink to={ROUTES.map} className={navLinkClass}>
<Icon name="pin" />
<span>Mapa ofert</span>
</NavLink>
<NavLink to={ROUTES.priceHistory} className={navLinkClass}>
<Icon name="chart" />
<span>Historia cen</span>
</NavLink>
<NavLink to={ROUTES.creditCalculator} className={navLinkClass}>
<Icon name="percent" />
<span>Kalkulator zdolności</span>
</NavLink>
</div>
<div className="mobile-menu-group">
<h3>Moje konto</h3>
{isLoggedIn ? (
<>
<NavLink to={ROUTES.account} end className={navLinkClass}>
<Icon name="house" />
<span>Panel główny</span>
</NavLink>
<NavLink to={ROUTES.accountListings} className={navLinkClass}>
<Icon name="document" />
<span>Moje oferty</span>
</NavLink>
<NavLink to={ROUTES.messages} className={navLinkClass}>
<Icon name="message" />
<span>Wiadomości</span>
{messagesUnreadCount > 0 && <b>{messagesUnreadCount > 99 ? '99+' : messagesUnreadCount}</b>}
</NavLink>
<NavLink to={ROUTES.notifications} className={navLinkClass}>
<Icon name="bell" />
<span>Powiadomienia</span>
{unreadCount > 0 && <b>{unreadCount > 99 ? '99+' : unreadCount}</b>}
</NavLink>
<NavLink to={ROUTES.accountSettings} className={navLinkClass}>
<Icon name="gear" />
<span>Dane i ustawienia</span>
</NavLink>
{isAdmin && (
<NavLink to={ROUTES.admin} className={navLinkClass}>
<Icon name="shield" />
<span>Panel admina</span>
</NavLink>
)}
<button
type="button"
className="mobile-menu-logout"
onClick={() => {
setIsMobileMenuOpen(false);
onLogout();
}}
>
<Icon name="lock" />
<span>Wyloguj</span>
</button>
</>
) : (
<>
<NavLink to={ROUTES.login} className={navLinkClass}>
<Icon name="user" />
<span>Zaloguj się</span>
</NavLink>
<NavLink to={ROUTES.register} className={navLinkClass}>
<Icon name="plus" />
<span>Załóż konto</span>
</NavLink>
</>
)}
</div>
<Link className="mobile-menu-cta" to={ROUTES.add}>
<Icon name="plus" />
<span>Dodaj ogłoszenie za darmo</span>
</Link>
</nav>
</div>
)}
</header>
);
}
// Dolny pasek nawigacji na telefonach - pieć najczesciej uzywanych miejsc w serwisie.
// Wyrozniony srodkowy przycisk prowadzi do dodawania ogloszenia (gosc trafi na logowanie).
function MobileNav() {
const { user } = useAuth();
return (
<nav className="mobile-nav" aria-label="Nawigacja mobilna">
<NavLink to={ROUTES.home} end className={navLinkClass}>
<Icon name="house" />
<span>Start</span>
</NavLink>
<NavLink to={ROUTES.buy} className={navLinkClass}>
<Icon name="search" />
<span>Szukaj</span>
</NavLink>
<Link className="mobile-nav-add" to={ROUTES.add} aria-label="Dodaj ogłoszenie">
<span className="mobile-nav-add-circle">
<Icon name="plus" />
</span>
<span>Dodaj</span>
</Link>
<NavLink to={ROUTES.favorites} className={navLinkClass}>
<Icon name="heart" />
<span>Ulubione</span>
</NavLink>
<NavLink to={user ? ROUTES.account : ROUTES.login} end className={navLinkClass}>
<Icon name="user" />
<span>{user ? 'Konto' : 'Zaloguj'}</span>
</NavLink>
</nav>
);
}
function NotificationsPage({ onOpenNotification }: { onOpenNotification: (notification: DisplayNotification) => void }) {
const [activeTab, setActiveTab] = useState<'all' | NotificationCategory>('all');
const { notifications: serverNotifications, unreadCount, markAllRead, clearAll } = useNotifications();