diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8f9880b..56c77f3 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1980,9 +1980,257 @@ const extraInfoSelectOptions = { sewageType: ['Wybierz', 'Kanalizacja', 'Szambo', 'Przydomowa oczyszczalnia'], }; +// 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. +type AddCategoryItem = { label: string; icon: string }; +type AddIntentCard = { key: 'sell' | 'rent'; transaction: string; title: string; subtitle: string; icon: string; items: AddCategoryItem[] }; + +const ADD_CATEGORY_ICONS: Record = { + Mieszkanie: 'apartment', + Dom: 'house', + Działka: 'tree', + 'Lokal użytkowy': 'building-plus', + 'Budynek użytkowy': 'city', + Pokój: 'sofa', + Garaż: 'car', +}; + +// Przykladowy tytul dopasowany do wybranej kategorii - dla kazdego rodzaju nieruchomosci inny, +// zeby podpowiedz mialai sens (np. dla dzialki nie sugerujemy "3-pokojowego mieszkania"). +const LISTING_TITLE_HINTS: Record = { + Mieszkanie: 'Np. 3-pokojowe mieszkanie po remoncie, gotowe do wprowadzenia', + Dom: 'Np. Dom wolnostojący z ogrodem w spokojnej okolicy', + Działka: 'Np. Działka budowlana 800 m² z dostępem do mediów', + 'Lokal użytkowy': 'Np. Lokal handlowy w centrum, witryna od ulicy', + 'Budynek użytkowy': 'Np. Budynek usługowo-magazynowy z parkingiem', + Pokój: 'Np. Umeblowany pokój w mieszkaniu, blisko centrum', + Garaż: 'Np. Garaż murowany z prądem, miejsce dla 1 auta', +}; +const DEFAULT_LISTING_TITLE_HINT = 'Np. Krótki, konkretny tytuł Twojej oferty'; + +const ADD_INTENT_CARDS: AddIntentCard[] = [ + { + key: 'sell', + transaction: 'Sprzedaż', + title: 'Chcę sprzedać', + subtitle: 'Wybierz kategorię nieruchomości, którą chcesz sprzedać.', + icon: 'tag', + items: ['Mieszkanie', 'Dom', 'Działka', 'Lokal użytkowy', 'Budynek użytkowy', 'Garaż'].map((label) => ({ label, icon: ADD_CATEGORY_ICONS[label] })), + }, + { + key: 'rent', + transaction: 'Wynajem', + title: 'Chcę wynająć', + subtitle: 'Wybierz kategorię nieruchomości, którą chcesz wynająć.', + icon: 'key', + items: ['Mieszkanie', 'Pokój', 'Dom', 'Działka', 'Lokal użytkowy', 'Budynek użytkowy', 'Garaż'].map((label) => ({ label, icon: ADD_CATEGORY_ICONS[label] })), + }, +]; + +// Ozdobna scenka (budynek + dom + drzewo + chmurki) w naglowku karty wyboru. Rysowana +// currentColor, wiec dziedziczy kolor karty (zielony dla sprzedazy, fioletowy dla wynajmu). +// Grafika pustego stanu galerii zdjec: dwie przezroczyste ramki zdjec z tlem, a na wierzchu domek. +function PhotosEmptyIllustration() { + return ( + + ); +} + +function AddSceneIllustration() { + return ( + + ); +} + +// Naglowek ekranu wyboru: wyśrodkowany tytul z podkresleniem i "iskierka", podtytul, trzy plakietki +// zaufania oraz ozdobne przerywane strzalki po bokach (zielona i fioletowa). +function AddChooserHero() { + return ( +
+ + + +

+ Dodaj ogłoszenie{' '} + + za darmo + + + +

+

Wystaw swoje ogłoszenie w kilka chwil – bez opłat i ukrytych kosztów.

+ +
+
+ + + +
100% darmoweBez prowizji i opłat
+
+
+ + + +
BezpiecznieTwoje dane są chronione
+
+
+ + + +
Szybko i łatwoTylko kilka kroków
+
+
+
+ ); +} + +// Karty wyboru na wejsciu do dodawania oferty. Klik w kategorie ustawia transakcje + rodzaj +// i wchodzi w kreator (onPick). +function AddCategoryChooser({ onPick }: { onPick: (transaction: string, propertyType: string) => void }) { + return ( +
+ {ADD_INTENT_CARDS.map((card) => ( +
+
+ +
+

{card.title}

+

{card.subtitle}

+
+ +
+
+ {card.items.map((item) => ( + + ))} +
+
+ ))} +
+ ); +} + +// Kroki kreatora dodawania w lewym pionowym stepperze. Wezel 0 = "Kategoria" (ekran wyboru, juz +// ukonczony), wezly 1..5 odpowiadaja krokom formularza (currentStep 0..4). +const ADD_STEP_META = [ + { title: 'Kategoria', desc: 'Wybierz, co chcesz sprzedać lub wynająć' }, + { title: 'Szczegóły oferty', desc: 'Podaj informacje o nieruchomości' }, + { title: 'Lokalizacja', desc: 'Określ adres i lokalizację' }, + { title: 'Zdjęcia', desc: 'Dodaj zdjęcia nieruchomości' }, + { title: 'Dodatkowe informacje', desc: 'Uzupełnij pozostałe szczegóły' }, + { title: 'Podgląd', desc: 'Sprawdź i opublikuj ogłoszenie' }, +]; + +function AddWizardSteps({ currentStep, onGoStep, onBackToCategory }: { + currentStep: number; + onGoStep: (step: number) => void; + onBackToCategory: () => void; +}) { + const activeNode = currentStep + 1; // przesuniecie o "Kategorie" na poczatku + return ( + + ); +} + const listingDetailsSelectOptions = { transaction: ['Sprzedaż', 'Wynajem'], - propertyType: ['Mieszkanie', 'Dom', 'Działka', 'Lokal użytkowy', 'Pokój'], + propertyType: ['Mieszkanie', 'Dom', 'Działka', 'Lokal użytkowy', 'Pokój', 'Budynek użytkowy', 'Garaż'], market: ['Wtórny', 'Pierwotny'], houseType: ['Wybierz', 'Wolnostojący', 'Bliźniak', 'Szeregowy', 'Segment skrajny', 'Segment środkowy', 'Rezydencja', 'Dworek'], houseFloors: ['Wybierz', 'Parterowy', '1', '2', '3+'], @@ -2028,21 +2276,87 @@ const INITIAL_LISTING_DETAILS = { */ 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: 'AL', dial: '+355', label: 'Albania', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'AT', dial: '+43', label: 'Austria', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'BE', dial: '+32', label: 'Belgia', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'BG', dial: '+359', label: 'Bułgaria', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'HR', dial: '+385', label: 'Chorwacja', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'CY', dial: '+357', label: 'Cypr', minDigits: 8, maxDigits: 8, example: '123 456 78' }, { 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: 'DK', dial: '+45', label: 'Dania', minDigits: 8, maxDigits: 8, example: '123 456 78' }, + { code: 'EE', dial: '+372', label: 'Estonia', minDigits: 8, maxDigits: 8, example: '123 456 78' }, + { code: 'FI', dial: '+358', label: 'Finlandia', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'FR', dial: '+33', label: 'Francja', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'DE', dial: '+49', label: 'Niemcy', minDigits: 10, maxDigits: 11, example: '1512 3456789' }, + { code: 'GR', dial: '+30', label: 'Grecja', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'HU', dial: '+36', label: 'Węgry', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'IS', dial: '+354', label: 'Islandia', minDigits: 7, maxDigits: 7, example: '123 456 7' }, { 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' }, + { code: 'LV', dial: '+371', label: 'Łotwa', minDigits: 8, maxDigits: 8, example: '123 456 78' }, + { code: 'LT', dial: '+370', label: 'Litwa', minDigits: 8, maxDigits: 8, example: '612 34567' }, + { code: 'LU', dial: '+352', label: 'Luksemburg', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'MT', dial: '+356', label: 'Malta', minDigits: 8, maxDigits: 8, example: '123 456 78' }, + { code: 'NL', dial: '+31', label: 'Holandia', minDigits: 9, maxDigits: 9, example: '612 345 678' }, + { code: 'NO', dial: '+47', label: 'Norwegia', minDigits: 8, maxDigits: 8, example: '123 456 78' }, + { code: 'PT', dial: '+351', label: 'Portugalia', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'RO', dial: '+40', label: 'Rumunia', minDigits: 9, maxDigits: 9, example: '712 345 678' }, + { code: 'RS', dial: '+381', label: 'Serbia', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'SK', dial: '+421', label: 'Słowacja', minDigits: 9, maxDigits: 9, example: '901 123 456' }, + { code: 'SI', dial: '+386', label: 'Słowenia', minDigits: 8, maxDigits: 8, example: '123 456 78' }, + { code: 'ES', dial: '+34', label: 'Hiszpania', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'SE', dial: '+46', label: 'Szwecja', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'CH', dial: '+41', label: 'Szwajcaria', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'UA', dial: '+380', label: 'Ukraina', minDigits: 9, maxDigits: 9, example: '501 234 567' }, + { code: 'GB', dial: '+44', label: 'Wielka Brytania', minDigits: 10, maxDigits: 10, example: '7700 900123' }, + { code: 'TR', dial: '+90', label: 'Turcja', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'US', dial: '+1', label: 'USA/Kanada', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'MX', dial: '+52', label: 'Meksyk', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'BR', dial: '+55', label: 'Brazylia', minDigits: 11, maxDigits: 11, example: '123 456 789 01' }, + { code: 'AR', dial: '+54', label: 'Argentyna', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'CL', dial: '+56', label: 'Chile', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'CO', dial: '+57', label: 'Kolumbia', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'AU', dial: '+61', label: 'Australia', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'NZ', dial: '+64', label: 'Nowa Zelandia', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'JP', dial: '+81', label: 'Japonia', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'KR', dial: '+82', label: 'Korea Południowa', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'CN', dial: '+86', label: 'Chiny', minDigits: 11, maxDigits: 11, example: '123 456 789 01' }, + { code: 'IN', dial: '+91', label: 'Indie', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'PK', dial: '+92', label: 'Pakistan', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'ID', dial: '+62', label: 'Indonezja', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'TH', dial: '+66', label: 'Tajlandia', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'VN', dial: '+84', label: 'Wietnam', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'SG', dial: '+65', label: 'Singapur', minDigits: 8, maxDigits: 8, example: '123 456 78' }, + { code: 'MY', dial: '+60', label: 'Malezja', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'ZA', dial: '+27', label: 'RPA', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'EG', dial: '+20', label: 'Egipt', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'MA', dial: '+212', label: 'Maroko', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'NG', dial: '+234', label: 'Nigeria', minDigits: 10, maxDigits: 10, example: '123 456 789 0' }, + { code: 'KE', dial: '+254', label: 'Kenia', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'AE', dial: '+971', label: 'ZEA', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'SA', dial: '+966', label: 'Arabia Saudyjska', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'IL', dial: '+972', label: 'Izrael', minDigits: 9, maxDigits: 9, example: '123 456 789' }, + { code: 'QA', dial: '+974', label: 'Katar', minDigits: 8, maxDigits: 8, example: '123 456 78' }, ] as const; const phoneCountryOf = (code: string) => PHONE_COUNTRIES.find((country) => country.code === code) ?? PHONE_COUNTRIES[0]; +// Numer telefonu z konta ("+48 500100200") rozbity na kraj (kod PHONE_COUNTRIES) i same cyfry - +// do wstepnego wypelnienia danych kontaktowych w formularzu dodawania oferty. +function contactPhoneFromUser(stored: string | null | undefined): { country: string; number: string } { + const compact = (stored ?? '').replace(/\s/g, ''); + if (!compact) { + return { country: 'PL', number: '' }; + } + const match = [...PHONE_COUNTRIES] + .sort((a, b) => b.dial.length - a.dial.length) + .find((country) => compact.startsWith(country.dial)); + if (!match) { + return { country: 'PL', number: compact.replace(/\D/g, '') }; + } + return { country: match.code, number: compact.slice(match.dial.length).replace(/\D/g, '') }; +} + // 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); @@ -2703,7 +3017,10 @@ const INITIAL_LISTING_LOCATION: ListingLocation = { }; // Promien obszaru pokazywanego zamiast dokladnego punktu (zgodny z rozrzutem wspolrzednych z API). -const APPROXIMATE_LOCATION_RADIUS_M = 300; +const APPROXIMATE_LOCATION_RADIUS_M = 1000; +// Zoom mapy w kroku Lokalizacja: dokladny (pinezka) vs przyblizony (mapa sie oddala, widac obszar). +const EXACT_LOCATION_ZOOM = 17; +const APPROXIMATE_LOCATION_ZOOM = 14; const INITIAL_LISTING_COORDS: LatLng = { lat: 50.0491, lng: 19.9617 }; @@ -13706,7 +14023,50 @@ function AddListingPage({ editListingId }: { editListingId?: number }) { const [editLoadError, setEditLoadError] = useState(''); const [currentStep, setCurrentStep] = useState(0); 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. + const [categoryChosen, setCategoryChosen] = useState(isEdit); + const showCategoryChooser = !isEdit && !categoryChosen; + const chooseCategory = (transaction: string, propertyType: string) => { + setListingDetails((current) => ({ ...current, transaction, propertyType })); + setCurrentStep(0); + setCategoryChosen(true); + }; + + // Dane kontaktowe wypelniamy z konta zalogowanego uzytkownika (imie i nazwisko, e-mail, telefon) - + // raz i tylko dla nowej oferty. Pola pozostaja w pelni edytowalne. Przy edycji ogloszenia dane + // pochodza z samego ogloszenia, wiec nie nadpisujemy ich kontem. + const contactPrefilledRef = useRef(false); + useEffect(() => { + if (isEdit || contactPrefilledRef.current || !user) { + return; + } + contactPrefilledRef.current = true; + const phone = contactPhoneFromUser(user.phone); + setListingDetails((current) => ({ + ...current, + contactName: current.contactName || user.fullName || '', + contactEmail: current.contactEmail || user.email || '', + contactPhoneCountry: current.contactPhone ? current.contactPhoneCountry : phone.country, + contactPhone: current.contactPhone || phone.number, + })); + }, [isEdit, user]); const [contactPhoneTouched, setContactPhoneTouched] = useState(false); + // Wybor kraju telefonu jako picker z flaga (jak w ustawieniach konta), a nie zwykly updateListingDetails('title', event.target.value)} placeholder="Np. 3-pokojowe mieszkanie po remoncie, gotowe do wprowadzenia" /> + updateListingDetails('title', event.target.value)} placeholder={LISTING_TITLE_HINTS[listingDetails.propertyType] ?? DEFAULT_LISTING_TITLE_HINT} /> {listingDetails.title.length}/100 @@ -14363,7 +14746,7 @@ function AddListingPage({ editListingId }: { editListingId?: number }) { onChange={(html) => updateListingDetails('description', html)} maxLength={3000} ariaLabel="Opis ogłoszenia" - placeholder="Opisz nieruchomość, jej stan, okolicę, komunikację, dodatkowe atuty..." + placeholder="Opisz szczegóły oferty: stan, lokalizację, atuty i najważniejsze informacje dla zainteresowanych..." /> @@ -14371,13 +14754,13 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {

Szczegóły nieruchomości

- updateListingDetails('price', value)} /> + updateListingDetails('price', value)} /> {isRentTransaction ? ( - updateExtraInfo('rent', value)} /> + updateExtraInfo('rent', value)} /> ) : ( )} - updateListingDetails('area', value)} /> + updateListingDetails('area', value)} /> {showRoomFields && updateListingDetails('rooms', value)} />} {showFlatFields && updateListingDetails('floor', value)} />} {showFlatFields && updateListingDetails('buildingFloors', value)} />} @@ -14386,9 +14769,10 @@ function AddListingPage({ editListingId }: { editListingId?: number }) { {isHouseProperty && updateListingDetails('houseGarage', value)} />} {isHouseProperty && updateListingDetails('houseBasement', value)} />} {isHouseProperty && updateListingDetails('houseAttic', value)} />} - {showBuildingFields && updateListingDetails('yearBuilt', value)} />} + {showBuildingFields && updateListingDetails('yearBuilt', value)} />} {showBuildingFields && updateListingDetails('condition', value)} />} updateListingDetails('ownership', value)} /> + updateListingDetails('market', value)} />
@@ -14399,15 +14783,39 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {