Zadania 1, 2, 9
This commit is contained in:
+201
-10
@@ -128,6 +128,8 @@ export type ApiListingDetail = {
|
||||
createdAt: string;
|
||||
viewsCount?: number;
|
||||
promotedUntil?: string | null;
|
||||
// true = wlasciciel publikuje tylko przyblizona lokalizacje (mapa rysuje obszar zamiast punktu).
|
||||
approximateLocation?: boolean;
|
||||
};
|
||||
|
||||
// Ostatnio przegladane oferty - trzymane lokalnie (per urzadzenie) do slajdera na stronie glownej.
|
||||
@@ -2020,10 +2022,74 @@ const INITIAL_LISTING_DETAILS = {
|
||||
condition: 'Wybierz',
|
||||
ownership: 'Wybierz',
|
||||
contactName: '',
|
||||
// Prefiks kraju trzymamy osobno od numeru krajowego - razem trafiaja do pola contactPhone oferty.
|
||||
contactPhoneCountry: 'PL',
|
||||
contactPhone: '',
|
||||
contactEmail: '',
|
||||
};
|
||||
|
||||
/**
|
||||
* Prefiksy telefoniczne dostepne przy ogloszeniu - Polska i kraje najczestsze na naszym rynku.
|
||||
* Liczba cyfr dotyczy numeru komorkowego bez prefiksu (dla Polski 9, np. +48 600 123 456).
|
||||
*/
|
||||
const PHONE_COUNTRIES = [
|
||||
{ code: 'PL', dial: '+48', label: 'Polska', minDigits: 9, maxDigits: 9, example: '600 123 456' },
|
||||
{ code: 'UA', dial: '+380', label: 'Ukraina', minDigits: 9, maxDigits: 9, example: '501 234 567' },
|
||||
{ code: 'DE', dial: '+49', label: 'Niemcy', minDigits: 10, maxDigits: 11, example: '1512 3456789' },
|
||||
{ code: 'GB', dial: '+44', label: 'Wielka Brytania', minDigits: 10, maxDigits: 10, example: '7700 900123' },
|
||||
{ code: 'BY', dial: '+375', label: 'Białoruś', minDigits: 9, maxDigits: 9, example: '291 234 567' },
|
||||
{ code: 'CZ', dial: '+420', label: 'Czechy', minDigits: 9, maxDigits: 9, example: '601 123 456' },
|
||||
{ code: 'SK', dial: '+421', label: 'Słowacja', minDigits: 9, maxDigits: 9, example: '901 123 456' },
|
||||
{ code: 'LT', dial: '+370', label: 'Litwa', minDigits: 8, maxDigits: 8, example: '612 34567' },
|
||||
{ code: 'RO', dial: '+40', label: 'Rumunia', minDigits: 9, maxDigits: 9, example: '712 345 678' },
|
||||
{ code: 'NL', dial: '+31', label: 'Holandia', minDigits: 9, maxDigits: 9, example: '612 345 678' },
|
||||
{ code: 'IE', dial: '+353', label: 'Irlandia', minDigits: 9, maxDigits: 9, example: '851 234 567' },
|
||||
{ code: 'IT', dial: '+39', label: 'Włochy', minDigits: 9, maxDigits: 10, example: '312 345 6789' },
|
||||
] as const;
|
||||
|
||||
const phoneCountryOf = (code: string) => PHONE_COUNTRIES.find((country) => country.code === code) ?? PHONE_COUNTRIES[0];
|
||||
|
||||
// Komunikat bledu albo null, gdy numer jest poprawny. Puste pole jest dozwolone - telefon jest opcjonalny.
|
||||
function validateContactPhone(countryCode: string, value: string): string | null {
|
||||
const country = phoneCountryOf(countryCode);
|
||||
const digits = value.replace(/\D/g, '');
|
||||
if (!digits) {
|
||||
return null;
|
||||
}
|
||||
if (digits.length < country.minDigits || digits.length > country.maxDigits) {
|
||||
const expected = country.minDigits === country.maxDigits
|
||||
? `${country.minDigits} cyfr`
|
||||
: `od ${country.minDigits} do ${country.maxDigits} cyfr`;
|
||||
return `Numer komórkowy dla kraju ${country.label} (${country.dial}) ma ${expected}. Podano ${digits.length}.`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Zapis do oferty: "+48 600 123 456". Dziewieciocyfrowe numery grupujemy po trzy dla czytelnosci.
|
||||
function composeContactPhone(countryCode: string, value: string): string | undefined {
|
||||
const country = phoneCountryOf(countryCode);
|
||||
const digits = value.replace(/\D/g, '');
|
||||
if (!digits) {
|
||||
return undefined;
|
||||
}
|
||||
const grouped = digits.length === 9 ? digits.replace(/(\d{3})(\d{3})(\d{3})/, '$1 $2 $3') : digits;
|
||||
return `${country.dial} ${grouped}`;
|
||||
}
|
||||
|
||||
// Odwrotnosc composeContactPhone przy wczytywaniu oferty do edycji (najdluzszy pasujacy prefiks wygrywa).
|
||||
function splitContactPhone(stored: string | null | undefined): { country: string; number: string } {
|
||||
const raw = (stored ?? '').replace(/[\s-]/g, '');
|
||||
const match = [...PHONE_COUNTRIES]
|
||||
.sort((a, b) => b.dial.length - a.dial.length)
|
||||
.find((country) => raw.startsWith(country.dial));
|
||||
if (match) {
|
||||
return { country: match.code, number: match.dial === '+48' ? formatPolishPhone(raw.slice(match.dial.length)) : raw.slice(match.dial.length) };
|
||||
}
|
||||
return { country: 'PL', number: formatPolishPhone(raw.replace(/^\+/, '')) };
|
||||
}
|
||||
|
||||
const formatPolishPhone = (digits: string) => (digits.length === 9 ? digits.replace(/(\d{3})(\d{3})(\d{3})/, '$1 $2 $3') : digits);
|
||||
|
||||
const INITIAL_EXTRA_INFO = {
|
||||
rent: '',
|
||||
deposit: '',
|
||||
@@ -2623,6 +2689,8 @@ type ListingLocation = {
|
||||
street: string;
|
||||
building: string;
|
||||
unit: string;
|
||||
// Zaznaczone = na mapie pokazujemy przyblizony obszar zamiast dokladnego punktu.
|
||||
approximateLocation: boolean;
|
||||
};
|
||||
|
||||
type ListingPhoto = {
|
||||
@@ -2638,8 +2706,12 @@ const INITIAL_LISTING_LOCATION: ListingLocation = {
|
||||
street: '',
|
||||
building: '',
|
||||
unit: '',
|
||||
approximateLocation: false,
|
||||
};
|
||||
|
||||
// Promien obszaru pokazywanego zamiast dokladnego punktu (zgodny z rozrzutem wspolrzednych z API).
|
||||
const APPROXIMATE_LOCATION_RADIUS_M = 300;
|
||||
|
||||
const INITIAL_LISTING_COORDS: LatLng = { lat: 50.0491, lng: 19.9617 };
|
||||
|
||||
function locationSearchQuery(location: ListingLocation) {
|
||||
@@ -13268,7 +13340,8 @@ function apiDetailToWizardState(detail: ApiListingDetail) {
|
||||
condition: orWybierz(detail.condition),
|
||||
ownership: orWybierz(detail.ownership),
|
||||
contactName: detail.contactName ?? '',
|
||||
contactPhone: detail.contactPhone ?? '',
|
||||
contactPhoneCountry: splitContactPhone(detail.contactPhone).country,
|
||||
contactPhone: splitContactPhone(detail.contactPhone).number,
|
||||
contactEmail: detail.contactEmail ?? '',
|
||||
};
|
||||
const extraInfo: typeof INITIAL_EXTRA_INFO = {
|
||||
@@ -13289,6 +13362,7 @@ function apiDetailToWizardState(detail: ApiListingDetail) {
|
||||
district: detail.district ?? '',
|
||||
street: detail.street ?? '',
|
||||
building: detail.building ?? '',
|
||||
approximateLocation: detail.approximateLocation ?? false,
|
||||
};
|
||||
const photos: ListingPhoto[] = (detail.photos ?? []).map((url, index) => ({
|
||||
id: `edit-photo-${index}`,
|
||||
@@ -13313,6 +13387,7 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
const [editLoadError, setEditLoadError] = useState('');
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [listingDetails, setListingDetails] = useState(INITIAL_LISTING_DETAILS);
|
||||
const [contactPhoneTouched, setContactPhoneTouched] = useState(false);
|
||||
const [location, setLocation] = useState<ListingLocation>(INITIAL_LISTING_LOCATION);
|
||||
const [photos, setPhotos] = useState<ListingPhoto[]>([]);
|
||||
const [extraInfo, setExtraInfo] = useState(INITIAL_EXTRA_INFO);
|
||||
@@ -13334,6 +13409,8 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
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);
|
||||
const isRentTransaction = listingDetails.transaction.trim().toLowerCase() === 'wynajem';
|
||||
const numericPrice = Number(listingDetails.price.replace(',', '.'));
|
||||
const numericArea = Number(listingDetails.area.replace(',', '.'));
|
||||
@@ -13387,6 +13464,11 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
setPublishError('Uzupełnij poprawną cenę, powierzchnię i liczbę pokoi.');
|
||||
return;
|
||||
}
|
||||
if (contactPhoneError) {
|
||||
setPublishState('error');
|
||||
setPublishError(`${contactPhoneError} (krok „Szczegóły”).`);
|
||||
return;
|
||||
}
|
||||
|
||||
const addressValue = [location.street, location.building, location.district]
|
||||
.map((part) => part.trim())
|
||||
@@ -13427,7 +13509,7 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
condition: clean(listingDetails.condition),
|
||||
ownership: clean(listingDetails.ownership),
|
||||
contactName: clean(listingDetails.contactName),
|
||||
contactPhone: clean(listingDetails.contactPhone),
|
||||
contactPhone: composeContactPhone(listingDetails.contactPhoneCountry, listingDetails.contactPhone),
|
||||
contactEmail: clean(listingDetails.contactEmail),
|
||||
rentExtra: Number.isFinite(rentNum) && rentNum > 0 ? rentNum : undefined,
|
||||
availableFrom: isRentTransaction ? clean(extraInfo.availableFrom) : undefined,
|
||||
@@ -13443,6 +13525,7 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
amenities: selectedAmenities,
|
||||
photos: photoData,
|
||||
virtualTourUrl: normalizedVirtualTour,
|
||||
approximateLocation: location.approximateLocation,
|
||||
};
|
||||
|
||||
if (isEdit) {
|
||||
@@ -13994,7 +14077,30 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
<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="Telefon" value={listingDetails.contactPhone} placeholder="Np. 600 123 456" onChange={(value) => updateListingDetails('contactPhone', value)} />
|
||||
<label className="add-field add-phone-field">
|
||||
<span>Telefon komórkowy</span>
|
||||
<div className="add-phone-row">
|
||||
<select
|
||||
value={listingDetails.contactPhoneCountry}
|
||||
onChange={(event) => updateListingDetails('contactPhoneCountry', event.target.value)}
|
||||
aria-label="Prefiks kraju"
|
||||
>
|
||||
{PHONE_COUNTRIES.map((country) => (
|
||||
<option key={country.code} value={country.code}>{country.dial} {country.label}</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="tel"
|
||||
inputMode="numeric"
|
||||
autoComplete="tel-national"
|
||||
value={listingDetails.contactPhone}
|
||||
placeholder={`Np. ${phoneCountryOf(listingDetails.contactPhoneCountry).example}`}
|
||||
onChange={(event) => updateListingDetails('contactPhone', event.target.value.replace(/[^\d ]/g, ''))}
|
||||
onBlur={() => setContactPhoneTouched(true)}
|
||||
/>
|
||||
</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)} />
|
||||
</div>
|
||||
<div className="contact-actions">
|
||||
@@ -14049,6 +14155,21 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label className="add-location-privacy">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={location.approximateLocation}
|
||||
onChange={(event) => setLocation((current) => ({ ...current, approximateLocation: event.target.checked }))}
|
||||
/>
|
||||
<span>
|
||||
<strong>Nie pokazuj dokładnego adresu na mapie</strong>
|
||||
<small>
|
||||
Zamiast punktu kupujący zobaczą przybliżony obszar - koło o promieniu ok. {APPROXIMATE_LOCATION_RADIUS_M} m,
|
||||
a w ogłoszeniu pojawi się tylko dzielnica i miasto. Ulica i numer pozostaną widoczne wyłącznie dla Ciebie i administracji.
|
||||
</small>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<LocationPickerMap
|
||||
center={locationMapCenter}
|
||||
marker={locationCenter}
|
||||
@@ -20044,6 +20165,7 @@ function SatelliteMap({
|
||||
onMapClick,
|
||||
onMarkerClick,
|
||||
size,
|
||||
areaRadiusMeters,
|
||||
}: {
|
||||
markers: string[][];
|
||||
center?: LatLng;
|
||||
@@ -20055,6 +20177,8 @@ function SatelliteMap({
|
||||
onMapClick?: () => void;
|
||||
onMarkerClick?: (offerId: number) => void;
|
||||
size?: number;
|
||||
// Podany promien (w metrach) zastepuje pinezke kolem - uzywany dla przyblizonej lokalizacji.
|
||||
areaRadiusMeters?: number;
|
||||
}) {
|
||||
const baseCenter = center ?? CITY_COORDS.Warszawa;
|
||||
const [mapCenter, setMapCenter] = useState(baseCenter);
|
||||
@@ -20121,6 +20245,22 @@ function SatelliteMap({
|
||||
}, [geoMarkers, mapCenter.lat, mapCenter.lng, currentZoom, viewW, viewH]);
|
||||
const shouldRenderStaticMarkers = geoMarkers == null;
|
||||
|
||||
// Kolo przyblizonej lokalizacji: srednica wynika ze skali kafelkow (zoom i szerokosc geograficzna),
|
||||
// a pozycja z rzutowania punktu ogloszenia, dzieki czemu obszar trzyma sie mapy przy przesuwaniu.
|
||||
const areaCircle = useMemo(() => {
|
||||
if (!areaRadiusMeters) {
|
||||
return null;
|
||||
}
|
||||
const metersPerPixel = (156543.03392 * Math.cos((baseCenter.lat * Math.PI) / 180)) / 2 ** currentZoom;
|
||||
const worldCenter = projectToWorld(mapCenter.lat, mapCenter.lng, currentZoom);
|
||||
const worldPoint = projectToWorld(baseCenter.lat, baseCenter.lng, currentZoom);
|
||||
return {
|
||||
diameter: (2 * areaRadiusMeters) / metersPerPixel,
|
||||
left: viewW / 2 + (worldPoint.x - worldCenter.x),
|
||||
top: viewH / 2 + (worldPoint.y - worldCenter.y),
|
||||
};
|
||||
}, [areaRadiusMeters, baseCenter.lat, baseCenter.lng, mapCenter.lat, mapCenter.lng, currentZoom, viewW, viewH]);
|
||||
|
||||
const changeZoom = (delta: number) => {
|
||||
setCurrentZoom((current) => Math.max(6, Math.min(19, current + delta)));
|
||||
};
|
||||
@@ -20227,9 +20367,17 @@ function SatelliteMap({
|
||||
<span />
|
||||
Szukaj na bieżącej mapie
|
||||
</button>
|
||||
{areaCircle && (
|
||||
<span
|
||||
className="map-area-circle"
|
||||
style={{ left: `${areaCircle.left}px`, top: `${areaCircle.top}px`, width: areaCircle.diameter, height: areaCircle.diameter }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{centerMarkerLabel && (
|
||||
<>
|
||||
<span className="map-center-pin" style={{ left: '50%', top: '52%' }} aria-hidden="true" />
|
||||
{/* Przy przyblizonej lokalizacji nie rysujemy pinezki - dokladny punkt zastepuje obszar. */}
|
||||
{!areaCircle && <span className="map-center-pin" style={{ left: '50%', top: '52%' }} aria-hidden="true" />}
|
||||
<span className="map-price map-price-center-label" style={{ left: '50%', top: '44%' }}>{centerMarkerLabel}</span>
|
||||
</>
|
||||
)}
|
||||
@@ -23824,6 +23972,9 @@ function useLiveListings(offerType: ApiOfferType, refreshMs = LIVE_LISTINGS_REFR
|
||||
return { items, loading };
|
||||
}
|
||||
|
||||
// Podpowiedz przy narzedziach wylaczonych we wlasnym ogloszeniu (kontakt, ulubione, alert ceny, zgloszenie).
|
||||
const OWNER_TOOL_HINT = 'To Twoje ogłoszenie - ta opcja jest dostępna tylko dla innych użytkowników.';
|
||||
|
||||
function ListingDetailPage({
|
||||
listingId,
|
||||
onStartChat,
|
||||
@@ -23987,6 +24138,7 @@ function ListingDetailPage({
|
||||
street: listing.street ?? '',
|
||||
building: listing.building ?? '',
|
||||
unit: '',
|
||||
approximateLocation: false,
|
||||
}, controller.signal)
|
||||
.then((point) => {
|
||||
if (point) {
|
||||
@@ -24068,7 +24220,9 @@ function ListingDetailPage({
|
||||
.filter(Boolean)
|
||||
.join(', ');
|
||||
const sellerName = listing.contactName?.trim() || 'Administracja Polska Lokalnie';
|
||||
const isOwner = Boolean(user && listing.ownerEmail && user.email.toLowerCase() === listing.ownerEmail.toLowerCase());
|
||||
// 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';
|
||||
const favorite = isFavorite(listing.id);
|
||||
const listingAlertEnabled = isListingAlertEnabled(listing.id);
|
||||
@@ -24125,7 +24279,11 @@ function ListingDetailPage({
|
||||
|
||||
const priceSuffix = listing.offerType === 'RENT' ? ' /mc' : '';
|
||||
const listingMapCenter = resolvedMapCenter ?? CITY_COORDS[listing.city] ?? POLAND_CENTER;
|
||||
const listingMapZoom = listing.street && listing.building ? 16 : listing.street ? 15 : listing.district ? 14 : 12;
|
||||
// Przy ukrytym adresie cofamy zoom, zeby cale kolo przyblizonego obszaru zmiescilo sie w kadrze.
|
||||
const approximateLocation = Boolean(listing.approximateLocation);
|
||||
const listingMapZoom = approximateLocation
|
||||
? 14
|
||||
: listing.street && listing.building ? 16 : listing.street ? 15 : listing.district ? 14 : 12;
|
||||
const openLocationInMaps = () => {
|
||||
const query = `${listingMapCenter.lat},${listingMapCenter.lng}`;
|
||||
window.open(`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(query)}`, '_blank', 'noopener,noreferrer');
|
||||
@@ -24462,9 +24620,14 @@ function ListingDetailPage({
|
||||
initialZoom={listingMapZoom}
|
||||
showCityTitle={false}
|
||||
onMapClick={openLocationInMaps}
|
||||
areaRadiusMeters={approximateLocation ? APPROXIMATE_LOCATION_RADIUS_M : undefined}
|
||||
/>
|
||||
</div>
|
||||
<p className="listing-detail-map-hint">Kliknij mapę, aby otworzyć dokładny punkt w Google Maps.</p>
|
||||
<p className="listing-detail-map-hint">
|
||||
{approximateLocation
|
||||
? `Sprzedający nie udostępnia dokładnego adresu - zaznaczony obszar to okolica nieruchomości (promień ok. ${APPROXIMATE_LOCATION_RADIUS_M} m).`
|
||||
: 'Kliknij mapę, aby otworzyć dokładny punkt w Google Maps.'}
|
||||
</p>
|
||||
<div className="listing-detail-meta-actions">
|
||||
<div className="listing-detail-meta">
|
||||
<span><Icon name="clock" /> {listingRelativeDate(listing.createdAt)}</span>
|
||||
@@ -24474,7 +24637,13 @@ function ListingDetailPage({
|
||||
<span className={`ld-status ld-status-${listing.status.toLowerCase()}`}>{LISTING_STATUS_LABELS[listing.status]}</span>
|
||||
)}
|
||||
</div>
|
||||
<button type="button" className="listing-detail-report-btn" onClick={openReportModal}>
|
||||
<button
|
||||
type="button"
|
||||
className="listing-detail-report-btn"
|
||||
onClick={openReportModal}
|
||||
disabled={isOwner}
|
||||
title={isOwner ? OWNER_TOOL_HINT : undefined}
|
||||
>
|
||||
<Icon name="warning" /> Zgłoś
|
||||
</button>
|
||||
</div>
|
||||
@@ -24495,7 +24664,19 @@ function ListingDetailPage({
|
||||
{listing.yearBuilt && <div><Icon name="calendar" /><strong>{listing.yearBuilt}</strong><span>Rok budowy</span></div>}
|
||||
</div>
|
||||
|
||||
<button type="button" className="listing-detail-primary" onClick={revealContact}>
|
||||
{isOwner && (
|
||||
<p className="listing-detail-owner-note">
|
||||
<Icon name="user" /> To Twoje ogłoszenie. Widzisz je dokładnie tak jak inni, ale narzędzia dla kupujących - kontakt, ulubione, alert ceny i zgłoszenie - są wyłączone.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="listing-detail-primary"
|
||||
onClick={revealContact}
|
||||
disabled={isOwner}
|
||||
title={isOwner ? OWNER_TOOL_HINT : undefined}
|
||||
>
|
||||
{isPhoneRevealed
|
||||
? (listing.contactPhone
|
||||
? <><Icon name="phone" /> {listing.contactPhone}</>
|
||||
@@ -24503,7 +24684,13 @@ function ListingDetailPage({
|
||||
: <><Icon name="phone" /> Skontaktuj się ze sprzedającym</>}
|
||||
</button>
|
||||
|
||||
<button type="button" className="listing-detail-secondary" onClick={startChatWithSeller}>
|
||||
<button
|
||||
type="button"
|
||||
className="listing-detail-secondary"
|
||||
onClick={startChatWithSeller}
|
||||
disabled={isOwner}
|
||||
title={isOwner ? OWNER_TOOL_HINT : undefined}
|
||||
>
|
||||
<Icon name="message" /> Napisz wiadomość
|
||||
</button>
|
||||
|
||||
@@ -24555,6 +24742,8 @@ function ListingDetailPage({
|
||||
type="button"
|
||||
className={`listing-detail-fav ${favorite ? 'active' : ''}`}
|
||||
onClick={() => onToggleFavorite(listing)}
|
||||
disabled={isOwner}
|
||||
title={isOwner ? OWNER_TOOL_HINT : undefined}
|
||||
>
|
||||
<Icon name={favorite ? 'heart-filled' : 'heart'} /> {favorite ? 'W ulubionych' : 'Dodaj do ulubionych'}
|
||||
</button>
|
||||
@@ -24563,6 +24752,8 @@ function ListingDetailPage({
|
||||
type="button"
|
||||
className={`listing-detail-alert ${listingAlertEnabled ? 'active' : ''}`}
|
||||
onClick={() => onToggleListingAlert(listing)}
|
||||
disabled={isOwner}
|
||||
title={isOwner ? OWNER_TOOL_HINT : undefined}
|
||||
>
|
||||
<Icon name="clock" /> {listingAlertEnabled ? 'Alert aktywny' : 'Utwórz alert ceny'}
|
||||
</button>
|
||||
|
||||
@@ -30493,3 +30493,124 @@ a.listing-detail-back {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
/* Wlasne ogloszenie: narzedzia przeznaczone dla innych uzytkownikow sa wylaczone i opisane w karcie */
|
||||
.listing-detail-owner-note {
|
||||
align-items: flex-start;
|
||||
background: #f6fbf8;
|
||||
border: 1px solid #cfe8d8;
|
||||
border-radius: 10px;
|
||||
color: #17653e;
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
gap: 8px;
|
||||
line-height: 1.45;
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.listing-detail-owner-note svg {
|
||||
flex-shrink: 0;
|
||||
height: 16px;
|
||||
margin-top: 1px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.listing-detail-primary:disabled,
|
||||
.listing-detail-secondary:disabled,
|
||||
.listing-detail-fav:disabled,
|
||||
.listing-detail-alert:disabled,
|
||||
.listing-detail-report-btn:disabled {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.listing-detail-primary:disabled:hover {
|
||||
background: #129357;
|
||||
}
|
||||
|
||||
.listing-detail-report-btn:disabled:hover {
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
/* Przyblizona lokalizacja: zamiast pinezki mapa rysuje obszar, w ktorym znajduje sie nieruchomosc */
|
||||
.map-area-circle {
|
||||
background: rgba(18, 167, 100, 0.22);
|
||||
border: 2px solid rgba(18, 167, 100, 0.85);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.35) inset;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Krok lokalizacji w dodawaniu ogloszenia: zgoda na publikowanie samej okolicy */
|
||||
.add-location-privacy {
|
||||
align-items: flex-start;
|
||||
background: #f6fbf8;
|
||||
border: 1px solid #cfe8d8;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.add-location-privacy input {
|
||||
accent-color: #12a764;
|
||||
flex-shrink: 0;
|
||||
height: 18px;
|
||||
margin-top: 1px;
|
||||
width: 18px;
|
||||
}
|
||||
|
||||
.add-location-privacy span {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.add-location-privacy strong {
|
||||
color: #14223a;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.add-location-privacy small {
|
||||
color: #5b6b82;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Telefon w ogloszeniu: prefiks kraju obok numeru krajowego */
|
||||
.add-phone-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.add-phone-row select {
|
||||
flex: 0 0 auto;
|
||||
max-width: 152px;
|
||||
padding-left: 10px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.add-phone-row input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Komunikat bledu jest zwyklym wierszem pod polem - w odroznieniu od podpowiedzi
|
||||
(.add-field > small), ktore sa pozycjonowane absolutnie wewnatrz pola. */
|
||||
.add-field > small.add-field-error {
|
||||
bottom: auto;
|
||||
color: #d5493c;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1.4;
|
||||
position: static;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user