+245
-61
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user