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}
+
+ )}
+