@@ -332,6 +332,10 @@ public class AuthService {
|
||||
}
|
||||
user.setPhone(phone);
|
||||
user.setAddress(request.address() != null && !request.address().isBlank() ? request.address().trim() : null);
|
||||
// NIP: null = nie zmieniaj (zapis czesciowy nie moze skasowac NIP-u firmy), pusty = wyczysc.
|
||||
if (request.nip() != null) {
|
||||
user.setNip(request.nip().isBlank() ? null : request.nip().trim());
|
||||
}
|
||||
user.setContactPreference(request.contactPreference() != null ? request.contactPreference() : ContactPreference.EMAIL_AND_PHONE);
|
||||
user.setPreferredLanguage(request.preferredLanguage() != null ? request.preferredLanguage() : PreferredLanguage.PL);
|
||||
if (request.birthDate() != null && !request.birthDate().isBlank()) {
|
||||
|
||||
@@ -11,6 +11,9 @@ public record UpdateProfileRequest(
|
||||
String phone,
|
||||
String birthDate,
|
||||
String address,
|
||||
// NIP firmy. null = nie zmieniaj (zapisy czesciowe, np. jezyka, nie kasuja NIP-u);
|
||||
// pusty ciag = wyczysc. Dotyczy kont firmowych.
|
||||
String nip,
|
||||
ContactPreference contactPreference,
|
||||
PreferredLanguage preferredLanguage
|
||||
) {
|
||||
|
||||
@@ -94,7 +94,72 @@ public class GusService {
|
||||
throw new ResponseStatusException(HttpStatus.NOT_FOUND,
|
||||
"Nie znaleziono firmy o podanym numerze NIP w rejestrze REGON.");
|
||||
}
|
||||
return new GusCompanyResponse(dane.get("Nazwa"), nip, dane.get("Regon"), buildAddress(dane));
|
||||
|
||||
String name = dane.get("Nazwa");
|
||||
String regon = dane.get("Regon");
|
||||
String address = buildAddress(dane);
|
||||
|
||||
// Wyszukiwanie (DaneSzukajPodmioty) zwraca skrocony rekord - dla czesci podmiotow (czesto
|
||||
// jednoosobowych dzialalnosci) nazwa lub adres sa puste i sa dostepne dopiero w pelnym
|
||||
// raporcie. Dociagamy go tylko wtedy, gdy czegos brakuje. Blad raportu nie moze zepsuc
|
||||
// dzialajacych przypadkow, wiec zostajemy przy danych z wyszukiwania.
|
||||
if ((isBlank(name) || isBlank(address)) && !isBlank(regon)) {
|
||||
try {
|
||||
Map<String, String> full = fullReport(endpoint, key, regon, dane.get("Typ"));
|
||||
if (isBlank(name)) {
|
||||
name = full.get("Nazwa");
|
||||
}
|
||||
if (isBlank(address)) {
|
||||
String fullAddress = buildAddress(full);
|
||||
if (!isBlank(fullAddress)) {
|
||||
address = fullAddress;
|
||||
}
|
||||
}
|
||||
} catch (ResponseStatusException ignored) {
|
||||
// Pelny raport jest tylko uzupelnieniem - brak odpowiedzi nie przerywa rejestracji.
|
||||
}
|
||||
}
|
||||
|
||||
return new GusCompanyResponse(name, nip, regon, address);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pelny raport podmiotu (DanePobierzPelnyRaport) - uzupelnia nazwe i adres, gdy nie ma ich w
|
||||
* wyniku wyszukiwania. Nazwa raportu i nazwy pol zaleza od typu podmiotu (osoba prawna vs fizyczna).
|
||||
* Zwrocone pola mapujemy na te same klucze co wyszukiwanie, zeby uzyc wspolnego buildAddress.
|
||||
*/
|
||||
private Map<String, String> fullReport(String endpoint, String key, String regon, String typ) {
|
||||
boolean legal = typ != null && typ.trim().toUpperCase().startsWith("P");
|
||||
String report = legal ? "BIR11OsPrawna" : "BIR11OsFizycznaDaneOgolne";
|
||||
String prefix = legal ? "praw_" : "fiz_";
|
||||
String body = """
|
||||
<ns:DanePobierzPelnyRaport>
|
||||
<ns:pRegon>%s</ns:pRegon>
|
||||
<ns:pNazwaRaportu>%s</ns:pNazwaRaportu>
|
||||
</ns:DanePobierzPelnyRaport>""".formatted(regon, report);
|
||||
String response = call(endpoint, "DanePobierzPelnyRaport", body, session(endpoint, key));
|
||||
Map<String, String> raw = firstDane(resultOf(response, "DanePobierzPelnyRaportResult"));
|
||||
if (raw == null) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<String, String> mapped = new HashMap<>();
|
||||
putIfPresent(mapped, "Nazwa", raw.get(prefix + "nazwa"));
|
||||
putIfPresent(mapped, "Ulica", raw.get(prefix + "adSiedzUlica_Nazwa"));
|
||||
putIfPresent(mapped, "NrNieruchomosci", raw.get(prefix + "adSiedzNumerNieruchomosci"));
|
||||
putIfPresent(mapped, "NrLokalu", raw.get(prefix + "adSiedzNumerLokalu"));
|
||||
putIfPresent(mapped, "KodPocztowy", raw.get(prefix + "adSiedzKodPocztowy"));
|
||||
putIfPresent(mapped, "Miejscowosc", raw.get(prefix + "adSiedzMiejscowosc_Nazwa"));
|
||||
return mapped;
|
||||
}
|
||||
|
||||
private static void putIfPresent(Map<String, String> target, String key, String value) {
|
||||
if (!isBlank(value)) {
|
||||
target.put(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean isBlank(String value) {
|
||||
return value == null || value.isBlank();
|
||||
}
|
||||
|
||||
// Pusty wynik oznacza wygasla sesje - wtedy logujemy sie ponownie i probujemy raz jeszcze.
|
||||
|
||||
@@ -51,6 +51,11 @@ public record ListingDetailResponse(
|
||||
Long ownerId,
|
||||
// Zdjecie profilowe wlasciciela (data URL) albo null - wtedy karta sprzedajacego rysuje inicjaly.
|
||||
String ownerAvatar,
|
||||
// Typ konta wlasciciela ("PERSONAL"/"COMPANY") albo null, gdy ogloszenie nie ma konta w serwisie.
|
||||
// Po tym front rozroznia oferte prywatna od firmowej (etykieta na karcie, jak na Otodom).
|
||||
String ownerAccountType,
|
||||
// Nazwa wlasciciela do etykiety oferty firmowej (dla konta firmowego). Dla oferty prywatnej pomijana.
|
||||
String ownerName,
|
||||
ListingStatus status,
|
||||
Instant createdAt,
|
||||
Long viewsCount,
|
||||
@@ -63,7 +68,7 @@ public record ListingDetailResponse(
|
||||
private static final double METERS_PER_DEGREE = 111_320d;
|
||||
|
||||
public static ListingDetailResponse from(PropertyListing listing, Long ownerId, String ownerAvatar) {
|
||||
return from(listing, ownerId, ownerAvatar, false);
|
||||
return from(listing, ownerId, ownerAvatar, null, null, false);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +77,8 @@ public record ListingDetailResponse(
|
||||
* przesuwane w obrebie obszaru. Przesuniecie liczymy z identyfikatora ogloszenia, wiec dla danej
|
||||
* oferty jest zawsze takie samo - inaczej kolo skakaloby po mapie przy kazdym odswiezeniu.
|
||||
*/
|
||||
public static ListingDetailResponse from(PropertyListing listing, Long ownerId, String ownerAvatar, boolean masked) {
|
||||
public static ListingDetailResponse from(PropertyListing listing, Long ownerId, String ownerAvatar,
|
||||
String ownerAccountType, String ownerName, boolean masked) {
|
||||
String street = masked ? null : listing.getStreet();
|
||||
String building = masked ? null : listing.getBuilding();
|
||||
String address = masked ? approximateAddress(listing) : listing.getAddress();
|
||||
@@ -118,6 +124,8 @@ public record ListingDetailResponse(
|
||||
listing.getOwnerEmail(),
|
||||
ownerId,
|
||||
ownerAvatar,
|
||||
ownerAccountType,
|
||||
ownerName,
|
||||
listing.getStatus(),
|
||||
listing.getCreatedAt(),
|
||||
listing.getViewsCount(),
|
||||
|
||||
@@ -62,9 +62,11 @@ public class ListingService {
|
||||
private ListingDetailResponse detailOf(PropertyListing listing, boolean masked) {
|
||||
AppUser owner = ownerOf(listing);
|
||||
if (owner == null) {
|
||||
return ListingDetailResponse.from(listing, null, null, masked);
|
||||
return ListingDetailResponse.from(listing, null, null, null, null, masked);
|
||||
}
|
||||
return ListingDetailResponse.from(listing, owner.getId(), userSettingsService.avatarFor(owner.getId()), masked);
|
||||
String ownerAccountType = owner.getAccountType() == null ? null : owner.getAccountType().name();
|
||||
return ListingDetailResponse.from(listing, owner.getId(), userSettingsService.avatarFor(owner.getId()),
|
||||
ownerAccountType, owner.getFullName(), masked);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+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>
|
||||
)}
|
||||
|
||||
@@ -125,6 +125,8 @@ export type ProfileUpdate = {
|
||||
phone?: string;
|
||||
birthDate?: string;
|
||||
address?: string;
|
||||
// NIP firmy - pomijamy dla kont prywatnych. Pominiete (undefined) = backend nie zmienia NIP-u.
|
||||
nip?: string;
|
||||
contactPreference?: ContactPreference;
|
||||
preferredLanguage?: PreferredLanguage;
|
||||
};
|
||||
|
||||
+197
-53
@@ -860,12 +860,14 @@ svg {
|
||||
max-width: 440px;
|
||||
}
|
||||
|
||||
.free-card button {
|
||||
.free-card button,
|
||||
.free-card-cta {
|
||||
align-items: center;
|
||||
background: #0b9f5c;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
@@ -875,9 +877,11 @@ svg {
|
||||
min-height: 42px;
|
||||
min-width: 250px;
|
||||
padding: 0 17px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.free-card button strong {
|
||||
.free-card button strong,
|
||||
.free-card-cta strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -3104,10 +3108,55 @@ a.city-card:hover {
|
||||
}
|
||||
|
||||
.result-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
padding: 21px 0 18px;
|
||||
}
|
||||
|
||||
/* Etykieta typu oferty (prywatna/firmowa) - stopka karty jak na Otodom.
|
||||
margin-top:auto zsuwa ja na sam dol karty w widoku listy; linia oddziela od reszty. */
|
||||
.result-seller-type {
|
||||
align-items: center;
|
||||
border-top: 1px solid #e9eef4;
|
||||
color: #8793a3;
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
gap: 6px;
|
||||
margin-top: auto;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.result-seller-type.company {
|
||||
color: #0f7a48;
|
||||
}
|
||||
|
||||
.result-seller-type svg {
|
||||
height: 14px;
|
||||
opacity: 0.85;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
/* Klikalna nazwa firmy - musi lezec nad nakladka linku karty (::after, z-index 1),
|
||||
inaczej klik trafialby w link do oferty zamiast w profil firmy. Pasek (border-top) zostaje
|
||||
pelnej szerokosci jak na ofertach prywatnych - podkreslamy tylko sama nazwe firmy. */
|
||||
.result-seller-type.is-link {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
transition: color 0.15s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.result-seller-type.is-link:hover {
|
||||
color: #0a6b3f;
|
||||
}
|
||||
|
||||
.result-seller-type.is-link:hover .seller-name {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.tag-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -10066,6 +10115,31 @@ a.city-card:hover {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.map-offer-cta {
|
||||
align-items: center;
|
||||
background: #0b8f52;
|
||||
border-radius: 10px;
|
||||
color: #ffffff;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
gap: 8px;
|
||||
justify-content: center;
|
||||
margin-top: 14px;
|
||||
padding: 11px 14px;
|
||||
text-decoration: none;
|
||||
transition: background 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.map-offer-cta:hover {
|
||||
background: #0a7a46;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.map-offer-cta svg {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.district-ranking-page {
|
||||
background: #f7fafc;
|
||||
border-top: 1px solid #e6edf3;
|
||||
@@ -12410,7 +12484,8 @@ a.city-card:hover {
|
||||
top: 610px;
|
||||
}
|
||||
|
||||
.free-card button {
|
||||
.free-card button,
|
||||
.free-card .free-card-cta {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@@ -16398,6 +16473,14 @@ a.city-card:hover {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.message-business-card-type {
|
||||
color: #0f7a48;
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.message-business-card-grid {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
@@ -21217,6 +21300,12 @@ a.city-card:hover {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Sekcja danych firmy (nazwa + NIP) - odstep od nastepnej sekcji, zeby tekst pod NIP
|
||||
nie przyklejal sie do etykiety "Nazwa uzytkownika". */
|
||||
.profile-form-grid.settings-company-grid {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.profile-extra-form {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@@ -22225,37 +22314,6 @@ a.city-card:hover {
|
||||
width: 13px;
|
||||
}
|
||||
|
||||
.public-profile-rating {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.public-profile-rating strong {
|
||||
color: #18293f;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.public-profile-rating span {
|
||||
color: #f2b51e;
|
||||
display: inline-flex;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.public-profile-rating span svg {
|
||||
fill: currentColor;
|
||||
height: 13px;
|
||||
stroke: currentColor;
|
||||
width: 13px;
|
||||
}
|
||||
|
||||
.public-profile-rating small {
|
||||
color: #6f7f92;
|
||||
font-size: 11px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.public-profile-summary blockquote {
|
||||
color: #3a4d66;
|
||||
font-size: 12px;
|
||||
@@ -22288,13 +22346,6 @@ a.city-card:hover {
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.public-profile-grid {
|
||||
align-items: start;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: 1.35fr 0.95fr;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.public-profile-listings {
|
||||
align-self: start;
|
||||
@@ -22469,14 +22520,12 @@ a.city-card:hover {
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.public-profile-reviews .settings-card-head,
|
||||
.public-profile-listings .settings-card-head {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.public-profile-reviews .settings-card-head .ghost,
|
||||
.public-profile-listings .settings-card-head .ghost {
|
||||
border: 0;
|
||||
color: #109253;
|
||||
@@ -22596,9 +22645,6 @@ a.city-card:hover {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.public-profile-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@@ -25814,6 +25860,111 @@ a.city-card:hover {
|
||||
stroke-width: 1.75;
|
||||
}
|
||||
|
||||
/* Slider zdjec na karcie oferty (galeria bez wchodzenia w oferte, jak Otodom).
|
||||
Kolejne zdjecia leza na sobie i przelaczamy je krzyzowym zanikaniem. */
|
||||
.result-photo-slide {
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
transition: opacity 0.35s ease;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.result-photo-slide.is-active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Strzalki i kropki to <button> wewnatrz .result-photo - globalna regula
|
||||
.result-photo button (kolko w prawym gornym rogu) je psuje, wiec nadpisujemy. */
|
||||
.real-result-card .result-photo-nav {
|
||||
align-items: center;
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
color: #14223a;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
transition: opacity 0.16s ease, background 0.16s ease;
|
||||
width: 32px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.real-result-card .result-photo-nav.prev { left: 12px; right: auto; }
|
||||
.real-result-card .result-photo-nav.next { right: 12px; }
|
||||
.real-result-card .result-photo-nav:hover { background: #ffffff; }
|
||||
|
||||
.real-result-card .result-photo-nav svg {
|
||||
height: 16px;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2.2;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.real-result-card .result-photo-nav.prev svg { transform: rotate(90deg); }
|
||||
.real-result-card .result-photo-nav.next svg { transform: rotate(-90deg); }
|
||||
|
||||
/* Strzalki pokazujemy przy najechaniu na karte (desktop); na dotyku - zawsze. */
|
||||
.real-result-card:hover .result-photo-nav { opacity: 1; }
|
||||
|
||||
@media (hover: none) {
|
||||
.real-result-card .result-photo-nav { opacity: 1; }
|
||||
}
|
||||
|
||||
.result-photo-counter {
|
||||
align-items: center;
|
||||
background: rgba(20, 34, 58, 0.6);
|
||||
border-radius: 999px;
|
||||
bottom: 12px;
|
||||
color: #ffffff;
|
||||
display: inline-flex;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
gap: 5px;
|
||||
padding: 4px 9px;
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.result-photo-counter svg { height: 13px; width: 13px; }
|
||||
|
||||
.result-photo-dots {
|
||||
bottom: 14px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.real-result-card .result-photo-dots button {
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
height: 7px;
|
||||
padding: 0;
|
||||
position: static;
|
||||
transition: background 0.16s ease, transform 0.16s ease;
|
||||
width: 7px;
|
||||
}
|
||||
|
||||
.real-result-card .result-photo-dots button.is-active {
|
||||
background: #ffffff;
|
||||
transform: scale(1.25);
|
||||
}
|
||||
|
||||
/* ===== Strzalki galerii w widoku szczegolow ===== */
|
||||
|
||||
.ld-gallery-arrow {
|
||||
@@ -32496,13 +32647,6 @@ a.listing-detail-back {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Slot ocen zostaje na swoim miejscu, dopoki nie zbieramy opinii o uzytkownikach. */
|
||||
.standalone-profile-rating small {
|
||||
color: #7e8fa3;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.standalone-profile .settings-footer-note {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user