From c981b3cb93b239651d55cf628065d5e40ceb1b0f Mon Sep 17 00:00:00 2001 From: pascal Date: Sat, 5 Sep 2026 20:54:11 +0200 Subject: [PATCH] blokady --- frontend/src/App.tsx | 106 ++++++++++++++++++++++++++++++++++++---- frontend/src/styles.css | 12 +++++ 2 files changed, 109 insertions(+), 9 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f6c16ef..9f7c862 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1833,6 +1833,9 @@ const sellTestimonials = [ 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 = [ { icon: 'document', @@ -3244,7 +3247,11 @@ function locationZoom(location: ListingLocation) { return 12; } -async function geocodeListingLocation(location: ListingLocation, signal: AbortSignal): Promise { +// 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 { const query = locationSearchQuery(location); if (!location.city.trim() || query.length < 4) { return null; @@ -3264,7 +3271,10 @@ async function geocodeListingLocation(location: ListingLocation, signal: AbortSi if (!result) { 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> { @@ -14226,6 +14236,10 @@ function AddListingPage({ editListingId }: { editListingId?: number }) { const isEdit = typeof editListingId === 'number'; const [editLoadError, setEditLoadError] = useState(''); 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); // 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. @@ -14283,6 +14297,9 @@ function AddListingPage({ editListingId }: { editListingId?: number }) { const [locationMapCenter, setLocationMapCenter] = useState(INITIAL_LISTING_COORDS); const [locationMapZoom, setLocationMapZoom] = useState(16); 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 photoInputRef = useRef(null); const photoUrlsRef = useRef([]); @@ -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 formattedSecurityDeposit = numericSecurityDeposit > 0 ? `${new Intl.NumberFormat('pl-PL').format(numericSecurityDeposit)} zł` : 'Nie podano'; 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 hasVirtualTour = normalizedVirtualTourUrl.length > 0; const virtualTourEmbedPreview = hasVirtualTour ? getVirtualTourEmbed(normalizedVirtualTourUrl) : null; @@ -14573,7 +14592,56 @@ function AddListingPage({ editListingId }: { editListingId?: number }) { }; }, [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) => { + // 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); window.requestAnimationFrame(() => { document.querySelector('.add-page')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); @@ -14799,15 +14867,17 @@ function AddListingPage({ editListingId }: { editListingId?: number }) { const timer = window.setTimeout(() => { setLocationStatus('Szukam adresu na mapie...'); geocodeListingLocation(location, controller.signal) - .then((point) => { - if (!point) { + .then((result) => { + if (!result) { + setLocationResolved(false); setLocationStatus('Nie znaleziono dokładnego adresu. Przybliż mapę ręcznie.'); return; } - const nextCenter = clampCenter(point); + const nextCenter = clampCenter(result.point); setLocationCenter(nextCenter); setLocationMapCenter(nextCenter); setLocationMapZoom(location.approximateLocation ? APPROXIMATE_LOCATION_ZOOM : locationZoom(location)); + setLocationResolved(result.precise); setLocationStatus(location.approximateLocation ? 'Ustawiliśmy przybliżony obszar 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') { return; } + setLocationResolved(false); setLocationStatus('Nie udało się pobrać lokalizacji. Spróbuj kliknąć punkt na mapie.'); }); }, 650); @@ -14876,9 +14947,14 @@ function AddListingPage({ editListingId }: { editListingId?: number }) { building: details.building ?? '', 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.'); }) - .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 ( @@ -15048,6 +15124,12 @@ function AddListingPage({ editListingId }: { editListingId?: number }) { Twoje dane są bezpieczne. Nie udostępniamy ich osobom trzecim.

+ {showDetailsError && detailsStepError && ( +

+ Uzupełnij wymagane pola, aby przejść do lokalizacji: {detailsStepError} +

+ )} +