diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a02974b..f20b1e6 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,7 @@ import { Fragment, type CSSProperties, 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, Navigate, Outlet, Route, Routes, useLocation, useNavigate, useNavigationType, useParams, useSearchParams } from 'react-router-dom'; -import { ROUTES, GUIDE_TOPIC_ROUTES, GUIDE_TOPICS, guideTopicBySlug, guideTopicListPath, articlePath, guideAddPath, listingPath, publicProfilePath, publicProfileNickPath, listingEditPath, mapPath, buyCityPath, negotiationPath, priceHistoryPath, companiesPath, advertisingPath, adminTabPath, ADMIN_PATH_TO_TAB, type AdminTabKey } from './routes'; +import { ROUTES, GUIDE_TOPICS, guideTopicBySlug, guideTopicListPath, articlePath, guideAddPath, listingPath, publicProfilePath, publicProfileNickPath, listingEditPath, mapPath, buyCityPath, negotiationPath, priceHistoryPath, companiesPath, advertisingPath, adminTabPath, ADMIN_PATH_TO_TAB, type AdminTabKey } from './routes'; import { guideContents, type GuideContent, type GuideBlock } from './guides-data'; import { ProtectedRoute } from './ProtectedRoute'; import { Avatar, initialsFrom } from './Avatar'; @@ -203,233 +203,13 @@ const LIVE_LISTINGS_REFRESH_MS = 15000; type NotificationCategory = 'listing' | 'messages' | 'system'; -type UserNotification = { - id: string; - title: string; - description: string; - timeLabel: string; - dayLabel: 'Dzisiaj' | 'Wczoraj'; - category: NotificationCategory; - icon: string; - unread?: boolean; - action?: 'arrow'; -}; - -const userNotifications: UserNotification[] = [ - { - id: 'new-message', - title: 'Nowa wiadomość', - description: 'Masz nową wiadomość od użytkownika Anna Nowak', - timeLabel: '11:24', - dayLabel: 'Dzisiaj', - category: 'messages', - icon: 'message', - unread: true, - }, - { - id: 'saved-listing', - title: 'Ogłoszenie zapisane', - description: 'Mieszkanie 2-pokojowe na Mokotowie zostało zapisane do ulubionych.', - timeLabel: '10:15', - dayLabel: 'Dzisiaj', - category: 'listing', - icon: 'heart', - unread: true, - }, - { - id: 'matching-listings', - title: 'Nowe ogłoszenia dopasowane do Ciebie', - description: 'Zobacz 3 nowe oferty mieszkań, które mogą Cię zainteresować.', - timeLabel: '09:43', - dayLabel: 'Dzisiaj', - category: 'listing', - icon: 'bell', - action: 'arrow', - }, - { - id: 'price-drop', - title: 'Zmiana ceny', - description: 'Cena mieszkania „Apartament z widokiem” spadła o 20 000 zł.', - timeLabel: '09:12', - dayLabel: 'Dzisiaj', - category: 'listing', - icon: 'coin', - action: 'arrow', - }, - { - id: 'phone-verified', - title: 'Numer telefonu zweryfikowany', - description: 'Twój numer telefonu został pomyślnie zweryfikowany.', - timeLabel: '08:30', - dayLabel: 'Dzisiaj', - category: 'system', - icon: 'check', - unread: true, - }, - { - id: 'support-reply', - title: 'Wiadomość od obsługi serwisu Polska Lokalnie', - description: 'Dziękujemy za kontakt. Odpowiemy najszybciej jak to możliwe.', - timeLabel: 'Wczoraj, 18:20', - dayLabel: 'Wczoraj', - category: 'messages', - icon: 'mail', - action: 'arrow', - }, - { - id: 'listing-views', - title: 'Twoje ogłoszenie ma nowe wyświetlenia', - description: 'Twoje ogłoszenie „Kawalerka w centrum” zostało wyświetlone 23 razy.', - timeLabel: 'Wczoraj, 16:05', - dayLabel: 'Wczoraj', - category: 'listing', - icon: 'eye', - unread: true, - }, - { - id: 'new-review', - title: 'Nowa opinia', - description: 'Katarzyna Zielińska wystawiła Ci pozytywną opinię.', - timeLabel: 'Wczoraj, 12:47', - dayLabel: 'Wczoraj', - category: 'system', - icon: 'star', - action: 'arrow', - }, -]; - const meetingsStorageKey = 'polskalokalnie-account-meetings'; -const listingsStorageKey = 'polskalokalnie-account-listings'; - -function getMeetingReminderNotifications(referenceDate = new Date()): UserNotification[] { - if (typeof window === 'undefined') { - return []; - } - - type StoredMeeting = { - id: string; - title: string; - location: string; - dateKey: string; - start: string; - status?: string; - }; - - try { - const raw = window.localStorage.getItem(meetingsStorageKey); - if (!raw) { - return []; - } - - const parsed = JSON.parse(raw); - if (!Array.isArray(parsed)) { - return []; - } - - const nowMs = referenceDate.getTime(); - const oneDayMs = 24 * 60 * 60 * 1000; - - return parsed - .filter((item): item is StoredMeeting => ( - item - && typeof item.id === 'string' - && typeof item.title === 'string' - && typeof item.location === 'string' - && typeof item.dateKey === 'string' - && typeof item.start === 'string' - )) - .filter((meeting) => (meeting.status ?? 'planned') === 'planned') - .map((meeting) => { - const meetingDateTime = new Date(`${meeting.dateKey}T${meeting.start}:00`); - const meetingMs = meetingDateTime.getTime(); - const reminderStartMs = meetingMs - oneDayMs; - const isInReminderWindow = nowMs >= reminderStartMs && nowMs < meetingMs; - return { meeting, meetingDateTime, isInReminderWindow }; - }) - .filter((row) => row.isInReminderWindow) - .sort((left, right) => left.meetingDateTime.getTime() - right.meetingDateTime.getTime()) - .map(({ meeting, meetingDateTime }) => ({ - id: `meeting-reminder-${meeting.id}`, - title: 'Przypomnienie o spotkaniu', - description: `${meeting.title} o ${meeting.start} (${meeting.location}).`, - timeLabel: meetingDateTime.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }), - dayLabel: 'Dzisiaj' as const, - category: 'system' as const, - icon: 'calendar', - unread: true, - action: 'arrow' as const, - })); - } catch { - return []; - } -} - -function getListingResumeNotifications(referenceDate = new Date()): UserNotification[] { - if (typeof window === 'undefined') { - return []; - } - - type StoredListing = { - id: string; - title: string; - status?: string; - pausedUntil?: string | null; - }; - - try { - const raw = window.localStorage.getItem(listingsStorageKey); - if (!raw) { - return []; - } - - const parsed = JSON.parse(raw); - if (!Array.isArray(parsed)) { - return []; - } - - const nowMs = referenceDate.getTime(); - const oneDayMs = 24 * 60 * 60 * 1000; - - return parsed - .filter((item): item is StoredListing => ( - item - && typeof item.id === 'string' - && typeof item.title === 'string' - && typeof item.status === 'string' - )) - .filter((item) => item.status === 'paused' && typeof item.pausedUntil === 'string' && item.pausedUntil.length > 0) - .map((item) => { - const resumeDateTime = new Date(item.pausedUntil as string); - const resumeMs = resumeDateTime.getTime(); - const reminderStartMs = resumeMs - oneDayMs; - const isInReminderWindow = nowMs >= reminderStartMs && nowMs < resumeMs; - return { item, resumeDateTime, isInReminderWindow }; - }) - .filter((row) => row.isInReminderWindow) - .sort((left, right) => left.resumeDateTime.getTime() - right.resumeDateTime.getTime()) - .map(({ item, resumeDateTime }) => ({ - id: `listing-resume-${item.id}`, - title: 'Jutro ogłoszenie zostanie przywrócone', - description: `${item.title} zostanie automatycznie wznowione ${resumeDateTime.toLocaleDateString('pl-PL')} o ${resumeDateTime.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' })}.`, - timeLabel: resumeDateTime.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' }), - dayLabel: 'Dzisiaj' as const, - category: 'listing' as const, - icon: 'bell', - unread: true, - action: 'arrow' as const, - })); - } catch { - return []; - } -} - -function getAllNotifications(referenceDate = new Date()): UserNotification[] { - return [ - ...getListingResumeNotifications(referenceDate), - ...getMeetingReminderNotifications(referenceDate), - ...userNotifications, - ]; -} +// Identyfikatory przykladowych spotkan, ktore wczesniejsza wersja zapisywala kazdemu w localStorage. +// Odfiltrowujemy je przy odczycie, zeby zniknely takze u osob, ktore juz je maja zapisane. +const LEGACY_DEMO_MEETING_IDS = new Set([ + 'meeting-1', 'meeting-2', 'meeting-3', 'meeting-4', + 'meeting-test-now-1', 'meeting-test-week-ago-1', 'meeting-test-prev-month-1', +]); type FavoriteTab = 'buy' | 'rent'; @@ -653,17 +433,13 @@ function findBestCityAlertOffer( )); } -function calculateCityAlertPerM2(subscription: CityPriceAlertSubscription, listings: ApiListingSummary[]): number { +// null = brak pasujacych ogloszen, wiec nie ma z czego policzyc sredniej. Wczesniej podstawiana +// byla szacunkowa stawka z tabeli, przez co znikniecie ogloszen wygladalo jak zmiana ceny. +function calculateCityAlertPerM2(subscription: CityPriceAlertSubscription, listings: ApiListingSummary[]): number | null { const filtered = filterCityAlertListings(subscription, listings); if (filtered.length === 0) { - const estimate = estimateHistoryLocationPrice(subscription.label); - const fallback = Math.round( - estimate.price - * HISTORY_PROPERTY_MULTIPLIERS[subscription.propertyType] - * HISTORY_MARKET_MULTIPLIERS[subscription.marketType], - ); - return Math.max(1, fallback); + return null; } const averagePerM2 = filtered.reduce((sum, item) => sum + (item.price / item.area), 0) / filtered.length; @@ -887,14 +663,6 @@ const RANKING_METRIC_LABELS: Record = { trend: 'Największy wzrost cen', }; -// Cena i metraz liczbowo (PLN, m²) - etykiety powstaja przy renderowaniu, wiec ida za ustawieniami. -const listings = [ - { location: 'Kraków, Zabłocie', rooms: 3, area: 62, price: 749000, badge: 'Nowe' }, - { location: 'Warszawa, Mokotów', rooms: 2, area: 45, price: 679000, badge: 'Premium' }, - { location: 'Wrocław, Kępa Mieszczańska', rooms: 4, area: 78, price: 899000, badge: 'Nowe' }, - { location: 'Gdańsk, Wrzeszcz', rooms: 3, area: 56, price: 619000, badge: '' }, -]; - const cities = [ { name: 'Warszawa', position: '48% 55%' }, { name: 'Kraków', position: '54% 54%' }, @@ -969,225 +737,6 @@ type BuyOffer = { type PropertySearchType = 'Mieszkanie' | 'Dom' | 'Działka' | 'Lokal użytkowy' | 'Pokój'; -const buyListings: BuyOffer[] = [ - { - title: 'Mieszkanie 3-pokojowe', - propertyType: 'Mieszkanie', - city: 'Warszawa', - district: 'Warszawa, Mokotów, Stegny', - rooms: 3, - area: 62, - priceValue: 749000, - market: 'wtorny', - year: 1980, - date: 'Dodane dzisiaj', - dateOrder: 0, - score: 'Ocena oferty 8.7/10', - saving: 'Możliwe do negocjacji: ok. 35 000 zł', - tags: ['Okazja', 'Bez prowizji'], - position: '47% 58%', - image: 'hero', - }, - { - title: 'Mieszkanie 2-pokojowe', - propertyType: 'Mieszkanie', - city: 'Warszawa', - district: 'Warszawa, Praga-Południe, Grochów', - rooms: 2, - area: 45, - priceValue: 699000, - market: 'pierwotny', - year: 2023, - date: 'Dodane dzisiaj', - dateOrder: 0, - score: 'Ocena oferty 8.2/10', - saving: 'Możliwe do negocjacji: ok. 20 000 zł', - tags: ['Nowe', 'Bez prowizji'], - position: '86% 55%', - image: 'city', - }, - { - title: 'Mieszkanie 3-pokojowe', - propertyType: 'Mieszkanie', - city: 'Warszawa', - district: 'Warszawa, Bemowo, Górczewska', - rooms: 3, - area: 55, - priceValue: 699000, - market: 'wtorny', - year: 2010, - date: 'Dodane wczoraj', - dateOrder: 1, - score: 'Ocena oferty 7.9/10', - saving: 'Możliwe do negocjacji: ok. 25 000 zł', - tags: ['Bez prowizji'], - position: '58% 62%', - image: 'hero', - }, - { - title: 'Dom 4-pokojowy', - propertyType: 'Dom', - city: 'Warszawa', - district: 'Warszawa, Bielany, Chomiczówka', - rooms: 4, - area: 72, - priceValue: 789000, - market: 'wtorny', - year: 1995, - date: 'Dodane 2 dni temu', - dateOrder: 2, - score: 'Ocena oferty 8.1/10', - saving: 'Cena niższa od podobnych ofert', - tags: ['Okazja', 'Bez prowizji'], - position: '33% 58%', - image: 'hero', - }, - { - title: 'Mieszkanie 2-pokojowe', - propertyType: 'Mieszkanie', - city: 'Kraków', - district: 'Kraków, Podgórze, Zabłocie', - rooms: 2, - area: 48, - priceValue: 679000, - market: 'pierwotny', - year: 2024, - date: 'Dodane dzisiaj', - dateOrder: 0, - score: 'Ocena oferty 8.5/10', - saving: 'Cena rynkowa dla lokalizacji', - tags: ['Nowe', 'Bez prowizji'], - position: '52% 55%', - image: 'city', - }, - { - title: 'Mieszkanie 3-pokojowe', - propertyType: 'Mieszkanie', - city: 'Kraków', - district: 'Kraków, Krowodrza, Bronowice', - rooms: 3, - area: 64, - priceValue: 845000, - market: 'wtorny', - year: 2008, - date: 'Dodane wczoraj', - dateOrder: 1, - score: 'Ocena oferty 8.0/10', - saving: 'Możliwe do negocjacji: ok. 30 000 zł', - tags: ['Bez prowizji'], - position: '44% 60%', - image: 'hero', - }, - { - title: 'Dom z ogrodem', - propertyType: 'Dom', - city: 'Wrocław', - district: 'Wrocław, Krzyki, Borek', - rooms: 4, - area: 78, - priceValue: 899000, - market: 'wtorny', - year: 2001, - date: 'Dodane dzisiaj', - dateOrder: 0, - score: 'Ocena oferty 8.3/10', - saving: 'Możliwe do negocjacji: ok. 40 000 zł', - tags: ['Okazja', 'Bez prowizji'], - position: '60% 50%', - image: 'hero', - }, - { - title: 'Mieszkanie 2-pokojowe', - propertyType: 'Mieszkanie', - city: 'Wrocław', - district: 'Wrocław, Stare Miasto, Kępa Mieszczańska', - rooms: 2, - area: 41, - priceValue: 569000, - market: 'pierwotny', - year: 2025, - date: 'Dodane 2 dni temu', - dateOrder: 2, - score: 'Ocena oferty 8.6/10', - saving: 'Nowa inwestycja, cena deweloperska', - tags: ['Nowe', 'Bez prowizji'], - position: '38% 54%', - image: 'city', - }, - { - title: 'Mieszkanie 3-pokojowe', - propertyType: 'Mieszkanie', - city: 'Gdańsk', - district: 'Gdańsk, Wrzeszcz, Strzyża', - rooms: 3, - area: 58, - priceValue: 749000, - market: 'wtorny', - year: 2015, - date: 'Dodane dzisiaj', - dateOrder: 0, - score: 'Ocena oferty 8.4/10', - saving: 'Cena niższa od podobnych ofert', - tags: ['Okazja', 'Bez prowizji'], - position: '66% 48%', - image: 'hero', - }, - { - title: 'Pokój inwestycyjny', - propertyType: 'Pokój', - city: 'Gdańsk', - district: 'Gdańsk, Śródmieście, Wyspa Spichrzów', - rooms: 1, - area: 29, - priceValue: 459000, - market: 'pierwotny', - year: 2024, - date: 'Dodane wczoraj', - dateOrder: 1, - score: 'Ocena oferty 8.1/10', - saving: 'Idealne pod inwestycję', - tags: ['Nowe', 'Bez prowizji'], - position: '72% 52%', - image: 'city', - }, - { - title: 'Działka budowlana', - propertyType: 'Działka', - city: 'Poznań', - district: 'Poznań, Jeżyce, Sołacz', - rooms: 3, - area: 61, - priceValue: 689000, - market: 'wtorny', - year: 2012, - date: 'Dodane 3 dni temu', - dateOrder: 3, - score: 'Ocena oferty 7.8/10', - saving: 'Możliwe do negocjacji: ok. 22 000 zł', - tags: ['Bez prowizji'], - position: '50% 58%', - image: 'hero', - }, - { - title: 'Lokal użytkowy', - propertyType: 'Lokal użytkowy', - city: 'Łódź', - district: 'Łódź, Śródmieście, Nowe Centrum', - rooms: 2, - area: 44, - priceValue: 379000, - market: 'wtorny', - year: 2018, - date: 'Dodane dzisiaj', - dateOrder: 0, - score: 'Ocena oferty 8.0/10', - saving: 'Bardzo dobra cena za m²', - tags: ['Okazja', 'Bez prowizji'], - position: '54% 56%', - image: 'city', - }, -]; - const POLISH_CITIES = [ 'Warszawa', 'Kraków', 'Łódź', 'Wrocław', 'Poznań', 'Gdańsk', 'Szczecin', 'Bydgoszcz', 'Lublin', 'Białystok', 'Katowice', 'Gdynia', 'Częstochowa', 'Radom', 'Sosnowiec', @@ -1218,8 +767,6 @@ const ROOM_WORDS: Record = { 1: 'pokój', 2: 'pokoje', 3: 'pokoj const POPULAR_CITIES = ['Warszawa', 'Kraków', 'Wrocław', 'Gdańsk', 'Poznań', 'Łódź']; -const DEFAULT_RECENT = ['Warszawa Mokotów', 'Kraków Zabłocie', 'Wrocław Krzyki']; - type SortKey = 'default' | 'new' | 'old' | 'cheap' | 'expensive' | 'small' | 'big'; const SORT_OPTIONS: { value: SortKey; label: string }[] = [ @@ -1489,110 +1036,6 @@ function buildComparableTransactions(activeOffer: BuyOffer, source: BuyOffer[], }); } -type RentOffer = { - title: string; - propertyType: PropertySearchType; - city: string; - district: string; - rooms: number; - area: number; - priceValue: number; - market: 'wtorny' | 'pierwotny'; - floor: string; - buildingFloors: string; - date: string; - dateOrder: number; - tags: string[]; - amenities: string[]; - position: string; - forStudents: boolean; - petsAllowed: boolean; - parkingIncluded: boolean; -}; - -const rentListings: RentOffer[] = [ - { - title: 'Mieszkanie 2-pokojowe', - propertyType: 'Mieszkanie', - city: 'Warszawa', - district: 'Warszawa, Mokotów, Służew', - rooms: 2, - area: 42, - priceValue: 3300, - market: 'wtorny', - floor: '3', - buildingFloors: '6', - date: 'Dodane dzisiaj', - dateOrder: 0, - tags: ['Nowe', 'Bez prowizji'], - amenities: ['Umeblowane', 'Balkon', 'Winda'], - position: '47% 58%', - forStudents: true, - petsAllowed: false, - parkingIncluded: false, - }, - { - title: 'Mieszkanie 1-pokojowe', - propertyType: 'Mieszkanie', - city: 'Warszawa', - district: 'Warszawa, Praga-Południe, Grochów', - rooms: 1, - area: 28, - priceValue: 2200, - market: 'wtorny', - floor: '2', - buildingFloors: '4', - date: 'Dodane dzisiaj', - dateOrder: 0, - tags: ['Bez prowizji'], - amenities: ['Umeblowane', 'Winda'], - position: '86% 55%', - forStudents: true, - petsAllowed: false, - parkingIncluded: false, - }, - { - title: 'Mieszkanie 3-pokojowe', - propertyType: 'Mieszkanie', - city: 'Warszawa', - district: 'Warszawa, Bemowo, Górce', - rooms: 3, - area: 55, - priceValue: 3800, - market: 'pierwotny', - floor: '1', - buildingFloors: '5', - date: 'Dodane wczoraj', - dateOrder: 1, - tags: ['Bez prowizji'], - amenities: ['Umeblowane', 'Balkon', 'Parking'], - position: '58% 62%', - forStudents: false, - petsAllowed: true, - parkingIncluded: true, - }, - { - title: 'Mieszkanie 2-pokojowe', - propertyType: 'Mieszkanie', - city: 'Warszawa', - district: 'Warszawa, Wola, Nowolipki', - rooms: 2, - area: 38, - priceValue: 2900, - market: 'wtorny', - floor: '4', - buildingFloors: '7', - date: 'Dodane wczoraj', - dateOrder: 1, - tags: ['Bez prowizji'], - amenities: ['Umeblowane', 'Winda'], - position: '33% 58%', - forStudents: false, - petsAllowed: true, - parkingIncluded: false, - }, -]; - // slug trafia do adresu listy firm: /firmy?kategoria=remonty const serviceCategories = [ { @@ -2781,51 +2224,6 @@ function articleCountLabel(count: number): string { return `${count} artykułów`; } -// Kategoria karty artykułu (WERSALIKAMI) -> trasa poradnika danego tematu. Dzięki temu klik w -// polecany artykuł otwiera pełny poradnik z miejscem reklamowym dla firm. -const ARTICLE_CATEGORY_ROUTES: Record = Object.fromEntries( - Object.entries(GUIDE_TOPIC_ROUTES).map(([title, route]) => [title.toUpperCase(), route]), -); - -const guideArticles = [ - { - category: 'KREDYTY HIPOTECZNE', - title: 'Jak obliczyć zdolność kredytową w 2026 roku?', - text: 'Praktyczny przewodnik krok po kroku z aktualnymi przykładami.', - readTime: '8 min czytania', - views: '12 450', - image: cityImage, - position: '24% 58%', - }, - { - category: 'KUPNO MIESZKANIA', - title: 'Na co zwrócić uwagę przy odbiorze mieszkania?', - text: 'Sprawdź listę rzeczy, które warto skontrolować przed podpisaniem protokołu.', - readTime: '10 min czytania', - views: '9 876', - image: heroImage, - position: '47% 58%', - }, - { - category: 'WYNAJEM', - title: 'Umowa najmu okazjonalnego - wzór i omówienie', - text: 'Kompletny wzór umowy z objaśnieniami i praktycznymi wskazówkami.', - readTime: '6 min czytania', - views: '7 532', - image: heroImage, - position: '74% 55%', - }, - { - category: 'INWESTOWANIE', - title: 'Gdzie warto kupić mieszkanie inwestycyjne w 2026?', - text: 'Analiza miast i dzielnic z największym potencjałem wzrostu.', - readTime: '12 min czytania', - views: '11 302', - image: cityImage, - position: '82% 56%', - }, -]; - const quickGuides = [ { icon: 'scale', @@ -2857,57 +2255,26 @@ const quickGuides = [ const guideRelatedOffers = [ { title: 'Mieszkania w Krakowie', - count: '2 450 ofert', image: cityImage, position: '88% 56%', }, { title: 'Mieszkania w Warszawie', - count: '5 120 ofert', image: cityImage, position: '18% 48%', }, { title: 'Nowe mieszkania', - count: '3 280 ofert', image: cityImage, position: '74% 52%', }, { title: 'Okazje cenowe', - count: '1 230 ofert', image: heroImage, position: '47% 58%', }, ]; -const mapMarkers = [ - ['649 tys.', '72%', '20%'], - ['590 tys.', '50%', '33%'], - ['669 tys.', '82%', '32%'], - ['799 tys.', '18%', '45%'], - ['699 tys.', '18%', '61%'], - ['699 tys.', '60%', '59%'], - ['790 tys.', '84%', '60%'], - ['390 tys.', '61%', '73%'], - ['760 tys.', '42%', '86%'], - ['900 tys.', '72%', '86%'], - ['360 tys.', '82%', '77%'], -]; - -const rentMapMarkers = [ - ['2 600 zł', '30%', '19%'], - ['3 300 zł', '68%', '19%'], - ['2 800 zł', '88%', '25%'], - ['2 200 zł', '49%', '33%'], - ['3 800 zł', '18%', '43%'], - ['2 400 zł', '15%', '62%'], - ['3 900 zł', '62%', '65%'], - ['2 900 zł', '78%', '68%'], - ['2 900 zł', '28%', '78%'], - ['2 300 zł', '75%', '88%'], -]; - type LatLng = { lat: number; lng: number }; const POLAND_CENTER: LatLng = { lat: 52.1, lng: 19.3 }; @@ -3514,6 +2881,10 @@ function App() { subscriptions.forEach((subscription) => { const storageKey = cityAlertStorageKey(subscription); const currentPerM2 = calculateCityAlertPerM2(subscription, saleListings); + if (currentPerM2 === null) { + // Bez ogloszen nie ma sredniej - zostawiamy ostatni pomiar do porownania pozniej. + return; + } const previousPerM2 = previousSnapshots[storageKey]?.perM2; if (Number.isFinite(previousPerM2) && previousPerM2 !== currentPerM2) { @@ -4401,7 +3772,7 @@ function MapSearchRoute() { function NegotiationRoute() { const location = useLocation(); const offerFromState = (location.state as { offer?: BuyOffer } | null)?.offer ?? null; - const [selectedOffer, setSelectedOffer] = useState(offerFromState ?? buyListings[0] ?? null); + const [selectedOffer, setSelectedOffer] = useState(offerFromState); useEffect(() => { if (offerFromState) { @@ -4409,7 +3780,8 @@ function NegotiationRoute() { } }, [offerFromState]); - return ; + // Analiza negocjacji nie ma jeszcze zrodla danych - bez oferty strona pokazuje pusty stan. + return ; } // Nieznany adres: czytelny komunikat zamiast pustego ekranu. @@ -6713,11 +6085,11 @@ function NegotiationPage({

Ile możesz utargować?

-

Brak ogłoszeń do analizy. Wróć do listy ofert i wybierz mieszkanie.

+

Brak danych do analizy.

Brak danych -

Aktualnie nie mamy żadnego ogłoszenia, dla którego można policzyć rekomendowaną cenę transakcyjną.

+

Analiza rekomendowanej ceny transakcyjnej będzie dostępna wkrótce.

@@ -6924,7 +6296,7 @@ function AccountSidebar() { } const now = new Date(); const todayKey = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; - return parsed.filter((item) => item && item.dateKey === todayKey && item.status !== 'cancelled').length; + return parsed.filter((item) => item && !LEGACY_DEMO_MEETING_IDS.has(item.id) && item.dateKey === todayKey && item.status !== 'cancelled').length; } catch { return 0; } @@ -7099,12 +6471,6 @@ function AccountDashboardPage({ }; const dashboardWidgetsStorageKey = 'polskalokalnie-dashboard-widgets'; - const activityItems = [ - { icon: 'search', label: 'Zapisane wyszukiwania', count: savedSearches.length, action: 'Zobacz wszystkie' }, - { icon: 'heart', label: 'Ulubione ogłoszenia', count: 12, action: 'Zobacz wszystkie' }, - { icon: 'bell', label: 'Aktywne alerty cenowe', count: 3, action: 'Zarządzaj alertami' }, - { icon: 'message', label: 'Nieprzeczytane wiadomości', count: 4, action: 'Zobacz wiadomości' }, - ]; // Realne dane panelu: ostatnio oglądane oferty (localStorage) i alerty cenowe użytkownika // (to samo źródło co strona /konto/alerty-cenowe). @@ -8320,10 +7686,12 @@ type PriceAlertLocationCardModel = { label: string; meta: string; historyTo: string; - chart: ReturnType; - currentPerM2: number; + // null = mniej niz dwa realne pomiary, wiec nie ma czego rysowac. + chart: ReturnType | null; + // null = brak ogloszen w lokalizacji. + currentPerM2: number | null; changePercent: number; - // Czy zmiana pochodzi z realnie zarejestrowanego zdarzenia, czy z szacowanego trendu. + // Czy alert zarejestrowal juz zmiane sredniej ceny. changeIsReal: boolean; changeCaption: string; notificationsEnabled: boolean; @@ -8384,35 +7752,41 @@ function PriceAlertLocationCard({
- {formatPricePerArea(card.currentPerM2)} - - - {' '}{formatAlertPercent(card.changePercent)} - + {card.currentPerM2 === null ? '—' : formatPricePerArea(card.currentPerM2)} + {card.changeIsReal && ( + + + {' '}{formatAlertPercent(card.changePercent)} + + )}
- - - - - - - - - - + {card.chart && ( + <> + + + + + + + + + + - + + + )} {/* Zmiana zarejestrowana przez alert; do czasu pierwszej - szacunek z trendu. */}
@@ -8569,25 +7943,16 @@ function AccountPriceAlertsPage({ // --- Alerty lokalizacji (dodane w historii cen) jako karty z wykresem --- const locationCards: PriceAlertLocationCardModel[] = useMemo(() => cityAlerts .map((subscription) => { - const market = HISTORY_PRICE_MARKETS.find((item) => item.key === subscription.key) - ?? resolveHistoryMarket(subscription.label); // Stawka liczona z realnych ogloszen na sprzedaz - dokladnie ta sama, ktora // porownuje monitoring w tle, wiec naglowek karty zgadza sie z zarejestrowana zmiana. const currentPerM2 = calculateCityAlertPerM2(subscription, saleListings); - const modelSeries = generateHistorySeries( - market, - subscription.propertyType, - subscription.marketType, - '2026', - chartMonths, - ); - // Przeskalowanie modelu tak, by konczyl sie na realnej stawce - inaczej wykres - // pokazywalby inne pieniadze niz liczba nad nim. - const anchor = modelSeries[modelSeries.length - 1] || 1; - const series = modelSeries.map((value) => Math.max(1, Math.round((value / anchor) * currentPerM2))); - const previousPerM2 = series.length > 1 ? series[series.length - 2] : currentPerM2; + // Wykres tylko z zarejestrowanych zmian (zdarzenia sa od najnowszego) - bez nich nie rysujemy nic. + const subscriptionEvents = cityPriceEvents.filter((event) => event.subscriptionKey === subscription.key); + const series = currentPerM2 === null + ? [] + : [...subscriptionEvents].reverse().map((event) => event.previousPerM2).concat(currentPerM2); - const lastEvent = cityPriceEvents.find((event) => event.subscriptionKey === subscription.key) ?? null; + const lastEvent = subscriptionEvents[0] ?? null; const eventAmount = lastEvent ? lastEvent.currentPerM2 - lastEvent.previousPerM2 : 0; return { @@ -8597,17 +7962,16 @@ function AccountPriceAlertsPage({ label: subscription.label, meta: `${subscription.propertyType} • ${subscription.marketType}`, historyTo: priceHistoryPath(subscription.label, subscription.propertyType, subscription.marketType), - chart: buildPriceAlertChart(series), + chart: series.length >= 2 ? buildPriceAlertChart(series) : null, currentPerM2, - // Gdy alert zarejestrowal juz zmiane - pokazujemy ja, bo to fakt. Zanim to nastapi - // pokazujemy szacowana zmiane miesiac do miesiaca, zeby kafelek nie byl pusty. + // Tylko zmiana zarejestrowana przez alert - bez niej nie pokazujemy zadnego procentu. changePercent: lastEvent ? priceAlertChangePercent(lastEvent.previousPerM2, lastEvent.currentPerM2) - : priceAlertChangePercent(previousPerM2, currentPerM2), + : 0, changeIsReal: Boolean(lastEvent), changeCaption: lastEvent ? `${formatPricePerArea(Math.abs(eventAmount))} ${eventAmount <= 0 ? 'taniej' : 'drożej'} • ${priceAlertRelativeTime(lastEvent.changedAt)}` - : 'Szacowana zmiana miesiąc do miesiąca', + : currentPerM2 === null ? 'Brak ogłoszeń w tej lokalizacji' : 'Brak zarejestrowanych zmian', notificationsEnabled: subscription.notificationsEnabled !== false, onToggle: () => toggleCityAlertNotifications(subscription.key), onRemove: () => removeCityAlert(subscription.key), @@ -9542,113 +8906,21 @@ function AccountMeetingsPage() { end: '10:30', type: 'buy' as MeetingType, }); - const defaultMeetings = useMemo(() => { - const now = new Date(); - const addDays = (days: number) => { - const date = new Date(now); - date.setDate(date.getDate() + days); - return formatDateKey(date); - }; - const addMonths = (months: number) => { - const date = new Date(now); - date.setMonth(date.getMonth() + months); - return formatDateKey(date); - }; - - return [ - { - id: 'meeting-1', - title: 'Spotkanie z kupującym', - subtitle: 'Mieszkanie 3-pokojowe, 62 m²', - location: 'ul. Mokotowska 45, Warszawa', - dateKey: addDays(0), - start: '10:00', - end: '10:30', - type: 'buy', - status: 'planned', - }, - { - id: 'meeting-2', - title: 'Spotkanie z wynajmującym', - subtitle: 'Mieszkanie 2-pokojowe, 45 m²', - location: 'ul. Puławska 10, Warszawa', - dateKey: addDays(0), - start: '12:00', - end: '12:30', - type: 'rent', - status: 'planned', - }, - { - id: 'meeting-3', - title: 'Spotkanie własne', - subtitle: 'Konsultacja z doradcą', - location: 'Online', - dateKey: addDays(0), - start: '15:00', - end: '15:30', - type: 'own', - status: 'planned', - }, - { - id: 'meeting-4', - title: 'Spotkanie z kupującym', - subtitle: 'Mieszkanie 4-pokojowe, 78 m²', - location: 'ul. Dobra 12, Warszawa', - dateKey: addDays(1), - start: '17:30', - end: '18:00', - type: 'buy', - status: 'planned', - }, - { - id: 'meeting-test-now-1', - title: 'Test: szybka konsultacja', - subtitle: 'Spotkanie testowe na dziś', - location: 'Biuro Polska Lokalnie, Warszawa', - dateKey: addDays(0), - start: '18:30', - end: '19:00', - type: 'own', - status: 'planned', - }, - { - id: 'meeting-test-week-ago-1', - title: 'Test: historia sprzed tygodnia', - subtitle: 'Weryfikacja widoku archiwum', - location: 'Online', - dateKey: addDays(-7), - start: '11:00', - end: '11:30', - type: 'rent', - status: 'completed', - }, - { - id: 'meeting-test-prev-month-1', - title: 'Test: poprzedni miesiąc', - subtitle: 'Spotkanie archiwalne', - location: 'ul. Grzybowska 22, Warszawa', - dateKey: addMonths(-1), - start: '09:30', - end: '10:00', - type: 'buy', - status: 'completed', - }, - ]; - }, []); const [meetings, setMeetings] = useState(() => { try { const raw = window.localStorage.getItem(storageKey); if (!raw) { - return defaultMeetings; + return []; } const parsed = JSON.parse(raw); if (!Array.isArray(parsed)) { - return defaultMeetings; + return []; } - const normalized = parsed + return parsed .filter((item): item is MeetingItem => ( item && typeof item.id === 'string' + && !LEGACY_DEMO_MEETING_IDS.has(item.id) && typeof item.title === 'string' && typeof item.subtitle === 'string' && typeof item.location === 'string' @@ -9659,9 +8931,8 @@ function AccountMeetingsPage() { && (item.status === 'planned' || item.status === 'completed' || item.status === 'cancelled') )) .sort((left, right) => `${left.dateKey}-${left.start}`.localeCompare(`${right.dateKey}-${right.start}`)); - return normalized.length > 0 ? normalized : defaultMeetings; } catch { - return defaultMeetings; + return []; } }); @@ -9669,18 +8940,6 @@ function AccountMeetingsPage() { window.localStorage.setItem(storageKey, JSON.stringify(meetings)); }, [meetings]); - useEffect(() => { - const requiredTestIds = ['meeting-test-now-1', 'meeting-test-week-ago-1', 'meeting-test-prev-month-1']; - setMeetings((current) => { - const existingIds = new Set(current.map((meeting) => meeting.id)); - const missing = defaultMeetings.filter((meeting) => requiredTestIds.includes(meeting.id) && !existingIds.has(meeting.id)); - if (missing.length === 0) { - return current; - } - return [...current, ...missing].sort((left, right) => `${left.dateKey}-${left.start}`.localeCompare(`${right.dateKey}-${right.start}`)); - }); - }, [defaultMeetings]); - const monthLabel = calendarMonth.toLocaleDateString('pl-PL', { month: 'long', year: 'numeric' }); const todayKey = formatDateKey(new Date()); const isPastDateKey = (dateKey: string) => dateKey < todayKey; @@ -10329,237 +9588,6 @@ function AccountListingsPage({ onOpenListing }: { onOpenListing: (id: number) => type ListingTab = 'all' | 'sale' | 'rent' | 'archived'; - const listingDefaults: ListingItem[] = [ - { - id: 'listing-1', - title: 'Mieszkanie 3-pokojowe, 62 m²', - address: 'ul. Mokotowska 45, Warszawa', - area: 62, - rooms: 3, - floor: 4, - price: 850000, - monthlyFees: 0, - views: 245, - saves: 18, - addedDate: '2025-05-10', - image: heroImage, - imagePosition: '47% 58%', - category: 'sale', - status: 'active', - pausedUntil: null, - promoted: false, - }, - { - id: 'listing-2', - title: 'Mieszkanie 2-pokojowe, 45 m²', - address: 'ul. Puławska 10, Warszawa', - area: 45, - rooms: 2, - floor: 2, - price: 650000, - monthlyFees: 0, - views: 312, - saves: 25, - addedDate: '2025-05-02', - image: cityImage, - imagePosition: '52% 55%', - category: 'sale', - status: 'active', - pausedUntil: null, - promoted: true, - }, - { - id: 'listing-3', - title: 'Mieszkanie 4-pokojowe, 78 m²', - address: 'ul. Dobra 12, Warszawa', - area: 78, - rooms: 4, - floor: 5, - price: 1120000, - monthlyFees: 0, - views: 189, - saves: 12, - addedDate: '2025-04-28', - image: loginRoomImage, - imagePosition: '60% 52%', - category: 'sale', - status: 'active', - pausedUntil: null, - promoted: false, - }, - { - id: 'listing-4', - title: 'Mieszkanie 2-pokojowe, 50 m²', - address: 'ul. Krucza 5, Warszawa', - area: 50, - rooms: 2, - floor: 3, - price: 2800, - monthlyFees: 600, - views: 156, - saves: 7, - addedDate: '2025-04-15', - image: heroImage, - imagePosition: '64% 60%', - category: 'rent', - status: 'active', - pausedUntil: null, - promoted: false, - }, - { - id: 'listing-5', - title: 'Kawalerka, 28 m²', - address: 'ul. Złota 65, Warszawa', - area: 28, - rooms: 1, - floor: 10, - price: 2200, - monthlyFees: 500, - views: 98, - saves: 3, - addedDate: '2025-04-05', - image: cityImage, - imagePosition: '56% 48%', - category: 'rent', - status: 'paused', - pausedUntil: null, - promoted: false, - }, - { - id: 'listing-test-1', - title: 'Test: Apartament 3-pokojowy, 71 m²', - address: 'ul. Grzybowska 80, Warszawa', - area: 71, - rooms: 3, - floor: 6, - price: 979000, - monthlyFees: 0, - views: 421, - saves: 44, - addedDate: '2025-06-12', - image: heroImage, - imagePosition: '54% 52%', - category: 'sale', - status: 'active', - pausedUntil: null, - promoted: true, - }, - { - id: 'listing-test-2', - title: 'Test: Mieszkanie 2-pokojowe, 39 m²', - address: 'ul. Płocka 16, Warszawa', - area: 39, - rooms: 2, - floor: 1, - price: 3100, - monthlyFees: 450, - views: 178, - saves: 16, - addedDate: '2025-06-03', - image: cityImage, - imagePosition: '49% 56%', - category: 'rent', - status: 'active', - pausedUntil: null, - promoted: false, - }, - { - id: 'listing-test-3', - title: 'Test: Loft 1-pokojowy, 33 m²', - address: 'ul. Targowa 24, Warszawa', - area: 33, - rooms: 1, - floor: 4, - price: 2600, - monthlyFees: 390, - views: 96, - saves: 9, - addedDate: '2025-05-21', - image: loginRoomImage, - imagePosition: '63% 50%', - category: 'rent', - status: 'archived', - pausedUntil: null, - promoted: false, - }, - { - id: 'listing-test-4', - title: 'Test: Penthouse 5-pokojowy, 122 m²', - address: 'ul. Sienna 39, Warszawa', - area: 122, - rooms: 5, - floor: 11, - price: 1990000, - monthlyFees: 0, - views: 538, - saves: 61, - addedDate: '2025-06-22', - image: heroImage, - imagePosition: '58% 48%', - category: 'sale', - status: 'active', - pausedUntil: null, - promoted: true, - }, - { - id: 'listing-test-5', - title: 'Test: Mieszkanie 3-pokojowe, 67 m²', - address: 'ul. Jana Kazimierza 31, Warszawa', - area: 67, - rooms: 3, - floor: 7, - price: 905000, - monthlyFees: 0, - views: 287, - saves: 29, - addedDate: '2025-06-18', - image: cityImage, - imagePosition: '50% 55%', - category: 'sale', - status: 'active', - pausedUntil: null, - promoted: false, - }, - { - id: 'listing-test-6', - title: 'Test: Mieszkanie 2-pokojowe, 41 m²', - address: 'ul. Chłodna 22, Warszawa', - area: 41, - rooms: 2, - floor: 5, - price: 3200, - monthlyFees: 520, - views: 233, - saves: 21, - addedDate: '2025-06-15', - image: loginRoomImage, - imagePosition: '62% 52%', - category: 'rent', - status: 'active', - pausedUntil: null, - promoted: false, - }, - { - id: 'listing-test-7', - title: 'Test: Studio 1-pokojowe, 30 m²', - address: 'ul. Francuska 14, Warszawa', - area: 30, - rooms: 1, - floor: 2, - price: 2400, - monthlyFees: 380, - views: 143, - saves: 13, - addedDate: '2025-06-09', - image: cityImage, - imagePosition: '57% 49%', - category: 'rent', - status: 'paused', - pausedUntil: '2026-07-10T09:00:00.000Z', - promoted: false, - }, - ]; - const [activeTab, setActiveTab] = useState('all'); const [query, setQuery] = useState(''); const [feedback, setFeedback] = useState(''); @@ -10568,22 +9596,7 @@ function AccountListingsPage({ onOpenListing }: { onOpenListing: (id: number) => const [pauseModalListingId, setPauseModalListingId] = useState(null); const [pauseDuration, setPauseDuration] = useState<'7' | '14' | '30' | 'manual'>('7'); const [promoteModalListingId, setPromoteModalListingId] = useState(null); - const [listings, setListings] = useState(() => { - try { - const raw = window.localStorage.getItem(listingsStorageKey); - if (!raw) { - return listingDefaults; - } - const parsed = JSON.parse(raw); - return Array.isArray(parsed) ? parsed : listingDefaults; - } catch { - return listingDefaults; - } - }); - - useEffect(() => { - window.localStorage.setItem(listingsStorageKey, JSON.stringify(listings)); - }, [listings]); + const [listings, setListings] = useState([]); // Prawdziwe ogloszenia uzytkownika z backendu (/listings/mine). const [apiListings, setApiListings] = useState([]); @@ -10692,18 +9705,6 @@ function AccountListingsPage({ onOpenListing }: { onOpenListing: (id: number) => return () => window.clearInterval(intervalId); }, []); - useEffect(() => { - const requiredTestIds = ['listing-test-1', 'listing-test-2', 'listing-test-3', 'listing-test-4', 'listing-test-5', 'listing-test-6', 'listing-test-7']; - setListings((current) => { - const existingIds = new Set(current.map((listing) => listing.id)); - const missing = listingDefaults.filter((listing) => requiredTestIds.includes(listing.id) && !existingIds.has(listing.id)); - if (missing.length === 0) { - return current; - } - return [...current, ...missing]; - }); - }, []); - useEffect(() => { if (!feedback) { return; @@ -13800,10 +12801,9 @@ function CardSlider({ ariaLabel, children, autoScroll = false }: { ariaLabel: st function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (city: string, mode?: 'buy' | 'rent') => void }) { const navigate = useNavigate(); const { formatPrice, formatPricePerArea, formatArea, showRecentlyViewedOnHome } = useDisplay(); - const [favoriteListings, setFavoriteListings] = useState>(new Set()); const [searchMode, setSearchMode] = useState<'buy' | 'rent'>('buy'); const [recentlyViewed] = useState(() => readRecentlyViewed()); - const { items: liveSaleListings } = useLiveListings('SALE'); + const { items: liveSaleListings, loading: liveSaleLoading } = useLiveListings('SALE'); // Liczba ogloszen na zywo per miasto do sekcji "Przegladaj miasta". Bierzemy wszystkie zatwierdzone // ogloszenia (sprzedaz + wynajem) z /api/listings i grupujemy po znormalizowanej nazwie miasta. @@ -13840,18 +12840,6 @@ function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (ci }; }, []); - const toggleFavoriteListing = (location: string) => { - setFavoriteListings((current) => { - const next = new Set(current); - if (next.has(location)) { - next.delete(location); - } else { - next.add(location); - } - return next; - }); - }; - return ( <>
@@ -13952,9 +12940,16 @@ function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (ci Zobacz wszystkie
+ {liveSaleListings.length === 0 ? ( + !liveSaleLoading && ( +
+ Nie ma jeszcze ogłoszeń sprzedaży. + Dodaj pierwsze ogłoszenie +
+ ) + ) : ( - {liveSaleListings.length >= 4 ? ( - liveSaleListings.slice(0, 12).map((item) => ( + {liveSaleListings.slice(0, 12).map((item) => (
{listingMarketBadge(item.market)} @@ -13968,34 +12963,9 @@ function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (ci
- )) - ) : ( - listings.map((listing, index) => ( -
-
- {listing.badge && {listing.badge}} - -
-
-

{listing.location}

- {`Mieszkanie · ${listingRoomsText(listing.rooms)} · ${formatArea(listing.area)}`} -
- {formatPrice(listing.price)} - {formatPricePerArea(listing.price / listing.area)} -
-
-
- )) - )} + ))}
+ )} @@ -16950,6 +15920,21 @@ function GuidesPage() { .catch(() => { /* brak sieci - licznik pokaże sam wbudowany poradnik */ }); return () => { cancelled = true; }; }, []); + // Polecane artykuly: najczesciej czytane artykuly uzytkownikow ze wszystkich tematow. + // API zwraca artykuly per temat, wiec laczymy odpowiedzi dla kazdego tematu. + const [featuredArticles, setFeaturedArticles] = useState(null); + useEffect(() => { + let cancelled = false; + Promise.all(GUIDE_TOPICS.map((topic) => ( + apiFetch(`/articles?topic=${encodeURIComponent(topic.slug)}`).catch(() => [] as ArticleCard[]) + ))) + .then((perTopic) => { + if (!cancelled) { + setFeaturedArticles(perTopic.flat().sort((left, right) => right.views - left.views).slice(0, 4)); + } + }); + return () => { cancelled = true; }; + }, []); return (
@@ -17009,30 +15994,34 @@ function GuidesPage() {

Polecane artykuły

Zobacz wszystkie -
- {guideArticles.map((article) => ( - -
+ Nie ma jeszcze artykułów użytkowników. + Napisz pierwszy artykuł +
+ ) : ( +
+ {(featuredArticles ?? []).map((article) => ( + - {article.category} -
-
-

{article.title}

-

{article.text}

-
- {article.readTime} - {article.views} +
+ {guideTopicBySlug(article.topicSlug)?.title.toUpperCase() ?? 'PORADNIK'}
-
- - ))} -
+
+

{article.title}

+

{article.excerpt}

+
+ {article.readMinutes} min czytania + {article.views} +
+
+ + ))} +
+ )}
@@ -17043,7 +16032,6 @@ function GuidesPage() {

{offer.title}

- {offer.count} Zobacz oferty
= 3 ? 10 : 0) + (form.amenities.length > 4 ? 8 : 0) + (form.yearBuilt ? 6 : 0) + (form.ownership ? 4 : 0) + (form.propertyType === 'Dom' && form.plotArea ? 4 : 0) + (form.propertyType === 'Lokal użytkowy' && form.commercialPurpose ? 4 : 0)), 50, 90); const propertyLabel = form.propertyType || 'Nieruchomość'; const propertyDetails = [ { icon: 'house', label: 'Typ nieruchomości', value: propertyLabel }, @@ -18508,7 +17493,6 @@ function calculateApartmentValuation(form: ValuationForm, display: ValuationDisp minPerMeter, maxPerMeter, averagePerMeter, - confidence, validUntil, details: [ { icon: 'pin', label: 'Lokalizacja', value: form.location }, @@ -18532,7 +17516,7 @@ function calculateApartmentValuation(form: ValuationForm, display: ValuationDisp }; } -async function downloadValuationReportPdf(form: ValuationForm, valuation: ValuationResult, confidenceLabel: string, fmt: PriceFormatters & { areaSuffix: string; formatPricePerAreaNumber: (plnPerSquareMeter: number) => string }) { +async function downloadValuationReportPdf(form: ValuationForm, valuation: ValuationResult, fmt: PriceFormatters & { areaSuffix: string; formatPricePerAreaNumber: (plnPerSquareMeter: number) => string }) { if (!valuation.ready) { window.alert('Najpierw uzupełnij dane potrzebne do wyceny.'); return; @@ -18637,7 +17621,7 @@ async function downloadValuationReportPdf(form: ValuationForm, valuation: Valuat }, { table: { - widths: ['*', 92], + widths: ['*'], body: [[ { border: [false, false, false, false], @@ -18648,17 +17632,6 @@ async function downloadValuationReportPdf(form: ValuationForm, valuation: Valuat { text: `Zakres za ${fmt.areaSuffix}: ${fmt.formatPricePerAreaNumber(valuation.minPerMeter)} - ${fmt.formatPricePerArea(valuation.maxPerMeter)}`, style: 'cardText' }, ], }, - { - border: [false, false, false, false], - fillColor: '#10a15e', - color: '#ffffff', - alignment: 'center', - margin: [0, 10, 0, 8], - stack: [ - { text: `${valuation.confidence}%`, fontSize: 18, bold: true, margin: [0, 0, 0, 2] }, - { text: confidenceLabel, fontSize: 8, bold: true }, - ], - }, ]], }, layout: { @@ -19411,9 +18384,6 @@ function ValuationPage() { { label: 'Wynik wyceny', done: isResultComplete }, ]; const progressValue = Math.round((valuationCompletionSteps.filter((item) => item.done).length / valuationCompletionSteps.length) * 100); - const valuationConfidenceLabel = valuation.ready - ? valuation.confidence >= 80 ? 'Wysoka dokładność' : valuation.confidence >= 65 ? 'Dobra dokładność' : 'Orientacyjna wycena' - : 'Czeka na dane'; const visibleValuationDrivers = isValuationDetailsStep || isValuationConditionStep ? [ { icon: 'house', title: 'Poznaj wartość', text: 'Sprawdź realną wartość swojej nieruchomości.' }, @@ -19598,7 +18568,7 @@ function ValuationPage() {
- + Rozpocznij sprzedaż
@@ -19614,18 +18584,6 @@ function ValuationPage() { {valuation.ready ? `Wycena ważna do ${valuation.validUntil}` : 'Podaj dane, aby obliczyć wycenę'}
-
-

Porównanie z rynkiem lokalnym

-
- {valuationConfidenceLabel} - {valuation.ready ? `${valuation.confidence}% danych wpływających na wycenę` : 'Podaj lokalizację i metraż, aby policzyć porównanie.'} -
-
- -
-

{valuation.ready ? 'Im więcej szczegółów o lokalu, budynku i stanie prawnym, tym węższy przedział ceny.' : 'Wynik pojawi się po uzupełnieniu podstawowych danych.'}

-
-

Co dalej?

{valuationNextActions.map((action) => ( @@ -21299,7 +20257,7 @@ function MegaSearch({ filters, onOpenMap, onSearch, mode = 'buy' }: { chips, resetAll, } = filters; - const [recent, setRecent] = useState(DEFAULT_RECENT); + const [recent, setRecent] = useState([]); const [openField, setOpenField] = useState<'lokalizacja' | 'rodzaj' | 'cena' | 'metraz' | 'pokoje' | 'rynek' | 'more' | null>(null); const [suggestions, setSuggestions] = useState([]); const [suggestLoading, setSuggestLoading] = useState(false); @@ -21504,15 +20462,19 @@ function MegaSearch({ filters, onOpenMap, onSearch, mode = 'buy' }: { ))} -

Ostatnio wyszukiwane

-
- {recent.map((item) => ( - - ))} -
+ {recent.length > 0 && ( + <> +

Ostatnio wyszukiwane

+
+ {recent.map((item) => ( + + ))} +
+ + )} )} @@ -21853,7 +20815,6 @@ const HISTORY_CITY_BASE_PRICES: Record = { const HISTORY_POPULAR_MARKETS = ['Warszawa, Mokotów', 'Kraków, Zabłocie', 'Wrocław, Krzyki', 'Gdańsk, Wrzeszcz', 'Poznań, Jeżyce']; const HISTORY_PROPERTY_MULTIPLIERS: Record = { Mieszkanie: 1, Dom: 0.78, 'Lokal użytkowy': 1.12 }; const HISTORY_MARKET_MULTIPLIERS: Record = { 'Wtórny i pierwotny': 1, Wtórny: 0.97, Pierwotny: 1.08 }; -const HISTORY_2026_SOURCE_NOTE = 'Dane orientacyjne: wykres pokazuje model rynku 2026 na podstawie lokalizacji i korekt cen za m². Nie jest to gwarancja ceny transakcyjnej ani oficjalna wycena.'; const HISTORY_LOCATION_TYPE_MULTIPLIERS: Record = { dzielnica: 1, miasto: 0.96, @@ -22047,22 +21008,6 @@ function historyTodayLabel(today = historyToday()) { return new Intl.DateTimeFormat('pl-PL', { day: '2-digit', month: '2-digit', year: 'numeric' }).format(today); } -function generateHistorySeries(market: PriceHistoryMarket, propertyType: HistoryPropertyType, marketType: HistoryMarketType, period: HistoryPeriod, pointCount: number) { - void period; - const months = Math.max(1, pointCount); - const latest = market.apartmentM2 * HISTORY_PROPERTY_MULTIPLIERS[propertyType] * HISTORY_MARKET_MULTIPLIERS[marketType]; - const elapsedMonths = Math.max(0, months - 1); - const periodTrend = market.trend24 * (elapsedMonths / 24); - const start = latest / (1 + periodTrend); - return Array.from({ length: months }, (_, index) => { - const progress = months === 1 ? 1 : index / (months - 1); - const seasonal = Math.sin((index + market.key.length) * 0.82) * market.volatility * 0.34; - const localDip = Math.exp(-Math.pow(progress - 0.32, 2) / 0.012) * market.volatility * -0.55; - const recovery = progress > 0.66 ? (progress - 0.66) * market.volatility * 0.45 : 0; - return Math.round(start + (latest - start) * progress + latest * (seasonal + localDip + recovery)); - }); -} - function historyTicks(minPrice: number, maxPrice: number) { if (minPrice === 0 && maxPrice === 0) { return [0]; @@ -22114,20 +21059,14 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) { }); const shownSuggestions = mergeHistorySuggestions(localSuggestions, suggestions); const hasSelectedMarket = selectedMarket != null; - const activeMarket = selectedMarket; const activeLocationLabel = selectedMarket?.label ?? 'wybranej lokalizacji'; const todayLabel = historyTodayLabel(); const monthLabels = historyMonthLabels(); - const priceHistoryPoints = activeMarket ? generateHistorySeries(activeMarket, propertyType, marketType, period, monthLabels.length) : Array.from({ length: monthLabels.length }, () => 0); - const latestPrice = priceHistoryPoints[priceHistoryPoints.length - 1]; - const firstPrice = priceHistoryPoints[0]; + // Historia cen nie ma jeszcze zrodla danych - wykres i statystyki zostaja puste do czasu podpiecia API. + const priceHistoryPoints = Array.from({ length: monthLabels.length }, () => 0); + const hasHistoryData = priceHistoryPoints.some((price) => price > 0); const minPrice = Math.min(...priceHistoryPoints); const maxPrice = Math.max(...priceHistoryPoints); - const averagePrice = Math.round(priceHistoryPoints.reduce((sum, price) => sum + price, 0) / priceHistoryPoints.length); - const changePercent = firstPrice > 0 ? ((latestPrice - firstPrice) / firstPrice) * 100 : 0; - const changeValue = latestPrice - firstPrice; - const minIndex = priceHistoryPoints.indexOf(minPrice); - const maxIndex = priceHistoryPoints.indexOf(maxPrice); const tickValues = historyTicks(minPrice, maxPrice); const chartValues = priceHistoryPoints; const chartTicks = tickValues; @@ -22135,14 +21074,6 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) { const chartAxisMax = maxPrice; // Tabela porownania to ostatnie sprawdzone lokalizacje tego konta - bez podpowiedzi z listy. const displayedComparisons = recentMarkets; - const trendLabel = changePercent >= 6 ? 'Trend wzrostowy' : changePercent >= 1 ? 'Rynek stabilny z lekkim wzrostem' : 'Korekta cenowa'; - const trendText = !hasSelectedMarket - ? `Wpisz i wybierz lokalizację, żeby zobaczyć realny przebieg cen za ${areaSuffix} w modelu 2026.` - : changePercent >= 6 - ? `Ceny w tej lokalizacji wzrosły o ${changePercent.toFixed(1).replace('.', ',')}% od stycznia do ${todayLabel}.` - : changePercent >= 1 - ? `Ceny zmieniły się umiarkowanie: ${changePercent.toFixed(1).replace('.', ',')}% od stycznia do ${todayLabel}.` - : `Widać spadek o ${Math.abs(changePercent).toFixed(1).replace('.', ',')}% od stycznia do ${todayLabel}.`; const chartWidth = 920; const chartHeight = 250; const chartPaddingTop = 86; @@ -22440,15 +21371,13 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) {

Średnia cena za {areaSuffix} w {activeLocationLabel}

- {formatPricePerArea(latestPrice)} -

= 0 ? 'positive' : 'negative'}>{changePercent >= 0 ? '+' : ''}{changePercent.toFixed(1).replace('.', ',')}% ({HISTORY_PERIOD_OPTIONS.find((option) => option.value === period)?.label.toLowerCase()})

- {HISTORY_2026_SOURCE_NOTE} + —
Cena za {areaSuffix}
setHoveredHistoryIndex(null)} > @@ -22464,15 +21393,19 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) { - {chartTicks.map((tick) => { - const range = chartAxisMax - chartAxisMin || 1; - const y = chartPaddingTop + (1 - (tick - chartAxisMin) / range) * chartInnerHeight; - const labelY = Math.max(13, Math.min(chartHeight - 10, y - 7)); - return {formatPricePerArea(tick)}; - })} - - - {pointCoords.map((point, index) => )} + {hasHistoryData && ( + <> + {chartTicks.map((tick) => { + const range = chartAxisMax - chartAxisMin || 1; + const y = chartPaddingTop + (1 - (tick - chartAxisMin) / range) * chartInnerHeight; + const labelY = Math.max(13, Math.min(chartHeight - 10, y - 7)); + return {formatPricePerArea(tick)}; + })} + + + {pointCoords.map((point, index) => )} + + )} {hasSelectedMarket && hoveredPoint && } {hasSelectedMarket && hoveredPoint && ( @@ -22480,15 +21413,15 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) { )} - {!hasSelectedMarket &&
{formatPricePerArea(0)}Wpisz lokalizację, żeby wczytać ceny 2026 dla miasta, mniejszej miejscowości albo wsi.
} +
Brak danychHistoria cen będzie dostępna wkrótce.
{monthLabels.map((month, index) => {month})}
-
Najniższa cena 2026 do dziś{formatPricePerArea(minPrice)}{monthLabels[minIndex] || 'w okresie'}
-
Najwyższa cena 2026 do dziś{formatPricePerArea(maxPrice)}{monthLabels[maxIndex] || 'w okresie'}
-
= 0 ? 'positive' : 'negative'}>Zmiana od stycznia{changePercent >= 0 ? '+' : ''}{changePercent.toFixed(1).replace('.', ',')}%{changeValue >= 0 ? '+' : ''}{formatPricePerArea(changeValue)}
-
Średnia cena 2026 do dziś{formatPricePerArea(averagePrice)}
+
Najniższa cena—
+
Najwyższa cena—
+
Zmiana od stycznia—
+
Średnia cena—
@@ -22510,11 +21443,7 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) {

Wnioski z analizy

-
-

{trendLabel}{trendText}

-

Najniższy punkt wykresuNajkorzystniejszy poziom od stycznia do {todayLabel} to {formatPricePerArea(minPrice)} ({monthLabels[minIndex] || 'w okresie'}).

-

Warto obserwować{hasSelectedMarket ? `${propertyType} na rynku „${marketType}” ma aktualnie średnią ${formatPricePerArea(latestPrice)} w lokalizacji ${activeLocationLabel}.` : 'Po wyborze lokalizacji pokażemy średnią cenę za jednostkę powierzchni, zmianę i punkty minimum/maksimum dla modelu 2026.'}

-
+

Brak danych do analizy.

@@ -22531,7 +21460,7 @@ function PriceHistoryPage({ user }: { user: AuthUser | null }) { onClick={() => applyMarket(market)} > {historyShortMarketLabel(market)} - {formatPricePerArea(Math.round(market.apartmentM2 * HISTORY_PROPERTY_MULTIPLIERS[propertyType] * HISTORY_MARKET_MULTIPLIERS[marketType]))} + — ({ - title: offer.title, - city: offer.city, - district: offer.district, - rooms: offer.rooms, - area: offer.area, - priceValue: offer.priceValue, - market: offer.market, - tags: offer.tags, - image: 'hero', - meta: `${roomsLabel(offer.rooms)} • ${display.formatArea(offer.area)} • Najem`, - })) - : buyListings.map((offer) => ({ - title: offer.title, - city: offer.city, - district: offer.district, - rooms: offer.rooms, - area: offer.area, - priceValue: offer.priceValue, - market: offer.market, - tags: offer.tags, - image: offer.image, - meta: offerMeta(offer, display), - })); - - const sourceOffers = liveListings.length > 0 - ? liveListings.map((listing) => toMapOfferPreview(listing, listing, display)) - : fallbackOffers; + const sourceOffers = liveListings.map((listing) => toMapOfferPreview(listing, listing, display)); const results = sourceOffers .map((offer, index) => ({ offer, index })) @@ -25306,122 +24207,6 @@ function MessagesPage({ { id: 'other', icon: 'dots', title: 'Inny powód', description: 'Coś innego, o czym naszym zdaniem powinniśmy wiedzieć.' }, ] as const; - const seedNow = Date.now(); - const makeMessage = ( - id: string, - text: string, - time: string, - mine: boolean, - minutesAgo: number, - extra?: { readByRecipient?: boolean; unread?: boolean }, - ): ChatMessage => ({ - id, - text, - time, - mine, - sentAt: seedNow - (minutesAgo * 60 * 1000), - ...extra, - }); - - const initialThreads: ChatThread[] = [ - { - id: 'anna', - name: 'Anna Nowak', - avatarImage: loginRoomImage, - status: 'Ostatnio aktywna dziś o 11:20', - listing: { - listingId: 1, - title: 'Mieszkanie 3-pokojowe', - price: 748000, - city: 'Warszawa', - district: 'Mokotów, Stegny', - area: 62, - rooms: 3, - floor: '4', - buildingFloors: '8', - thumbnail: heroImage, - }, - messages: [ - makeMessage('anna-1', 'Dzień dobry, czy mieszkanie jest nadal dostępne?', '11:20', false, 34, { unread: true }), - makeMessage('anna-2', 'Dzień dobry! Tak, mieszkanie jest nadal dostępne.', '11:21', true, 33, { readByRecipient: true }), - makeMessage('anna-3', 'Świetnie! Czy moglibyśmy umówić się na prezentację w tym tygodniu?', '11:22', false, 32, { unread: true }), - makeMessage('anna-4', 'Oczywiście, jestem dostępny w czwartek po 17:00 albo w piątek po 16:00. Która opcja Panią interesuje?', '11:23', true, 31, { readByRecipient: true }), - makeMessage('anna-5', 'Czwartek po 17:00 będzie idealnie. Do zobaczenia!', '11:24', false, 30), - ], - }, - { - id: 'piotr', - name: 'Piotr Kowalski', - avatarImage: cityImage, - status: 'Ostatnio aktywny wczoraj', - listing: null, - messages: [ - makeMessage('piotr-1', 'Dziękuję za odpowiedź! Chętnie umówię się na prezentację.', 'Wczoraj', false, 250, { unread: true }), - makeMessage('piotr-2', 'Mam wolny termin w środę po 18:00.', 'Wczoraj', true, 249, { readByRecipient: false }), - makeMessage('piotr-3', 'Czy ten termin jest nadal aktualny?', '11:31', false, 28, { unread: true }), - ], - }, - { - id: 'marta', - name: 'Marta Wiśniewska', - avatarImage: heroImage, - status: 'Ostatnio aktywna 22.05', - listing: null, - messages: [ - makeMessage('marta-1', 'Czy istnieje możliwość negocjacji ceny?', '22.05', false, 700), - makeMessage('marta-2', 'Tak, możemy rozmawiać o cenie po obejrzeniu mieszkania.', '22.05', true, 699, { readByRecipient: true }), - makeMessage('marta-3', 'Rozumiem, dziękuję. Czy miejsce parkingowe jest w cenie?', '22.05', false, 698), - makeMessage('marta-4', 'Dopytam jeszcze: czy jest komórka lokatorska?', '10:58', false, 61, { unread: true }), - ], - }, - { - id: 'kamil', - name: 'Kamil Zieliński', - avatarImage: cityImage, - status: 'Ostatnio aktywny 21.05', - listing: null, - messages: [ - makeMessage('kamil-1', 'Super, dziękuję. Do usłyszenia!', '21.05', false, 940), - makeMessage('kamil-2', 'W razie pytań jestem dostępny od 9:00 do 18:00.', '21.05', true, 939, { readByRecipient: false }), - makeMessage('kamil-3', 'Cześć, czy mogę zadzwonić dziś po 17:00?', '09:44', false, 135, { unread: true }), - ], - }, - { - id: 'joanna', - name: 'Joanna Wójcik', - avatarImage: loginRoomImage, - status: 'Ostatnio aktywna 20.05', - listing: null, - messages: [ - makeMessage('joanna-1', 'Wysłałaś zdjęcie', '20.05', false, 1180), - makeMessage('joanna-2', 'Dziękuję, zdjęcia są bardzo pomocne.', '20.05', true, 1179, { readByRecipient: true }), - makeMessage('joanna-3', 'Czy mogę prosić jeszcze o rzut mieszkania?', '20.05', false, 1178, { unread: true }), - ], - }, - { - id: 'tomasz', - name: 'Tomasz Lewandowski', - avatarImage: heroImage, - status: 'Ostatnio aktywny 19.05', - listing: null, - messages: [ - makeMessage('tomasz-1', 'Dzień dobry, interesuje mnie to mieszkanie.', '19.05', false, 1390), - ], - }, - { - id: 'paulina', - name: 'Paulina Kaczmarek', - avatarImage: loginRoomImage, - status: 'Ostatnio aktywna 18.05', - listing: null, - messages: [ - makeMessage('paulina-1', 'Dziękuję za informacje.', '18.05', false, 1650), - makeMessage('paulina-2', 'Nie ma sprawy. Proszę dać znać, jeśli będzie Pani zainteresowana spotkaniem.', '18.05', true, 1649, { readByRecipient: true }), - makeMessage('paulina-3', 'Jasne, odezwę się jutro rano.', '18.05', false, 1648), - ], - }, - ]; - const [query, setQuery] = useState(''); const [activeFilter, setActiveFilter] = useState('all'); const [isFiltersOpen, setIsFiltersOpen] = useState(false); @@ -25450,8 +24235,8 @@ function MessagesPage({ const [reportSubmittedReasonTitle, setReportSubmittedReasonTitle] = useState(''); const [isSubmittingReport, setIsSubmittingReport] = useState(false); const [reportSubmitError, setReportSubmitError] = useState(''); - const [threads, setThreads] = useState(initialThreads); - const [activeThreadId, setActiveThreadId] = useState('anna'); + const [threads, setThreads] = useState([]); + const [activeThreadId, setActiveThreadId] = useState(''); const attachmentInputRef = useRef(null); const reportAttachmentInputRef = useRef(null); const { user, updateProfile } = useAuth(); @@ -25542,26 +24327,6 @@ function MessagesPage({ const unreadThreadsCount = threadItems.filter((thread) => thread.unreadCount > 0).length; const readThreadsCount = threadItems.length - unreadThreadsCount; - const activeThread = filteredThreads.find((thread) => thread.id === activeThreadId) - ?? threadItems.find((thread) => thread.id === activeThreadId) - ?? threadItems[0]; - - const activeThreadMessages = threads.find((thread) => thread.id === activeThread.id)?.messages ?? []; - const activeThreadListing = activeThread.listing; - const activeThreadListingFloor = activeThreadListing ? listingFloorText(activeThreadListing.floor, activeThreadListing.buildingFloors) : null; - const activeThreadListingLocation = activeThreadListing - ? [activeThreadListing.city, activeThreadListing.district].filter(Boolean).join(', ') - : ''; - const activeThreadListingSpecs = activeThreadListing - ? `${formatArea(activeThreadListing.area)} • ${listingRoomsText(activeThreadListing.rooms)}${activeThreadListingFloor ? ` • ${activeThreadListingFloor} piętro` : ''}` - : ''; - const isActiveThreadBlocked = blockedThreadIds.includes(activeThread.id); - const selectedReportReasonDetails = reportReasons.find((reason) => reason.id === selectedReportReason); - const isOtherReasonSelected = selectedReportReason === 'other'; - const normalizedReportDetails = reportDetails.trim(); - const needsMoreDetails = isOtherReasonSelected && normalizedReportDetails.length < 10; - const canSubmitReport = !isSubmittingReport && !reportAttachmentError && !needsMoreDetails; - useEffect(() => { if (filteredThreads.length === 0) { return; @@ -25646,6 +24411,47 @@ function MessagesPage({ onThreadTargetHandled(); }, [initialThreadTarget, onThreadTargetHandled]); + const activeThread = filteredThreads.find((thread) => thread.id === activeThreadId) + ?? threadItems.find((thread) => thread.id === activeThreadId) + ?? threadItems[0] as (typeof threadItems)[number] | undefined; + + // Bez zadnej rozmowy (np. zanim wczyta sie watek z administracja) nie ma czego pokazac. + if (!activeThread) { + return ( +
+
+
+
+ Panel główny + + Wiadomości +
+

Wiadomości

+
+
+ Nie masz jeszcze żadnych wiadomości. +
+
+
+ ); + } + + const activeThreadMessages = threads.find((thread) => thread.id === activeThread.id)?.messages ?? []; + const activeThreadListing = activeThread.listing; + const activeThreadListingFloor = activeThreadListing ? listingFloorText(activeThreadListing.floor, activeThreadListing.buildingFloors) : null; + const activeThreadListingLocation = activeThreadListing + ? [activeThreadListing.city, activeThreadListing.district].filter(Boolean).join(', ') + : ''; + const activeThreadListingSpecs = activeThreadListing + ? `${formatArea(activeThreadListing.area)} • ${listingRoomsText(activeThreadListing.rooms)}${activeThreadListingFloor ? ` • ${activeThreadListingFloor} piętro` : ''}` + : ''; + const isActiveThreadBlocked = blockedThreadIds.includes(activeThread.id); + const selectedReportReasonDetails = reportReasons.find((reason) => reason.id === selectedReportReason); + const isOtherReasonSelected = selectedReportReason === 'other'; + const normalizedReportDetails = reportDetails.trim(); + const needsMoreDetails = isOtherReasonSelected && normalizedReportDetails.length < 10; + const canSubmitReport = !isSubmittingReport && !reportAttachmentError && !needsMoreDetails; + const markIncomingAsRead = (threadId: string) => { setThreads((current) => current.map((thread) => { if (thread.id !== threadId) { diff --git a/frontend/src/routes.ts b/frontend/src/routes.ts index e23d8d5..1c59bc5 100644 --- a/frontend/src/routes.ts +++ b/frontend/src/routes.ts @@ -86,11 +86,6 @@ export function guideAddPath(topicSlug?: string): string { return topicSlug ? `${ROUTES.guideAdd}?temat=${encodeURIComponent(topicSlug)}` : ROUTES.guideAdd; } -// Zgodność wstecz: mapa tytuł tematu -> wbudowany poradnik (używana przy kartach polecanych artykułów). -export const GUIDE_TOPIC_ROUTES: Record = Object.fromEntries( - GUIDE_TOPICS.map((topic) => [topic.title, topic.guide]), -); - // Szczegóły oferty: identyfikator trafia do adresu, nie do stanu Reacta. export function listingPath(id: number): string { return `/oferta/${id}`;