diff --git a/AGENTS.md b/AGENTS.md index 299611b..ca1e47f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,21 +53,21 @@ Zasady: - **Nie buduj nawigacji na stanie.** Zakazane jako mechanizm przełączania stron: `useState('view')`, `setView`, `activeView`, `selectedPage`, `selectedTab` i pochodne. - **Nie przełączaj całych stron warunkowym renderowaniem** bez zmiany adresu (`{view === 'x' && }`). - **Do nawigacji używaj** `Link`, `NavLink`, `useNavigate` oraz tras zadeklarowanych w `Routes`. -- **Identyfikatory obiektów trzymaj w parametrach URL**: `/oferta/:id`, `/profil/:id`, `/wiadomosci/:conversationId` — nie w stanie Reacta. +- **Identyfikatory obiektów trzymaj w parametrach URL**: `/oferta/:id`, `/profil/:id`, `/wiadomosci/:conversationId` - nie w stanie Reacta. - **Filtry, sortowanie, wyszukiwanie i paginację zapisuj w query string** wszędzie tam, gdzie użytkownik powinien móc skopiować lub odświeżyć widok. - **Zakładki paneli twórz jako trasy zagnieżdżone**: `/konto/ustawienia`, `/admin/uzytkownicy`. -- **Trasy wymagające logowania zabezpieczaj `ProtectedRoute`** (`frontend/src/ProtectedRoute.tsx`). Pamiętaj, że to zabezpieczenie interfejsu — autoryzację egzekwuje backend. +- **Trasy wymagające logowania zabezpieczaj `ProtectedRoute`** (`frontend/src/ProtectedRoute.tsx`). Pamiętaj, że to zabezpieczenie interfejsu - autoryzację egzekwuje backend. - **Nie używaj `href="#"`** ani przycisków imitujących linki. - **Każdy link musi działać** po odświeżeniu (F5), przy bezpośrednim wejściu z adresu, po użyciu Wstecz/Dalej i po otwarciu w nowej karcie. Gdzie co leży: -- `frontend/src/routes.ts` — centralna mapa adresów (`ROUTES`) i helpery (`listingPath`, `mapPath`, `adminTabPath`). Nowy widok zaczynaj od wpisu tutaj, nie od literału ścieżki w JSX. -- `frontend/src/ProtectedRoute.tsx` — bramka autoryzacji, obsługuje stan `loading` z `AuthProvider`. -- `frontend/src/App.tsx` — deklaracja ``. -- `nginx/default.conf` — `try_files $uri $uri/ /index.html` (SPA fallback, już skonfigurowany). +- `frontend/src/routes.ts` - centralna mapa adresów (`ROUTES`) i helpery (`listingPath`, `mapPath`, `adminTabPath`). Nowy widok zaczynaj od wpisu tutaj, nie od literału ścieżki w JSX. +- `frontend/src/ProtectedRoute.tsx` - bramka autoryzacji, obsługuje stan `loading` z `AuthProvider`. +- `frontend/src/App.tsx` - deklaracja ``. +- `nginx/default.conf` - `try_files $uri $uri/ /index.html` (SPA fallback, już skonfigurowany). -Przed zakończeniem każdego zadania sprawdź, czy dodane widoki mają trasy i czy adres zmienia się podczas nawigacji. Jeżeli istniejący kod łamie ten standard, nie powielaj starego rozwiązania — dostosuj go do React Routera. +Przed zakończeniem każdego zadania sprawdź, czy dodane widoki mają trasy i czy adres zmienia się podczas nawigacji. Jeżeli istniejący kod łamie ten standard, nie powielaj starego rozwiązania - dostosuj go do React Routera. ## Behavioral Guidelines To Reduce Common LLM Coding Mistakes diff --git a/backend/src/main/java/pl/polskalokalnie/config/DataSeeder.java b/backend/src/main/java/pl/polskalokalnie/config/DataSeeder.java index 4a9b4e9..5eabbf3 100644 --- a/backend/src/main/java/pl/polskalokalnie/config/DataSeeder.java +++ b/backend/src/main/java/pl/polskalokalnie/config/DataSeeder.java @@ -74,7 +74,7 @@ public class DataSeeder implements CommandLineRunner { OfferType.SALE, PropertyType.HOUSE, "Kraków", "ul. Podgórska 8", new BigDecimal("1290000"), 145.0, 5, ListingStatus.PENDING)); listingRepository.save(demoListing( - "Kawalerka do wynajęcia — Wrocław", "Umeblowana kawalerka w centrum, dostępna od zaraz.", + "Kawalerka do wynajęcia - Wrocław", "Umeblowana kawalerka w centrum, dostępna od zaraz.", OfferType.RENT, PropertyType.APARTMENT, "Wrocław", "ul. Krupnicza 3", new BigDecimal("2600"), 30.0, 1, ListingStatus.PENDING)); } diff --git a/backend/src/main/java/pl/polskalokalnie/listing/ListingController.java b/backend/src/main/java/pl/polskalokalnie/listing/ListingController.java index 020ca46..3ba9602 100644 --- a/backend/src/main/java/pl/polskalokalnie/listing/ListingController.java +++ b/backend/src/main/java/pl/polskalokalnie/listing/ListingController.java @@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -58,6 +59,16 @@ public class ListingController { .body(response); } + // Edycja wlasnego ogloszenia (pelna aktualizacja pol; status bez zmian). + @PutMapping("/{id}") + public ListingDetailResponse update( + @PathVariable Long id, + @Valid @RequestBody ListingCreateRequest request, + Authentication authentication + ) { + return listingService.updateOwn(id, authentication.getName(), request); + } + // Wstrzymanie/wznowienie wlasnego ogloszenia (status=PAUSED lub APPROVED). @PatchMapping("/{id}/status") public ListingResponse updateStatus( diff --git a/backend/src/main/java/pl/polskalokalnie/listing/ListingService.java b/backend/src/main/java/pl/polskalokalnie/listing/ListingService.java index 25b58ee..84b094f 100644 --- a/backend/src/main/java/pl/polskalokalnie/listing/ListingService.java +++ b/backend/src/main/java/pl/polskalokalnie/listing/ListingService.java @@ -100,6 +100,33 @@ public class ListingService { request.district(), request.street(), request.contactName()); PropertyListing listing = new PropertyListing(); + applyRequest(listing, request); + listing.setOwnerEmail(ownerEmail); + listing.setStatus(ListingStatus.PENDING); + + return ListingDetailResponse.from(listingRepository.save(listing)); + } + + // Edycja wlasnego ogloszenia. Waliduje tresc filtrem, ale NIE zmienia statusu - + // zatwierdzone ogloszenie pozostaje widoczne od razu po zapisie zmian. + @Transactional + public ListingDetailResponse updateOwn(Long id, String ownerEmail, ListingCreateRequest request) { + PropertyListing listing = listingRepository.findById(id) + .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "Listing not found")); + requireOwner(listing, ownerEmail); + + textModerationService.validateOrThrow( + request.title(), request.description(), request.city(), request.address(), + request.district(), request.street(), request.contactName()); + + applyRequest(listing, request); + // ownerEmail i status pozostaja bez zmian. + + return ListingDetailResponse.from(listingRepository.save(listing)); + } + + // Wspolne mapowanie pol requestu na encje (uzywane przez create i updateOwn). + private void applyRequest(PropertyListing listing, ListingCreateRequest request) { listing.setTitle(request.title().trim()); listing.setDescription(request.description().trim()); listing.setOfferType(request.offerType()); @@ -140,11 +167,6 @@ public class ListingService { listing.setPhotos(photos); listing.setCoverPhoto(photos.isEmpty() ? null : photos.get(0)); listing.setVirtualTourUrl(normalizeVirtualTourUrl(request.virtualTourUrl())); - - listing.setOwnerEmail(ownerEmail); - listing.setStatus(ListingStatus.PENDING); - - return ListingDetailResponse.from(listingRepository.save(listing)); } // --- Moderacja (admin) --- diff --git a/backend/src/main/java/pl/polskalokalnie/listing/PropertyListing.java b/backend/src/main/java/pl/polskalokalnie/listing/PropertyListing.java index 5e5dc9f..317d843 100644 --- a/backend/src/main/java/pl/polskalokalnie/listing/PropertyListing.java +++ b/backend/src/main/java/pl/polskalokalnie/listing/PropertyListing.java @@ -29,7 +29,7 @@ public class PropertyListing { @Column(nullable = false, length = 160) private String title; - @Column(nullable = false, length = 3000) + @Column(nullable = false, columnDefinition = "text") private String description; @Enumerated(EnumType.STRING) diff --git a/backend/src/main/java/pl/polskalokalnie/mailconfig/MailConfigController.java b/backend/src/main/java/pl/polskalokalnie/mailconfig/MailConfigController.java index 7c85161..fe5220a 100644 --- a/backend/src/main/java/pl/polskalokalnie/mailconfig/MailConfigController.java +++ b/backend/src/main/java/pl/polskalokalnie/mailconfig/MailConfigController.java @@ -55,7 +55,7 @@ public class MailConfigController { SendResult result = emailSender.send( mailConfigService.getSmtp(), request.recipient().trim(), - "Test konfiguracji SMTP – Polska Lokalnie", + "Test konfiguracji SMTP - Polska Lokalnie", "

To jest wiadomość testowa potwierdzająca poprawną konfigurację poczty SMTP.

"); return TestResultResponse.from(result); } @@ -65,7 +65,7 @@ public class MailConfigController { SendResult result = smsSender.send( mailConfigService.getSms(), request.recipient().trim(), - "Test konfiguracji bramki SMS – Polska Lokalnie"); + "Test konfiguracji bramki SMS - Polska Lokalnie"); return TestResultResponse.from(result); } diff --git a/frontend/index.html b/frontend/index.html index a7821a3..7b13523 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3,6 +3,8 @@ + + Polska Lokalnie diff --git a/frontend/package.json b/frontend/package.json index 061f929..20cc428 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,15 @@ "test:e2e": "playwright test" }, "dependencies": { + "@tiptap/extension-character-count": "^2.11.5", + "@tiptap/extension-link": "^2.11.5", + "@tiptap/extension-placeholder": "^2.11.5", + "@tiptap/extension-underline": "^2.11.5", + "@tiptap/pm": "^2.11.5", + "@tiptap/react": "^2.11.5", + "@tiptap/starter-kit": "^2.11.5", "@types/pdfmake": "^0.3.3", + "dompurify": "^3.2.4", "pdfmake": "^0.3.11", "react": "18.3.1", "react-dom": "18.3.1", diff --git a/frontend/public/favicon.png b/frontend/public/favicon.png new file mode 100644 index 0000000..bb90012 Binary files /dev/null and b/frontend/public/favicon.png differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e70d3ef..10e6555 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,7 @@ import { Fragment, type ChangeEvent as ReactChangeEvent, type ClipboardEvent as ReactClipboardEvent, type Dispatch, type DragEvent as ReactDragEvent, type FormEvent as ReactFormEvent, type KeyboardEvent as ReactKeyboardEvent, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent, type ReactNode, type SetStateAction, type WheelEvent as ReactWheelEvent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; import './styles.css'; import { Link, NavLink, Outlet, Route, Routes, useLocation, useNavigate, useNavigationType, useParams, useSearchParams } from 'react-router-dom'; -import { ROUTES, listingPath, mapPath, negotiationPath, companiesPath, adminTabPath, ADMIN_PATH_TO_TAB, type AdminTabKey } from './routes'; +import { ROUTES, listingPath, listingEditPath, mapPath, negotiationPath, companiesPath, adminTabPath, ADMIN_PATH_TO_TAB, type AdminTabKey } from './routes'; import { ProtectedRoute } from './ProtectedRoute'; import { apiFetch, useAuth } from './auth'; import type { AuthUser, ContactPreference, PreferredLanguage } from './auth'; @@ -9,10 +9,13 @@ import { useNotifications, toDisplayNotifications } from './notifications'; import type { ServerNotification, DisplayNotification } from './notifications'; import { getManualTranslation } from './i18nOverrides'; import { AdminLeadsView, AdminCampaignsView, AdminMailConfigView } from './admin/marketing'; +import { RichTextEditor, SafeHtml, stripHtml, isRichTextEmpty } from './RichText'; const heroImage = new URL('./assets/hero-interior.png', import.meta.url).href; const cityImage = new URL('./assets/city-panorama.png', import.meta.url).href; const loginRoomImage = new URL('./assets/login-room.png', import.meta.url).href; +const plLogoLight = new URL('./assets/pl_logo_light.png', import.meta.url).href; +const plLogoDark = new URL('./assets/pl_logo_dark.png', import.meta.url).href; const SMS_VERIFICATION_CODE = '123456'; // Podswietlenie aktywnej pozycji menu wyznacza NavLink na podstawie adresu. @@ -110,6 +113,47 @@ export type ApiListingDetail = { viewsCount?: number; }; +// Ostatnio przegladane oferty - trzymane lokalnie (per urzadzenie) do slajdera na stronie glownej. +type RecentlyViewedItem = { + id: number; + title: string; + city: string; + district: string | null; + price: number; + offerType: ApiOfferType; + area: number; + rooms: number; + photo: string | null; +}; + +const RECENTLY_VIEWED_KEY = 'polskalokalnie-recently-viewed'; + +function readRecentlyViewed(): RecentlyViewedItem[] { + if (typeof window === 'undefined') { + return []; + } + try { + const raw = window.localStorage.getItem(RECENTLY_VIEWED_KEY); + const parsed = raw ? JSON.parse(raw) : []; + return Array.isArray(parsed) ? parsed : []; + } catch { + return []; + } +} + +function pushRecentlyViewed(item: RecentlyViewedItem): void { + if (typeof window === 'undefined') { + return; + } + try { + const list = readRecentlyViewed().filter((entry) => entry.id !== item.id); + list.unshift(item); + window.localStorage.setItem(RECENTLY_VIEWED_KEY, JSON.stringify(list.slice(0, 12))); + } catch { + /* brak dostepu do localStorage - pomijamy */ + } +} + type LiveListingSummary = ApiListingSummary & { lat: number | null; lng: number | null; @@ -668,8 +712,8 @@ const benefits = [ }, { icon: 'shuffle', - title: 'AI dopasowuje', - text: 'kupujących', + title: 'Dopasowani', + text: 'kupujący', }, { icon: 'shield', @@ -699,7 +743,7 @@ const buyQuickFilters = [ const insightCards = [ { icon: 'gear', - title: 'AI wycena mieszkania', + title: 'Wycena mieszkania', text: 'Sprawdź ile warte jest Twoje mieszkanie w 2 minuty', action: 'Wycena za darmo', path: ROUTES.valuation, @@ -721,7 +765,7 @@ const insightCards = [ { icon: 'coin', title: 'Ile możesz utargować?', - text: 'AI podpowie realistyczną cenę transakcyjną', + text: 'Poznaj realistyczną cenę transakcyjną', action: 'Sprawdź teraz', path: ROUTES.negotiation, }, @@ -1188,14 +1232,14 @@ const RENT_PRICE_PRESETS: { label: string; value: number }[] = [ ]; const MORE_FILTERS: { key: string; label: string; options: string[] }[] = [ - { key: 'pietro', label: 'Piętro', options: ['Dowolne', 'Parter', '1–3 piętro', '4–10 piętro', 'Powyżej 10'] }, + { key: 'pietro', label: 'Piętro', options: ['Dowolne', 'Parter', '1-3 piętro', '4-10 piętro', 'Powyżej 10'] }, { key: 'balkon', label: 'Balkon / Taras', options: ['Dowolne', 'Balkon', 'Taras', 'Loggia', 'Ogródek'] }, { key: 'stan', label: 'Stan mieszkania', options: ['Dowolne', 'Do wykończenia', 'Do odświeżenia', 'Dobry', 'Po remoncie', 'Wysoki standard'] }, { key: 'winda', label: 'Winda', options: ['Dowolne', 'Tak', 'Nie'] }, { key: 'ogrzewanie', label: 'Ogrzewanie', options: ['Dowolne', 'Miejskie', 'Gazowe', 'Elektryczne', 'Pompa ciepła'] }, { key: 'parking', label: 'Miejsce parkingowe', options: ['Dowolne', 'Garaż', 'Hala garażowa', 'Miejsce naziemne', 'Brak'] }, { key: 'czynsz', label: 'Czynsz', options: ['Dowolny', 'do 400 zł', 'do 600 zł', 'do 800 zł', 'do 1000 zł'] }, - { key: 'rok', label: 'Rok budowy', options: ['Dowolny', 'Do 1970', '1971–2000', '2001–2015', 'Po 2015'] }, + { key: 'rok', label: 'Rok budowy', options: ['Dowolny', 'Do 1970', '1971-2000', '2001-2015', 'Po 2015'] }, ]; const PRICE_SLIDER_MAX = 2000000; @@ -1205,9 +1249,9 @@ const RENT_PRICE_SLIDER_STEP = 100; const AREA_PRESETS: { label: string; min: number; max: number }[] = [ { label: 'do 35 m²', min: 0, max: 35 }, - { label: '35–50 m²', min: 35, max: 50 }, - { label: '50–70 m²', min: 50, max: 70 }, - { label: '70–90 m²', min: 70, max: 90 }, + { label: '35-50 m²', min: 35, max: 50 }, + { label: '50-70 m²', min: 50, max: 70 }, + { label: '70-90 m²', min: 70, max: 90 }, { label: 'pow. 90 m²', min: 90, max: 0 }, ]; @@ -1250,6 +1294,32 @@ async function imageUrlToDataUrl(url: string, maxDim = 1280, quality = 0.72): Pr } } +/** + * Kompresja zdjecia przy wgrywaniu: jesli szerokosc przekracza maxWidth, skalujemy do maxWidth + * (proporcjonalnie), inaczej zostawiamy oryginalny rozmiar. Zwraca dataURL JPEG. Gdy cos zawiedzie, + * wraca obiektowy URL oryginalu, zeby dodawanie zdjec zawsze dzialalo. + */ +async function compressImageFile(file: File, maxWidth = 1024, quality = 0.82): Promise { + try { + const bitmap = await createImageBitmap(file); + const scale = Math.min(1, maxWidth / bitmap.width); + const width = Math.max(1, Math.round(bitmap.width * scale)); + const height = Math.max(1, Math.round(bitmap.height * scale)); + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (!ctx) { + return URL.createObjectURL(file); + } + ctx.drawImage(bitmap, 0, 0, width, height); + bitmap.close(); + return canvas.toDataURL('image/jpeg', quality); + } catch { + return URL.createObjectURL(file); + } +} + /** * Bezpieczny generator ID. crypto.randomUUID istnieje tylko w bezpiecznym kontekscie (HTTPS/localhost); * przy dostepie po IP w sieci lokalnej jest undefined i rzucalby wyjatek, wywalajac apke do bialego ekranu. @@ -2111,8 +2181,6 @@ function getVirtualTourEmbed(url: string): VirtualTourEmbedInfo | null { return null; } -const EDIT_LISTING_DRAFT_STORAGE_KEY = 'polskalokalnie-edit-listing-draft'; - const addFooterBenefits = [ { icon: 'lock', @@ -3468,12 +3536,18 @@ function App() { } /> } /> - {/* Narzedzia publiczne - dostepne tak samo dla goscia i zalogowanego, - bez lewego menu konta (jak /kupno czy /wynajem) */} + {/* /wycena to pozycja gornego menu - dziala niezaleznie jak /kupno czy /wynajem, + bez lewego menu konta. */} } /> - } /> - } /> - } /> + + {/* Narzedzia dostepne publicznie (jak /kupno), ale otwierane takze z lewego + menu konta. AccountShell pokazuje menu tylko zalogowanemu, a goscia + przepuszcza do samej strony - dzieki temu menu nie znika po kliknieciu. */} + }> + } /> + } /> + } /> + } /> } /> @@ -3503,6 +3577,7 @@ function App() { } /> } /> } /> + } /> {/* Pozostale pozycje lewego menu konta - ten sam uklad co /konto/wyszukiwania */} }> @@ -3554,6 +3629,19 @@ function ListingDetailRoute(props: { return ; } +// Dedykowana strona edycji ogloszenia: /edytuj-ogloszenie/:id. Identyfikator z adresu, +// dane pobierane z API i wczytywane do kreatora (ten sam formularz co dodawanie, tryb edycji). +function AddListingEditRoute() { + const { id } = useParams<{ id: string }>(); + const listingId = Number(id); + + if (!Number.isFinite(listingId) || listingId <= 0) { + return ; + } + + return ; +} + // Mapa czyta miasto i typ oferty z query stringa (/mapa?miasto=Krakow&typ=wynajem). function MapSearchRoute() { const [searchParams] = useSearchParams(); @@ -3708,7 +3796,7 @@ const ADMIN_PROPERTY_LABELS: Record = { function formatAdminDate(iso: string): string { const date = new Date(iso); if (Number.isNaN(date.getTime())) { - return '—'; + return '-'; } return date.toLocaleDateString('pl-PL', { day: '2-digit', month: '2-digit', year: 'numeric' }); } @@ -4340,7 +4428,7 @@ function AdminPage() {
- + - Wiadomości (wkrótce)
@@ -4376,7 +4464,7 @@ function AdminPage() { {item.city} • {formatAdminDate(item.createdAt)} {item.price.toLocaleString('pl-PL')} zł - {item.ownerEmail || '—'} + {item.ownerEmail || '-'} {ADMIN_STATUS_LABELS[item.status]} ))} @@ -4480,7 +4568,7 @@ function AdminPage() { > Zostaw ogłoszenie - ) : '—'} + ) : '-'} {!item.listingDeleted && ( @@ -4499,7 +4587,7 @@ function AdminPage() { Usuń ogłoszenie )} - {item.listingDeleted && '—'} + {item.listingDeleted && '-'} } + {item.isApi && item.apiId != null && item.status !== 'archived' && } {(item.isApi ? (item.apiStatus === 'APPROVED' || item.apiStatus === 'PAUSED') : item.status !== 'archived') && } {!item.isApi && } @@ -10898,7 +10943,7 @@ function LoginPage({ onLogin }: { onLogin: (user: AuthUser) => void }) { Twoje konto zostało utworzone i oczekuje na weryfikację przez administratora.

- Trwa weryfikacja konta, aby zapewnić bezpieczeństwo i wysoką jakość ogłoszeń w serwisie. Zwykle zajmuje to tylko chwilę. Możesz już korzystać z serwisu – pełny dostęp zostanie odblokowany po weryfikacji. + Trwa weryfikacja konta, aby zapewnić bezpieczeństwo i wysoką jakość ogłoszeń w serwisie. Zwykle zajmuje to tylko chwilę. Możesz już korzystać z serwisu - pełny dostęp zostanie odblokowany po weryfikacji.

+
+ {children} +
+ + + ); +} + function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (city: string, mode?: 'buy' | 'rent') => void }) { const navigate = useNavigate(); const [favoriteListings, setFavoriteListings] = useState>(new Set()); + const [searchMode, setSearchMode] = useState<'buy' | 'rent'>('buy'); + const [recentlyViewed] = useState(() => readRecentlyViewed()); + const { items: liveSaleListings } = useLiveListings('SALE'); const toggleFavoriteListing = (location: string) => { setFavoriteListings((current) => { @@ -10974,7 +11047,7 @@ function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (ci Sprzedaj mieszkanie za darmo. -

Bez prowizji. Bez ukrytych opłat. Znajdź kupca szybciej z pomocą AI.

+

Bez prowizji. Bez ukrytych opłat. Znajdź szybciej klienta.

{benefits.map((benefit) => ( @@ -10991,10 +11064,10 @@ function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (ci
-