blokady
Deploy Polska Lokalnie / deploy (push) Successful in 11s

This commit is contained in:
2026-09-05 20:54:11 +02:00
parent e7db409828
commit c981b3cb93
2 changed files with 109 additions and 9 deletions
+97 -9
View File
@@ -1833,6 +1833,9 @@ const sellTestimonials = [
const addProgressSteps = ['Szczegóły', 'Lokalizacja', 'Zdjęcia', 'Dodatkowe informacje', 'Podgląd ogłoszenia']; const addProgressSteps = ['Szczegóły', 'Lokalizacja', 'Zdjęcia', 'Dodatkowe informacje', 'Podgląd ogłoszenia'];
// Minimalna dlugosc opisu ogloszenia w znakach - warunek przejscia z kroku "Szczegóły" dalej.
const MIN_DESCRIPTION_CHARS = 30;
const listingTips = [ const listingTips = [
{ {
icon: 'document', icon: 'document',
@@ -3244,7 +3247,11 @@ function locationZoom(location: ListingLocation) {
return 12; return 12;
} }
async function geocodeListingLocation(location: ListingLocation, signal: AbortSignal): Promise<LatLng | null> { // Wynik geokodowania adresu: pozycja pinezki plus informacja, czy trafilismy w konkretny budynek
// (Nominatim zwrocil numer domu). Ta precyzja decyduje, czy da sie pominac reczne pola ulicy i numeru.
type GeocodeResult = { point: LatLng; precise: boolean };
async function geocodeListingLocation(location: ListingLocation, signal: AbortSignal): Promise<GeocodeResult | null> {
const query = locationSearchQuery(location); const query = locationSearchQuery(location);
if (!location.city.trim() || query.length < 4) { if (!location.city.trim() || query.length < 4) {
return null; return null;
@@ -3264,7 +3271,10 @@ async function geocodeListingLocation(location: ListingLocation, signal: AbortSi
if (!result) { if (!result) {
return null; return null;
} }
return { lat: Number(result.lat), lng: Number(result.lon) }; return {
point: { lat: Number(result.lat), lng: Number(result.lon) },
precise: Boolean(result.address?.house_number),
};
} }
async function reverseGeocodeListingLocation(point: LatLng, signal: AbortSignal): Promise<Partial<ListingLocation>> { async function reverseGeocodeListingLocation(point: LatLng, signal: AbortSignal): Promise<Partial<ListingLocation>> {
@@ -14226,6 +14236,10 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
const isEdit = typeof editListingId === 'number'; const isEdit = typeof editListingId === 'number';
const [editLoadError, setEditLoadError] = useState(''); const [editLoadError, setEditLoadError] = useState('');
const [currentStep, setCurrentStep] = useState(0); const [currentStep, setCurrentStep] = useState(0);
// Komunikat blokady kroku "Szczegóły"/"Lokalizacja" - pokazywany dopiero po próbie przejścia
// dalej, zeby nie krzyczec o brakach zanim uzytkownik cokolwiek wypelni.
const [showDetailsError, setShowDetailsError] = useState(false);
const [showLocationError, setShowLocationError] = useState(false);
const [listingDetails, setListingDetails] = useState(INITIAL_LISTING_DETAILS); const [listingDetails, setListingDetails] = useState(INITIAL_LISTING_DETAILS);
// Ekran startowy z wyborem "Chcę sprzedać / wynająć" + kategorii. Przy edycji pomijamy go i od razu // Ekran startowy z wyborem "Chcę sprzedać / wynająć" + kategorii. Przy edycji pomijamy go i od razu
// pokazujemy formularz. Wybor kategorii ustawia transakcje i rodzaj, a potem wchodzimy w kreator. // pokazujemy formularz. Wybor kategorii ustawia transakcje i rodzaj, a potem wchodzimy w kreator.
@@ -14283,6 +14297,9 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
const [locationMapCenter, setLocationMapCenter] = useState<LatLng>(INITIAL_LISTING_COORDS); const [locationMapCenter, setLocationMapCenter] = useState<LatLng>(INITIAL_LISTING_COORDS);
const [locationMapZoom, setLocationMapZoom] = useState(16); const [locationMapZoom, setLocationMapZoom] = useState(16);
const [locationStatus, setLocationStatus] = useState('Kliknij na mapie albo wpisz adres, a pinezka ustawi się automatycznie.'); const [locationStatus, setLocationStatus] = useState('Kliknij na mapie albo wpisz adres, a pinezka ustawi się automatycznie.');
// true = mapa odnalazla konkretny budynek (adres z numerem) - wtedy nie zmuszamy do recznego
// wpisania ulicy i numeru, bo pinezka juz jednoznacznie wskazuje lokalizacje.
const [locationResolved, setLocationResolved] = useState(false);
const skipLocationSearch = useRef(false); const skipLocationSearch = useRef(false);
const photoInputRef = useRef<HTMLInputElement>(null); const photoInputRef = useRef<HTMLInputElement>(null);
const photoUrlsRef = useRef<string[]>([]); const photoUrlsRef = useRef<string[]>([]);
@@ -14305,6 +14322,8 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
const formattedDeposit = numericDeposit > 0 ? `Czynsz administracyjny: ${new Intl.NumberFormat('pl-PL').format(numericDeposit)} zł` : 'Czynsz administracyjny: nie podano'; const formattedDeposit = numericDeposit > 0 ? `Czynsz administracyjny: ${new Intl.NumberFormat('pl-PL').format(numericDeposit)} zł` : 'Czynsz administracyjny: nie podano';
const formattedSecurityDeposit = numericSecurityDeposit > 0 ? `${new Intl.NumberFormat('pl-PL').format(numericSecurityDeposit)} zł` : 'Nie podano'; const formattedSecurityDeposit = numericSecurityDeposit > 0 ? `${new Intl.NumberFormat('pl-PL').format(numericSecurityDeposit)} zł` : 'Nie podano';
const previewTitle = listingDetails.title.trim() || 'Tytuł ogłoszenia'; const previewTitle = listingDetails.title.trim() || 'Tytuł ogłoszenia';
// Dlugosc opisu w znakach liczona z czystego tekstu (bez znacznikow HTML edytora) - do minimum 30.
const descriptionCharCount = stripHtml(listingDetails.description).trim().length;
const normalizedVirtualTourUrl = extraInfo.virtualTourUrl.trim(); const normalizedVirtualTourUrl = extraInfo.virtualTourUrl.trim();
const hasVirtualTour = normalizedVirtualTourUrl.length > 0; const hasVirtualTour = normalizedVirtualTourUrl.length > 0;
const virtualTourEmbedPreview = hasVirtualTour ? getVirtualTourEmbed(normalizedVirtualTourUrl) : null; const virtualTourEmbedPreview = hasVirtualTour ? getVirtualTourEmbed(normalizedVirtualTourUrl) : null;
@@ -14573,7 +14592,56 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
}; };
}, [isEdit, editListingId]); }, [isEdit, editListingId]);
// Warunek przejscia z kroku "Szczegóły" dalej - te same wymagane pola, ktore i tak sprawdza
// publikacja, tyle ze egzekwowane juz przy wyjsciu z pierwszej strony. Pusty string = brak bledu.
const detailsStepError = (() => {
if (!listingDetails.title.trim()) return 'Podaj tytuł ogłoszenia.';
if (isRichTextEmpty(listingDetails.description)) return 'Dodaj opis ogłoszenia.';
if (descriptionCharCount < MIN_DESCRIPTION_CHARS) {
return `Opis musi mieć co najmniej ${MIN_DESCRIPTION_CHARS} znaków (obecnie ${descriptionCharCount}).`;
}
if (!(numericPrice > 0)) return isRentTransaction ? 'Podaj poprawny czynsz miesięczny.' : 'Podaj poprawną cenę.';
if (!(numericArea > 0)) return 'Podaj poprawną powierzchnię.';
if (showRoomFields) {
const roomsValue = Number(listingDetails.rooms);
if (!Number.isFinite(roomsValue) || roomsValue < 1) return 'Wybierz liczbę pokoi.';
}
if (contactPhoneError) return contactPhoneError;
return '';
})();
// Warunek przejscia z kroku "Lokalizacja" do zdjec - pola oznaczone gwiazdka na tej stronie.
// Wyjatek: gdy mapa odnalazla konkretny budynek (locationResolved), pinezka wystarcza za ulice
// i numer - uzytkownik mogl wpisac caly adres w jedno pole, a i tak wiemy gdzie jest oferta.
const locationStepError = (() => {
if (!location.city.trim()) return 'Podaj miasto.';
if (locationResolved) return '';
if (!location.street.trim()) return 'Podaj ulicę.';
if (!location.building.trim()) return 'Podaj numer budynku.';
return '';
})();
const goToStep = (step: number) => { const goToStep = (step: number) => {
// Blokada wyjscia z pierwszej strony (currentStep 0 = "Szczegóły"), gdy brakuje wymaganych pol.
// Dotyczy zarowno przycisku "Dalej", jak i skoku naprzod w bocznym stepperze; wstecz przepuszczamy.
if (currentStep === 0 && step > 0 && detailsStepError) {
setShowDetailsError(true);
setContactPhoneTouched(true);
window.requestAnimationFrame(() => {
document.querySelector('.add-step-error')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
return;
}
// Analogiczna blokada wyjscia z kroku "Lokalizacja" (currentStep 1) do zdjec i dalej.
if (currentStep === 1 && step > 1 && locationStepError) {
setShowLocationError(true);
window.requestAnimationFrame(() => {
document.querySelector('.add-step-error')?.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
return;
}
setShowDetailsError(false);
setShowLocationError(false);
setCurrentStep(step); setCurrentStep(step);
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
document.querySelector('.add-page')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); document.querySelector('.add-page')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
@@ -14799,15 +14867,17 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
const timer = window.setTimeout(() => { const timer = window.setTimeout(() => {
setLocationStatus('Szukam adresu na mapie...'); setLocationStatus('Szukam adresu na mapie...');
geocodeListingLocation(location, controller.signal) geocodeListingLocation(location, controller.signal)
.then((point) => { .then((result) => {
if (!point) { if (!result) {
setLocationResolved(false);
setLocationStatus('Nie znaleziono dokładnego adresu. Przybliż mapę ręcznie.'); setLocationStatus('Nie znaleziono dokładnego adresu. Przybliż mapę ręcznie.');
return; return;
} }
const nextCenter = clampCenter(point); const nextCenter = clampCenter(result.point);
setLocationCenter(nextCenter); setLocationCenter(nextCenter);
setLocationMapCenter(nextCenter); setLocationMapCenter(nextCenter);
setLocationMapZoom(location.approximateLocation ? APPROXIMATE_LOCATION_ZOOM : locationZoom(location)); setLocationMapZoom(location.approximateLocation ? APPROXIMATE_LOCATION_ZOOM : locationZoom(location));
setLocationResolved(result.precise);
setLocationStatus(location.approximateLocation setLocationStatus(location.approximateLocation
? 'Ustawiliśmy przybliżony obszar na podstawie wpisanego adresu.' ? 'Ustawiliśmy przybliżony obszar na podstawie wpisanego adresu.'
: 'Pinezka została ustawiona na podstawie wpisanego adresu.'); : 'Pinezka została ustawiona na podstawie wpisanego adresu.');
@@ -14816,6 +14886,7 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
if (error instanceof DOMException && error.name === 'AbortError') { if (error instanceof DOMException && error.name === 'AbortError') {
return; return;
} }
setLocationResolved(false);
setLocationStatus('Nie udało się pobrać lokalizacji. Spróbuj kliknąć punkt na mapie.'); setLocationStatus('Nie udało się pobrać lokalizacji. Spróbuj kliknąć punkt na mapie.');
}); });
}, 650); }, 650);
@@ -14876,9 +14947,14 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
building: details.building ?? '', building: details.building ?? '',
unit: '', unit: '',
})); }));
// Trafienie w konkretny budynek (jest numer) znosi wymog recznego wpisania ulicy i numeru.
setLocationResolved(Boolean(details.building));
setLocationStatus('Pola lokalizacji zostały uzupełnione z mapy.'); setLocationStatus('Pola lokalizacji zostały uzupełnione z mapy.');
}) })
.catch(() => setLocationStatus('Nie udało się odczytać adresu z mapy. Przesuń pinezkę bliżej budynku.')); .catch(() => {
setLocationResolved(false);
setLocationStatus('Nie udało się odczytać adresu z mapy. Przesuń pinezkę bliżej budynku.');
});
}; };
return ( return (
@@ -15048,6 +15124,12 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
Twoje dane są bezpieczne. Nie udostępniamy ich osobom trzecim. Twoje dane są bezpieczne. Nie udostępniamy ich osobom trzecim.
</p> </p>
{showDetailsError && detailsStepError && (
<p className="add-field-error add-step-error" role="alert">
Uzupełnij wymagane pola, aby przejść do lokalizacji: {detailsStepError}
</p>
)}
<div className="add-wizard-nav"> <div className="add-wizard-nav">
<button className="add-nav-back" type="button" onClick={() => (isEdit ? navigate(-1) : setCategoryChosen(false))}> <button className="add-nav-back" type="button" onClick={() => (isEdit ? navigate(-1) : setCategoryChosen(false))}>
<Icon name="chevron" /> Wstecz <Icon name="chevron" /> Wstecz
@@ -15129,6 +15211,12 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
{locationStatus} {locationStatus}
</p> </p>
{showLocationError && locationStepError && (
<p className="add-field-error add-step-error" role="alert">
Uzupełnij wymagane pola, aby przejść do zdjęć: {locationStepError}
</p>
)}
<div className="add-location-actions"> <div className="add-location-actions">
<button className="back-step-button" type="button" onClick={() => goToStep(0)}> <button className="back-step-button" type="button" onClick={() => goToStep(0)}>
Wstecz Wstecz
@@ -27072,9 +27160,9 @@ function ListingDetailPage({
unit: '', unit: '',
approximateLocation: false, approximateLocation: false,
}, controller.signal) }, controller.signal)
.then((point) => { .then((result) => {
if (point) { if (result) {
setResolvedMapCenter(clampCenter(point)); setResolvedMapCenter(clampCenter(result.point));
} }
}) })
.catch(() => { .catch(() => {
+12
View File
@@ -4520,6 +4520,18 @@ a.city-card:hover {
margin-top: 4px; margin-top: 4px;
} }
.add-step-error {
background: #fdecea;
border: 1px solid #f5c2bd;
border-radius: 10px;
color: #b5342a;
font-size: 14px;
font-weight: 700;
line-height: 1.4;
margin: 0 0 12px;
padding: 12px 14px;
}
.add-nav-back { .add-nav-back {
align-items: center; align-items: center;
background: #ffffff; background: #ffffff;