This commit is contained in:
+377
-138
@@ -1978,8 +1978,196 @@ const extraInfoSelectOptions = {
|
||||
fencing: ['Wybierz', 'Tak', 'Częściowe', 'Nie'],
|
||||
accessRoad: ['Wybierz', 'Asfalt', 'Kostka', 'Utwardzony', 'Szutrowy', 'Polny'],
|
||||
sewageType: ['Wybierz', 'Kanalizacja', 'Szambo', 'Przydomowa oczyszczalnia'],
|
||||
// Nowe slowniki dla dynamicznej sekcji "Dodatkowe informacje" (per kategoria).
|
||||
landType: ['Wybierz', 'Budowlana', 'Rolna', 'Rekreacyjna', 'Leśna', 'Inwestycyjna', 'Siedliskowa', 'Rolno-budowlana'],
|
||||
landPurpose: ['Wybierz', 'Mieszkaniowa', 'Usługowa', 'Przemysłowa', 'Rolnicza', 'Rekreacyjna', 'Mieszana'],
|
||||
planningStatus: ['Wybierz', 'MPZP', 'Warunki zabudowy (WZ)', 'Brak / w trakcie'],
|
||||
plotShape: ['Wybierz', 'Prostokąt', 'Kwadrat', 'Trapez', 'Wąska i długa', 'Nieregularny'],
|
||||
localPurpose: ['Wybierz', 'Handlowy', 'Usługowy', 'Biurowy', 'Gastronomiczny', 'Magazynowy', 'Medyczny', 'Produkcyjny', 'Inny'],
|
||||
buildingKind: ['Wybierz', 'Biurowy', 'Handlowy', 'Magazynowy', 'Produkcyjny', 'Usługowy', 'Magazynowo-biurowy', 'Hotelowy'],
|
||||
garageType: ['Wybierz', 'Garaż murowany', 'Hala garażowa', 'Miejsce postojowe', 'Blaszak', 'Podziemny'],
|
||||
garageLevel: ['Wybierz', 'Naziemny', 'Podziemny (-1)', 'Podziemny (-2)', 'Wielopoziomowy'],
|
||||
legalStatus: ['Wybierz', 'Uregulowany', 'Do uregulowania', 'Z obciążeniem hipotecznym'],
|
||||
landRegister: ['Wybierz', 'Tak', 'Nie', 'W trakcie zakładania'],
|
||||
roommates: ['Wybierz', 'Brak', '1', '2', '3', '4+'],
|
||||
roommatePreference: ['Wybierz', 'Bez znaczenia', 'Kobiety', 'Mężczyźni', 'Studenci', 'Osoby pracujące'],
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Dynamiczna sekcja "Dodatkowe informacje" (krok 4 kreatora).
|
||||
// Dla kazdej kategorii pokazujemy tylko pola, ktore realnie maja sens dla kupujacego/najemcy.
|
||||
// Kafelki (checkboxy) zapisuja sie przez tablice media[]/amenities[] i sa widoczne na ogloszeniu.
|
||||
// Selecty/pola tekstowe zapisuja sie tam, gdzie istnieje kolumna w bazie (furnishing, buildingType,
|
||||
// buildingMaterial, windows, exposure, roomHeight, availableFrom); pozostale zyja w podgladzie kreatora.
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
// Ikony kafelkow: najpierw z istniejacych list opcji, potem uzupelnienie dla nowych etykiet.
|
||||
const EXTRA_TILE_ICONS: Record<string, string> = {
|
||||
...Object.fromEntries([...mediaOptions, ...amenityOptions, ...houseAmenityOptions].map((option) => [option.label, option.icon])),
|
||||
Klimatyzacja: 'snow',
|
||||
'Wentylacja mechaniczna': 'snow',
|
||||
'Wejście z ulicy': 'pin',
|
||||
Witryna: 'screen',
|
||||
'Zaplecze socjalne': 'sofa',
|
||||
'Dostęp 24/7': 'gear',
|
||||
'Plac manewrowy': 'truck',
|
||||
'Rampa rozładunkowa': 'truck',
|
||||
'Brama automatyczna': 'car',
|
||||
Oświetlenie: 'sun',
|
||||
'Kanał samochodowy': 'car',
|
||||
'Media przy granicy': 'link',
|
||||
'Dostęp do drogi publicznej': 'pin',
|
||||
'Widok na las/wodę': 'tree',
|
||||
'Dostęp do kuchni': 'flame',
|
||||
'Dostęp do łazienki': 'sun',
|
||||
'Osobna łazienka': 'shield',
|
||||
Pralka: 'snow',
|
||||
Ogród: 'tree',
|
||||
};
|
||||
const tileIcon = (label: string): string => EXTRA_TILE_ICONS[label] ?? 'spark';
|
||||
|
||||
type ExtraStore = 'extra' | 'details';
|
||||
type ExtraFieldDef = {
|
||||
kind: 'select' | 'input' | 'date';
|
||||
store: ExtraStore;
|
||||
key: string;
|
||||
label: string;
|
||||
options?: string[];
|
||||
suffix?: string;
|
||||
placeholder?: string;
|
||||
numeric?: 'integer' | 'decimal';
|
||||
};
|
||||
const O = extraInfoSelectOptions;
|
||||
const sel = (key: string, label: string, options: string[]): ExtraFieldDef => ({ kind: 'select', store: 'extra', key, label, options });
|
||||
const numField = (key: string, label: string, suffix?: string, placeholder?: string, numeric: 'integer' | 'decimal' = 'decimal'): ExtraFieldDef => ({ kind: 'input', store: 'extra', key, label, suffix, placeholder, numeric });
|
||||
const txtField = (key: string, label: string, placeholder?: string): ExtraFieldDef => ({ kind: 'input', store: 'extra', key, label, placeholder });
|
||||
const dateField = (key: string, label: string): ExtraFieldDef => ({ kind: 'date', store: 'extra', key, label });
|
||||
|
||||
// Pola najmu wspolne dla wiekszosci kategorii (prepend przy transakcji "Wynajem").
|
||||
const RENT_LEAD_FIELDS: ExtraFieldDef[] = [
|
||||
sel('rentalPeriod', 'Okres najmu', O.rentalPeriod),
|
||||
numField('deposit', 'Kaucja', 'zł', 'Np. 6000'),
|
||||
dateField('availableFrom', 'Dostępne od'),
|
||||
];
|
||||
// Pola prawne wazne przy zakupie (append przy transakcji "Sprzedaż").
|
||||
const SALE_LEGAL_FIELDS: ExtraFieldDef[] = [
|
||||
sel('legalStatus', 'Stan prawny', O.legalStatus),
|
||||
sel('landRegister', 'Księga wieczysta', O.landRegister),
|
||||
];
|
||||
|
||||
type CategoryExtra = {
|
||||
fields: ExtraFieldDef[];
|
||||
media: string[];
|
||||
amenities: string[];
|
||||
rentAmenities?: string[];
|
||||
legal?: boolean; // dolacz pola prawne przy sprzedazy
|
||||
rentLead?: boolean; // dolacz pola najmu przy wynajmie (domyslnie true)
|
||||
};
|
||||
|
||||
const CATEGORY_EXTRA: Record<string, CategoryExtra> = {
|
||||
Mieszkanie: {
|
||||
legal: true,
|
||||
fields: [
|
||||
sel('buildingType', 'Typ zabudowy', O.buildingType),
|
||||
sel('furnishing', 'Umeblowanie', O.furnishing),
|
||||
sel('buildingMaterial', 'Materiał budynku', O.buildingMaterial),
|
||||
sel('windows', 'Okna', O.windows),
|
||||
sel('exposure', 'Ekspozycja okien', O.exposure),
|
||||
numField('roomHeight', 'Wysokość pomieszczeń', 'cm', 'Np. 270', 'integer'),
|
||||
],
|
||||
media: ['Ogrzewanie miejskie', 'Ogrzewanie gazowe', 'Ogrzewanie elektryczne', 'Ogrzewanie podłogowe', 'Woda miejska', 'Prąd', 'Gaz', 'Internet', 'Światłowód', 'Telewizja kablowa', 'Klimatyzacja'],
|
||||
amenities: ['Balkon', 'Taras', 'Winda', 'Parking', 'Garaż', 'Piwnica', 'Komórka lokatorska', 'Monitoring', 'Ochrona', 'Dostęp dla niepełnosprawnych'],
|
||||
rentAmenities: ['Dla studentów', 'Zwierzęta mile widziane'],
|
||||
},
|
||||
Dom: {
|
||||
legal: true,
|
||||
fields: [
|
||||
numField('plotArea', 'Powierzchnia działki', 'm²', 'Np. 650'),
|
||||
sel('buildingMaterial', 'Materiał', O.buildingMaterial),
|
||||
sel('roofType', 'Dach', O.roofType),
|
||||
sel('roofCover', 'Pokrycie dachu', O.roofCover),
|
||||
sel('furnishing', 'Umeblowanie', O.furnishing),
|
||||
sel('windows', 'Okna', O.windows),
|
||||
sel('exposure', 'Ekspozycja', O.exposure),
|
||||
sel('accessRoad', 'Dojazd', O.accessRoad),
|
||||
sel('fencing', 'Ogrodzenie', O.fencing),
|
||||
sel('sewageType', 'Kanalizacja / szambo', O.sewageType),
|
||||
],
|
||||
media: ['Ogrzewanie gazowe', 'Ogrzewanie elektryczne', 'Ogrzewanie podłogowe', 'Woda miejska', 'Prąd', 'Gaz', 'Internet', 'Światłowód', 'Pompa ciepła', 'Fotowoltaika', 'Rekuperacja', 'Klimatyzacja'],
|
||||
amenities: ['Ogród', 'Kominek', 'Basen', 'Sauna', 'Ogród zimowy', 'Altana', 'Automatyczna brama', 'Rolety zewnętrzne', 'Monitoring zewnętrzny', 'System alarmowy', 'Ładowarka do samochodu elektrycznego', 'Grill murowany', 'Plac zabaw'],
|
||||
rentAmenities: ['Dla studentów', 'Zwierzęta mile widziane'],
|
||||
},
|
||||
Działka: {
|
||||
legal: true,
|
||||
fields: [
|
||||
sel('landType', 'Typ działki', O.landType),
|
||||
sel('landPurpose', 'Przeznaczenie', O.landPurpose),
|
||||
sel('planningStatus', 'Plan / warunki zabudowy', O.planningStatus),
|
||||
sel('plotShape', 'Kształt działki', O.plotShape),
|
||||
txtField('plotDimensions', 'Wymiary', 'Np. 20 × 40 m'),
|
||||
sel('accessRoad', 'Dojazd', O.accessRoad),
|
||||
sel('fencing', 'Ogrodzenie', O.fencing),
|
||||
],
|
||||
media: ['Prąd', 'Woda miejska', 'Gaz', 'Kanalizacja'],
|
||||
amenities: ['Media przy granicy', 'Dostęp do drogi publicznej', 'Widok na las/wodę'],
|
||||
},
|
||||
'Lokal użytkowy': {
|
||||
legal: true,
|
||||
fields: [
|
||||
sel('localPurpose', 'Przeznaczenie lokalu', O.localPurpose),
|
||||
numField('roomHeight', 'Wysokość pomieszczeń', 'cm', 'Np. 320', 'integer'),
|
||||
sel('furnishing', 'Wyposażenie', O.furnishing),
|
||||
],
|
||||
media: ['Ogrzewanie miejskie', 'Ogrzewanie gazowe', 'Ogrzewanie elektryczne', 'Woda miejska', 'Prąd', 'Gaz', 'Internet', 'Światłowód', 'Klimatyzacja', 'Wentylacja mechaniczna', 'Alarm'],
|
||||
amenities: ['Wejście z ulicy', 'Witryna', 'Zaplecze socjalne', 'Parking', 'Dostęp 24/7', 'Monitoring', 'Ochrona', 'Dostęp dla niepełnosprawnych'],
|
||||
},
|
||||
'Budynek użytkowy': {
|
||||
legal: true,
|
||||
fields: [
|
||||
sel('buildingKind', 'Rodzaj obiektu', O.buildingKind),
|
||||
numField('plotArea', 'Powierzchnia działki', 'm²', 'Np. 1500'),
|
||||
numField('roomHeight', 'Wysokość', 'cm', 'Np. 450', 'integer'),
|
||||
],
|
||||
media: ['Ogrzewanie gazowe', 'Ogrzewanie elektryczne', 'Woda miejska', 'Prąd', 'Gaz', 'Kanalizacja', 'Internet', 'Światłowód', 'Klimatyzacja', 'Wentylacja mechaniczna'],
|
||||
amenities: ['Plac manewrowy', 'Rampa rozładunkowa', 'Parking', 'Brama automatyczna', 'Monitoring', 'Ochrona', 'Dostęp 24/7'],
|
||||
},
|
||||
Garaż: {
|
||||
legal: true,
|
||||
fields: [
|
||||
sel('garageType', 'Typ garażu', O.garageType),
|
||||
sel('garageLevel', 'Poziom', O.garageLevel),
|
||||
],
|
||||
media: ['Prąd'],
|
||||
amenities: ['Oświetlenie', 'Brama automatyczna', 'Monitoring', 'Ochrona', 'Dostęp 24/7', 'Kanał samochodowy'],
|
||||
},
|
||||
Pokój: {
|
||||
rentLead: true,
|
||||
fields: [
|
||||
sel('roommates', 'Liczba współlokatorów', O.roommates),
|
||||
sel('roommatePreference', 'Preferencje współlokatorów', O.roommatePreference),
|
||||
sel('furnishing', 'Umeblowanie', O.furnishing),
|
||||
],
|
||||
media: ['Internet', 'Światłowód', 'Telewizja kablowa'],
|
||||
amenities: ['Dostęp do kuchni', 'Dostęp do łazienki', 'Osobna łazienka', 'Balkon', 'Pralka', 'Parking', 'Dla studentów', 'Zwierzęta mile widziane'],
|
||||
},
|
||||
};
|
||||
|
||||
// Zbiera pola/kafelki dla wybranej kategorii i transakcji. Nieznana kategoria -> jak mieszkanie.
|
||||
function resolveCategoryExtra(category: string, isRent: boolean): { fields: ExtraFieldDef[]; mediaTiles: string[]; amenityTiles: string[] } {
|
||||
const cfg = CATEGORY_EXTRA[category] ?? CATEGORY_EXTRA.Mieszkanie;
|
||||
const fields: ExtraFieldDef[] = [
|
||||
...(isRent && cfg.rentLead !== false ? RENT_LEAD_FIELDS : []),
|
||||
...cfg.fields,
|
||||
...(!isRent && cfg.legal ? SALE_LEGAL_FIELDS : []),
|
||||
];
|
||||
const amenityTiles = [...cfg.amenities, ...(isRent ? (cfg.rentAmenities ?? []) : [])];
|
||||
return { fields, mediaTiles: cfg.media, amenityTiles };
|
||||
}
|
||||
|
||||
// Klucze extraInfo z wlasna kolumna w bazie - tylko te czyscimy przy zmianie kategorii, bo tylko
|
||||
// one moglyby wyciec do zapisanego ogloszenia (reszta pol zyje wylacznie w podgladzie kreatora).
|
||||
const PERSISTED_EXTRA_SELECTS = ['furnishing', 'buildingType', 'buildingMaterial', 'windows', 'exposure', 'roomHeight'] as const;
|
||||
|
||||
// Ekran startowy dodawania oferty: dwie karty (sprzedaż/wynajem) z listą kategorii. Wybór ustawia
|
||||
// transakcje i rodzaj nieruchomosci w formularzu. Rodzaje spoza "Mieszkanie"/"Dom" backend zapisuje
|
||||
// jako APARTMENT (enum ma tylko APARTMENT/HOUSE) - etykieta zyje w formularzu, logika sie zgadza.
|
||||
@@ -2420,6 +2608,22 @@ const INITIAL_EXTRA_INFO = {
|
||||
windows: 'Wybierz',
|
||||
exposure: 'Wybierz',
|
||||
roomHeight: '',
|
||||
// Pola kategorii-specyficzne dodane do dynamicznej sekcji "Dodatkowe informacje". Trzymane na
|
||||
// froncie (podglad kreatora); do bazy trafiaja tylko te, ktore maja swoja kolumne (furnishing,
|
||||
// buildingType, buildingMaterial, windows, exposure, roomHeight, availableFrom) oraz tagi media/amenities.
|
||||
landType: 'Wybierz',
|
||||
landPurpose: 'Wybierz',
|
||||
planningStatus: 'Wybierz',
|
||||
plotShape: 'Wybierz',
|
||||
plotDimensions: '',
|
||||
localPurpose: 'Wybierz',
|
||||
buildingKind: 'Wybierz',
|
||||
garageType: 'Wybierz',
|
||||
garageLevel: 'Wybierz',
|
||||
legalStatus: 'Wybierz',
|
||||
landRegister: 'Wybierz',
|
||||
roommates: 'Wybierz',
|
||||
roommatePreference: 'Wybierz',
|
||||
virtualTourUrl: '',
|
||||
};
|
||||
|
||||
@@ -12557,9 +12761,6 @@ function AccountPublicProfilePage() {
|
||||
{myListings.map((listing) => {
|
||||
const isRent = listing.offerType === 'RENT';
|
||||
const location = [listing.district, listing.city].filter(Boolean).join(', ') || listing.city;
|
||||
const floorLabel = listing.floor
|
||||
? (listing.buildingFloors ? `${listing.floor}/${listing.buildingFloors} piętro` : `${listing.floor} piętro`)
|
||||
: null;
|
||||
return (
|
||||
<article
|
||||
className="public-listing-card"
|
||||
@@ -12576,11 +12777,14 @@ function AccountPublicProfilePage() {
|
||||
<strong>{isRent ? formatMonthlyPrice(listing.price) : formatPrice(listing.price)}</strong>
|
||||
<h3>{listing.title}</h3>
|
||||
<small><Icon name="pin" /> {location}</small>
|
||||
<div className="public-listing-meta">
|
||||
<span><Icon name="scale" /> {formatArea(listing.area)}</span>
|
||||
<span><Icon name="user" /> {listing.rooms} {listing.rooms === 1 ? 'pokój' : 'pokoje'}</span>
|
||||
{floorLabel && <span><Icon name="building" /> {floorLabel}</span>}
|
||||
</div>
|
||||
<SpecTiles
|
||||
className="compact"
|
||||
items={[
|
||||
{ icon: 'area', value: formatArea(listing.area), caption: 'Powierzchnia' },
|
||||
{ icon: 'plan', value: `${listing.rooms}`, caption: listing.rooms === 1 ? 'Pokój' : 'Pokoje' },
|
||||
...(listing.floor ? [{ icon: 'stairs', value: listing.buildingFloors ? `${listing.floor}/${listing.buildingFloors}` : `${listing.floor}`, caption: 'Piętro' }] : []),
|
||||
]}
|
||||
/>
|
||||
<div className="public-listing-foot">
|
||||
<em>{listingRelativeDate(listing.createdAt) || 'Dodane niedawno'}</em>
|
||||
</div>
|
||||
@@ -14086,7 +14290,6 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
const isPhotosStep = currentStep === 2;
|
||||
const isExtraInfoStep = currentStep === 3;
|
||||
const isFinalPreviewStep = currentStep === 4;
|
||||
const showListingPreviewDetails = isPhotosStep || isExtraInfoStep;
|
||||
const mainPhoto = photos[0];
|
||||
// Komunikat o zlym numerze pokazujemy po opuszczeniu pola, zeby nie krzyczec przy pierwszej cyfrze.
|
||||
const contactPhoneError = validateContactPhone(listingDetails.contactPhoneCountry, listingDetails.contactPhone);
|
||||
@@ -14223,32 +14426,28 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
};
|
||||
|
||||
const isHouseProperty = listingDetails.propertyType === 'Dom';
|
||||
const isSaleHouse = isHouseProperty && !isRentTransaction;
|
||||
const isLandProperty = listingDetails.propertyType === 'Działka';
|
||||
const showFlatFields = !isHouseProperty && !isLandProperty;
|
||||
const showRoomFields = !isLandProperty;
|
||||
const showBuildingFields = !isLandProperty;
|
||||
const showBuildingExtraFields = !isLandProperty;
|
||||
const visibleMediaOptions = mediaOptions.filter((option) => {
|
||||
if (isSaleHouse && option.label === 'Kanalizacja') {
|
||||
return false;
|
||||
// Dynamiczna zawartosc kroku "Dodatkowe informacje" - pola i kafelki zalezne od kategorii i transakcji.
|
||||
const extraSection = resolveCategoryExtra(listingDetails.propertyType, isRentTransaction);
|
||||
const activeExtraFields = extraSection.fields;
|
||||
const visibleMediaOptions = extraSection.mediaTiles.map((label) => ({ label, icon: tileIcon(label) }));
|
||||
const visibleAmenityOptions = extraSection.amenityTiles.map((label) => ({ label, icon: tileIcon(label) }));
|
||||
// Odczyt/zapis pola z konfiguracji do wlasciwego stanu (extraInfo albo listingDetails).
|
||||
const extraFieldValue = (field: ExtraFieldDef): string => {
|
||||
const source = field.store === 'details' ? listingDetails : extraInfo;
|
||||
const raw = (source as unknown as Record<string, unknown>)[field.key];
|
||||
return typeof raw === 'string' ? raw : '';
|
||||
};
|
||||
const setExtraField = (field: ExtraFieldDef, value: string) => {
|
||||
if (field.store === 'details') {
|
||||
updateListingDetails(field.key as keyof typeof INITIAL_LISTING_DETAILS, value);
|
||||
} else {
|
||||
updateExtraInfo(field.key as keyof typeof INITIAL_EXTRA_INFO, value);
|
||||
}
|
||||
return !isLandProperty || ['Prąd', 'Gaz', 'Woda miejska', 'Kanalizacja'].includes(option.label);
|
||||
});
|
||||
const hiddenAmenityLabels = isLandProperty
|
||||
? ['Balkon', 'Taras', 'Winda', 'Piwnica', 'Komórka lokatorska', 'Dostęp dla niepełnosprawnych', 'Monitoring']
|
||||
: isHouseProperty
|
||||
? ['Winda', 'Komórka lokatorska', 'Garaż', 'Piwnica', 'Strych']
|
||||
: ['Strych', 'Ogrodzenie'];
|
||||
const availableAmenityOptions = isHouseProperty
|
||||
? [...amenityOptions, ...houseAmenityOptions]
|
||||
: amenityOptions;
|
||||
const visibleAmenityOptions = availableAmenityOptions.filter((option) => {
|
||||
if (!isRentTransaction && (option.label === 'Dla studentów' || option.label === 'Zwierzęta mile widziane')) {
|
||||
return false;
|
||||
}
|
||||
return !hiddenAmenityLabels.includes(option.label);
|
||||
});
|
||||
};
|
||||
const mediaBillingEntries = isRentTransaction
|
||||
? [
|
||||
...selectedMedia
|
||||
@@ -14266,20 +14465,21 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
: []),
|
||||
]
|
||||
: [];
|
||||
const previewArea = listingDetails.area.trim() ? `${listingDetails.area.replace('.', ',')} m²` : 'Powierzchnia';
|
||||
const previewRooms = listingDetails.rooms !== 'Wybierz' ? `${listingDetails.rooms} ${listingDetails.rooms === '1' ? 'pokój' : ['2', '3', '4'].includes(listingDetails.rooms) ? 'pokoje' : 'pokoi'}` : 'Pokoje';
|
||||
const previewFloor = listingDetails.floor !== 'Wybierz' ? `Piętro ${listingDetails.floor}${listingDetails.buildingFloors !== 'Wybierz' ? `/${listingDetails.buildingFloors}` : ''}` : 'Piętro';
|
||||
const previewLocation = [
|
||||
location.city,
|
||||
location.district,
|
||||
location.estate,
|
||||
[location.street, location.building].filter(Boolean).join(' '),
|
||||
].filter((part) => part.trim()).join(', ') || 'Lokalizacja';
|
||||
const previewMetaItems = [
|
||||
{ icon: 'building', label: previewArea },
|
||||
...(showRoomFields ? [{ icon: 'house', label: previewRooms }] : []),
|
||||
...(showFlatFields ? [{ icon: 'city', label: previewFloor }] : []),
|
||||
{ icon: 'check', label: listingDetails.market },
|
||||
// Rdzenne parametry jako kafelki (ikona -> wartosc -> podpis), tak jak na stronie ogloszenia.
|
||||
const previewSpecTiles: SpecTile[] = [
|
||||
{ icon: 'area', value: listingDetails.area.trim() ? `${listingDetails.area.replace('.', ',')} m²` : '—', caption: 'Powierzchnia' },
|
||||
...(showRoomFields ? [{ icon: 'plan', value: listingDetails.rooms !== 'Wybierz' ? listingDetails.rooms : '—', caption: 'Pokoje' }] : []),
|
||||
...(showFlatFields ? [{ icon: 'stairs', value: listingDetails.floor !== 'Wybierz' ? `${listingDetails.floor}${listingDetails.buildingFloors !== 'Wybierz' ? `/${listingDetails.buildingFloors}` : ''}` : '—', caption: 'Piętro' }] : []),
|
||||
{ icon: 'check', value: listingDetails.market, caption: 'Rynek' },
|
||||
];
|
||||
// Dodatkowe cechy (umeblowanie, udogodnienia, media) jako drobne chipy pod kafelkami - w kroku "Dodatkowe informacje".
|
||||
const previewExtraChips = [
|
||||
...(isExtraInfoStep && extraInfo.furnishing !== 'Wybierz' ? [{ icon: 'sofa', label: extraInfo.furnishing === 'Tak' ? 'Umeblowane' : `Umeblowanie: ${extraInfo.furnishing}` }] : []),
|
||||
...(isExtraInfoStep && extraInfo.buildingType !== 'Wybierz' ? [{ icon: 'building', label: extraInfo.buildingType }] : []),
|
||||
...(['Balkon', 'Taras', 'Ogródek', 'Winda', 'Parking', 'Garaż', 'Piwnica']
|
||||
@@ -14288,7 +14488,7 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
...(['Internet', 'Klimatyzacja', 'Ogrzewanie miejskie', 'Gaz']
|
||||
.filter((label) => isExtraInfoStep && selectedMedia.includes(label))
|
||||
.map((label) => ({ icon: label === 'Internet' ? 'wifi' : label === 'Klimatyzacja' ? 'snow' : label === 'Gaz' || label === 'Ogrzewanie miejskie' ? 'flame' : 'spark', label }))),
|
||||
].slice(0, isExtraInfoStep ? 8 : 4);
|
||||
].slice(0, 6);
|
||||
const finalGalleryImages = photos.length > 0
|
||||
? photos.map((photo) => photo.url)
|
||||
: [heroImage, heroImage, cityImage, heroImage, cityImage];
|
||||
@@ -14297,24 +14497,32 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
...selectedAmenities.filter((label) => visibleAmenityOptions.some((option) => option.label === label)).map((label) => ({ icon: label === 'Parking' || label === 'Garaż' ? 'car' : label === 'Ogródek' ? 'wifi' : 'building', label })),
|
||||
...selectedMedia.filter((label) => visibleMediaOptions.some((option) => option.label === label)).map((label) => ({ icon: label === 'Internet' ? 'wifi' : label === 'Klimatyzacja' ? 'snow' : label === 'Gaz' || label.includes('Ogrzewanie') ? 'flame' : label === 'Telefon' ? 'phone' : 'spark', label })),
|
||||
];
|
||||
// Podglad "Szczegoly" budujemy z: stalych pol kroku 1 (rynek, forma wlasnosci, rok, stan, dom),
|
||||
// dynamicznych pol kroku "Dodatkowe informacje" (te same, ktore widzi uzytkownik w formularzu -
|
||||
// dzieki temu podglad zawsze zgadza sie z formularzem) oraz rozliczen najmu.
|
||||
const finalDetails = [
|
||||
{ label: 'Rynek', value: listingDetails.market },
|
||||
{ label: 'Forma własności', value: listingDetails.ownership },
|
||||
...(!isRentTransaction ? [{ label: 'Forma własności', value: listingDetails.ownership }] : []),
|
||||
...(showBuildingFields ? [{ label: 'Rok budowy', value: listingDetails.yearBuilt || 'Nie podano' }] : []),
|
||||
...(showBuildingFields ? [{ label: 'Stan nieruchomości', value: listingDetails.condition }] : []),
|
||||
...(isHouseProperty ? [{ label: 'Typ domu', value: listingDetails.houseType !== 'Wybierz' ? listingDetails.houseType : 'Nie podano' }] : []),
|
||||
...(isHouseProperty ? [{ label: 'Liczba kondygnacji', value: listingDetails.houseFloors !== 'Wybierz' ? listingDetails.houseFloors : 'Nie podano' }] : []),
|
||||
...(isSaleHouse ? [{ label: 'Powierzchnia działki', value: extraInfo.plotArea ? `${extraInfo.plotArea} m²` : 'Nie podano' }] : []),
|
||||
...(isHouseProperty ? [{ label: 'Garaż', value: listingDetails.houseGarage !== 'Wybierz' ? listingDetails.houseGarage : 'Nie podano' }] : []),
|
||||
...(isHouseProperty ? [{ label: 'Podpiwniczenie', value: listingDetails.houseBasement !== 'Wybierz' ? listingDetails.houseBasement : 'Nie podano' }] : []),
|
||||
...(isHouseProperty ? [{ label: 'Strych', value: listingDetails.houseAttic !== 'Wybierz' ? listingDetails.houseAttic : 'Nie podano' }] : []),
|
||||
...(isSaleHouse ? [{ label: 'Dach', value: extraInfo.roofType !== 'Wybierz' ? extraInfo.roofType : 'Nie podano' }] : []),
|
||||
...(isSaleHouse ? [{ label: 'Pokrycie dachu', value: extraInfo.roofCover !== 'Wybierz' ? extraInfo.roofCover : 'Nie podano' }] : []),
|
||||
...(isSaleHouse && selectedAmenities.includes('Ogrodzenie') ? [{ label: 'Ogrodzenie', value: extraInfo.fencing !== 'Wybierz' ? extraInfo.fencing : 'Nie podano' }] : []),
|
||||
...(isSaleHouse ? [{ label: 'Dojazd', value: extraInfo.accessRoad !== 'Wybierz' ? extraInfo.accessRoad : 'Nie podano' }] : []),
|
||||
...(isSaleHouse && selectedMedia.includes('Gospodarka ściekowa') ? [{ label: 'Gospodarka ściekowa', value: extraInfo.sewageType !== 'Wybierz' ? extraInfo.sewageType : 'Nie podano' }] : []),
|
||||
...(isRentTransaction ? [{ label: 'Czynsz administracyjny', value: extraInfo.rent ? `${extraInfo.rent} zł` : 'Nie podano' }] : []),
|
||||
// Dynamiczne pola sekcji "Dodatkowe informacje" - tylko wypelnione (kaucja pokazana osobno nizej).
|
||||
...activeExtraFields
|
||||
.filter((field) => field.key !== 'deposit')
|
||||
.map((field) => {
|
||||
const raw = extraFieldValue(field);
|
||||
if (!raw || raw === 'Wybierz') {
|
||||
return null;
|
||||
}
|
||||
return { label: field.label, value: field.suffix ? `${raw} ${field.suffix}` : raw };
|
||||
})
|
||||
.filter((row): row is { label: string; value: string } => row !== null),
|
||||
...(isRentTransaction ? [{ label: 'Kaucja', value: formattedSecurityDeposit }] : []),
|
||||
...(isRentTransaction ? [{ label: 'Okres najmu', value: extraInfo.rentalPeriod !== 'Wybierz' ? extraInfo.rentalPeriod : 'Nie podano' }] : []),
|
||||
...(isRentTransaction ? [...mediaBillingEntries, ...amenityBillingEntries].map((item) => {
|
||||
const mode = extraInfo.mediaBilling[item.key] ?? item.options[0];
|
||||
const amount = (extraInfo.mediaBillingAmounts[item.key] ?? '').trim();
|
||||
@@ -14326,9 +14534,6 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
value,
|
||||
};
|
||||
}) : []),
|
||||
...(showBuildingFields ? [{ label: 'Stan nieruchomości', value: listingDetails.condition }] : []),
|
||||
...(showBuildingExtraFields ? [{ label: 'Umeblowanie', value: extraInfo.furnishing }] : []),
|
||||
...(!isSaleHouse ? [{ label: 'Dostępne od', value: extraInfo.availableFrom || 'Do uzgodnienia' }] : []),
|
||||
{ label: 'Ogłoszenie dodane', value: 'Dzisiaj' },
|
||||
];
|
||||
|
||||
@@ -14528,17 +14733,21 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
yearBuilt: showBuildingFields ? current.yearBuilt : '',
|
||||
condition: showBuildingFields ? current.condition : 'Wybierz',
|
||||
}));
|
||||
if (!showBuildingExtraFields) {
|
||||
setExtraInfo((current) => ({
|
||||
...current,
|
||||
furnishing: 'Wybierz',
|
||||
buildingType: 'Wybierz',
|
||||
buildingMaterial: 'Wybierz',
|
||||
windows: 'Wybierz',
|
||||
exposure: 'Wybierz',
|
||||
roomHeight: '',
|
||||
}));
|
||||
// Wyczysc te zapisywane pola extra, ktorych nowa kategoria nie uzywa - inaczej wartosc ustawiona
|
||||
// przy poprzednim rodzaju nieruchomosci (np. "Typ zabudowy" mieszkania) wycieklaby do zapisu.
|
||||
const activeKeys = new Set([
|
||||
...resolveCategoryExtra(listingDetails.propertyType, true).fields,
|
||||
...resolveCategoryExtra(listingDetails.propertyType, false).fields,
|
||||
].map((field) => field.key));
|
||||
setExtraInfo((current) => {
|
||||
const patch: Partial<typeof INITIAL_EXTRA_INFO> = {};
|
||||
for (const key of PERSISTED_EXTRA_SELECTS) {
|
||||
if (!activeKeys.has(key) && current[key] !== INITIAL_EXTRA_INFO[key]) {
|
||||
patch[key] = INITIAL_EXTRA_INFO[key];
|
||||
}
|
||||
}
|
||||
return Object.keys(patch).length ? { ...current, ...patch } : current;
|
||||
});
|
||||
}, [listingDetails.propertyType]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -14735,8 +14944,8 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
<section className="add-form-card">
|
||||
<label className="add-field full">
|
||||
<span>Tytuł ogłoszenia</span>
|
||||
<input value={listingDetails.title} maxLength={100} onChange={(event) => updateListingDetails('title', event.target.value)} placeholder={LISTING_TITLE_HINTS[listingDetails.propertyType] ?? DEFAULT_LISTING_TITLE_HINT} />
|
||||
<small>{listingDetails.title.length}/100</small>
|
||||
<input value={listingDetails.title} maxLength={70} onChange={(event) => updateListingDetails('title', event.target.value)} placeholder={LISTING_TITLE_HINTS[listingDetails.propertyType] ?? DEFAULT_LISTING_TITLE_HINT} />
|
||||
<small>{listingDetails.title.length}/70</small>
|
||||
</label>
|
||||
|
||||
<div className="add-field full">
|
||||
@@ -14828,7 +15037,9 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
</div>
|
||||
{contactPhoneTouched && contactPhoneError && <small className="add-field-error">{contactPhoneError}</small>}
|
||||
</label>
|
||||
<AddInput label="E-mail" value={listingDetails.contactEmail} type="email" placeholder="Np. jan.kowalski@email.pl" onChange={(value) => updateListingDetails('contactEmail', value)} />
|
||||
{/* E-mail kontaktowy jest na stale adresem konta (z rejestracji) - pokazujemy go,
|
||||
ale nie pozwalamy edytowac, zeby kontakt zawsze trafial na wlasciwe konto. */}
|
||||
<AddInput label="E-mail (adres konta)" value={listingDetails.contactEmail || user?.email || ''} type="email" readOnly />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -14975,8 +15186,8 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
{index === 0 && <span className="main-photo-badge">Zdjęcie główne</span>}
|
||||
<button className="remove-photo-button" type="button" aria-label={`Usuń zdjęcie ${photo.name}`} onClick={() => removePhoto(photo.id)}>×</button>
|
||||
<div className="photo-order-controls" aria-label="Zmień kolejność zdjęcia">
|
||||
<button type="button" disabled={index === 0} onClick={() => movePhoto(photo.id, -1)} aria-label={`Przesuń zdjęcie ${photo.name} w lewo`}>‹</button>
|
||||
<button type="button" disabled={index === photos.length - 1} onClick={() => movePhoto(photo.id, 1)} aria-label={`Przesuń zdjęcie ${photo.name} w prawo`}>›</button>
|
||||
<button className="prev" type="button" disabled={index === 0} onClick={() => movePhoto(photo.id, -1)} aria-label={`Przesuń zdjęcie ${photo.name} w lewo`}><Icon name="chevron" /></button>
|
||||
<button className="next" type="button" disabled={index === photos.length - 1} onClick={() => movePhoto(photo.id, 1)} aria-label={`Przesuń zdjęcie ${photo.name} w prawo`}><Icon name="chevron" /></button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
@@ -15044,33 +15255,33 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
|
||||
{isExtraInfoStep && (
|
||||
<>
|
||||
<div className="add-title add-step-title">
|
||||
<h1>Dodatkowe informacje</h1>
|
||||
<section className="add-form-card add-extra-card">
|
||||
{/* Naglowek kroku wewnatrz bialej karty - taki sam styl jak "Dodaj zdjecia"/"Lokalizacja". */}
|
||||
<div className="add-card-heading">
|
||||
<h2>Dodatkowe informacje</h2>
|
||||
<p>Podaj więcej szczegółów, które pomogą wyróżnić Twoje ogłoszenie.</p>
|
||||
</div>
|
||||
|
||||
<section className="add-form-card add-extra-card">
|
||||
<div className="extra-section">
|
||||
<h2>Informacje uzupełniające</h2>
|
||||
<div className="extra-details-grid">
|
||||
{isRentTransaction && <AddSelect label="Okres najmu" value={extraInfo.rentalPeriod} options={extraInfoSelectOptions.rentalPeriod} onChange={(value) => updateExtraInfo('rentalPeriod', value)} />}
|
||||
{isRentTransaction && <AddInput label="Kaucja" value={extraInfo.deposit} suffix="zł" placeholder="Np. 6000" onChange={(value) => updateExtraInfo('deposit', value)} />}
|
||||
{!isSaleHouse && <AddInput label="Dostępne od" value={extraInfo.availableFrom} type="date" onChange={(value) => updateExtraInfo('availableFrom', value)} />}
|
||||
{isSaleHouse && <AddInput label="Powierzchnia działki" value={extraInfo.plotArea} suffix="m²" placeholder="Np. 650" onChange={(value) => updateExtraInfo('plotArea', value)} />}
|
||||
{showBuildingExtraFields && <AddSelect label="Umeblowanie" value={extraInfo.furnishing} options={extraInfoSelectOptions.furnishing} onChange={(value) => updateExtraInfo('furnishing', value)} />}
|
||||
{showBuildingExtraFields && !isHouseProperty && <AddSelect label="Typ zabudowy" value={extraInfo.buildingType} options={extraInfoSelectOptions.buildingType} onChange={(value) => updateExtraInfo('buildingType', value)} />}
|
||||
{isSaleHouse && <AddSelect label="Dach" value={extraInfo.roofType} options={extraInfoSelectOptions.roofType} onChange={(value) => updateExtraInfo('roofType', value)} />}
|
||||
{isSaleHouse && <AddSelect label="Pokrycie dachu" value={extraInfo.roofCover} options={extraInfoSelectOptions.roofCover} onChange={(value) => updateExtraInfo('roofCover', value)} />}
|
||||
{showBuildingExtraFields && <AddSelect label="Materiał budynku" value={extraInfo.buildingMaterial} options={extraInfoSelectOptions.buildingMaterial} onChange={(value) => updateExtraInfo('buildingMaterial', value)} />}
|
||||
{showBuildingExtraFields && <AddSelect label="Okna" value={extraInfo.windows} options={extraInfoSelectOptions.windows} onChange={(value) => updateExtraInfo('windows', value)} />}
|
||||
{showBuildingExtraFields && <AddSelect label="Ekspozycja" value={extraInfo.exposure} options={extraInfoSelectOptions.exposure} onChange={(value) => updateExtraInfo('exposure', value)} />}
|
||||
{isSaleHouse && <AddSelect label="Dojazd" value={extraInfo.accessRoad} options={extraInfoSelectOptions.accessRoad} onChange={(value) => updateExtraInfo('accessRoad', value)} />}
|
||||
{showBuildingExtraFields && <AddInput label="Wysokość pomieszczeń" value={extraInfo.roomHeight} suffix="cm" onChange={(value) => updateExtraInfo('roomHeight', value)} />}
|
||||
{/* Zestaw pol zalezny od kategorii i transakcji - patrz CATEGORY_EXTRA / resolveCategoryExtra. */}
|
||||
{activeExtraFields.map((field) => {
|
||||
const value = extraFieldValue(field);
|
||||
const onChange = (next: string) => setExtraField(field, next);
|
||||
if (field.kind === 'select') {
|
||||
return <AddSelect key={field.key} label={field.label} value={value} options={field.options} onChange={onChange} />;
|
||||
}
|
||||
if (field.kind === 'date') {
|
||||
return <AddInput key={field.key} label={field.label} value={value} type="date" onChange={onChange} />;
|
||||
}
|
||||
return <AddInput key={field.key} label={field.label} value={value} suffix={field.suffix} placeholder={field.placeholder} numeric={field.numeric} onChange={onChange} />;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="extra-section">
|
||||
<h2>{isSaleHouse ? 'Media' : 'Media i opłaty'}</h2>
|
||||
<h2>{isRentTransaction ? 'Media i opłaty' : 'Media'}</h2>
|
||||
<div className="extra-option-grid media-option-grid">
|
||||
{visibleMediaOptions.map((option) => (
|
||||
<label className={selectedMedia.includes(option.label) ? 'selected' : ''} key={option.label}>
|
||||
@@ -15081,12 +15292,6 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isSaleHouse && selectedMedia.includes('Gospodarka ściekowa') && (
|
||||
<div className="extra-details-grid" style={{ marginTop: 12 }}>
|
||||
<AddSelect label="Gospodarka ściekowa" value={extraInfo.sewageType} options={extraInfoSelectOptions.sewageType} onChange={(value) => updateExtraInfo('sewageType', value)} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isRentTransaction && mediaBillingEntries.length > 0 && (
|
||||
<div className="media-billing-panel">
|
||||
<h3>Sposób rozliczenia wybranych opłat</h3>
|
||||
@@ -15132,12 +15337,6 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{isSaleHouse && (
|
||||
<div className="extra-details-grid" style={{ marginTop: 12 }}>
|
||||
{selectedAmenities.includes('Ogrodzenie') && <AddSelect label="Ogrodzenie" value={extraInfo.fencing} options={extraInfoSelectOptions.fencing} onChange={(value) => updateExtraInfo('fencing', value)} />}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isRentTransaction && amenityBillingEntries.length > 0 && (
|
||||
<div className="media-billing-panel">
|
||||
<h3>Sposób rozliczenia udogodnień</h3>
|
||||
@@ -15187,8 +15386,13 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
{isFinalPreviewStep && (
|
||||
<>
|
||||
<div className="add-title add-step-title">
|
||||
<h1>Podgląd ogłoszenia</h1>
|
||||
<p>Sprawdź, jak będzie wyglądać Twoje ogłoszenie.</p>
|
||||
<h1>
|
||||
Tak będzie wyglądać <span className="add-title-accent">Twoje ogłoszenie</span>
|
||||
<svg className="add-title-spark" viewBox="0 0 44 40" fill="none" aria-hidden="true">
|
||||
<path d="M6 27 18 12 M17 31 31 15 M28 32 38 21" />
|
||||
</svg>
|
||||
</h1>
|
||||
<p>Sprawdź wszystkie informacje przed publikacją. Możesz jeszcze wrócić i coś zmienić.</p>
|
||||
</div>
|
||||
|
||||
<section className="add-form-card final-preview-card">
|
||||
@@ -15217,11 +15421,7 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
{!isRentTransaction && <small>{formattedPricePerMeter}</small>}
|
||||
</strong>
|
||||
{isRentTransaction && <span className="final-rent-deposit">{formattedDeposit}</span>}
|
||||
<div className="final-meta-row">
|
||||
{previewMetaItems.slice(0, 4).map((item) => (
|
||||
<span key={item.label}><Icon name={item.icon} /> <small>{item.label}</small></span>
|
||||
))}
|
||||
</div>
|
||||
<SpecTiles items={previewSpecTiles} className="final-specs" />
|
||||
|
||||
<div className="final-description-block">
|
||||
<h3>Opis ogłoszenia</h3>
|
||||
@@ -15371,14 +15571,14 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
<section className="listing-preview-card">
|
||||
<h2>Podgląd ogłoszenia</h2>
|
||||
<div
|
||||
className={`preview-image ${showListingPreviewDetails ? 'photos-preview-image' : ''}`}
|
||||
className="preview-image photos-preview-image"
|
||||
style={{
|
||||
backgroundImage: `url(${mainPhoto?.url ?? heroImage})`,
|
||||
backgroundPosition: mainPhoto ? 'center' : undefined,
|
||||
backgroundSize: mainPhoto ? 'cover' : undefined,
|
||||
}}
|
||||
>
|
||||
<span><Icon name="camera" /> {showListingPreviewDetails ? `${Math.max(photos.length, 1)}/20` : '1/8'}</span>
|
||||
<span><Icon name="camera" /> {Math.max(photos.length, 1)}/20</span>
|
||||
</div>
|
||||
<div className="preview-body">
|
||||
<p>{previewTitle}</p>
|
||||
@@ -15387,26 +15587,16 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
{!isRentTransaction && <small>{formattedPricePerMeter}</small>}
|
||||
</strong>
|
||||
{isRentTransaction && <span className="preview-rent-deposit">{formattedDeposit}</span>}
|
||||
{showListingPreviewDetails ? (
|
||||
<>
|
||||
<span><Icon name="pin" /> {previewLocation}</span>
|
||||
<div className="preview-meta preview-meta-icons">
|
||||
{previewMetaItems.map((item) => (
|
||||
{/* Podglad wyglada tak samo na kazdym kroku kreatora - kafelki (ikona/wartosc/podpis)
|
||||
plus lokalizacja i ewentualne dodatkowe cechy. */}
|
||||
<span className="preview-loc"><Icon name="pin" /> {previewLocation}</span>
|
||||
<SpecTiles items={previewSpecTiles} className="preview-specs" />
|
||||
{previewExtraChips.length > 0 && (
|
||||
<div className="preview-chip-row">
|
||||
{previewExtraChips.map((item) => (
|
||||
<span key={item.label}><Icon name={item.icon} /> {item.label}</span>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="preview-meta">
|
||||
<span>{listingDetails.propertyType}</span>
|
||||
<span>{previewArea}</span>
|
||||
{showRoomFields && <span>{previewRooms}</span>}
|
||||
{showFlatFields && <span>{previewFloor}</span>}
|
||||
</div>
|
||||
{isLocationStep && <span><Icon name="pin" /> {previewLocation}</span>}
|
||||
<span><Icon name="clock" /> Dzisiaj</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
@@ -22286,7 +22476,15 @@ function RealListingsBand({
|
||||
const viewMode = viewParam === 'kompaktowa' || viewParam === 'kafelki' ? viewParam : 'lista';
|
||||
const sortParam = (searchParams.get('sortuj') ?? 'default') as SortKey;
|
||||
const sort: SortKey = SORT_OPTIONS.some((option) => option.value === sortParam) ? sortParam : 'default';
|
||||
// Ogloszenia wyroznione sa platne za pozycje - zawsze ida na gore listy, niezaleznie od wybranego
|
||||
// sortowania. Dopiero w obrebie tej samej grupy (wyroznione / zwykle) dziala wybrany porzadek.
|
||||
const isPromotedOffer = (offer: typeof visible[number]) =>
|
||||
!!offer.promotedUntil && new Date(offer.promotedUntil).getTime() > Date.now();
|
||||
const sorted = [...visible].sort((a, b) => {
|
||||
const promoDiff = (isPromotedOffer(b) ? 1 : 0) - (isPromotedOffer(a) ? 1 : 0);
|
||||
if (promoDiff !== 0) {
|
||||
return promoDiff;
|
||||
}
|
||||
const at = new Date(a.createdAt).getTime();
|
||||
const bt = new Date(b.createdAt).getTime();
|
||||
switch (sort) {
|
||||
@@ -22372,12 +22570,12 @@ function RealListingsBand({
|
||||
{pageItems.map((item) => {
|
||||
const favorite = isFavorite(item.id);
|
||||
const floor = listingFloorText(item.floor, item.buildingFloors);
|
||||
const meta = [
|
||||
formatArea(item.area),
|
||||
listingRoomsText(item.rooms),
|
||||
floor ? `${floor} piętro` : null,
|
||||
item.yearBuilt ? `${item.yearBuilt} r.` : null,
|
||||
].filter(Boolean).join(' • ');
|
||||
const resultSpecTiles: SpecTile[] = [
|
||||
{ icon: 'area', value: formatArea(item.area), caption: 'Powierzchnia' },
|
||||
{ icon: 'plan', value: `${item.rooms}`, caption: item.rooms === 1 ? 'Pokój' : 'Pokoje' },
|
||||
...(floor ? [{ icon: 'stairs', value: `${floor}`, caption: 'Piętro' }] : []),
|
||||
...(item.yearBuilt ? [{ icon: 'calendar', value: `${item.yearBuilt}`, caption: 'Rok budowy' }] : []),
|
||||
];
|
||||
// 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';
|
||||
@@ -22387,6 +22585,9 @@ function RealListingsBand({
|
||||
// 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">
|
||||
{!!item.promotedUntil && new Date(item.promotedUntil).getTime() > Date.now() && (
|
||||
<span className="result-promoted-badge"><Icon name="spark" /> Wyróżnione</span>
|
||||
)}
|
||||
<div className="result-photo">
|
||||
<ResultCardGallery photos={item.photos} cover={item.coverPhoto} alt={item.title} />
|
||||
{offerType === 'SALE' && (
|
||||
@@ -22394,9 +22595,6 @@ function RealListingsBand({
|
||||
<span className="green-tag">{listingMarketBadge(item.market)}</span>
|
||||
</div>
|
||||
)}
|
||||
{!!item.promotedUntil && new Date(item.promotedUntil).getTime() > Date.now() && (
|
||||
<span className="result-promoted-badge"><Icon name="spark" /> Wyróżnione</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={favorite ? 'real-favorite-button saved' : 'real-favorite-button'}
|
||||
@@ -22414,7 +22612,7 @@ function RealListingsBand({
|
||||
<div className="result-info">
|
||||
<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>
|
||||
<SpecTiles items={resultSpecTiles} className="compact result-specs" />
|
||||
{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).
|
||||
@@ -24451,11 +24649,14 @@ function PublicProfilePage() {
|
||||
<strong>{formatPrice(listing.price)}{listing.offerType === 'RENT' ? ' / mies.' : ''}</strong>
|
||||
<h3>{listing.title}</h3>
|
||||
<small><Icon name="pin" /> {[listing.city, listing.district].filter(Boolean).join(', ')}</small>
|
||||
<div className="public-listing-meta">
|
||||
{listing.area !== null && <span><Icon name="scale" /> {formatArea(listing.area)}</span>}
|
||||
{listing.rooms !== null && <span><Icon name="sofa" /> {listingRoomsText(listing.rooms)}</span>}
|
||||
{listing.floor && <span><Icon name="building" /> {listingFloorText(listing.floor, listing.buildingFloors)} piętro</span>}
|
||||
</div>
|
||||
<SpecTiles
|
||||
className="compact"
|
||||
items={[
|
||||
...(listing.area !== null ? [{ icon: 'area', value: formatArea(listing.area), caption: 'Powierzchnia' }] : []),
|
||||
...(listing.rooms !== null ? [{ icon: 'plan', value: `${listing.rooms}`, caption: listing.rooms === 1 ? 'Pokój' : 'Pokoje' }] : []),
|
||||
...(listing.floor ? [{ icon: 'stairs', value: `${listingFloorText(listing.floor, listing.buildingFloors)}`, caption: 'Piętro' }] : []),
|
||||
]}
|
||||
/>
|
||||
<div className="public-listing-foot">
|
||||
<em>{listingRelativeDate(listing.createdAt) || 'Dodane niedawno'}</em>
|
||||
</div>
|
||||
@@ -26435,6 +26636,23 @@ function SearchField({ icon, label, value }: { icon: string; label: string; valu
|
||||
);
|
||||
}
|
||||
|
||||
// Wspolny wiersz "kafelkow" ze specyfikacja: ikona -> duza wartosc -> maly podpis. Uzywany na stronie
|
||||
// ogloszenia, na kartach wynikow i w podgladzie kreatora, zeby wszedzie wygladalo tak samo.
|
||||
type SpecTile = { icon: string; value: string; caption: string };
|
||||
function SpecTiles({ items, className }: { items: SpecTile[]; className?: string }) {
|
||||
return (
|
||||
<div className={className ? `spec-tiles ${className}` : 'spec-tiles'}>
|
||||
{items.map((item) => (
|
||||
<div className="spec-tile" key={item.caption}>
|
||||
<Icon name={item.icon} />
|
||||
<strong>{item.value}</strong>
|
||||
<span>{item.caption}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AddSelect({ label, value, options = [value], onChange }: { label: string; value: string; options?: string[]; onChange?: (value: string) => void }) {
|
||||
return (
|
||||
<label className="add-field">
|
||||
@@ -27380,9 +27598,9 @@ function ListingDetailPage({
|
||||
</div>
|
||||
|
||||
<div className="listing-detail-stats">
|
||||
<div><Icon name="screen" /><strong>{formatArea(listing.area)}</strong><span>Powierzchnia</span></div>
|
||||
<div><Icon name="sofa" /><strong>{listing.rooms}</strong><span>Pokoje</span></div>
|
||||
{floorText && <div><Icon name="building" /><strong>{floorText}</strong><span>Piętro</span></div>}
|
||||
<div><Icon name="area" /><strong>{formatArea(listing.area)}</strong><span>Powierzchnia</span></div>
|
||||
<div><Icon name="plan" /><strong>{listing.rooms}</strong><span>Pokoje</span></div>
|
||||
{floorText && <div><Icon name="stairs" /><strong>{floorText}</strong><span>Piętro</span></div>}
|
||||
{listing.yearBuilt && <div><Icon name="calendar" /><strong>{listing.yearBuilt}</strong><span>Rok budowy</span></div>}
|
||||
</div>
|
||||
|
||||
@@ -28429,6 +28647,27 @@ function Icon({ name }: { name: string }) {
|
||||
<path d="M19 14.5A8.5 8.5 0 1 1 9.5 5a7 7 0 0 0 9.5 9.5Z" />
|
||||
</svg>
|
||||
);
|
||||
// Powierzchnia (metraz) - naroznikowe ramki jak "obszar", czysty styl liniowy.
|
||||
case 'area':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4 9V5a1 1 0 0 1 1-1h4M15 4h4a1 1 0 0 1 1 1v4M20 15v4a1 1 0 0 1-1 1h-4M9 20H5a1 1 0 0 1-1-1v-4" />
|
||||
</svg>
|
||||
);
|
||||
// Liczba pokoi - rzut pomieszczen (plan) ze scianka i przejsciem.
|
||||
case 'plan':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M4 4h16v16H4Z M12 4v7 M4 14h6 M14 14h6" />
|
||||
</svg>
|
||||
);
|
||||
// Pietro - schody.
|
||||
case 'stairs':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M3 19h4v-4h4v-4h4v-4h4" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
+201
-7
@@ -3128,6 +3128,13 @@ a.city-card:hover {
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
/* Widok listy (domyslny): to kafelki spychaja tresc na dol, wiec stopka idzie tuz pod nie -
|
||||
bez wlasnego margin-top:auto, ktory rozbilby wolna przestrzen na pol. Kompaktowy i kafelki
|
||||
zachowuja oryginalne przypiecie stopki do dolu. */
|
||||
.result-list.view-lista .result-seller-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.result-seller-type.company {
|
||||
color: #0f7a48;
|
||||
}
|
||||
@@ -4202,6 +4209,27 @@ a.city-card:hover {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Zielony akcent w naglowku (np. "Twoje ogloszenie" w podgladzie przed publikacja). */
|
||||
.add-title-accent {
|
||||
color: #12a764;
|
||||
}
|
||||
|
||||
/* Ozdobne zielone kreski ("iskierka") tuz obok napisu "Twoje ogloszenie". */
|
||||
.add-title-spark {
|
||||
color: #12a764;
|
||||
display: inline-block;
|
||||
height: 34px;
|
||||
margin-left: 10px;
|
||||
vertical-align: middle;
|
||||
width: 38px;
|
||||
}
|
||||
|
||||
.add-title-spark path {
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-width: 3;
|
||||
}
|
||||
|
||||
.add-title h1 span {
|
||||
color: #10a15e;
|
||||
}
|
||||
@@ -4923,6 +4951,13 @@ a.city-card:hover {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Pole tylko do odczytu (np. e-mail z konta, cena za m2) - wyszarzone, zeby bylo widac, ze zablokowane. */
|
||||
.add-field input[readonly] {
|
||||
background: #f1f5fa;
|
||||
color: #5f7085;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.add-field input::placeholder,
|
||||
.editor-box textarea::placeholder {
|
||||
color: #9aa5b4;
|
||||
@@ -5747,13 +5782,22 @@ a.city-card:hover {
|
||||
border-radius: 999px;
|
||||
color: #ffffff;
|
||||
display: grid;
|
||||
font-size: 20px;
|
||||
font-weight: 950;
|
||||
height: 28px;
|
||||
place-items: center;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.photo-order-controls button svg {
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
/* Daszek chevronu rysowany pionowo - obracamy go na strzalke w lewo/prawo,
|
||||
tak samo jak w sliderze kart i nawigacji zdjec w wynikach. Wczesniejsze glify
|
||||
tekstowe (< >) mialy asymetryczne odstepy i nie stawaly rowno w kolku. */
|
||||
.photo-order-controls button.prev svg { transform: rotate(90deg); }
|
||||
.photo-order-controls button.next svg { transform: rotate(-90deg); }
|
||||
|
||||
.photo-order-controls button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.42;
|
||||
@@ -5863,6 +5907,15 @@ a.city-card:hover {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
/* Karta "Dodatkowe informacje" ma padding:0, a jej sekcje wlasny padding 22px 24px z linia na dole.
|
||||
Naglowek kroku musi dostac ten sam padding, zeby rownal sie z trescia ponizej i mial wlasciwy
|
||||
odstep (a nie dotykal krawedzi karty). */
|
||||
.add-extra-card .add-card-heading {
|
||||
border-bottom: 1px solid #e6edf3;
|
||||
margin-bottom: 0;
|
||||
padding: 22px 24px;
|
||||
}
|
||||
|
||||
.extra-section h2 {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
@@ -25682,6 +25735,136 @@ a.city-card:hover {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Wspolny wiersz kafelkow specyfikacji (ikona -> wartosc -> podpis) uzywany na kartach wynikow
|
||||
i w podgladzie kreatora. Wyglada tak samo jak .listing-detail-stats na stronie ogloszenia. */
|
||||
.spec-tiles {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.spec-tile {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spec-tile svg {
|
||||
color: #129357;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.spec-tile strong {
|
||||
color: #13243d;
|
||||
font-size: 15px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.spec-tile span {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Podglad w kreatorze: kafelki z lekka linia oddzielajaca (krok "Podglad") oraz odstepem (panel boczny). */
|
||||
.spec-tiles.final-specs {
|
||||
border-bottom: 1px solid #e4ebf2;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.spec-tiles.preview-specs {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* Drobne chipy z dodatkowymi cechami pod kafelkami w panelu podgladu. */
|
||||
.preview-chip-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.preview-chip-row span {
|
||||
align-items: center;
|
||||
color: #536981;
|
||||
display: inline-flex;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.preview-chip-row svg {
|
||||
color: #536981;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
/* Wariant kompaktowy - dla gestych kart na liscie wynikow. */
|
||||
.spec-tiles.compact {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.spec-tiles.compact .spec-tile svg {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.spec-tiles.compact .spec-tile strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.spec-tiles.compact .spec-tile span {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Lista wynikow (zakladka kupno/wynajem): kafelki mniejsze i dosuniete do lewej, zeby nie
|
||||
rozciagaly sie na cala szerokosc karty i nie wygladaly ciezko. */
|
||||
.spec-tiles.result-specs {
|
||||
gap: 16px;
|
||||
justify-content: flex-start;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
/* Widok listy: kafelki siadaja tuz nad linia oddzielajaca stopke (nie wisza w polowie karty). */
|
||||
.result-list.view-lista .result-specs {
|
||||
margin-top: auto;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.spec-tiles.result-specs .spec-tile {
|
||||
flex: 0 0 auto;
|
||||
gap: 0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.spec-tiles.result-specs .spec-tile svg {
|
||||
display: block;
|
||||
height: 16px;
|
||||
margin: 0 auto -1px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
/* Wartosc i podpis zwarte - bez luznego odstepu, zeby ikona + liczba + podpis tworzyly jeden
|
||||
blok "przyklejony" do siebie, a nie trzy elementy wiszace w powietrzu. */
|
||||
.spec-tiles.result-specs .spec-tile strong {
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.spec-tiles.result-specs .spec-tile span {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.spec-tiles.result-specs .spec-tile strong {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.spec-tiles.result-specs .spec-tile span {
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
.listing-detail-primary {
|
||||
align-items: center;
|
||||
background: #129357;
|
||||
@@ -26397,6 +26580,7 @@ a.city-card:hover {
|
||||
|
||||
.real-result-card {
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition: box-shadow 0.15s, transform 0.15s;
|
||||
}
|
||||
|
||||
@@ -32546,20 +32730,30 @@ a.listing-detail-back {
|
||||
.listing-promoted-tag svg { height: 12px; width: 12px; }
|
||||
.ld-badge-promoted { align-items: center; background: #12a764; color: #ffffff; display: inline-flex; gap: 5px; }
|
||||
.ld-badge-promoted svg { height: 13px; width: 13px; }
|
||||
/* Plakietka "Wyróżnione" - w samym prawym gornym rogu KARTY (nad kolumna ceny, ale wysunieta do
|
||||
rogu, nie sklejona z cena). Bialy napis, kolor (granatowo-niebieski gradient) taki sam jak przycisk
|
||||
"Promuj" w zakladce moich ofert. Oglosznia platne za pozycje ida tez na sama gore listy (isPromotedOffer). */
|
||||
.result-promoted-badge {
|
||||
align-items: center;
|
||||
background: #12a764;
|
||||
background: linear-gradient(120deg, #0f2f5b, #1d5fa8);
|
||||
border-radius: 999px;
|
||||
bottom: 12px;
|
||||
color: #ffffff;
|
||||
display: inline-flex;
|
||||
font-size: 11px;
|
||||
font-weight: 850;
|
||||
gap: 5px;
|
||||
left: 12px;
|
||||
padding: 4px 10px;
|
||||
padding: 5px 12px;
|
||||
position: absolute;
|
||||
box-shadow: 0 3px 10px rgba(15, 31, 58, 0.22);
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
box-shadow: 0 4px 12px rgba(15, 47, 91, 0.32);
|
||||
}
|
||||
|
||||
.result-promoted-badge,
|
||||
.result-promoted-badge svg {
|
||||
color: #ffffff;
|
||||
}
|
||||
.result-promoted-badge svg { height: 12px; width: 12px; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user