From 3e394998a31d8445f518ab4e69e08f4f2c1d6d66 Mon Sep 17 00:00:00 2001 From: pascal Date: Fri, 24 Jul 2026 00:37:04 +0200 Subject: [PATCH] Strona /firmy, CTA jako linki i testy Playwright routingu Nowa trasa: - /firmy - lista firm i uslug na istniejacych komponentach (.directory-card) i stylistyce sekcji uslug; filtr kategorii w query stringu (/firmy?kategoria=remonty) - serviceCategories dostaly slug, nowa baza companyDirectory pokrywa wszystkie 8 kategorii, wiec filtrowanie dziala na realnych danych - "Zobacz firmy" prowadzi do listy z wybrana kategoria, "Zobacz wszystkie" do pelnej listy - ostatnie dwie atrapy href="#" usuniete CTA zamienione na (wylacznie czysta nawigacja, bez akcji): - 35 przyciskow onClick={() => navigate(x)} zamienionych automatycznie - 5 skrotow panelu admina (setTab) -> Link do /admin/* - karty narzedzi konta i kafelek poradnika: zamiast role="button" prawdziwy link - pozycje menu konta i popovera powiadomien -> Link + zamkniecie popovera - karty ofert: link rozciagniety na cala karte (.card-stretched-link) zamiast role="button", bez zagniezdzania przyciskow w - bez zmian zostaly przyciski wykonujace akcje: publikacja, zapis draftu, wyszukiwanie, oznaczanie powiadomien, bramki logowania Wyglad 1:1 - rozszerzone selektory kontenerow o warianty , reset :where(a) o zerowej specyficznosci oraz odtworzenie centrowania tam, gdzie - +
@@ -4311,7 +4349,7 @@ function AdminPage() {

Ostatnie ogłoszenia

- + Zobacz wszystkie
{recentListings.length === 0 ? (

Brak ogłoszeń do wyświetlenia.

@@ -4341,7 +4379,7 @@ function AdminPage() {

Aktywność użytkowników

- + Zobacz wszystkich
{recentUsers.length === 0 ? (

Brak użytkowników do wyświetlenia.

@@ -4367,10 +4405,10 @@ function AdminPage() {

Szybkie akcje

- - - - + Dodaj nowe ogłoszenie + Zarządzaj kategoriami + Zarządzaj lokalizacjami + Zarządzaj banerami
@@ -5414,7 +5452,7 @@ function NegotiationPage({

Nasi eksperci pomogą Ci przejść przez proces zakupu i wynegocjować najlepszą cenę.

- + Skontaktuj się z ekspertem Pierwsza konsultacja gratis
@@ -5825,7 +5863,7 @@ function AccountDashboardPage({

Moje wyszukiwania

- + Zobacz wszystkie
{savedSearches.slice(0, 4).map((row) => ( @@ -5852,7 +5890,7 @@ function AccountDashboardPage({ ))} {savedSearches.length === 0 &&

Nie masz jeszcze zapisanych wyszukiwań. Wejdź w Kup lub Wynajem i kliknij dzwonek „Zapisz wyszukiwanie”.

} - + Utwórz nowe wyszukiwanie
), @@ -5863,24 +5901,14 @@ function AccountDashboardPage({
{tools.map((tool) => ( -
navigate(tool.path)} - onKeyDown={(event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - navigate(tool.path); - } - }} - > + // Kafelek prowadzi na inna strone, wiec jest linkiem - bez protezy role="button". +
{tool.title} {tool.text}
-
+ ))}
@@ -6103,7 +6131,7 @@ function AccountSavedSearchesPage({

Zapisane wyszukiwania

Wróć do swoich filtrów jednym kliknięciem i przejdź od razu do odpowiedniej listy ofert.

- + Dodaj nowe
@@ -6199,9 +6227,9 @@ function AccountSavedSearchesPage({
Nie masz jeszcze zapisanych wyszukiwań

Ustaw filtry w Kup lub Wynajem i kliknij dzwonek „Zapisz wyszukiwanie”, a pojawią się tutaj.

- +
)} @@ -6834,7 +6862,7 @@ function AccountPriceAlertsPage({
)} - + Dodaj kolejne lokalizacje
@@ -6851,16 +6879,16 @@ function AccountPriceAlertsPage({ Usuń )) : ( - + )} - + Zarządzaj alertami
@@ -8913,7 +8941,7 @@ function AccountSettingsPage() { {getDisplayName(user)} Konto Premium - + Edytuj
Adres e-mail{user?.email}
@@ -9851,7 +9879,7 @@ function AccountProfileEditPage() { {coverPhotoName && (
- + Podgląd profilu
)} @@ -9905,9 +9933,9 @@ function AccountProfileEditPage() { {profileVisibility === 'publiczna' && 'Zobacz jak inni widzą Twój profil.'} {profileVisibility === 'kontakty' && 'Profil widzą tylko zaakceptowane kontakty.'} - +
@@ -10191,7 +10219,7 @@ function AccountPublicProfilePage() {

Jak inni widzą Twój profil

Zobacz, jak Twój profil prezentuje się publicznie dla innych użytkowników Mieszko.

- + Edytuj profil
@@ -10955,10 +10983,10 @@ function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (ci
- - + + Wynajem
@@ -10979,7 +11007,7 @@ function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (ci

{card.title}

{card.text}

- + {card.action} ))} @@ -12372,13 +12400,13 @@ function AddListingPage() { znajdziesz je w „Moich ogłoszeniach” ze statusem „Oczekuje”.

- - +
) : ( @@ -12728,10 +12756,10 @@ function GuideBuyingPage() {
- + Kupno mieszkania @@ -13163,10 +13191,10 @@ function GuideBuyingPage() {

Gotowy, by znaleźć swoje mieszkanie?

Przeglądaj aktualne oferty dopasowane do Twoich potrzeb i budżetu.

- +
@@ -13174,22 +13202,22 @@ function GuideBuyingPage() {

Kalkulatory

- - - - +
@@ -13261,30 +13289,25 @@ function GuidesPage() {
{guideTopics.map((topic) => { const isBuying = topic.title === 'Kupno mieszkania'; - return ( -
navigate(ROUTES.guideBuying) : undefined} - onKeyDown={ - isBuying - ? (event) => { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - navigate(ROUTES.guideBuying); - } - } - : undefined - } - > + // Klikalny kafelek to link do poradnika; pozostale nie maja jeszcze tresci. + const content = ( + <>

{topic.title}

{topic.text}

{topic.count} + + ); + + return isBuying ? ( + + {content} + + ) : ( +
+ {content}
); })} @@ -13418,7 +13441,7 @@ function SellPage() { Pełne wsparcie
- + Dodaj ogłoszenie za darmo Zajmie Ci to tylko 2 minuty @@ -13462,7 +13485,7 @@ function SellPage() {

Sprawdź wartość swojego mieszkania

Bezpłatna wycena online w 30 sekund

Podaj adres mieszkania
- + Wyceń mieszkanie
Przykładowa wycena Kraków, Zabłocie @@ -13545,7 +13568,7 @@ function SellPage() { Gotowy na szybką i bezpieczną sprzedaż? Dołącz do tysięcy osób, które już sprzedały z nami swoje mieszkanie.
- + Dodaj ogłoszenie za darmo Zajmie Ci to tylko 2 minuty
@@ -15123,7 +15146,7 @@ function ValuationPage() {
- + Rozpocznij sprzedaż
@@ -15454,6 +15477,121 @@ function ValuationPage() { ); } +// Lista firm i usług pod /firmy. Kategoria pochodzi z query stringa (?kategoria=remonty), +// więc widok da się skopiować, odświeżyć i otworzyć w nowej karcie. +function CompaniesPage() { + const [searchParams, setSearchParams] = useSearchParams(); + const categoryParam = searchParams.get('kategoria'); + const activeCategory = serviceCategories.find((item) => item.slug === categoryParam) ?? null; + + const companies = activeCategory + ? companyDirectory.filter((company) => company.category === activeCategory.slug) + : companyDirectory; + + // Zmiana filtra to zmiana adresu - replace, zeby nie zasmiecac historii przy przelaczaniu. + const selectCategory = (slug: string | null) => { + setSearchParams(slug ? { kategoria: slug } : {}, { replace: true }); + }; + + return ( +
+
+
+
+ Strona główna + + Firmy i usługi + + {activeCategory ? activeCategory.title : 'Wszystkie firmy'} +
+ +
+

{activeCategory ? activeCategory.title : 'Wszystkie firmy i usługi'}

+

+ {activeCategory + ? activeCategory.text + : 'Sprawdzone firmy i specjaliści z całej Polski. Porównaj oferty, oceny i opinie klientów.'} +

+ + {companies.length === 1 ? '1 firma' : `${companies.length} firm`} + {activeCategory ? ` w kategorii „${activeCategory.title}”` : ' w bazie'} + +
+ + + +
+
+ + + + +
+ +
+ {companies.map((company) => ( +
+
+
+

{company.title}

+

{company.rating}

+ {company.text} + {company.city} + Zweryfikowana firma +
+ + +
+ ))} +
+
+ +
+

Jak to działa?

+
+ {serviceSteps.map((step) => ( +
+ + + +
+

{step.title}

+

{step.text}

+
+
+ ))} +
+
+
+
+
+ ); +} + function ServicesPage() { return (
@@ -15491,7 +15629,7 @@ function ServicesPage() {

{category.title}

{category.text}

-
Zobacz firmy + Zobacz firmy ))} @@ -15500,7 +15638,7 @@ function ServicesPage() {

Polecane firmy

- Zobacz wszystkie + Zobacz wszystkie
{serviceCompanies.map((company) => ( @@ -17137,14 +17275,10 @@ function RealListingsBand({ item.yearBuilt ? `${item.yearBuilt} r.` : null, ].filter(Boolean).join(' • '); return ( -
onOpenListing(item.id)} - onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); onOpenListing(item.id); } }} - > + // Karta prowadzi do stron oferty. Tytul jest linkiem rozciagnietym na cala + // karte (::after), dzieki czemu mamy prawdziwy - do skopiowania i + // otwarcia w nowej karcie - bez zagniezdzania przyciskow w . +
{offerType === 'SALE' && (
@@ -17166,7 +17300,7 @@ function RealListingsBand({
-

{item.title}

+

{item.title}

{[item.city, item.district].filter(Boolean).join(', ')}

{meta}
@@ -17174,9 +17308,9 @@ function RealListingsBand({
{formatPln(item.price)}{item.offerType === 'RENT' ? ' /mc' : ''} {listingRelativeDate(item.createdAt)} - +
); @@ -18353,10 +18487,10 @@ function MapSearchPage({ initialCity, offerType }: { initialCity: string; offerT return (
- +
@@ -18558,10 +18692,8 @@ function Header({ }; }, [isAccountMenuOpen]); - const goToAccountSection = (targetPath: string) => { - setIsAccountMenuOpen(false); - navigate(targetPath); - }; + // Pozycje menu sa linkami; zamkniecie popovera to efekt uboczny kliknięcia, nie nawigacja. + const closeAccountMenu = () => setIsAccountMenuOpen(false); return (
@@ -18627,15 +18759,9 @@ function Header({

Powiadomienia

- +
@@ -18697,15 +18823,15 @@ function Header({ {isAccountMenuOpen && (
- - - + + {activeTab === 'buy' ? 'Przejdź do ofert Kupuję' : 'Przejdź do ofert Wynajmuję'}
)} @@ -20885,9 +21011,9 @@ function ListingDetailPage({

{error ?? 'Nie znaleziono ogłoszenia.'}

- +
); @@ -21158,11 +21284,11 @@ function ListingDetailPage({ return (
- + Panel główny - + {listing.title}
@@ -21349,9 +21475,9 @@ function ListingDetailPage({
- +
Udostępnij ofertę diff --git a/frontend/src/auth.tsx b/frontend/src/auth.tsx index 0c26e83..ae143eb 100644 --- a/frontend/src/auth.tsx +++ b/frontend/src/auth.tsx @@ -104,21 +104,42 @@ export function AuthProvider({ children }: { children: ReactNode }) { } let cancelled = false; setLoading(true); - apiFetch('/auth/me') - .then((me) => { + + // Sesje konczymy wylacznie wtedy, gdy backend odrzuci token (401/403). + // Chwilowy blad sieci albo 5xx nie moze wylogowywac uzytkownika - taki blad + // ponawiamy raz, a token zostaje, wiec kolejne wejscie na strone go odzyska. + const loadProfile = async (canRetry: boolean): Promise => { + try { + const me = await apiFetch('/auth/me'); if (!cancelled) { setUser(me); setLoading(false); } - }) - .catch(() => { - if (!cancelled) { + } catch (error) { + if (cancelled) { + return; + } + const status = (error as { status?: number }).status; + if (status === 401 || status === 403) { window.localStorage.removeItem(TOKEN_KEY); setToken(null); setUser(null); setLoading(false); + return; } - }); + if (canRetry) { + await new Promise((resolve) => { window.setTimeout(resolve, 400); }); + if (!cancelled) { + await loadProfile(false); + } + return; + } + setUser(null); + setLoading(false); + } + }; + + void loadProfile(true); return () => { cancelled = true; }; diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index f5a4356..bfe8a61 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -12,6 +12,7 @@ export const ROUTES = { comparison: '/konto/porownanie', add: '/dodaj-ogloszenie', services: '/firmy-i-uslugi', + companies: '/firmy', guides: '/poradniki', guideBuying: '/poradniki/kupno-mieszkania', creditCalculator: '/kalkulator-zdolnosci', @@ -59,6 +60,12 @@ export function mapPath(city?: string, offerType?: 'SALE' | 'RENT'): string { return query ? `${ROUTES.map}?${query}` : ROUTES.map; } +// Lista firm i usług: kategoria w query stringu, dzięki czemu widok da się +// skopiować, odświeżyć i otworzyć w nowej karcie. +export function companiesPath(categorySlug?: string): string { + return categorySlug ? `${ROUTES.companies}?kategoria=${encodeURIComponent(categorySlug)}` : ROUTES.companies; +} + // Zakładki panelu administratora jako podścieżki /admin/*. export const ADMIN_TAB_PATHS = { dashboard: '', diff --git a/frontend/src/styles.css b/frontend/src/styles.css index df27c4b..2e5bfcd 100644 --- a/frontend/src/styles.css +++ b/frontend/src/styles.css @@ -589,7 +589,7 @@ svg { width: max-content; } -.search-tabs button { +.search-tabs button, .search-tabs a { background: rgba(255, 255, 255, 0.88); border: 0; color: #657284; @@ -600,11 +600,11 @@ svg { padding: 10px 20px; } -.search-tabs button:first-child { +.search-tabs button:first-child, .search-tabs a:first-child { border-radius: 6px 0 0 0; } -.search-tabs button:last-child { +.search-tabs button:last-child, .search-tabs a:last-child { border-radius: 0 6px 0 0; } @@ -798,7 +798,7 @@ svg { font-size: 12px; } -.tool-card button { +.tool-card button, .tool-card a { align-self: end; background: #f8fafc; border: 1px solid #e2e8ef; @@ -3183,7 +3183,7 @@ svg { margin-top: 6px; } -.result-side button { +.result-side button, .result-side a { align-self: end; background: #0b9f5c; border: 0; @@ -5922,7 +5922,7 @@ svg { padding: 15px 16px; } -.sell-valuation-card button { +.sell-valuation-card button, .sell-valuation-card a { background: #0b9f5c; border: 0; border-radius: 6px; @@ -8331,7 +8331,7 @@ svg { margin-top: 3px; } -.sell-bottom-cta button { +.sell-bottom-cta button, .sell-bottom-cta a { background: #0b9f5c; border: 0; border-radius: 6px; @@ -11417,7 +11417,7 @@ svg { width: 100%; } - .search-tabs button { + .search-tabs button, .search-tabs a { flex: 1; min-width: 0; padding-left: 10px; @@ -18271,7 +18271,7 @@ svg { gap: 8px; } -.price-alerts-active-items button { +.price-alerts-active-items button, .price-alerts-active-items a { align-items: center; background: #fbfcfe; border: 1px solid #e2eaf3; @@ -18405,7 +18405,7 @@ svg { } .settings-card-head.split > button, -.settings-profile-row > button { +.settings-profile-row > button, .settings-profile-row > a { background: #ffffff; border: 1px solid #d9e2ec; border-radius: 6px; @@ -20757,7 +20757,7 @@ svg { gap: 12px; } -.public-profile-head > button { +.public-profile-head > button, .public-profile-head > a { align-items: center; background: #ffffff; border: 1px solid #d9e2ec; @@ -20771,7 +20771,7 @@ svg { padding: 0 14px; } -.public-profile-head > button svg { +.public-profile-head > button svg, .public-profile-head > a svg { height: 14px; width: 14px; } @@ -21559,7 +21559,7 @@ svg { margin: 0; } -.admin-card-head button { +.admin-card-head button, .admin-card-head a { background: transparent; border: 0; color: #159e5d; @@ -21636,7 +21636,7 @@ svg { padding: 12px 18px 18px; } -.admin-quick-actions-grid button { +.admin-quick-actions-grid button, .admin-quick-actions-grid a { align-items: center; background: #f8fbff; border: 1px solid #e0e8f2; @@ -21650,11 +21650,11 @@ svg { padding: 0 12px; } -.admin-quick-actions-grid button svg:first-child { +.admin-quick-actions-grid button svg:first-child, .admin-quick-actions-grid a svg:first-child { color: #109253; } -.admin-quick-actions-grid button svg:last-child { +.admin-quick-actions-grid button svg:last-child, .admin-quick-actions-grid a svg:last-child { margin-left: auto; opacity: 0.6; } @@ -22853,7 +22853,7 @@ svg { } .listing-detail-back, -.listing-detail-crumbs button { +.listing-detail-crumbs button, .listing-detail-crumbs a { align-items: center; background: none; border: none; @@ -26789,3 +26789,151 @@ svg { /* Routing: naglowkowe CTA jako - zachowanie wygladu przyciskow */ .post-button { align-content: center; text-decoration: none; } + +/* Lista firm i uslug (/firmy) - korzysta z istniejacych kart .directory-card */ +.companies-frame { + display: block; + margin: 0 auto; + max-width: 1420px; + padding: 0 96px; +} + +.companies-frame .services-main { padding-right: 0; } + +.companies-head { margin: 18px 0 20px; } + +.companies-head h1 { + color: #111a2e; + font-size: 30px; + font-weight: 900; + line-height: 1.15; + margin: 0 0 8px; +} + +.companies-head p { + color: #5b6472; + font-size: 15px; + margin: 0 0 10px; + max-width: 720px; +} + +.companies-count { + color: #109253; + font-size: 13px; + font-weight: 800; +} + +.companies-filters { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 20px; +} + +.companies-chip { + align-items: center; + background: #ffffff; + border: 1px solid #dce4ec; + border-radius: 999px; + color: #27384f; + cursor: pointer; + display: inline-flex; + font-size: 13px; + font-weight: 800; + gap: 7px; + padding: 8px 14px; +} + +.companies-chip svg { color: #7d8ca0; font-size: 14px; } + +.companies-chip span { + background: #eef3f8; + border-radius: 999px; + color: #5b6472; + font-size: 11px; + font-weight: 900; + padding: 1px 7px; +} + +.companies-chip:hover { border-color: #b9c6d4; } + +.companies-chip.active { + background: #ebf6f0; + border-color: #13a360; + color: #109253; +} + +.companies-chip.active svg { color: #109253; } +.companies-chip.active span { background: #d7efe2; color: #0d7a45; } + +.companies-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } + +@media (max-width: 1280px) { + .companies-frame { padding: 0 24px; } + .companies-list { grid-template-columns: minmax(0, 1fr); } +} + +@media (max-width: 720px) { + .companies-head h1 { font-size: 24px; } + .companies-frame { padding: 0 14px; } +} + +/* Routing: CTA nawigacyjne renderuja sie jako (Link). :where() ma zerowa + specyficznosc, wiec kazda istniejaca regula klasy nadal wygrywa - to tylko + zdejmuje domyslne podkreslenie i niebieski kolor linku. */ +:where(a) { + color: inherit; + text-decoration: none; +} + +/* Routing: CTA o ukladzie blokowym odtwarzaja centrowanie tresci znane z