poprawki
Deploy Polska Lokalnie / deploy (push) Successful in 12s

This commit is contained in:
2026-09-03 16:49:33 +02:00
parent 52e1f2cb96
commit d973b44e47
8 changed files with 531 additions and 119 deletions
+245 -61
View File
@@ -127,6 +127,10 @@ export type ApiListingDetail = {
ownerId: number | null;
// Zdjecie profilowe wlasciciela - null, gdy go nie ustawil albo ogloszenie nie ma konta w serwisie.
ownerAvatar: string | null;
// Typ konta wlasciciela - po nim rozrozniamy oferte prywatna od firmowej. null = brak konta w serwisie.
ownerAccountType: AccountType | null;
// Nazwa wlasciciela - uzywana jako etykieta oferty firmowej.
ownerName: string | null;
status: ApiListingStatus;
createdAt: string;
viewsCount?: number;
@@ -181,6 +185,14 @@ function pushRecentlyViewed(item: RecentlyViewedItem): void {
type LiveListingSummary = ApiListingSummary & {
lat: number | null;
lng: number | null;
// Pelna galeria z detalu - pobierana i tak dla lat/lng, wiec karta moze pokazac slider zdjec
// bez wchodzenia w oferte (jak na Otodom).
photos: string[];
// Typ konta wlasciciela i jego nazwa - do etykiety "Oferta prywatna" / oferty firmowej na karcie.
ownerAccountType: AccountType | null;
ownerName: string | null;
// Konto wlasciciela - etykieta oferty firmowej linkuje do jego profilu publicznego (/profil/:id).
ownerId: number | null;
};
const LIVE_LISTINGS_REFRESH_MS = 15000;
@@ -829,11 +841,11 @@ const insightCards = [
path: ROUTES.districtRanking,
},
{
icon: 'coin',
title: 'Ile możesz utargować?',
text: 'Poznaj realistyczną cenę transakcyjną',
icon: 'percent',
title: 'Zdolność kredytowa',
text: 'Sprawdź na jaki kredyt Cię stać',
action: 'Sprawdź teraz',
path: ROUTES.negotiation,
path: ROUTES.creditCalculator,
},
];
@@ -6833,7 +6845,8 @@ function AccountDashboardPage({
<div className="account-main">
<div className="account-heading">
<div>
<h1>Witaj, {getFirstName(getDisplayName(user))}! <span aria-hidden="true">👋</span></h1>
{/* Firma nie ma imienia - witamy pelna nazwa firmy zamiast pierwszego slowa. */}
<h1>Witaj, {user?.accountType === 'COMPANY' ? getDisplayName(user) : getFirstName(getDisplayName(user))}! <span aria-hidden="true">👋</span></h1>
<p>Zarządzaj swoimi wyszukiwaniami, ofertami i ulubionymi ogłoszeniami.</p>
</div>
<button
@@ -10750,6 +10763,10 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
const { applySettings: applyDisplaySettings } = useDisplay();
const navigate = useNavigate();
// Konta firmowe maja jedno pole "Nazwa firmy" zamiast imienia i nazwiska, bez daty urodzenia,
// a adres opisuje siedzibe firmy. Konta prywatne dzialaja bez zmian.
const isCompany = user?.accountType === 'COMPANY';
type Tone = 'success' | 'error' | 'info';
type Banner = { text: string; tone: Tone } | null;
@@ -10762,6 +10779,8 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
const [identity, setIdentity] = useState(() => ({
firstName: '',
lastName: '',
companyName: '',
nip: '',
nick: '',
phonePrefix: DEFAULT_PHONE_COUNTRY.code as string,
phoneDigits: '',
@@ -10773,6 +10792,7 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
}));
const [identityBanner, setIdentityBanner] = useState<Banner>(null);
const [isSavingIdentity, setIsSavingIdentity] = useState(false);
const [isGusLoading, setIsGusLoading] = useState(false);
const [isSuggestingNick, setIsSuggestingNick] = useState(false);
const [isPhoneTouched, setIsPhoneTouched] = useState(false);
const [isPhoneCountryOpen, setIsPhoneCountryOpen] = useState(false);
@@ -10874,6 +10894,9 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
...current,
firstName: getFirstName(user.fullName || ''),
lastName: getLastName(user.fullName || ''),
// Dla firmy cala nazwa siedzi w fullName - nie rozbijamy jej na imie/nazwisko.
companyName: user.fullName || '',
nip: user.nip ?? '',
nick: user.nick ?? '',
phonePrefix: phone.prefix,
phoneDigits: phone.digits,
@@ -10949,14 +10972,42 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
setIdentity((current) => ({ ...current, [field]: value }));
};
// Pobranie danych firmy z rejestru REGON po NIP-ie - uzupelnia nazwe firmy i adres siedziby,
// tak samo jak przy rejestracji konta firmowego.
const fetchCompanyFromGus = async () => {
if (isGusLoading) {
return;
}
setIsGusLoading(true);
setIdentityBanner(null);
try {
const company = await apiFetch<{ name: string | null; nip: string; regon: string | null; address: string | null }>(
`/gus/company?nip=${encodeURIComponent(identity.nip.trim())}`,
);
setIdentity((current) => ({
...current,
nip: company.nip,
companyName: company.name ?? current.companyName,
address: company.address ?? current.address,
}));
setIdentityBanner({ text: `Pobrano dane z rejestru REGON${company.regon ? ` (REGON ${company.regon})` : ''}. Sprawdź je i zapisz.`, tone: 'success' });
} catch (error) {
setIdentityBanner({ text: error instanceof Error ? error.message : 'Nie udało się pobrać danych z GUS.', tone: 'error' });
} finally {
setIsGusLoading(false);
}
};
/**
* Prosi serwer o wolna nazwe uzytkownika dla imienia i nazwiska z formularza. Uzytkownik nie wpisuje
* nicka recznie - przyjmuje propozycje, a wolne miejsce w puli sprawdza baza, nie przegladarka.
*/
const suggestNickFromName = async () => {
const fullName = `${identity.firstName.trim()} ${identity.lastName.trim()}`.trim();
const fullName = isCompany
? identity.companyName.trim()
: `${identity.firstName.trim()} ${identity.lastName.trim()}`.trim();
if (!fullName) {
setIdentityBanner({ text: 'Podaj imię i nazwisko, żeby dopasować nazwę użytkownika.', tone: 'error' });
setIdentityBanner({ text: isCompany ? 'Podaj nazwę firmy, żeby dopasować nazwę użytkownika.' : 'Podaj imię i nazwisko, żeby dopasować nazwę użytkownika.', tone: 'error' });
return;
}
setIsSuggestingNick(true);
@@ -11051,9 +11102,11 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
setIdentityBanner({ text: `Numer telefonu dla kraju ${selectedCountry.country} musi mieć ${selectedCountry.digits} cyfr.`, tone: 'error' });
return;
}
const fullName = `${identity.firstName.trim()} ${identity.lastName.trim()}`.trim();
const fullName = isCompany
? identity.companyName.trim()
: `${identity.firstName.trim()} ${identity.lastName.trim()}`.trim();
if (!fullName) {
setIdentityBanner({ text: 'Podaj imię i nazwisko.', tone: 'error' });
setIdentityBanner({ text: isCompany ? 'Podaj nazwę firmy.' : 'Podaj imię i nazwisko.', tone: 'error' });
return;
}
@@ -11064,7 +11117,10 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
fullName,
nick: identity.nick.trim() || undefined,
phone: composedPhone || undefined,
birthDate: identity.birthDate || undefined,
// NIP wysylamy tylko dla firmy (dla osoby prywatnej undefined = backend go nie zmienia).
nip: isCompany ? identity.nip.trim() : undefined,
// Firma nie ma daty urodzenia - nie wysylamy jej z konta firmowego.
birthDate: isCompany ? undefined : (identity.birthDate || undefined),
address: identity.address.trim() || undefined,
contactPreference: identity.contactPreference,
preferredLanguage: identity.preferredLanguage,
@@ -11390,16 +11446,48 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
<div className="settings-separator" />
<div className="profile-form-grid two-col">
<label>
<span>Imię</span>
<input value={identity.firstName} onChange={(event) => setIdentityField('firstName', event.target.value)} />
</label>
<label>
<span>Nazwisko</span>
<input value={identity.lastName} onChange={(event) => setIdentityField('lastName', event.target.value)} />
</label>
</div>
{isCompany ? (
<div className="profile-form-grid settings-company-grid">
<label>
<span>Nazwa firmy</span>
<input
value={identity.companyName}
placeholder="Wpisz nazwę firmy"
onChange={(event) => setIdentityField('companyName', event.target.value)}
/>
</label>
<label className="login-field-gus">
<span>NIP</span>
<div className="input-action-row">
<input
value={identity.nip}
inputMode="numeric"
placeholder="123-456-78-90"
onChange={(event) => setIdentityField('nip', event.target.value)}
/>
<button
type="button"
onClick={() => { void fetchCompanyFromGus(); }}
disabled={isGusLoading || identity.nip.replace(/\D/g, '').length !== 10}
>
{isGusLoading ? 'Pobieram...' : 'Pobierz z GUS'}
</button>
</div>
<small>Wpisz 10 cyfr numeru NIP, aby pobrać nazwę i adres firmy z rejestru REGON.</small>
</label>
</div>
) : (
<div className="profile-form-grid two-col">
<label>
<span>Imię</span>
<input value={identity.firstName} onChange={(event) => setIdentityField('firstName', event.target.value)} />
</label>
<label>
<span>Nazwisko</span>
<input value={identity.lastName} onChange={(event) => setIdentityField('lastName', event.target.value)} />
</label>
</div>
)}
<div className="profile-form-grid">
<label>
@@ -11409,7 +11497,7 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
jest polem do swobodnej edycji (podszywanie sie, zajmowanie nazw). */}
<input value={identity.nick} readOnly />
<button type="button" onClick={() => { void suggestNickFromName(); }} disabled={isSuggestingNick}>
{isSuggestingNick ? 'Sprawdzam...' : 'Dopasuj do imienia i nazwiska'}
{isSuggestingNick ? 'Sprawdzam...' : (isCompany ? 'Dopasuj do nazwy firmy' : 'Dopasuj do imienia i nazwiska')}
</button>
</div>
<small>
@@ -11505,22 +11593,25 @@ function AccountSettingsPage({ onSearchPreferencesSaved }: { onSearchPreferences
)}
</label>
<label>
<span>Data urodzenia (opcjonalnie)</span>
<input
type="date"
max={todayIsoDate}
value={identity.birthDate}
onChange={(event) => setIdentityField('birthDate', event.target.value)}
/>
<small>Nie pokazujemy jej innym użytkownikom.</small>
</label>
{/* Data urodzenia dotyczy tylko osob prywatnych - firma jej nie ma. */}
{!isCompany && (
<label>
<span>Data urodzenia (opcjonalnie)</span>
<input
type="date"
max={todayIsoDate}
value={identity.birthDate}
onChange={(event) => setIdentityField('birthDate', event.target.value)}
/>
<small>Nie pokazujemy jej innym użytkownikom.</small>
</label>
)}
<label>
<span>Adres zamieszkania (opcjonalnie)</span>
<span>{isCompany ? 'Adres siedziby firmy (opcjonalnie)' : 'Adres zamieszkania (opcjonalnie)'}</span>
<input
value={identity.address}
placeholder="Np. Łódź, ul. Piotrkowska 12"
placeholder={isCompany ? 'Np. Warszawa, ul. Marszałkowska 1' : 'Np. Łódź, ul. Piotrkowska 12'}
onChange={(event) => setIdentityField('address', event.target.value)}
/>
</label>
@@ -13267,10 +13358,10 @@ function HomePage({ filters, onOpenMap }: { filters: FilterState; onOpenMap: (ci
<div>
<h2>Dodaj ogłoszenie za darmo</h2>
<p>Dotrzyj do tysięcy potencjalnych kupujących. Bez prowizji. Bez zobowiązań.</p>
<button>
<Link className="free-card-cta" to={ROUTES.add}>
<span>Dodaj ogłoszenie</span>
<strong>0 zł</strong>
</button>
</Link>
<small>Zajmie Ci to tylko 2 minuty!</small>
</div>
<div className="house-graphic" aria-hidden="true">
@@ -13608,6 +13699,9 @@ function apiDetailToWizardState(detail: ApiListingDetail) {
function AddListingPage({ editListingId }: { editListingId?: number }) {
const navigate = useNavigate();
const { user } = useAuth();
// Konta firmowe podaja w kontakcie nazwe firmy / osobe kontaktowa zamiast samego imienia.
const isCompanyAccount = user?.accountType === 'COMPANY';
const isEdit = typeof editListingId === 'number';
const [editLoadError, setEditLoadError] = useState('');
const [currentStep, setCurrentStep] = useState(0);
@@ -14301,7 +14395,7 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
<section className="add-form-card">
<h2>Dane kontaktowe</h2>
<div className="add-three-grid">
<AddInput label="Imię" value={listingDetails.contactName} placeholder="Np. Jan" onChange={(value) => updateListingDetails('contactName', value)} />
<AddInput label={isCompanyAccount ? 'Nazwa firmy / osoba kontaktowa' : 'Imię'} value={listingDetails.contactName} placeholder={isCompanyAccount ? 'Np. Biuro Nieruchomości XYZ' : 'Np. Jan'} onChange={(value) => updateListingDetails('contactName', value)} />
<label className="add-field add-phone-field">
<span>Telefon komórkowy</span>
<div className="add-phone-row">
@@ -20506,7 +20600,6 @@ function MegaSearch({ filters, onOpenMap, onSearch, mode = 'buy' }: {
<div className="mega-bar">
<div className="mega-cell">
<div className={`mega-field ${city ? 'has-value' : ''} ${openField === 'lokalizacja' ? 'active' : ''}`} role="button" tabIndex={0} onClick={() => toggleField('lokalizacja')}>
<Icon name="pin" />
<span>
<small>Lokalizacja</small>
<strong>{cityInput || 'Cała Polska'}</strong>
@@ -20607,7 +20700,6 @@ function MegaSearch({ filters, onOpenMap, onSearch, mode = 'buy' }: {
<div className="mega-cell">
<button type="button" className={`mega-field ${openField === 'rodzaj' ? 'active' : ''}`} onClick={() => toggleField('rodzaj')}>
<Icon name="building" />
<span>
<small>Rodzaj nieruchomości</small>
<strong>{propertyType}</strong>
@@ -21676,6 +21768,65 @@ const OFFER_VIEW_OPTIONS: { mode: string; icon: string; label: string }[] = [
{ mode: 'kafelki', icon: 'grid', label: 'Kafelki' },
];
/**
* Slider zdjec na karcie oferty - pozwala przegladac galerie bez wchodzenia w oferte (jak Otodom).
* Zdjecia sa juz pobrane w useLiveListings (razem z lat/lng), wiec nic dodatkowego nie sciagamy.
* Wlasny stan indeksu wymaga osobnego komponentu - w .map() karty nie mozna uzyc hooka.
*/
function ResultCardGallery({ photos, cover, alt }: { photos: string[]; cover: string | null; alt: string }) {
const images = photos.length > 0 ? photos : [cover ?? heroImage];
const total = images.length;
const [index, setIndex] = useState(0);
const current = Math.min(index, total - 1);
// Karta jest linkiem (nakladka ::after) - strzalki i kropki nie moga go wyzwalac.
const step = (event: ReactMouseEvent, delta: number) => {
event.preventDefault();
event.stopPropagation();
setIndex((prev) => (prev + delta + total) % total);
};
const jump = (event: ReactMouseEvent, target: number) => {
event.preventDefault();
event.stopPropagation();
setIndex(target);
};
return (
<>
{images.map((src, i) => (
<span
key={i}
className={`result-photo-slide ${i === current ? 'is-active' : ''}`}
style={{ backgroundImage: `url(${src})` }}
aria-hidden={i === current ? undefined : true}
role="img"
aria-label={i === current ? alt : undefined}
/>
))}
{total > 1 && (
<>
<button type="button" className="result-photo-nav prev" aria-label="Poprzednie zdjęcie" onClick={(event) => step(event, -1)}>
<Icon name="chevron" />
</button>
<button type="button" className="result-photo-nav next" aria-label="Następne zdjęcie" onClick={(event) => step(event, 1)}>
<Icon name="chevron" />
</button>
<span className="result-photo-counter"><Icon name="camera" /> {current + 1} / {total}</span>
<span className="result-photo-dots">
{images.slice(0, 8).map((_, i) => (
<button
type="button"
key={i}
className={i === current ? 'is-active' : ''}
aria-label={`Zdjęcie ${i + 1}`}
onClick={(event) => jump(event, i)}
/>
))}
</span>
</>
)}
</>
);
}
function RealListingsBand({
offerType,
onOpenListing,
@@ -21814,12 +21965,17 @@ function RealListingsBand({
floor ? `${floor} piętro` : null,
item.yearBuilt ? `${item.yearBuilt} r.` : null,
].filter(Boolean).join(' • ');
// Etykieta jak na Otodom: konto firmowe pokazuje nazwe firmy, kazde inne (prywatne lub
// ogloszenie bez konta w serwisie) - "Oferta prywatna".
const isCompanyOffer = item.ownerAccountType === 'COMPANY';
const sellerTypeLabel = isCompanyOffer ? (item.ownerName?.trim() || 'Oferta firmowa') : 'Oferta prywatna';
return (
// Karta prowadzi do stron oferty. Tytul jest linkiem rozciagnietym na cala
// karte (::after), dzieki czemu mamy prawdziwy <a href> - do skopiowania i
// otwarcia w nowej karcie - bez zagniezdzania przyciskow w <a>.
<article key={item.id} className="result-card real-result-card">
<div className="result-photo" style={{ backgroundImage: `url(${item.coverPhoto ?? heroImage})` }}>
<div className="result-photo">
<ResultCardGallery photos={item.photos} cover={item.coverPhoto} alt={item.title} />
{offerType === 'SALE' && (
<div className="real-photo-badges">
<span className="green-tag">{listingMarketBadge(item.market)}</span>
@@ -21846,6 +22002,21 @@ function RealListingsBand({
<h3><Link className="card-stretched-link" to={listingPath(item.id)}>{item.title}</Link></h3>
<p>{[item.city, item.district].filter(Boolean).join(', ')}</p>
<small>{meta}</small>
{isCompanyOffer && item.ownerId != null ? (
// Oferta firmowa z kontem w serwisie - klik w nazwe firmy otwiera jej profil publiczny.
// stopPropagation, bo cala karta jest linkiem do oferty (nakladka ::after).
<Link
className="result-seller-type company is-link"
to={publicProfilePath(item.ownerId)}
onClick={(event) => event.stopPropagation()}
>
<Icon name="building" /> <span className="seller-name">{sellerTypeLabel}</span>
</Link>
) : (
<span className={`result-seller-type ${isCompanyOffer ? 'company' : 'private'}`}>
<Icon name={isCompanyOffer ? 'building' : 'user'} /> {sellerTypeLabel}
</span>
)}
</div>
<div className="result-side">
@@ -23607,6 +23778,8 @@ type BusinessCard = {
fullName: string;
phone: string | null;
email: string;
// true = wizytowka firmy (etykieta "Nazwa firmy" w zapisie i podpis "Firma" na karcie).
isCompany?: boolean;
};
// Watek z administracja jest trzymany na serwerze jako zwykly tekst wiadomosci, wiec wizytowke
@@ -23614,10 +23787,14 @@ type BusinessCard = {
const BUSINESS_CARD_MARKER = '[wizytówka]';
const BUSINESS_CARD_TEXT = 'Przesyłam wizytówkę kontaktową.';
// Etykieta nazwy w wizytowce: firma ma "Nazwa firmy", osoba prywatna "Imię i nazwisko".
const BUSINESS_CARD_COMPANY_LABEL = 'Nazwa firmy';
const BUSINESS_CARD_PERSON_LABEL = 'Imię i nazwisko';
function formatBusinessCard(card: BusinessCard): string {
const lines = [
`${BUSINESS_CARD_MARKER} Wizytówka kontaktowa`,
`Imię i nazwisko: ${card.fullName}`,
`${card.isCompany ? BUSINESS_CARD_COMPANY_LABEL : BUSINESS_CARD_PERSON_LABEL}: ${card.fullName}`,
`E-mail: ${card.email}`,
];
if (card.phone) {
@@ -23641,7 +23818,11 @@ function parseBusinessCard(content: string): BusinessCard | null {
if (!email) {
return null;
}
return { fullName: readLine('Imię i nazwisko'), email, phone: readLine('Telefon') || null };
// Stare wizytowki maja tylko "Imię i nazwisko" - firmowa etykieta jest opcjonalna i wstecznie zgodna.
const companyName = readLine(BUSINESS_CARD_COMPANY_LABEL);
const isCompany = companyName.length > 0;
const fullName = isCompany ? companyName : readLine(BUSINESS_CARD_PERSON_LABEL);
return { fullName, email, phone: readLine('Telefon') || null, isCompany };
}
// Tresc wiadomosci z serwera zamieniona na czesci widoku: wizytowka renderuje sie jako karta,
@@ -23768,10 +23949,6 @@ function PublicProfilePage() {
</div>
{/* Opis "o mnie" z ustawien konta - pole opcjonalne, wiec pokazujemy tylko gdy wypelnione. */}
{profile.bio && <p className="public-profile-bio">{profile.bio}</p>}
{/* Ocen jeszcze nie zbieramy - slot zostaje, zeby uklad byl ten sam co na wlasnym profilu. */}
<div className="public-profile-rating standalone-profile-rating">
<small>Brak ocen</small>
</div>
</div>
<blockquote>
{profile.verified
@@ -23786,8 +23963,7 @@ function PublicProfilePage() {
</div>
</section>
<div className="public-profile-grid">
<section className="settings-card public-profile-listings">
<section className="settings-card public-profile-listings">
<div className="settings-card-head">
<h2>Ogłoszenia ({profile.listingsCount})</h2>
</div>
@@ -23823,15 +23999,7 @@ function PublicProfilePage() {
))}
</div>
)}
</section>
<section className="settings-card public-profile-reviews">
<div className="settings-card-head">
<h2>Opinie o użytkowniku</h2>
</div>
<p className="admin-empty">Ten użytkownik nie ma jeszcze opinii.</p>
</section>
</div>
</section>
<section className="settings-card public-profile-about">
<h2>O użytkowniku</h2>
@@ -24366,7 +24534,7 @@ function MessagesPage({
// Wizytowka zawsze z aktualnego profilu konta. Telefon trafia do niej tylko wtedy,
// gdy uzytkownik go poda - inaczej wysylamy sam adres e-mail.
const myBusinessCard: BusinessCard | null = user
? { fullName: user.fullName, email: user.email, phone: user.phone }
? { fullName: user.fullName, email: user.email, phone: user.phone, isCompany: user.accountType === 'COMPANY' }
: null;
const onChooseAttachment = () => {
@@ -24624,7 +24792,7 @@ function MessagesPage({
});
setIsPhonePromptOpen(false);
deliverBusinessCard({ fullName: updated.fullName, email: updated.email, phone: updated.phone });
deliverBusinessCard({ fullName: updated.fullName, email: updated.email, phone: updated.phone, isCompany: user.accountType === 'COMPANY' });
} catch (error) {
setPhonePromptError(error instanceof Error ? error.message : 'Nie udało się zapisać numeru telefonu.');
} finally {
@@ -24982,10 +25150,11 @@ function MessagesPage({
{message.businessCard && (
<div className="message-business-card" role="group" aria-label="Wizytówka kontaktowa">
<div className="message-business-card-top">
<span className="message-business-card-badge"><Icon name="user" /></span>
<span className="message-business-card-badge"><Icon name={message.businessCard.isCompany ? 'building' : 'user'} /></span>
<div>
<em>Wizytówka kontaktowa</em>
<strong>{message.businessCard.fullName}</strong>
{message.businessCard.isCompany && <small className="message-business-card-type">Firma</small>}
</div>
</div>
<div className="message-business-card-grid">
@@ -25984,12 +26153,20 @@ function useLiveListings(offerType: ApiOfferType, refreshMs = LIVE_LISTINGS_REFR
...summary,
lat: detail.lat,
lng: detail.lng,
photos: detail.photos ?? [],
ownerAccountType: detail.ownerAccountType ?? null,
ownerName: detail.ownerName ?? null,
ownerId: detail.ownerId ?? null,
} as LiveListingSummary;
} catch {
return {
...summary,
lat: null,
lng: null,
photos: [],
ownerAccountType: null,
ownerName: null,
ownerId: null,
} as LiveListingSummary;
}
}),
@@ -26278,7 +26455,15 @@ function ListingDetailPage({
// Wlasciciela rozpoznajemy po koncie powiazanym z ogloszeniem, a dla wpisow bez konta - po adresie e-mail.
const isOwner = Boolean(user && ((listing.ownerId != null && listing.ownerId === user.id)
|| (listing.ownerEmail && user.email.toLowerCase() === listing.ownerEmail.toLowerCase())));
const sellerRole = isOwner ? 'To Twoje ogłoszenie' : 'Oficjalny kontakt zespołem serwisu Polska Lokalnie';
// Podpis pod nazwa sprzedajacego: dla firmy "Firma", dla osoby "Osoba prywatna", a dla ogloszen
// bez konta w serwisie (dane demo) - dotychczasowy kontakt zespolu serwisu.
const sellerRole = isOwner
? 'To Twoje ogłoszenie'
: listing.ownerAccountType === 'COMPANY'
? 'Firma'
: listing.ownerAccountType === 'PERSONAL'
? 'Osoba prywatna'
: 'Oficjalny kontakt zespołem serwisu Polska Lokalnie';
const favorite = isFavorite(listing.id);
const listingAlertEnabled = isListingAlertEnabled(listing.id);
const virtualTourEmbed = listing.virtualTourUrl ? getVirtualTourEmbed(listing.virtualTourUrl) : null;
@@ -26754,7 +26939,6 @@ function ListingDetailPage({
<div>
<strong>{sellerName}</strong>
<small>{sellerRole}</small>
<small>Na PolskaLokalnie.pl od 2023</small>
{isPhoneRevealed && listing.contactPhone && (
<a href={`tel:${listing.contactPhone.replace(/\s+/g, '')}`}><Icon name="phone" /> {listing.contactPhone}</a>
)}