Martwe przyciski: podepnij pod prawdziwe strony albo oznacz "Wkrotce"
- AI Dopasowanie, "Zobacz kupujacych", kafelek 360 i Konto Premium: nieaktywne z plakietka "Wkrotce" zamiast przyciskow bez akcji. - Pomoc: jeden adres info@polskalokalnie.pl i prawdziwy numer +48 453 193 605 jako linki mailto/tel, czat na zywo usuniety, FAQ z prawdziwymi odpowiedziami zamiast jednego zdania. - Poradniki: dzialajaca wyszukiwarka (?szukaj= w adresie), szybkie poradniki linkuja do rozdzialow, martwe strzalki karuzeli usuniete. - Mapa: "Szukaj na biezacej mapie" zaweza liste do widocznych ofert (?obszar= w adresie). - Porownanie: wybrane oferty w adresie (?oferty=), "Udostepnij porownanie" kopiuje link. - Kafelek miast prowadzi na mape, karuzela opinii bez atrap strzalek. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+282
-90
@@ -2129,31 +2129,37 @@ function articleCountLabel(count: number): string {
|
|||||||
return `${count} artykułów`;
|
return `${count} artykułów`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skroty do wbudowanych poradnikow - kazdy prowadzi do rozdzialu, ktory opisuje dany temat.
|
||||||
const quickGuides = [
|
const quickGuides = [
|
||||||
{
|
{
|
||||||
icon: 'scale',
|
icon: 'scale',
|
||||||
title: 'Jak sprawdzić księgę wieczystą?',
|
title: 'Jak sprawdzić księgę wieczystą?',
|
||||||
text: 'Poradnik krok po kroku',
|
text: 'Poradnik krok po kroku',
|
||||||
|
to: ROUTES.guideLaw,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'percent',
|
icon: 'percent',
|
||||||
title: 'Podatek od sprzedaży mieszkania',
|
title: 'Podatek od sprzedaży mieszkania',
|
||||||
text: 'Kiedy trzeba zapłacić?',
|
text: 'Kiedy trzeba zapłacić?',
|
||||||
|
to: ROUTES.guideLaw,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'paint',
|
icon: 'paint',
|
||||||
title: 'Ile kosztuje remont mieszkania?',
|
title: 'Ile kosztuje remont mieszkania?',
|
||||||
text: 'Aktualne koszty robocizny i materiałów',
|
text: 'Aktualne koszty robocizny i materiałów',
|
||||||
|
to: ROUTES.guideRenovation,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'shield',
|
icon: 'shield',
|
||||||
title: 'Jak bezpiecznie wynająć mieszkanie?',
|
title: 'Jak bezpiecznie wynająć mieszkanie?',
|
||||||
text: 'Porady dla wynajmujących',
|
text: 'Porady dla wynajmujących',
|
||||||
|
to: ROUTES.guideRent,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'chart',
|
icon: 'chart',
|
||||||
title: 'PCC przy zakupie mieszkania',
|
title: 'PCC przy zakupie mieszkania',
|
||||||
text: 'Stawki i zwolnienia w 2026 roku',
|
text: 'Stawki i zwolnienia w 2026 roku',
|
||||||
|
to: ROUTES.guideLaw,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -6497,10 +6503,10 @@ function AccountSidebar() {
|
|||||||
|
|
||||||
<div className="account-premium-card">
|
<div className="account-premium-card">
|
||||||
<div>
|
<div>
|
||||||
<strong><Icon name="coin" /> Konto Premium</strong>
|
<strong><Icon name="coin" /> Konto Premium <em className="soon-tag">Wkrótce</em></strong>
|
||||||
<p>Zyskaj dostęp do dodatkowych narzędzi i raportów.</p>
|
<p>Zyskaj dostęp do dodatkowych narzędzi i raportów.</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button">Sprawdź korzyści</button>
|
<button type="button" disabled title={SOON_HINT}>Sprawdź korzyści</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
);
|
);
|
||||||
@@ -6816,7 +6822,6 @@ function AccountDashboardPage({
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="account-tools-more">Zobacz wszystkie narzędzia <Icon name="arrow" /></button>
|
|
||||||
</section>
|
</section>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@@ -7180,23 +7185,16 @@ function AccountHelpContactPage({
|
|||||||
text: string;
|
text: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Kanaly kontaktu: e-mail i telefon otwieraja program pocztowy albo dzwonia, formularz jest nizej na stronie.
|
||||||
const supportChannels = [
|
const supportChannels = [
|
||||||
{
|
|
||||||
id: 'live-chat',
|
|
||||||
icon: 'message',
|
|
||||||
iconTone: 'green',
|
|
||||||
title: 'Czat na żywo',
|
|
||||||
badge: 'Najszybsza pomoc',
|
|
||||||
description: 'Porozmawiaj z naszym konsultantem w czasie rzeczywistym.',
|
|
||||||
action: 'Dostępny 8:00 - 20:00',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'email',
|
id: 'email',
|
||||||
icon: 'mail',
|
icon: 'mail',
|
||||||
iconTone: 'blue',
|
iconTone: 'blue',
|
||||||
title: 'E-mail',
|
title: 'E-mail',
|
||||||
description: 'Napisz do nas, odpowiemy najszybciej jak to możliwe.',
|
description: 'Napisz do nas, odpowiemy najszybciej jak to możliwe.',
|
||||||
action: 'bok@polskalokalnie.pl',
|
action: SUPPORT_EMAIL,
|
||||||
|
href: `mailto:${SUPPORT_EMAIL}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'phone',
|
id: 'phone',
|
||||||
@@ -7204,7 +7202,8 @@ function AccountHelpContactPage({
|
|||||||
iconTone: 'violet',
|
iconTone: 'violet',
|
||||||
title: 'Telefon',
|
title: 'Telefon',
|
||||||
description: 'Zadzwoń do nas, chętnie odpowiemy na Twoje pytania.',
|
description: 'Zadzwoń do nas, chętnie odpowiemy na Twoje pytania.',
|
||||||
action: '+48 22 123 45 67',
|
action: SUPPORT_PHONE,
|
||||||
|
href: SUPPORT_PHONE_HREF,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'form',
|
id: 'form',
|
||||||
@@ -7216,14 +7215,33 @@ function AccountHelpContactPage({
|
|||||||
},
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
// Odpowiedzi opisuja to, co serwis naprawde robi - kazda prowadzi do wlasciwej strony.
|
||||||
const faqItems = [
|
const faqItems = [
|
||||||
'Jak dodać ogłoszenie?',
|
{
|
||||||
'Ile kosztuje dodanie ogłoszenia?',
|
question: 'Jak dodać ogłoszenie?',
|
||||||
'Jak wyróżnić moje ogłoszenie?',
|
answer: <>Wypełnij formularz na stronie <Link to={ROUTES.add}>Dodaj ogłoszenie</Link>. Po wysłaniu ogłoszenie czeka na akceptację obsługi i dopiero potem pojawia się na liście ofert.</>,
|
||||||
'Jak edytować lub usunąć ogłoszenie?',
|
},
|
||||||
'Jak działa porównywarka mieszkań?',
|
{
|
||||||
'Jak ustawić alerty cenowe?',
|
question: 'Ile kosztuje dodanie ogłoszenia?',
|
||||||
] as const;
|
answer: <>Nic. Dodanie i publikacja ogłoszenia są bezpłatne, nie pobieramy prowizji od sprzedaży. Płatne jest tylko dobrowolne wyróżnienie oferty.</>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Jak wyróżnić moje ogłoszenie?',
|
||||||
|
answer: <>W panelu <Link to={ROUTES.accountListings}>Moje oferty</Link> kliknij „Wyróżnij” przy zaakceptowanym ogłoszeniu. Wybierzesz czas wyróżnienia i zapłacisz przez AutoPay albo użyjesz kredytów z wcześniej kupionego pakietu.</>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Jak edytować lub usunąć ogłoszenie?',
|
||||||
|
answer: <>Obie opcje są w panelu <Link to={ROUTES.accountListings}>Moje oferty</Link>, przy każdym ogłoszeniu. Po edycji ogłoszenie ponownie trafia do akceptacji.</>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Jak działa porównywarka mieszkań?',
|
||||||
|
answer: <>Na liście ofert dodajesz mieszkania do porównania, a w <Link to={ROUTES.comparison}>Porównaniu</Link> widzisz je obok siebie: cenę, metraż, cenę za m² i pozostałe parametry.</>,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: 'Jak ustawić alerty cenowe?',
|
||||||
|
answer: <>Alert ceny ogłoszenia założysz przyciskiem na karcie oferty, a alert lokalizacji w <Link to={ROUTES.priceHistory}>Historii cen</Link>. Wszystkie zobaczysz w panelu <Link to={ROUTES.accountPriceAlerts}>Alerty cenowe</Link>. Nie wysyłamy jeszcze powiadomień e-mail.</>,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const moreHelpCards = [
|
const moreHelpCards = [
|
||||||
{
|
{
|
||||||
@@ -7421,18 +7439,24 @@ function AccountHelpContactPage({
|
|||||||
<span className={`help-contact-channel-icon ${channel.iconTone}`}><Icon name={channel.icon} /></span>
|
<span className={`help-contact-channel-icon ${channel.iconTone}`}><Icon name={channel.icon} /></span>
|
||||||
<div>
|
<div>
|
||||||
<h3>{channel.title}</h3>
|
<h3>{channel.title}</h3>
|
||||||
{'badge' in channel && channel.badge ? <em>{channel.badge}</em> : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p>{channel.description}</p>
|
<p>{channel.description}</p>
|
||||||
|
{'href' in channel && channel.href ? (
|
||||||
|
<a className="help-contact-channel-action" href={channel.href}>
|
||||||
|
{channel.action}
|
||||||
|
<Icon name="chevron" />
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="help-contact-channel-action"
|
className="help-contact-channel-action"
|
||||||
onClick={channel.id === 'form' ? highlightContactFormForThreeSeconds : undefined}
|
onClick={highlightContactFormForThreeSeconds}
|
||||||
>
|
>
|
||||||
{channel.action}
|
{channel.action}
|
||||||
<Icon name="chevron" />
|
<Icon name="chevron" />
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -7442,16 +7466,16 @@ function AccountHelpContactPage({
|
|||||||
<article className="help-contact-faq-card">
|
<article className="help-contact-faq-card">
|
||||||
<header>
|
<header>
|
||||||
<h2>Najczęściej zadawane pytania</h2>
|
<h2>Najczęściej zadawane pytania</h2>
|
||||||
<button type="button">Zobacz wszystkie <Icon name="arrow" /></button>
|
<Link to={ROUTES.guides}>Poradniki <Icon name="arrow" /></Link>
|
||||||
</header>
|
</header>
|
||||||
<div className="help-contact-faq-list">
|
<div className="help-contact-faq-list">
|
||||||
{faqItems.map((question) => (
|
{faqItems.map((item) => (
|
||||||
<details key={question}>
|
<details key={item.question}>
|
||||||
<summary>
|
<summary>
|
||||||
<span>{question}</span>
|
<span>{item.question}</span>
|
||||||
<Icon name="chevron" />
|
<Icon name="chevron" />
|
||||||
</summary>
|
</summary>
|
||||||
<p>Znajdziesz odpowiedź w naszym centrum pomocy. W razie potrzeby skorzystaj z formularza kontaktowego obok.</p>
|
<p>{item.answer}</p>
|
||||||
</details>
|
</details>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -8213,7 +8237,7 @@ function AccountPriceAlertsPage({
|
|||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
<h2>Twoje alerty</h2>
|
<h2>Twoje alerty</h2>
|
||||||
<p>Obserwowane ogłoszenia — powiadomimy Cię, gdy zmieni się ich cena</p>
|
<p>Obserwowane ogłoszenia — zmiany ich cen zobaczysz tutaj</p>
|
||||||
</div>
|
</div>
|
||||||
<Link className="price-alerts-create" to={ROUTES.buy}>
|
<Link className="price-alerts-create" to={ROUTES.buy}>
|
||||||
<Icon name="plus" /> Dodaj z ogłoszenia
|
<Icon name="plus" /> Dodaj z ogłoszenia
|
||||||
@@ -8367,14 +8391,40 @@ function AccountComparisonPage({ onOpenListing }: { onOpenListing: (id: number)
|
|||||||
const [propertyTypeFilter, setPropertyTypeFilter] = useState<ComparisonPropertyFilter>('ALL');
|
const [propertyTypeFilter, setPropertyTypeFilter] = useState<ComparisonPropertyFilter>('ALL');
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
const [pickerValue, setPickerValue] = useState<number | ''>('');
|
const [pickerValue, setPickerValue] = useState<number | ''>('');
|
||||||
const [selectedIds, setSelectedIds] = useState<number[]>([]);
|
// Wybrane oferty siedza w adresie (?oferty=1,4), wiec porownanie przezywa odswiezenie
|
||||||
|
// i da sie je wyslac linkiem ("Udostepnij porownanie").
|
||||||
|
const [comparisonParams, setComparisonParams] = useSearchParams();
|
||||||
|
const selectedIds = (comparisonParams.get('oferty') ?? '')
|
||||||
|
.split(',')
|
||||||
|
.map(Number)
|
||||||
|
.filter((id) => Number.isInteger(id) && id > 0);
|
||||||
|
const setSelectedIds = (update: number[] | ((current: number[]) => number[])) => {
|
||||||
|
const next = typeof update === 'function' ? update(selectedIds) : update;
|
||||||
|
setComparisonParams((prev) => {
|
||||||
|
const params = new URLSearchParams(prev);
|
||||||
|
if (next.length > 0) {
|
||||||
|
params.set('oferty', next.join(','));
|
||||||
|
} else {
|
||||||
|
params.delete('oferty');
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}, { replace: true });
|
||||||
|
};
|
||||||
const [selectedDetails, setSelectedDetails] = useState<Record<number, ApiListingDetail>>({});
|
const [selectedDetails, setSelectedDetails] = useState<Record<number, ApiListingDetail>>({});
|
||||||
|
const [shareInfo, setShareInfo] = useState('');
|
||||||
const [valuesGridOffset, setValuesGridOffset] = useState(0);
|
const [valuesGridOffset, setValuesGridOffset] = useState(0);
|
||||||
const summaryFirstMetricRef = useRef<HTMLElement | null>(null);
|
const summaryFirstMetricRef = useRef<HTMLElement | null>(null);
|
||||||
const firstValueCellRef = useRef<HTMLDivElement | null>(null);
|
const firstValueCellRef = useRef<HTMLDivElement | null>(null);
|
||||||
const { items, loading } = useLiveListings(offerType);
|
const { items, loading } = useLiveListings(offerType);
|
||||||
|
|
||||||
|
// Przelaczenie sprzedaz/najem czysci wybor, ale nie przy pierwszym wejsciu - inaczej
|
||||||
|
// skasowaloby oferty podane w adresie (udostepniony link).
|
||||||
|
const comparisonMountedRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!comparisonMountedRef.current) {
|
||||||
|
comparisonMountedRef.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSelectedIds([]);
|
setSelectedIds([]);
|
||||||
setPickerValue('');
|
setPickerValue('');
|
||||||
setSearch('');
|
setSearch('');
|
||||||
@@ -8382,8 +8432,12 @@ function AccountComparisonPage({ onOpenListing }: { onOpenListing: (id: number)
|
|||||||
}, [offerType]);
|
}, [offerType]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Dopiero gdy lista ofert dotarla - na pustej liscie skasowalibysmy wybor z adresu.
|
||||||
|
if (loading || items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setSelectedIds((current) => current.filter((id) => items.some((item) => item.id === id)));
|
setSelectedIds((current) => current.filter((id) => items.some((item) => item.id === id)));
|
||||||
}, [items]);
|
}, [items, loading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedIds.length === 0) {
|
if (selectedIds.length === 0) {
|
||||||
@@ -8773,10 +8827,23 @@ function AccountComparisonPage({ onOpenListing }: { onOpenListing: (id: number)
|
|||||||
<p>Porównaj do 4 mieszkań i wybierz najlepszą ofertę dla siebie.</p>
|
<p>Porównaj do 4 mieszkań i wybierz najlepszą ofertę dla siebie.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="comparison-head-actions">
|
<div className="comparison-head-actions">
|
||||||
<button type="button" className="ghost">Udostępnij porównanie</button>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="ghost"
|
||||||
|
disabled={selectedIds.length === 0}
|
||||||
|
onClick={() => {
|
||||||
|
const link = window.location.href;
|
||||||
|
navigator.clipboard?.writeText(link)
|
||||||
|
.then(() => setShareInfo('Skopiowano link do porównania.'))
|
||||||
|
.catch(() => setShareInfo(link));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Udostępnij porównanie
|
||||||
|
</button>
|
||||||
<button type="button" onClick={() => setSelectedIds([])} disabled={selectedIds.length === 0}>Wyczyść wszystko</button>
|
<button type="button" onClick={() => setSelectedIds([])} disabled={selectedIds.length === 0}>Wyczyść wszystko</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{shareInfo && <p className="comparison-share-info" role="status">{shareInfo}</p>}
|
||||||
|
|
||||||
<div className="comparison-workspace">
|
<div className="comparison-workspace">
|
||||||
<aside className="comparison-summary-column">
|
<aside className="comparison-summary-column">
|
||||||
@@ -13045,6 +13112,7 @@ function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (ci
|
|||||||
<div className="ai-label">
|
<div className="ai-label">
|
||||||
<Icon name="spark" />
|
<Icon name="spark" />
|
||||||
<span>Zainteresowani kupujący</span>
|
<span>Zainteresowani kupujący</span>
|
||||||
|
<em className="soon-tag">Wkrótce</em>
|
||||||
</div>
|
</div>
|
||||||
<strong>57 osób szuka podobnego mieszkania</strong>
|
<strong>57 osób szuka podobnego mieszkania</strong>
|
||||||
<div className="avatar-row">
|
<div className="avatar-row">
|
||||||
@@ -13053,7 +13121,7 @@ function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (ci
|
|||||||
))}
|
))}
|
||||||
<em>+53</em>
|
<em>+53</em>
|
||||||
</div>
|
</div>
|
||||||
<button>Zobacz kupujących</button>
|
<button type="button" disabled title={SOON_HINT}>Zobacz kupujących</button>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<aside className="tour-card" aria-label="Wirtualny spacer">
|
<aside className="tour-card" aria-label="Wirtualny spacer">
|
||||||
@@ -13062,6 +13130,7 @@ function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (ci
|
|||||||
<strong>Wirtualny spacer</strong>
|
<strong>Wirtualny spacer</strong>
|
||||||
<small>Zobacz mieszkanie w 3D</small>
|
<small>Zobacz mieszkanie w 3D</small>
|
||||||
</span>
|
</span>
|
||||||
|
<em className="soon-tag">Wkrótce</em>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -13181,13 +13250,11 @@ function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (ci
|
|||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
<article className="city-card all-cities">
|
<Link className="city-card all-cities" to={ROUTES.map} aria-label="Zobacz oferty na mapie całej Polski">
|
||||||
<span>Zobacz wszystkie miasta</span>
|
<span>Zobacz wszystkie miasta</span>
|
||||||
<strong>500+ lokalizacji</strong>
|
<strong>Cała Polska na mapie</strong>
|
||||||
<button aria-label="Zobacz wszystkie miasta">
|
<em><Icon name="arrow" /></em>
|
||||||
<Icon name="arrow" />
|
</Link>
|
||||||
</button>
|
|
||||||
</article>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -13248,6 +13315,16 @@ function RentPage({
|
|||||||
const { city, propertyType, pMin, pMax, aMin, aMax, rooms, wtorny, pierwotny, chips, resetAll } = filters;
|
const { city, propertyType, pMin, pMax, aMin, aMax, rooms, wtorny, pierwotny, chips, resetAll } = filters;
|
||||||
const { formatPrice } = useDisplay();
|
const { formatPrice } = useDisplay();
|
||||||
const [, setSearchParams] = useSearchParams();
|
const [, setSearchParams] = useSearchParams();
|
||||||
|
// "Szukaj na bieżącej mapie": numery ofert widocznych na mapie trafiaja do adresu (?obszar=...),
|
||||||
|
// wiec zawezony wynik przezywa odswiezenie. Myslnik = na mapie nie bylo zadnej oferty.
|
||||||
|
const searchVisibleOnMap = (offerIds: number[]) => {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
const next = new URLSearchParams(prev);
|
||||||
|
next.set('obszar', offerIds.length > 0 ? offerIds.join(',') : '-');
|
||||||
|
next.delete('strona');
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
const [isMapOpen, setIsMapOpen] = useState(false);
|
const [isMapOpen, setIsMapOpen] = useState(false);
|
||||||
const [studentOnly, setStudentOnly] = useState(false);
|
const [studentOnly, setStudentOnly] = useState(false);
|
||||||
const [petsOnly, setPetsOnly] = useState(false);
|
const [petsOnly, setPetsOnly] = useState(false);
|
||||||
@@ -13387,6 +13464,7 @@ function RentPage({
|
|||||||
cityLabel={city || 'Warszawa'}
|
cityLabel={city || 'Warszawa'}
|
||||||
geoMarkers={rentGeoMarkers}
|
geoMarkers={rentGeoMarkers}
|
||||||
onMapClick={() => setIsMapOpen(true)}
|
onMapClick={() => setIsMapOpen(true)}
|
||||||
|
onSearchVisible={searchVisibleOnMap}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -13394,9 +13472,10 @@ function RentPage({
|
|||||||
<div className="ai-label">
|
<div className="ai-label">
|
||||||
<Icon name="spark" />
|
<Icon name="spark" />
|
||||||
<span>AI Dopasowanie</span>
|
<span>AI Dopasowanie</span>
|
||||||
|
<em className="soon-tag">Wkrótce</em>
|
||||||
</div>
|
</div>
|
||||||
<p>Powiedz nam, czego szukasz, a AI znajdzie dla Ciebie idealne oferty wynajmu.</p>
|
<p>Powiedz nam, czego szukasz, a AI znajdzie dla Ciebie idealne oferty wynajmu.</p>
|
||||||
<button>
|
<button type="button" disabled title={SOON_HINT}>
|
||||||
<Icon name="spark" />
|
<Icon name="spark" />
|
||||||
Dopasuj mi mieszkanie
|
Dopasuj mi mieszkanie
|
||||||
</button>
|
</button>
|
||||||
@@ -14990,18 +15069,18 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
|||||||
<div>
|
<div>
|
||||||
<span><Icon name="phone" /></span>
|
<span><Icon name="phone" /></span>
|
||||||
<p>
|
<p>
|
||||||
<strong>+48 123 456 789</strong>
|
<a href={SUPPORT_PHONE_HREF}>{SUPPORT_PHONE}</a>
|
||||||
<small>Pon. - Pt. 9:00 - 17:00</small>
|
<small>Pon. - Pt. 9:00 - 17:00</small>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span><Icon name="mail" /></span>
|
<span><Icon name="mail" /></span>
|
||||||
<p>
|
<p>
|
||||||
<strong>pomoc@polskalokalnie.pl</strong>
|
<a href={`mailto:${SUPPORT_EMAIL}`}>{SUPPORT_EMAIL}</a>
|
||||||
<small>Odpowiadamy w ciągu 24h</small>
|
<small>Odpowiadamy w ciągu 24h</small>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button">Najczęściej zadawane pytania</button>
|
<Link className="help-card-faq" to={ROUTES.accountHelpContact}>Najczęściej zadawane pytania <Icon name="arrow" /></Link>
|
||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -16081,6 +16160,25 @@ function GuideArticleTemplate({ content }: { content: GuideContent }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kafelek artykulu uzytkownika - ten sam w polecanych i w wynikach wyszukiwania.
|
||||||
|
function ArticleTile({ article }: { article: ArticleCard }) {
|
||||||
|
return (
|
||||||
|
<Link className="article-card is-clickable" to={articlePath(article.id)}>
|
||||||
|
<div className="article-image" style={{ backgroundImage: `url(${heroImage})` }}>
|
||||||
|
<span>{guideTopicBySlug(article.topicSlug)?.title.toUpperCase() ?? 'PORADNIK'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="article-body">
|
||||||
|
<h3>{article.title}</h3>
|
||||||
|
<p>{article.excerpt}</p>
|
||||||
|
<div>
|
||||||
|
<span>{article.readMinutes} min czytania</span>
|
||||||
|
<span><Icon name="eye" /> {article.views}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function GuidesPage() {
|
function GuidesPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
// Liczba artykułów użytkowników w każdym temacie. Do tego doliczamy 1 wbudowany poradnik.
|
// Liczba artykułów użytkowników w każdym temacie. Do tego doliczamy 1 wbudowany poradnik.
|
||||||
@@ -16092,9 +16190,9 @@ function GuidesPage() {
|
|||||||
.catch(() => { /* brak sieci - licznik pokaże sam wbudowany poradnik */ });
|
.catch(() => { /* brak sieci - licznik pokaże sam wbudowany poradnik */ });
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, []);
|
}, []);
|
||||||
// Polecane artykuly: najczesciej czytane artykuly uzytkownikow ze wszystkich tematow.
|
// Wszystkie artykuly uzytkownikow - API zwraca je per temat, wiec laczymy odpowiedzi.
|
||||||
// API zwraca artykuly per temat, wiec laczymy odpowiedzi dla kazdego tematu.
|
// Z tej listy bierzemy polecane (najczesciej czytane) i wyniki wyszukiwania.
|
||||||
const [featuredArticles, setFeaturedArticles] = useState<ArticleCard[] | null>(null);
|
const [allArticles, setAllArticles] = useState<ArticleCard[] | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
Promise.all(GUIDE_TOPICS.map((topic) => (
|
Promise.all(GUIDE_TOPICS.map((topic) => (
|
||||||
@@ -16102,11 +16200,32 @@ function GuidesPage() {
|
|||||||
)))
|
)))
|
||||||
.then((perTopic) => {
|
.then((perTopic) => {
|
||||||
if (!cancelled) {
|
if (!cancelled) {
|
||||||
setFeaturedArticles(perTopic.flat().sort((left, right) => right.views - left.views).slice(0, 4));
|
setAllArticles(perTopic.flat());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => { cancelled = true; };
|
return () => { cancelled = true; };
|
||||||
}, []);
|
}, []);
|
||||||
|
const featuredArticles = allArticles && [...allArticles].sort((left, right) => right.views - left.views).slice(0, 4);
|
||||||
|
|
||||||
|
// Szukana fraza siedzi w adresie (/poradniki?szukaj=...), wiec wynik da sie odswiezyc i udostepnic.
|
||||||
|
const [guideParams, setGuideParams] = useSearchParams();
|
||||||
|
const guideQuery = guideParams.get('szukaj') ?? '';
|
||||||
|
const [guideSearchDraft, setGuideSearchDraft] = useState(guideQuery);
|
||||||
|
useEffect(() => {
|
||||||
|
setGuideSearchDraft(guideQuery);
|
||||||
|
}, [guideQuery]);
|
||||||
|
const submitGuideSearch = (event: ReactFormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const phrase = guideSearchDraft.trim();
|
||||||
|
setGuideParams(phrase ? { szukaj: phrase } : {});
|
||||||
|
};
|
||||||
|
const normalizedQuery = guideQuery.trim().toLowerCase();
|
||||||
|
const matchingArticles = normalizedQuery.length > 0 && allArticles
|
||||||
|
? allArticles.filter((article) => `${article.title} ${article.excerpt}`.toLowerCase().includes(normalizedQuery))
|
||||||
|
: [];
|
||||||
|
const matchingTopics = normalizedQuery.length > 0
|
||||||
|
? GUIDE_TOPICS.filter((topic) => topic.title.toLowerCase().includes(normalizedQuery))
|
||||||
|
: [];
|
||||||
return (
|
return (
|
||||||
<section className="guides-page" aria-label="Poradniki">
|
<section className="guides-page" aria-label="Poradniki">
|
||||||
<section className="guides-hero">
|
<section className="guides-hero">
|
||||||
@@ -16116,11 +16235,16 @@ function GuidesPage() {
|
|||||||
<div>
|
<div>
|
||||||
<h1>Centrum wiedzy <span>o nieruchomościach</span></h1>
|
<h1>Centrum wiedzy <span>o nieruchomościach</span></h1>
|
||||||
<p>Praktyczne poradniki, analizy rynku i eksperckie wskazówki, które pomogą Ci podjąć najlepsze decyzje.</p>
|
<p>Praktyczne poradniki, analizy rynku i eksperckie wskazówki, które pomogą Ci podjąć najlepsze decyzje.</p>
|
||||||
<div className="guides-search">
|
<form className="guides-search" onSubmit={submitGuideSearch}>
|
||||||
<Icon name="search" />
|
<Icon name="search" />
|
||||||
<input placeholder="O czym chcesz przeczytać?" />
|
<input
|
||||||
<button>Szukaj</button>
|
placeholder="O czym chcesz przeczytać?"
|
||||||
</div>
|
value={guideSearchDraft}
|
||||||
|
onChange={(event) => setGuideSearchDraft(event.target.value)}
|
||||||
|
aria-label="Szukaj w poradnikach"
|
||||||
|
/>
|
||||||
|
<button type="submit">Szukaj</button>
|
||||||
|
</form>
|
||||||
<div className="popular-links">
|
<div className="popular-links">
|
||||||
<span>Popularne:</span>
|
<span>Popularne:</span>
|
||||||
<Link to={ROUTES.creditCalculator}>Kredyt hipoteczny</Link>
|
<Link to={ROUTES.creditCalculator}>Kredyt hipoteczny</Link>
|
||||||
@@ -16134,13 +16258,43 @@ function GuidesPage() {
|
|||||||
|
|
||||||
<div className="guides-container guides-layout">
|
<div className="guides-container guides-layout">
|
||||||
<div className="guides-main">
|
<div className="guides-main">
|
||||||
<section className="guides-section">
|
{normalizedQuery.length > 0 && (
|
||||||
|
<section className="guides-section" aria-label="Wyniki wyszukiwania">
|
||||||
|
<div className="guide-topic-head">
|
||||||
|
<h2>Wyniki dla „{guideQuery.trim()}”</h2>
|
||||||
|
<Link to={ROUTES.guides}>Wyczyść wyszukiwanie</Link>
|
||||||
|
</div>
|
||||||
|
{matchingTopics.length > 0 && (
|
||||||
|
<div className="guide-topic-grid">
|
||||||
|
{matchingTopics.map((topic) => (
|
||||||
|
<Link className="guide-topic-card is-clickable" key={topic.slug} to={guideTopicListPath(topic.slug)}>
|
||||||
|
<span className={`guide-topic-icon ico-${topic.icon}`}>
|
||||||
|
<Icon name={topic.icon} />
|
||||||
|
</span>
|
||||||
|
<strong>{topic.title}</strong>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{matchingArticles.length > 0 ? (
|
||||||
|
<div className="article-grid">
|
||||||
|
{matchingArticles.map((article) => (
|
||||||
|
<ArticleTile key={article.id} article={article} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
matchingTopics.length === 0 && (
|
||||||
|
<p className="guides-search-empty">
|
||||||
|
{allArticles == null ? 'Szukamy…' : 'Nie znaleźliśmy artykułu ani tematu z tą frazą. Sprawdź tematy poniżej.'}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<section className="guides-section" id="tematy-poradnikow">
|
||||||
<div className="guide-topic-head">
|
<div className="guide-topic-head">
|
||||||
<h2>Wybierz temat</h2>
|
<h2>Wybierz temat</h2>
|
||||||
<div>
|
|
||||||
<button aria-label="Poprzednie"><Icon name="chevron" /></button>
|
|
||||||
<button aria-label="Następne"><Icon name="chevron" /></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="guide-topic-grid">
|
<div className="guide-topic-grid">
|
||||||
{guideTopics.map((topic) => {
|
{guideTopics.map((topic) => {
|
||||||
@@ -16174,23 +16328,7 @@ function GuidesPage() {
|
|||||||
) : (
|
) : (
|
||||||
<div className="article-grid">
|
<div className="article-grid">
|
||||||
{(featuredArticles ?? []).map((article) => (
|
{(featuredArticles ?? []).map((article) => (
|
||||||
<Link
|
<ArticleTile key={article.id} article={article} />
|
||||||
className="article-card is-clickable"
|
|
||||||
key={article.id}
|
|
||||||
to={articlePath(article.id)}
|
|
||||||
>
|
|
||||||
<div className="article-image" style={{ backgroundImage: `url(${heroImage})` }}>
|
|
||||||
<span>{guideTopicBySlug(article.topicSlug)?.title.toUpperCase() ?? 'PORADNIK'}</span>
|
|
||||||
</div>
|
|
||||||
<div className="article-body">
|
|
||||||
<h3>{article.title}</h3>
|
|
||||||
<p>{article.excerpt}</p>
|
|
||||||
<div>
|
|
||||||
<span>{article.readMinutes} min czytania</span>
|
|
||||||
<span><Icon name="eye" /> {article.views}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -16236,7 +16374,7 @@ function GuidesPage() {
|
|||||||
<h2>Szybkie poradniki</h2>
|
<h2>Szybkie poradniki</h2>
|
||||||
<div>
|
<div>
|
||||||
{quickGuides.map((guide) => (
|
{quickGuides.map((guide) => (
|
||||||
<article key={guide.title}>
|
<Link key={guide.title} to={guide.to}>
|
||||||
<span>
|
<span>
|
||||||
<Icon name={guide.icon} />
|
<Icon name={guide.icon} />
|
||||||
</span>
|
</span>
|
||||||
@@ -16245,13 +16383,13 @@ function GuidesPage() {
|
|||||||
<small>{guide.text}</small>
|
<small>{guide.text}</small>
|
||||||
</p>
|
</p>
|
||||||
<Icon name="chevron" />
|
<Icon name="chevron" />
|
||||||
</article>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<button>
|
<a href="#tematy-poradnikow">
|
||||||
Zobacz wszystkie poradniki
|
Zobacz wszystkie tematy
|
||||||
<Icon name="arrow" />
|
<Icon name="arrow" />
|
||||||
</button>
|
</a>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
@@ -17034,7 +17172,7 @@ function SellPage() {
|
|||||||
<section className="sell-section">
|
<section className="sell-section">
|
||||||
<h2>Zobacz opinie naszych użytkowników</h2>
|
<h2>Zobacz opinie naszych użytkowników</h2>
|
||||||
<div className="testimonial-grid">
|
<div className="testimonial-grid">
|
||||||
{sellTestimonials.map((testimonial, index) => (
|
{sellTestimonials.map((testimonial) => (
|
||||||
<article className="testimonial-card" key={testimonial.name}>
|
<article className="testimonial-card" key={testimonial.name}>
|
||||||
<div className="testimonial-head">
|
<div className="testimonial-head">
|
||||||
<span>{testimonial.name.charAt(0)}</span>
|
<span>{testimonial.name.charAt(0)}</span>
|
||||||
@@ -17050,15 +17188,9 @@ function SellPage() {
|
|||||||
))}
|
))}
|
||||||
<span>5.0</span>
|
<span>5.0</span>
|
||||||
</div>
|
</div>
|
||||||
{index === 0 && <button className="testimonial-nav prev">‹</button>}
|
|
||||||
{index === 2 && <button className="testimonial-nav next">›</button>}
|
|
||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="testimonial-dots">
|
|
||||||
<span />
|
|
||||||
<span />
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="sell-bottom-cta">
|
<section className="sell-bottom-cta">
|
||||||
@@ -22086,7 +22218,17 @@ function RealListingsBand({
|
|||||||
}, [sortOpen]);
|
}, [sortOpen]);
|
||||||
|
|
||||||
const cityFilter = (city ?? '').trim();
|
const cityFilter = (city ?? '').trim();
|
||||||
const visible = cityFilter ? items.filter((item) => cityStartsWith(item.city, cityFilter)) : items;
|
const byCity = cityFilter ? items.filter((item) => cityStartsWith(item.city, cityFilter)) : items;
|
||||||
|
// "Szukaj na bieżącej mapie" wpisuje numery widocznych ofert do adresu (?obszar=1,4,9),
|
||||||
|
// dzieki czemu zawezony wynik przezywa odswiezenie i da sie go wyslac linkiem.
|
||||||
|
const areaParam = searchParams.get('obszar');
|
||||||
|
const areaIds = areaParam ? new Set(areaParam.split(',').map(Number).filter((id) => !Number.isNaN(id))) : null;
|
||||||
|
const visible = areaIds ? byCity.filter((item) => areaIds.has(item.id)) : byCity;
|
||||||
|
const clearArea = () => {
|
||||||
|
const next = new URLSearchParams(searchParams);
|
||||||
|
next.delete('obszar');
|
||||||
|
setSearchParams(next);
|
||||||
|
};
|
||||||
|
|
||||||
// W trakcie ladowania pokazujemy animacje zamiast pustego miejsca; gdy dane przyjda i lista jest
|
// W trakcie ladowania pokazujemy animacje zamiast pustego miejsca; gdy dane przyjda i lista jest
|
||||||
// pusta - nie pokazujemy nic (jak dotad).
|
// pusta - nie pokazujemy nic (jak dotad).
|
||||||
@@ -22098,7 +22240,16 @@ function RealListingsBand({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (visible.length === 0) {
|
if (visible.length === 0) {
|
||||||
return null;
|
// Pusto po zawezeniu do obszaru mapy to co innego niz brak ofert - trzeba dac droge powrotna.
|
||||||
|
return areaIds ? (
|
||||||
|
<div className="real-listings-section" aria-label="Ogłoszenia dodane w serwisie">
|
||||||
|
<p className="map-area-note" role="status">
|
||||||
|
<Icon name="pin" />
|
||||||
|
<span>Na widocznym fragmencie mapy nie ma żadnej oferty.</span>
|
||||||
|
<button type="button" onClick={clearArea}>Pokaż wszystkie</button>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Widok prezentacji (lista/kompaktowa/kafelki) oraz numer strony trzymamy w query
|
// Widok prezentacji (lista/kompaktowa/kafelki) oraz numer strony trzymamy w query
|
||||||
@@ -22155,6 +22306,13 @@ function RealListingsBand({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="real-listings-section" aria-label="Ogłoszenia dodane w serwisie">
|
<div className="real-listings-section" aria-label="Ogłoszenia dodane w serwisie">
|
||||||
|
{areaIds && (
|
||||||
|
<p className="map-area-note" role="status">
|
||||||
|
<Icon name="pin" />
|
||||||
|
<span>Pokazujemy oferty widoczne na mapie.</span>
|
||||||
|
<button type="button" onClick={clearArea}>Pokaż wszystkie</button>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div className="real-listings-heading">
|
<div className="real-listings-heading">
|
||||||
<h3>Ogłoszenia dodane w serwisie</h3>
|
<h3>Ogłoszenia dodane w serwisie</h3>
|
||||||
<span>{visible.length}</span>
|
<span>{visible.length}</span>
|
||||||
@@ -22376,6 +22534,16 @@ function BuyPage({
|
|||||||
}) {
|
}) {
|
||||||
const { city, propertyType, pMin, pMax, aMin, aMax, rooms, wtorny, pierwotny, noFeeOnly, chips, resetAll, fromPreferences } = filters;
|
const { city, propertyType, pMin, pMax, aMin, aMax, rooms, wtorny, pierwotny, noFeeOnly, chips, resetAll, fromPreferences } = filters;
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
// "Szukaj na bieżącej mapie": numery ofert widocznych na mapie trafiaja do adresu (?obszar=...),
|
||||||
|
// wiec zawezony wynik przezywa odswiezenie. Myslnik = na mapie nie bylo zadnej oferty.
|
||||||
|
const searchVisibleOnMap = (offerIds: number[]) => {
|
||||||
|
setSearchParams((prev) => {
|
||||||
|
const next = new URLSearchParams(prev);
|
||||||
|
next.set('obszar', offerIds.length > 0 ? offerIds.join(',') : '-');
|
||||||
|
next.delete('strona');
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
};
|
||||||
const [isMapOpen, setIsMapOpen] = useState(false);
|
const [isMapOpen, setIsMapOpen] = useState(false);
|
||||||
|
|
||||||
// Miasto przekazane w adresie (np. z kafelka miasta na stronie glownej: /kupno?miasto=Warszawa)
|
// Miasto przekazane w adresie (np. z kafelka miasta na stronie glownej: /kupno?miasto=Warszawa)
|
||||||
@@ -22510,6 +22678,7 @@ function BuyPage({
|
|||||||
cityLabel={city || 'Warszawa'}
|
cityLabel={city || 'Warszawa'}
|
||||||
geoMarkers={saleGeoMarkers}
|
geoMarkers={saleGeoMarkers}
|
||||||
onMapClick={() => setIsMapOpen(true)}
|
onMapClick={() => setIsMapOpen(true)}
|
||||||
|
onSearchVisible={searchVisibleOnMap}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -22517,9 +22686,10 @@ function BuyPage({
|
|||||||
<div className="ai-label">
|
<div className="ai-label">
|
||||||
<Icon name="spark" />
|
<Icon name="spark" />
|
||||||
<span>AI Dopasowanie</span>
|
<span>AI Dopasowanie</span>
|
||||||
|
<em className="soon-tag">Wkrótce</em>
|
||||||
</div>
|
</div>
|
||||||
<p>Powiedz nam czego szukasz, a AI znajdzie oferty idealnie dopasowane do Ciebie.</p>
|
<p>Powiedz nam czego szukasz, a AI znajdzie oferty idealnie dopasowane do Ciebie.</p>
|
||||||
<button type="button">
|
<button type="button" disabled title={SOON_HINT}>
|
||||||
<Icon name="spark" />
|
<Icon name="spark" />
|
||||||
Dopasuj mi mieszkanie
|
Dopasuj mi mieszkanie
|
||||||
</button>
|
</button>
|
||||||
@@ -22591,6 +22761,7 @@ function SatelliteMap({
|
|||||||
geoMarkers,
|
geoMarkers,
|
||||||
onMapClick,
|
onMapClick,
|
||||||
onMarkerClick,
|
onMarkerClick,
|
||||||
|
onSearchVisible,
|
||||||
size,
|
size,
|
||||||
areaRadiusMeters,
|
areaRadiusMeters,
|
||||||
}: {
|
}: {
|
||||||
@@ -22603,6 +22774,8 @@ function SatelliteMap({
|
|||||||
geoMarkers?: { id: string; lat: number; lng: number; label: string; offerId?: number }[];
|
geoMarkers?: { id: string; lat: number; lng: number; label: string; offerId?: number }[];
|
||||||
onMapClick?: () => void;
|
onMapClick?: () => void;
|
||||||
onMarkerClick?: (offerId: number) => void;
|
onMarkerClick?: (offerId: number) => void;
|
||||||
|
// Zawezenie listy ofert do tych, ktore widac na mapie. Bez tej funkcji przycisku nie pokazujemy.
|
||||||
|
onSearchVisible?: (offerIds: number[]) => void;
|
||||||
size?: number;
|
size?: number;
|
||||||
// Podany promien (w metrach) zastepuje pinezke kolem - uzywany dla przyblizonej lokalizacji.
|
// Podany promien (w metrach) zastepuje pinezke kolem - uzywany dla przyblizonej lokalizacji.
|
||||||
areaRadiusMeters?: number;
|
areaRadiusMeters?: number;
|
||||||
@@ -22790,10 +22963,21 @@ function SatelliteMap({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="satellite-map-shade" />
|
<div className="satellite-map-shade" />
|
||||||
<button type="button" className="map-current">
|
{onSearchVisible && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="map-current"
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
onSearchVisible(positionedGeoMarkers
|
||||||
|
.map((marker) => marker.offerId)
|
||||||
|
.filter((offerId): offerId is number => offerId != null));
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span />
|
<span />
|
||||||
Szukaj na bieżącej mapie
|
Szukaj na bieżącej mapie
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
{areaCircle && (
|
{areaCircle && (
|
||||||
<span
|
<span
|
||||||
className="map-area-circle"
|
className="map-area-circle"
|
||||||
@@ -26602,8 +26786,16 @@ function useLiveListings(offerType: ApiOfferType, refreshMs = LIVE_LISTINGS_REFR
|
|||||||
return { items, loading };
|
return { items, loading };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Funkcje zapowiedziane, ale jeszcze niezbudowane - przycisk jest nieaktywny i ma plakietke "Wkrotce".
|
||||||
|
const SOON_HINT = 'Ta funkcja jest w przygotowaniu.';
|
||||||
|
|
||||||
|
// Jeden adres i jeden numer kontaktowy w calym serwisie - te same co w stopce i na stronach prawnych.
|
||||||
|
const SUPPORT_EMAIL = 'info@polskalokalnie.pl';
|
||||||
|
const SUPPORT_PHONE = '+48 453 193 605';
|
||||||
|
const SUPPORT_PHONE_HREF = 'tel:+48453193605';
|
||||||
|
|
||||||
// Podpowiedz przy narzedziach wylaczonych we wlasnym ogloszeniu (kontakt, ulubione, alert ceny, zgloszenie).
|
// Podpowiedz przy narzedziach wylaczonych we wlasnym ogloszeniu (kontakt, ulubione, alert ceny, zgloszenie).
|
||||||
const OWNER_TOOL_HINT = 'To Twoje ogłoszenie - ta opcja jest dostępna tylko dla innych użytkowników.';
|
const OWNER_TOOL_HINT ='To Twoje ogłoszenie - ta opcja jest dostępna tylko dla innych użytkowników.';
|
||||||
|
|
||||||
function ListingDetailPage({
|
function ListingDetailPage({
|
||||||
listingId,
|
listingId,
|
||||||
|
|||||||
+94
-54
@@ -1302,7 +1302,7 @@ a.city-card:hover {
|
|||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.all-cities button {
|
.all-cities em {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||||
@@ -4153,7 +4153,7 @@ a.city-card:hover {
|
|||||||
margin-top: 15px;
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-guides-card article {
|
.quick-guides-card :is(article, a) {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid #edf1f5;
|
border-bottom: 1px solid #edf1f5;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -4162,7 +4162,7 @@ a.city-card:hover {
|
|||||||
min-height: 64px;
|
min-height: 64px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-guides-card article > span {
|
.quick-guides-card :is(article, a) > span {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: #edf3ff;
|
background: #edf3ff;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
@@ -4174,17 +4174,17 @@ a.city-card:hover {
|
|||||||
width: 34px;
|
width: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-guides-card article:nth-child(2) > span {
|
.quick-guides-card :is(article, a):nth-child(2) > span {
|
||||||
background: #f1edff;
|
background: #f1edff;
|
||||||
color: #9b5cff;
|
color: #9b5cff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-guides-card article:nth-child(3) > span {
|
.quick-guides-card :is(article, a):nth-child(3) > span {
|
||||||
background: #fff4df;
|
background: #fff4df;
|
||||||
color: #f49a31;
|
color: #f49a31;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-guides-card article:nth-child(4) > span {
|
.quick-guides-card :is(article, a):nth-child(4) > span {
|
||||||
background: #e9f8ef;
|
background: #e9f8ef;
|
||||||
color: #10a15e;
|
color: #10a15e;
|
||||||
}
|
}
|
||||||
@@ -4208,7 +4208,7 @@ a.city-card:hover {
|
|||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick-guides-card article > svg {
|
.quick-guides-card :is(article, a) > svg {
|
||||||
color: #9aa5b4;
|
color: #9aa5b4;
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
}
|
}
|
||||||
@@ -9475,50 +9475,6 @@ a.city-card:hover {
|
|||||||
margin-left: 6px;
|
margin-left: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.testimonial-nav {
|
|
||||||
align-items: center;
|
|
||||||
background: #ffffff;
|
|
||||||
border: 1px solid #dce4ec;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: #526171;
|
|
||||||
display: grid;
|
|
||||||
font-size: 24px;
|
|
||||||
height: 34px;
|
|
||||||
place-items: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
width: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-nav.prev {
|
|
||||||
left: -18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-nav.next {
|
|
||||||
right: -18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-dots {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
justify-content: center;
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-dots span {
|
|
||||||
background: #14223a;
|
|
||||||
border-radius: 999px;
|
|
||||||
height: 6px;
|
|
||||||
opacity: 0.35;
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.testimonial-dots span:first-child {
|
|
||||||
opacity: 1;
|
|
||||||
width: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sell-bottom-cta {
|
.sell-bottom-cta {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background: linear-gradient(90deg, #f8fffb 0%, #effaf4 100%);
|
background: linear-gradient(90deg, #f8fffb 0%, #effaf4 100%);
|
||||||
@@ -12705,9 +12661,6 @@ a.city-card:hover {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.testimonial-nav {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.services-search,
|
.services-search,
|
||||||
.service-mini-search {
|
.service-mini-search {
|
||||||
@@ -35193,3 +35146,90 @@ a.listing-detail-secondary {
|
|||||||
color: #0b9f5c;
|
color: #0b9f5c;
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Zapowiedziane funkcje: plakietka "Wkrotce" i nieaktywny przycisk obok niej. */
|
||||||
|
.soon-tag {
|
||||||
|
background: #eef2f7;
|
||||||
|
border-radius: 999px;
|
||||||
|
color: #56657a;
|
||||||
|
font-size: 10px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
padding: 3px 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-card button:disabled,
|
||||||
|
.map-ai-card button:disabled,
|
||||||
|
.account-premium-card button:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tour-card .soon-tag {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Szybkie poradniki i wyszukiwarka poradnikow prowadza do prawdziwych stron. */
|
||||||
|
.quick-guides-card :is(a, article) {
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quick-guides-card > a:last-child {
|
||||||
|
align-items: center;
|
||||||
|
color: #0b9f5c;
|
||||||
|
display: inline-flex;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
gap: 8px;
|
||||||
|
padding-top: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-topic-head a {
|
||||||
|
color: #0b9f5c;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guides-search-empty {
|
||||||
|
color: #526171;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Zawezenie listy do ofert widocznych na mapie. */
|
||||||
|
.map-area-note {
|
||||||
|
align-items: center;
|
||||||
|
background: #f1f8f4;
|
||||||
|
border: 1px solid #cfe8db;
|
||||||
|
border-radius: 10px;
|
||||||
|
color: #27364d;
|
||||||
|
display: flex;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 800;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-area-note button {
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: #0b9f5c;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comparison-share-info {
|
||||||
|
color: #0b9f5c;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 900;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -458,3 +458,43 @@ test.describe('Negocjacje', () => {
|
|||||||
await expect(page.getByText('Nie znaleźliśmy tego ogłoszenia')).toBeVisible();
|
await expect(page.getByText('Nie znaleźliśmy tego ogłoszenia')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Standard: przyciski robia to, co obiecuja', () => {
|
||||||
|
test('wyszukiwarka poradnikow trzyma fraze w adresie i przezywa F5', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.guides);
|
||||||
|
await page.locator('.guides-search input').fill('kredyt');
|
||||||
|
await page.locator('.guides-search button').click();
|
||||||
|
await page.waitForFunction(() => window.location.search.includes('szukaj=kredyt'));
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.locator('section[aria-label="Wyniki wyszukiwania"]')).toContainText('kredyt');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('"Szukaj na biezacej mapie" zaweza liste do ofert z mapy', async ({ page }) => {
|
||||||
|
await goto(page, ROUTES.buy);
|
||||||
|
await page.locator('.map-card').first().waitFor();
|
||||||
|
await page.getByRole('button', { name: /Szukaj na bieżącej mapie/ }).first().click();
|
||||||
|
await page.waitForFunction(() => window.location.search.includes('obszar='));
|
||||||
|
await expect(page.locator('.map-area-note')).toBeVisible();
|
||||||
|
await page.locator('.map-area-note button').click();
|
||||||
|
await page.waitForFunction(() => !window.location.search.includes('obszar='));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('porownanie trzyma wybrane oferty w adresie', async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
await goto(page, ROUTES.buy);
|
||||||
|
await page.locator('.real-result-card').first().waitFor();
|
||||||
|
const href = await page.locator('.real-result-card .card-stretched-link').first().getAttribute('href');
|
||||||
|
const listingId = Number(href!.split('/').pop());
|
||||||
|
await goto(page, `${ROUTES.comparison}?oferty=${listingId}`);
|
||||||
|
await expect(page.locator('.comparison-main')).toContainText('Zobacz ogłoszenie');
|
||||||
|
await page.reload();
|
||||||
|
await expect(page.locator('.comparison-main')).toContainText('Zobacz ogłoszenie');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('kanaly kontaktu w pomocy to prawdziwe linki mailto i tel', async ({ page }) => {
|
||||||
|
await login(page);
|
||||||
|
await goto(page, ROUTES.accountHelpContact);
|
||||||
|
await expect(page.locator('a.help-contact-channel-action[href^="mailto:"]')).toHaveCount(1);
|
||||||
|
await expect(page.locator('a.help-contact-channel-action[href^="tel:"]')).toHaveCount(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user