This commit is contained in:
+592
-97
@@ -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<string, string> = {
|
||||
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<string, string> = {
|
||||
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 (
|
||||
<svg className="empty-photos-graphic" viewBox="0 0 152 104" fill="none" aria-hidden="true">
|
||||
<g opacity="0.6">
|
||||
<g transform="rotate(-8 50 50)">
|
||||
<rect x="26" y="28" width="48" height="44" rx="7" fill="#eef2f7" stroke="#d9e2ec" strokeWidth="1.6" />
|
||||
<circle cx="39" cy="41" r="3.6" fill="#ccd8e5" />
|
||||
<path d="M31 64l11-13 7 8 6-7 12 12" stroke="#ccd8e5" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</g>
|
||||
<g transform="rotate(8 102 50)">
|
||||
<rect x="78" y="28" width="48" height="44" rx="7" fill="#eef2f7" stroke="#d9e2ec" strokeWidth="1.6" />
|
||||
<circle cx="91" cy="41" r="3.6" fill="#ccd8e5" />
|
||||
<path d="M83 64l11-13 7 8 6-7 12 12" stroke="#ccd8e5" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</g>
|
||||
</g>
|
||||
<path d="M60 80V56l16-12 16 12v24Z" fill="#e6f8ef" stroke="#12a764" strokeWidth="2.6" strokeLinejoin="round" />
|
||||
<path d="M55 58 76 42 97 58" stroke="#12a764" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
|
||||
<rect x="70" y="66" width="12" height="14" rx="1" stroke="#12a764" strokeWidth="2.2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function AddSceneIllustration() {
|
||||
return (
|
||||
<svg className="add-chooser-scene" viewBox="0 0 170 96" fill="none" aria-hidden="true">
|
||||
<g opacity="0.65" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<path d="M110 22c1.5-6 11-6 13 0 6 0 6 8 0 8h-15c-5 0-4.5-8 2-8Z" />
|
||||
<path d="M136 34c1-4 7.5-4 8.5 0 4 0 4 6 0 6h-10.5c-4 0-3-6 2-6Z" />
|
||||
</g>
|
||||
<g stroke="currentColor" strokeWidth="2" strokeLinejoin="round">
|
||||
<circle cx="21" cy="57" r="11" fill="currentColor" fillOpacity="0.12" />
|
||||
<line x1="21" y1="66" x2="21" y2="85" />
|
||||
</g>
|
||||
<g stroke="currentColor" strokeWidth="2" strokeLinejoin="round">
|
||||
<rect x="45" y="31" width="40" height="54" rx="2" fill="currentColor" fillOpacity="0.1" />
|
||||
<g strokeWidth="1.6" opacity="0.75">
|
||||
<rect x="53" y="39" width="8" height="8" />
|
||||
<rect x="69" y="39" width="8" height="8" />
|
||||
<rect x="53" y="53" width="8" height="8" />
|
||||
<rect x="69" y="53" width="8" height="8" />
|
||||
<rect x="53" y="67" width="8" height="8" />
|
||||
<rect x="69" y="67" width="8" height="8" />
|
||||
</g>
|
||||
</g>
|
||||
<g stroke="currentColor" strokeWidth="2" strokeLinejoin="round">
|
||||
<path d="M97 85V55l22-14 22 14v30Z" fill="currentColor" fillOpacity="0.1" />
|
||||
<path d="M93 57l26-17 26 17" />
|
||||
<rect x="111" y="67" width="15" height="18" />
|
||||
</g>
|
||||
<line x1="7" y1="85" x2="163" y2="85" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="add-hero">
|
||||
<svg className="add-hero-arrow left" viewBox="0 0 120 96" fill="none" aria-hidden="true">
|
||||
<path d="M104 12C52 8 16 30 30 82" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeDasharray="7 9" />
|
||||
<path d="M18 66l12 18 16-8" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
<svg className="add-hero-arrow right" viewBox="0 0 120 96" fill="none" aria-hidden="true">
|
||||
<path d="M16 12c52-4 88 18 74 70" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeDasharray="7 9" />
|
||||
<path d="M102 66l-12 18-16-8" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
|
||||
<h1 className="add-hero-title">
|
||||
Dodaj ogłoszenie{' '}
|
||||
<span className="add-hero-hl">
|
||||
za darmo
|
||||
<svg className="add-hero-underline" viewBox="0 0 240 22" fill="none" aria-hidden="true">
|
||||
<path d="M6 14C60 4 176 4 234 12" stroke="currentColor" strokeWidth="5" strokeLinecap="round" />
|
||||
</svg>
|
||||
<svg className="add-hero-spark" viewBox="0 0 44 40" fill="none" aria-hidden="true">
|
||||
<path d="M22 4v13M13 10l5 7M31 10l-5 7" stroke="currentColor" strokeWidth="3" strokeLinecap="round" />
|
||||
</svg>
|
||||
</span>
|
||||
</h1>
|
||||
<p className="add-hero-sub">Wystaw swoje ogłoszenie w kilka chwil – bez opłat i ukrytych kosztów.</p>
|
||||
|
||||
<div className="add-hero-badges">
|
||||
<div className="add-hero-badge">
|
||||
<span className="add-hero-badge-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="11" fill="#12a764" />
|
||||
<path d="M7 12.4l3.3 3.3L17.2 9" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
<div><strong>100% darmowe</strong><small>Bez prowizji i opłat</small></div>
|
||||
</div>
|
||||
<div className="add-hero-badge">
|
||||
<span className="add-hero-badge-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M12 2.5l8 3v6c0 5-3.4 8.3-8 10-4.6-1.7-8-5-8-10v-6l8-3Z" fill="#12a764" />
|
||||
<path d="M8.4 12.2l2.6 2.6 4.6-5" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
</span>
|
||||
<div><strong>Bezpiecznie</strong><small>Twoje dane są chronione</small></div>
|
||||
</div>
|
||||
<div className="add-hero-badge">
|
||||
<span className="add-hero-badge-ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true">
|
||||
<path d="M13 2 4 14h6l-1 8 9-12h-6l1-8Z" fill="#12a764" />
|
||||
</svg>
|
||||
</span>
|
||||
<div><strong>Szybko i łatwo</strong><small>Tylko kilka kroków</small></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<div className="add-chooser" aria-label="Wybierz, co chcesz zrobić">
|
||||
{ADD_INTENT_CARDS.map((card) => (
|
||||
<section className={`add-chooser-card ${card.key}`} key={card.key} aria-label={card.title}>
|
||||
<header className="add-chooser-head">
|
||||
<span className="add-chooser-badge"><Icon name={card.icon} /></span>
|
||||
<div>
|
||||
<h2>{card.title}</h2>
|
||||
<p>{card.subtitle}</p>
|
||||
</div>
|
||||
<AddSceneIllustration />
|
||||
</header>
|
||||
<div className="add-chooser-list">
|
||||
{card.items.map((item) => (
|
||||
<button
|
||||
type="button"
|
||||
key={item.label}
|
||||
className="add-chooser-item"
|
||||
onClick={() => onPick(card.transaction, item.label)}
|
||||
>
|
||||
<span className="add-chooser-item-icon"><Icon name={item.icon} /></span>
|
||||
<span className="add-chooser-item-label">{item.label}</span>
|
||||
<Icon name="chevron" />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 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 (
|
||||
<aside className="add-steps" aria-label="Kroki dodawania ogłoszenia">
|
||||
<ol className="add-steps-list">
|
||||
{ADD_STEP_META.map((step, index) => {
|
||||
const done = index < activeNode;
|
||||
const active = index === activeNode;
|
||||
const clickable = index <= activeNode;
|
||||
const handle = () => (index === 0 ? onBackToCategory() : onGoStep(index - 1));
|
||||
return (
|
||||
<li key={step.title} className={`add-steps-item ${done ? 'done' : ''} ${active ? 'active' : ''}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="add-steps-btn"
|
||||
onClick={clickable ? handle : undefined}
|
||||
disabled={!clickable}
|
||||
aria-current={active ? 'step' : undefined}
|
||||
>
|
||||
<span className="add-steps-marker">{done ? <Icon name="check" /> : index + 1}</span>
|
||||
<span className="add-steps-text">
|
||||
<strong>{index + 1}. {step.title}</strong>
|
||||
<small>{step.desc}</small>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
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 <select>.
|
||||
const [isContactCountryOpen, setIsContactCountryOpen] = useState(false);
|
||||
const contactCountryRef = useRef<HTMLDivElement | null>(null);
|
||||
useEffect(() => {
|
||||
if (!isContactCountryOpen) {
|
||||
return;
|
||||
}
|
||||
const handler = (event: MouseEvent) => {
|
||||
if (contactCountryRef.current && !contactCountryRef.current.contains(event.target as Node)) {
|
||||
setIsContactCountryOpen(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handler);
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [isContactCountryOpen]);
|
||||
const [location, setLocation] = useState<ListingLocation>(INITIAL_LISTING_LOCATION);
|
||||
const [photos, setPhotos] = useState<ListingPhoto[]>([]);
|
||||
const [extraInfo, setExtraInfo] = useState(INITIAL_EXTRA_INFO);
|
||||
@@ -14102,18 +14462,6 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
};
|
||||
|
||||
// Ustawia wskazane zdjecie jako glowne (przenosi na poczatek listy = wyswietla sie pierwsze w ogloszeniu).
|
||||
const setMainPhoto = (photoId: string) => {
|
||||
setPhotos((current) => {
|
||||
const index = current.findIndex((photo) => photo.id === photoId);
|
||||
if (index <= 0) {
|
||||
return current;
|
||||
}
|
||||
const nextPhotos = [...current];
|
||||
const [photo] = nextPhotos.splice(index, 1);
|
||||
nextPhotos.unshift(photo);
|
||||
return nextPhotos;
|
||||
});
|
||||
};
|
||||
|
||||
const movePhoto = (photoId: string, direction: -1 | 1) => {
|
||||
setPhotos((current) => {
|
||||
@@ -14250,8 +14598,10 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
const nextCenter = clampCenter(point);
|
||||
setLocationCenter(nextCenter);
|
||||
setLocationMapCenter(nextCenter);
|
||||
setLocationMapZoom(locationZoom(location));
|
||||
setLocationStatus('Pinezka została ustawiona na podstawie wpisanego adresu.');
|
||||
setLocationMapZoom(location.approximateLocation ? APPROXIMATE_LOCATION_ZOOM : locationZoom(location));
|
||||
setLocationStatus(location.approximateLocation
|
||||
? 'Ustawiliśmy przybliżony obszar na podstawie wpisanego adresu.'
|
||||
: 'Pinezka została ustawiona na podstawie wpisanego adresu.');
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof DOMException && error.name === 'AbortError') {
|
||||
@@ -14266,11 +14616,43 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
};
|
||||
}, [isLocationStep, location]);
|
||||
|
||||
// Plynne oddalenie/przyblizenie mapy przy przelaczaniu "Nie pokazuj dokladnego adresu".
|
||||
// Zoom zmieniamy krok po kroku (poziomy kafelkow sa calkowite), co daje efekt animacji.
|
||||
const approxZoomAnimRef = useRef<number | null>(null);
|
||||
useEffect(() => {
|
||||
if (!isLocationStep) {
|
||||
return;
|
||||
}
|
||||
const target = location.approximateLocation ? APPROXIMATE_LOCATION_ZOOM : EXACT_LOCATION_ZOOM;
|
||||
if (approxZoomAnimRef.current !== null) {
|
||||
window.clearInterval(approxZoomAnimRef.current);
|
||||
}
|
||||
approxZoomAnimRef.current = window.setInterval(() => {
|
||||
setLocationMapZoom((z) => {
|
||||
if (z === target) {
|
||||
if (approxZoomAnimRef.current !== null) {
|
||||
window.clearInterval(approxZoomAnimRef.current);
|
||||
approxZoomAnimRef.current = null;
|
||||
}
|
||||
return z;
|
||||
}
|
||||
return z + (z < target ? 1 : -1);
|
||||
});
|
||||
}, 130);
|
||||
return () => {
|
||||
if (approxZoomAnimRef.current !== null) {
|
||||
window.clearInterval(approxZoomAnimRef.current);
|
||||
approxZoomAnimRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [location.approximateLocation, isLocationStep]);
|
||||
|
||||
const pickLocationPoint = (point: LatLng) => {
|
||||
const nextCenter = clampCenter(point);
|
||||
setLocationCenter(nextCenter);
|
||||
setLocationMapCenter(nextCenter);
|
||||
setLocationMapZoom((current) => Math.max(current, 16));
|
||||
// Przy przyblizonej lokalizacji nie doblizamy mapy - obszar ma pozostac widoczny.
|
||||
setLocationMapZoom((current) => (location.approximateLocation ? current : Math.max(current, 16)));
|
||||
setLocationStatus('Pobieram adres z wybranego punktu...');
|
||||
const controller = new AbortController();
|
||||
reverseGeocodeListingLocation(nextCenter, controller.signal)
|
||||
@@ -14299,7 +14681,7 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
<strong>Dodaj ogłoszenie</strong>
|
||||
</div>
|
||||
|
||||
{!isPhotosStep && !isExtraInfoStep && !isFinalPreviewStep && (
|
||||
{isEdit && !isPhotosStep && !isExtraInfoStep && !isFinalPreviewStep && (
|
||||
<div className="add-title">
|
||||
{isEdit ? (
|
||||
<>
|
||||
@@ -14314,45 +14696,46 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
<p>Wypełnij formularz i dotrzyj do tysięcy zainteresowanych.</p>
|
||||
</>
|
||||
)}
|
||||
{!isEdit && categoryChosen && currentStep === 0 && (
|
||||
<button type="button" className="add-change-category" onClick={() => setCategoryChosen(false)}>
|
||||
<Icon name="chevron" /> Zmień kategorię
|
||||
</button>
|
||||
)}
|
||||
{editLoadError && <p className="publish-feedback error" role="alert">{editLoadError}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="add-progress" aria-label="Postęp formularza">
|
||||
{addProgressSteps.map((step, index) => {
|
||||
const clickable = isEdit || index <= currentStep;
|
||||
const state = index === currentStep ? 'active' : index < currentStep ? 'completed' : '';
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
key={step}
|
||||
className={`add-step ${state}${clickable ? ' clickable' : ''}`}
|
||||
onClick={clickable ? () => goToStep(index) : undefined}
|
||||
disabled={!clickable}
|
||||
aria-current={index === currentStep ? 'step' : undefined}
|
||||
>
|
||||
<span>{index < currentStep ? <Icon name="check" /> : index + 1}</span>
|
||||
<strong>{step}</strong>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{showCategoryChooser ? (
|
||||
<div className="add-chooser-view">
|
||||
<AddChooserHero />
|
||||
<AddCategoryChooser onPick={chooseCategory} />
|
||||
</div>
|
||||
|
||||
<div className={`add-layout ${isFinalPreviewStep ? 'final-preview-layout' : ''}`}>
|
||||
) : (
|
||||
<>
|
||||
<div className={`add-layout add-layout-wizard ${isFinalPreviewStep ? 'final-preview-layout' : ''}`}>
|
||||
<AddWizardSteps currentStep={currentStep} onGoStep={goToStep} onBackToCategory={isEdit ? () => {} : () => setCategoryChosen(false)} />
|
||||
<div className="add-form-column">
|
||||
{/* Naglowek "Wybrana opcja" u gory na kazdym kroku (poza finalnym podgladem),
|
||||
nad animowana trescia kroku. */}
|
||||
{!isFinalPreviewStep && (
|
||||
<section className={`add-selected-option ${listingDetails.transaction === 'Wynajem' ? 'rent' : 'sell'}`}>
|
||||
<span className="add-selected-badge"><Icon name={ADD_CATEGORY_ICONS[listingDetails.propertyType] ?? 'building'} /></span>
|
||||
<div className="add-selected-text">
|
||||
<small>Wybrana opcja</small>
|
||||
<h2>{listingDetails.transaction === 'Wynajem' ? 'Wynajmij' : 'Sprzedaj'} – {listingDetails.propertyType}</h2>
|
||||
<p>Dodajesz ogłoszenie za darmo. Bez prowizji i ukrytych kosztów.</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
{/* key={currentStep} remontuje tresc przy zmianie kroku - dzieki temu odpala sie
|
||||
animacja pojawienia (Dalej/Wstecz). */}
|
||||
<div className="add-step-anim" key={currentStep}>
|
||||
{!isLocationStep && !isPhotosStep && !isExtraInfoStep && !isFinalPreviewStep && (
|
||||
<>
|
||||
<section className="add-form-card">
|
||||
<h2>Podstawowe informacje</h2>
|
||||
<div className="add-three-grid">
|
||||
<AddSelect label="Transakcja" value={listingDetails.transaction} options={listingDetailsSelectOptions.transaction} onChange={(value) => updateListingDetails('transaction', value)} />
|
||||
<AddSelect label="Rodzaj nieruchomości" value={listingDetails.propertyType} options={listingDetailsSelectOptions.propertyType} onChange={(value) => updateListingDetails('propertyType', value)} />
|
||||
<AddSelect label="Rynek" value={listingDetails.market} options={listingDetailsSelectOptions.market} onChange={(value) => updateListingDetails('market', value)} />
|
||||
</div>
|
||||
|
||||
<label className="add-field full">
|
||||
<span>Tytuł ogłoszenia</span>
|
||||
<input value={listingDetails.title} maxLength={100} onChange={(event) => updateListingDetails('title', event.target.value)} placeholder="Np. 3-pokojowe mieszkanie po remoncie, gotowe do wprowadzenia" />
|
||||
<input value={listingDetails.title} maxLength={100} onChange={(event) => updateListingDetails('title', event.target.value)} placeholder={LISTING_TITLE_HINTS[listingDetails.propertyType] ?? DEFAULT_LISTING_TITLE_HINT} />
|
||||
<small>{listingDetails.title.length}/100</small>
|
||||
</label>
|
||||
|
||||
@@ -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..."
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@@ -14371,13 +14754,13 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
<section className="add-form-card">
|
||||
<h2>Szczegóły nieruchomości</h2>
|
||||
<div className="add-three-grid">
|
||||
<AddInput label={isRentTransaction ? 'Czynsz miesięczny' : 'Cena'} value={listingDetails.price} suffix="zł" placeholder={isRentTransaction ? 'Np. 3200' : 'Np. 720000'} onChange={(value) => updateListingDetails('price', value)} />
|
||||
<AddInput label={isRentTransaction ? 'Czynsz miesięczny' : 'Cena'} value={listingDetails.price} suffix="zł" placeholder={isRentTransaction ? 'Np. 3200' : 'Np. 720000'} numeric="decimal" onChange={(value) => updateListingDetails('price', value)} />
|
||||
{isRentTransaction ? (
|
||||
<AddInput label="Czynsz administracyjny" value={extraInfo.rent} suffix="zł" placeholder="Np. 3000" onChange={(value) => updateExtraInfo('rent', value)} />
|
||||
<AddInput label="Czynsz administracyjny" value={extraInfo.rent} suffix="zł" placeholder="Np. 3000" numeric="decimal" onChange={(value) => updateExtraInfo('rent', value)} />
|
||||
) : (
|
||||
<AddInput label="Cena za m²" value={pricePerMeter} suffix="zł/m²" readOnly />
|
||||
)}
|
||||
<AddInput label="Powierzchnia" value={listingDetails.area} suffix="m²" placeholder="Np. 55" onChange={(value) => updateListingDetails('area', value)} />
|
||||
<AddInput label="Powierzchnia" value={listingDetails.area} suffix="m²" placeholder="Np. 55" numeric="decimal" onChange={(value) => updateListingDetails('area', value)} />
|
||||
{showRoomFields && <AddSelect label="Liczba pokoi" value={listingDetails.rooms} options={listingDetailsSelectOptions.rooms} onChange={(value) => updateListingDetails('rooms', value)} />}
|
||||
{showFlatFields && <AddSelect label="Piętro" value={listingDetails.floor} options={listingDetailsSelectOptions.floor} onChange={(value) => updateListingDetails('floor', value)} />}
|
||||
{showFlatFields && <AddSelect label="Liczba pięter w budynku" value={listingDetails.buildingFloors} options={listingDetailsSelectOptions.buildingFloors} onChange={(value) => updateListingDetails('buildingFloors', value)} />}
|
||||
@@ -14386,9 +14769,10 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
{isHouseProperty && <AddSelect label="Garaż" value={listingDetails.houseGarage} options={listingDetailsSelectOptions.houseGarage} onChange={(value) => updateListingDetails('houseGarage', value)} />}
|
||||
{isHouseProperty && <AddSelect label="Podpiwniczenie" value={listingDetails.houseBasement} options={listingDetailsSelectOptions.houseBasement} onChange={(value) => updateListingDetails('houseBasement', value)} />}
|
||||
{isHouseProperty && <AddSelect label="Strych" value={listingDetails.houseAttic} options={listingDetailsSelectOptions.houseAttic} onChange={(value) => updateListingDetails('houseAttic', value)} />}
|
||||
{showBuildingFields && <AddInput label="Rok budowy" value={listingDetails.yearBuilt} placeholder="Np. 2015" onChange={(value) => updateListingDetails('yearBuilt', value)} />}
|
||||
{showBuildingFields && <AddInput label="Rok budowy" value={listingDetails.yearBuilt} placeholder="Np. 2015" numeric="integer" onChange={(value) => updateListingDetails('yearBuilt', value)} />}
|
||||
{showBuildingFields && <AddSelect label="Stan nieruchomości" value={listingDetails.condition} options={listingDetailsSelectOptions.condition} onChange={(value) => updateListingDetails('condition', value)} />}
|
||||
<AddSelect label="Forma własności" value={listingDetails.ownership} options={listingDetailsSelectOptions.ownership} onChange={(value) => updateListingDetails('ownership', value)} />
|
||||
<AddSelect label="Rynek" value={listingDetails.market} options={listingDetailsSelectOptions.market} onChange={(value) => updateListingDetails('market', value)} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -14399,15 +14783,39 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
<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)}
|
||||
<div ref={contactCountryRef} className="phone-country-picker">
|
||||
<button
|
||||
type="button"
|
||||
className="phone-country-trigger"
|
||||
onClick={() => setIsContactCountryOpen((open) => !open)}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isContactCountryOpen}
|
||||
aria-label="Prefiks kraju"
|
||||
>
|
||||
<img src={`https://flagcdn.com/w20/${phoneCountryOf(listingDetails.contactPhoneCountry).code.toLowerCase()}.png`} alt={`Flaga ${phoneCountryOf(listingDetails.contactPhoneCountry).label}`} loading="lazy" />
|
||||
<span>{phoneCountryOf(listingDetails.contactPhoneCountry).dial}</span>
|
||||
<Icon name="chevron" />
|
||||
</button>
|
||||
{isContactCountryOpen && (
|
||||
<div className="phone-country-menu" role="listbox" aria-label="Wybór kierunkowego">
|
||||
{PHONE_COUNTRIES.map((country) => (
|
||||
<option key={country.code} value={country.code}>{country.dial} {country.label}</option>
|
||||
<button
|
||||
key={country.code}
|
||||
type="button"
|
||||
className={country.code === listingDetails.contactPhoneCountry ? 'active' : ''}
|
||||
onClick={() => {
|
||||
updateListingDetails('contactPhoneCountry', country.code);
|
||||
setIsContactCountryOpen(false);
|
||||
}}
|
||||
>
|
||||
<img src={`https://flagcdn.com/w20/${country.code.toLowerCase()}.png`} alt={`Flaga ${country.label}`} loading="lazy" />
|
||||
<span>{country.dial}</span>
|
||||
<small>{country.label}</small>
|
||||
</button>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="tel"
|
||||
inputMode="numeric"
|
||||
@@ -14422,25 +14830,30 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
</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">
|
||||
<button className="next-step-button" type="button" onClick={() => goToStep(1)}>
|
||||
Dalej: Lokalizacja
|
||||
<Icon name="arrow" />
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p className="secure-note">
|
||||
<Icon name="shield" />
|
||||
Twoje dane są bezpieczne. Nie udostępniamy ich osobom trzecim.
|
||||
</p>
|
||||
|
||||
<div className="add-wizard-nav">
|
||||
<button className="add-nav-back" type="button" onClick={() => (isEdit ? navigate(-1) : setCategoryChosen(false))}>
|
||||
<Icon name="chevron" /> Wstecz
|
||||
</button>
|
||||
<button className="add-nav-next" type="button" onClick={() => goToStep(1)}>
|
||||
Dalej: Lokalizacja <Icon name="arrow" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isLocationStep && (
|
||||
<section className="add-form-card add-location-card">
|
||||
<div className="add-card-heading">
|
||||
<h2>Lokalizacja nieruchomości</h2>
|
||||
<p className="add-location-subtitle">Podaj dokładną lokalizację nieruchomości.</p>
|
||||
<p>Podaj dokładną lokalizację nieruchomości.</p>
|
||||
</div>
|
||||
|
||||
<div className="add-location-grid">
|
||||
<label className="add-field">
|
||||
@@ -14494,6 +14907,8 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
marker={locationCenter}
|
||||
zoom={locationMapZoom}
|
||||
label={[location.street, location.building].filter(Boolean).join(' ') || location.city}
|
||||
approximate={location.approximateLocation}
|
||||
radiusMeters={APPROXIMATE_LOCATION_RADIUS_M}
|
||||
onChange={(center, zoom) => { setLocationMapCenter(center); setLocationMapZoom(zoom); }}
|
||||
onPick={pickLocationPoint}
|
||||
/>
|
||||
@@ -14517,19 +14932,18 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
|
||||
{isPhotosStep && (
|
||||
<>
|
||||
<div className="add-title add-step-title">
|
||||
<h1>Dodaj zdjęcia</h1>
|
||||
<section className="add-form-card add-photos-card">
|
||||
<div className="add-card-heading">
|
||||
<h2>Dodaj zdjęcia</h2>
|
||||
<p>Dodaj zdjęcia, które najlepiej prezentują Twoją nieruchomość.</p>
|
||||
</div>
|
||||
|
||||
<section className="add-form-card add-photos-card">
|
||||
<h2>Zdjęcia nieruchomości</h2>
|
||||
|
||||
<label className="photo-upload-box" onDragOver={(event) => event.preventDefault()} onDrop={handlePhotoDrop}>
|
||||
<input ref={photoInputRef} type="file" accept="image/png,image/jpeg" multiple onChange={handlePhotoInput} />
|
||||
<Icon name="upload" />
|
||||
<strong>Kliknij lub przeciągnij zdjęcia tutaj</strong>
|
||||
<span>Dodaj do 20 zdjęć w formacie JPG, PNG (max. 10 MB) - szersze niż 1024 px zmniejszymy automatycznie</span>
|
||||
<strong>Przeciągnij zdjęcia tutaj</strong>
|
||||
<span className="photo-upload-or">lub kliknij, aby wybrać pliki z urządzenia</span>
|
||||
<span>Dodaj do 20 zdjęć w formacie JPG, PNG (max. 10 MB).<br />Sugerowana rozdzielczość: min. 1600 x 1200 px.</span>
|
||||
</label>
|
||||
|
||||
<p className="photo-tip-note">
|
||||
@@ -14564,18 +14978,14 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
<button type="button" disabled={index === 0} onClick={() => movePhoto(photo.id, -1)} aria-label={`Przesuń zdjęcie ${photo.name} w lewo`}>‹</button>
|
||||
<button type="button" disabled={index === photos.length - 1} onClick={() => movePhoto(photo.id, 1)} aria-label={`Przesuń zdjęcie ${photo.name} w prawo`}>›</button>
|
||||
</div>
|
||||
{index !== 0 && (
|
||||
<button type="button" className="set-main-photo-button" onClick={() => setMainPhoto(photo.id)}>
|
||||
<Icon name="star" /> Ustaw jako główne
|
||||
</button>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="empty-photos-box">
|
||||
<Icon name="camera" />
|
||||
<p>Nie dodano jeszcze zdjęć. Pierwsze dodane zdjęcie będzie zdjęciem głównym.</p>
|
||||
<PhotosEmptyIllustration />
|
||||
<strong>Nie dodałeś jeszcze żadnych zdjęć</strong>
|
||||
<small>Pierwsze zdjęcie będzie zdjęciem głównym Twojego ogłoszenia.</small>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14914,6 +15324,7 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="add-sidebar" aria-label="Podgląd i pomoc">
|
||||
{isFinalPreviewStep ? (
|
||||
@@ -15062,6 +15473,8 @@ function AddListingPage({ editListingId }: { editListingId?: number }) {
|
||||
))}
|
||||
</div>
|
||||
</section>}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
@@ -22840,6 +23253,8 @@ function LocationPickerMap({
|
||||
label,
|
||||
onChange,
|
||||
onPick,
|
||||
approximate = false,
|
||||
radiusMeters = 300,
|
||||
}: {
|
||||
center: LatLng;
|
||||
marker: LatLng;
|
||||
@@ -22847,9 +23262,17 @@ function LocationPickerMap({
|
||||
label: string;
|
||||
onChange: (center: LatLng, zoom: number) => void;
|
||||
onPick: (point: LatLng) => void;
|
||||
// approximate = pokazuj obszar (plame) zamiast dokladnej pinezki; radiusMeters = promien tego obszaru.
|
||||
approximate?: boolean;
|
||||
radiusMeters?: number;
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [size, setSize] = useState({ w: 720, h: 278 });
|
||||
// Podglad: mapa uliczna (OpenStreetMap) albo satelita (ArcGIS World Imagery).
|
||||
const [mapType, setMapType] = useState<'map' | 'satellite'>('map');
|
||||
const tileUrl = (tileX: number, tileY: number) => (mapType === 'satellite'
|
||||
? `https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/${zoom}/${tileY}/${tileX}`
|
||||
: `https://tile.openstreetmap.org/${zoom}/${tileX}/${tileY}.png`);
|
||||
const [draftMarker, setDraftMarker] = useState<LatLng | null>(null);
|
||||
const mapDragRef = useRef<{ startX: number; startY: number; originX: number; originY: number; moved: boolean } | null>(null);
|
||||
const markerDragRef = useRef<{ moved: boolean } | null>(null);
|
||||
@@ -22866,6 +23289,28 @@ function LocationPickerMap({
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
// Scroll nad mapa zoomuje mapę i NIE przewija strony. React rejestruje onWheel jako pasywny,
|
||||
// wiec preventDefault nie zadziala - dlatego wpinamy natywny listener z passive: false. Poza
|
||||
// mapa (brak listenera) scroll dziala normalnie. Aktualne wartosci czytamy przez ref.
|
||||
const wheelStateRef = useRef({ zoom, center, onChange });
|
||||
wheelStateRef.current = { zoom, center, onChange };
|
||||
useEffect(() => {
|
||||
const node = containerRef.current;
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
const onWheelNative = (event: WheelEvent) => {
|
||||
event.preventDefault();
|
||||
const state = wheelStateRef.current;
|
||||
const nextZoom = Math.min(19, Math.max(5, state.zoom + (event.deltaY < 0 ? 1 : -1)));
|
||||
if (nextZoom !== state.zoom) {
|
||||
state.onChange(state.center, nextZoom);
|
||||
}
|
||||
};
|
||||
node.addEventListener('wheel', onWheelNative, { passive: false });
|
||||
return () => node.removeEventListener('wheel', onWheelNative);
|
||||
}, []);
|
||||
|
||||
const worldCenter = projectToWorld(center.lat, center.lng, zoom);
|
||||
const originX = worldCenter.x - size.w / 2;
|
||||
const originY = worldCenter.y - size.h / 2;
|
||||
@@ -22897,6 +23342,9 @@ function LocationPickerMap({
|
||||
const markerWorld = projectToWorld(shownMarker.lat, shownMarker.lng, zoom);
|
||||
const markerLeft = markerWorld.x - originX;
|
||||
const markerTop = markerWorld.y - originY;
|
||||
// Srednica plamy obszaru w pikselach: promien w metrach przeliczony na piksele w tej szerokosci i zoomie.
|
||||
const metersPerPixel = (156543.03392 * Math.cos((shownMarker.lat * Math.PI) / 180)) / (2 ** zoom);
|
||||
const areaDiameter = (2 * radiusMeters) / metersPerPixel;
|
||||
const changeZoom = (delta: number) => {
|
||||
const nextZoom = Math.min(19, Math.max(5, zoom + delta));
|
||||
onChange(center, nextZoom);
|
||||
@@ -22985,11 +23433,6 @@ function LocationPickerMap({
|
||||
}
|
||||
};
|
||||
|
||||
const onWheel = (event: ReactWheelEvent<HTMLDivElement>) => {
|
||||
event.preventDefault();
|
||||
changeZoom(event.deltaY < 0 ? 1 : -1);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="add-location-map real-location-map" ref={containerRef}>
|
||||
<div
|
||||
@@ -22998,7 +23441,6 @@ function LocationPickerMap({
|
||||
onPointerMove={onMapPointerMove}
|
||||
onPointerUp={onMapPointerUp}
|
||||
onPointerCancel={onMapPointerUp}
|
||||
onWheel={onWheel}
|
||||
>
|
||||
<div className="real-location-tiles" aria-hidden="true">
|
||||
{tiles.map((tile) => (
|
||||
@@ -23006,15 +23448,24 @@ function LocationPickerMap({
|
||||
alt=""
|
||||
draggable={false}
|
||||
height={tileSize}
|
||||
key={tile.key}
|
||||
key={`${mapType}-${tile.key}`}
|
||||
loading="lazy"
|
||||
src={`https://tile.openstreetmap.org/${zoom}/${tile.x}/${tile.y}.png`}
|
||||
src={tileUrl(tile.x, tile.y)}
|
||||
style={{ left: tile.left, top: tile.top }}
|
||||
width={tileSize}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{approximate ? (
|
||||
// Przyblizona lokalizacja: zamiast pinezki rysujemy "plame" - obszar o zadanym promieniu.
|
||||
<div
|
||||
className="add-location-area"
|
||||
style={{ left: markerLeft, top: markerTop, width: areaDiameter, height: areaDiameter }}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className="add-location-pin real-location-pin"
|
||||
style={{ left: markerLeft, top: markerTop }}
|
||||
@@ -23028,8 +23479,16 @@ function LocationPickerMap({
|
||||
<span className="add-location-map-label real-location-label" style={{ left: markerLeft, top: markerTop - 36 }}>
|
||||
{label}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="real-location-maptype" role="group" aria-label="Rodzaj mapy">
|
||||
<button type="button" className={mapType === 'map' ? 'active' : ''} onClick={() => setMapType('map')}>Mapa</button>
|
||||
<button type="button" className={mapType === 'satellite' ? 'active' : ''} onClick={() => setMapType('satellite')}>Satelita</button>
|
||||
</div>
|
||||
|
||||
{!approximate && (
|
||||
<div
|
||||
className="real-location-pin-tool"
|
||||
onPointerDown={onToolPointerDown}
|
||||
@@ -23040,13 +23499,19 @@ function LocationPickerMap({
|
||||
>
|
||||
<Icon name="pin" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="real-location-zoom">
|
||||
<button type="button" aria-label="Przybliż" onClick={() => changeZoom(1)}>+</button>
|
||||
<button type="button" aria-label="Oddal" onClick={() => changeZoom(-1)}>−</button>
|
||||
</div>
|
||||
<a className="real-location-attribution" href="https://www.openstreetmap.org/copyright" rel="noreferrer" target="_blank">
|
||||
© OpenStreetMap
|
||||
<a
|
||||
className="real-location-attribution"
|
||||
href={mapType === 'satellite' ? 'https://www.esri.com/' : 'https://www.openstreetmap.org/copyright'}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{mapType === 'satellite' ? '© Esri, Maxar' : '© OpenStreetMap'}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
@@ -25983,12 +26448,29 @@ function AddSelect({ label, value, options = [value], onChange }: { label: strin
|
||||
);
|
||||
}
|
||||
|
||||
function AddInput({ label, value, suffix, type = 'text', placeholder, readOnly, onChange }: { label: string; value: string; suffix?: string; type?: string; placeholder?: string; readOnly?: boolean; onChange?: (value: string) => void }) {
|
||||
function AddInput({ label, value, suffix, type = 'text', placeholder, readOnly, onChange, numeric }: { label: string; value: string; suffix?: string; type?: string; placeholder?: string; readOnly?: boolean; onChange?: (value: string) => void; numeric?: 'integer' | 'decimal' }) {
|
||||
// Pola liczbowe (cena, powierzchnia, rok budowy) przyjmuja tylko cyfry - litery i inne znaki sa
|
||||
// odrzucane od razu. Tryb "decimal" dopuszcza jeden separator dziesietny (kropka lub przecinek).
|
||||
const sanitize = (raw: string): string => {
|
||||
if (numeric === 'integer') {
|
||||
return raw.replace(/\D/g, '');
|
||||
}
|
||||
if (numeric === 'decimal') {
|
||||
const cleaned = raw.replace(/[^\d.,]/g, '');
|
||||
const sep = cleaned.search(/[.,]/);
|
||||
if (sep === -1) {
|
||||
return cleaned;
|
||||
}
|
||||
return cleaned.slice(0, sep + 1) + cleaned.slice(sep + 1).replace(/[.,]/g, '');
|
||||
}
|
||||
return raw;
|
||||
};
|
||||
const inputMode = numeric === 'integer' ? 'numeric' : numeric === 'decimal' ? 'decimal' : undefined;
|
||||
return (
|
||||
<label className="add-field">
|
||||
<span>{label}</span>
|
||||
<div className="input-with-suffix">
|
||||
<input type={type} placeholder={placeholder} readOnly={readOnly} {...(onChange || readOnly ? { value, onChange: (event) => onChange?.(event.target.value) } : { defaultValue: value })} />
|
||||
<input type={type} inputMode={inputMode} placeholder={placeholder} readOnly={readOnly} {...(onChange || readOnly ? { value, onChange: (event) => onChange?.(sanitize(event.target.value)) } : { defaultValue: value })} />
|
||||
{suffix && <em>{suffix}</em>}
|
||||
</div>
|
||||
</label>
|
||||
@@ -27549,6 +28031,19 @@ function Icon({ name }: { name: string }) {
|
||||
<path d="M12 12.6a2.4 2.4 0 1 0 0-4.8 2.4 2.4 0 0 0 0 4.8Z" />
|
||||
</svg>
|
||||
);
|
||||
case 'tree':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 2.5c1.9 0 3.5 1.2 4.1 2.9 1.9.3 3.4 2 3.4 4 0 2.3-1.9 4.1-4.1 4.1H8.6C6.4 13.5 4.5 11.7 4.5 9.4c0-2 1.5-3.6 3.4-3.9C8.5 3.7 10.1 2.5 12 2.5Z" />
|
||||
<path d="M12 13.5V21M9.3 21h5.4" />
|
||||
</svg>
|
||||
);
|
||||
case 'apartment':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M6 21V4.5A1.5 1.5 0 0 1 7.5 3h9A1.5 1.5 0 0 1 18 4.5V21M3.5 21h17M9.5 7h.1M14.5 7h.1M9.5 11h.1M14.5 11h.1M9.5 15h.1M14.5 15h.1M10 21v-3.5h4V21" />
|
||||
</svg>
|
||||
);
|
||||
case 'building':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
|
||||
+660
-35
@@ -4306,6 +4306,556 @@ a.city-card:hover {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Animacja pojawienia tresci kroku przy nawigacji Dalej/Wstecz (remount przez key={currentStep}). */
|
||||
.add-step-anim {
|
||||
animation: addStepIn 0.32s ease;
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
@keyframes addStepIn {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.add-step-anim { animation: none; }
|
||||
}
|
||||
|
||||
/* === Kreator dodawania oferty: lewy stepper | formularz | podglad === */
|
||||
.add-layout.add-layout-wizard {
|
||||
grid-template-columns: 244px minmax(0, 1fr) 340px;
|
||||
}
|
||||
|
||||
.add-steps {
|
||||
align-self: start;
|
||||
position: sticky;
|
||||
top: 92px;
|
||||
}
|
||||
|
||||
.add-steps-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.add-steps-item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* linia laczaca markery */
|
||||
.add-steps-item:not(:last-child)::after {
|
||||
background: #e3e9f1;
|
||||
content: "";
|
||||
left: 27px;
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
width: 2px;
|
||||
height: calc(100% - 26px);
|
||||
}
|
||||
|
||||
.add-steps-item.done:not(:last-child)::after {
|
||||
background: #12a764;
|
||||
}
|
||||
|
||||
.add-steps-btn {
|
||||
align-items: flex-start;
|
||||
background: none;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 20px 12px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-steps-btn:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.add-steps-item.active .add-steps-btn {
|
||||
background: #eaf7f0;
|
||||
}
|
||||
|
||||
.add-steps-marker {
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border: 2px solid #dbe3ec;
|
||||
border-radius: 50%;
|
||||
color: #9aa7b6;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
height: 30px;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
width: 30px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.add-steps-item.active .add-steps-marker,
|
||||
.add-steps-item.done .add-steps-marker {
|
||||
background: #12a764;
|
||||
border-color: #12a764;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.add-steps-marker svg { height: 15px; width: 15px; }
|
||||
|
||||
.add-steps-text strong {
|
||||
color: #22314a;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.add-steps-item.active .add-steps-text strong { color: #0f7a48; }
|
||||
|
||||
.add-steps-text small {
|
||||
color: #8794a6;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* Naglowek "Wybrana opcja" */
|
||||
.add-selected-option {
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border: 1px solid #edf1f6;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 22px rgba(24, 35, 57, 0.05);
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 18px 22px;
|
||||
}
|
||||
|
||||
.add-selected-badge {
|
||||
align-items: center;
|
||||
background: #e6f8ef;
|
||||
border-radius: 12px;
|
||||
color: #0f9257;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
height: 52px;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
}
|
||||
|
||||
.add-selected-badge svg { height: 26px; width: 26px; }
|
||||
|
||||
.add-selected-text small {
|
||||
color: #8794a6;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.add-selected-text h2 {
|
||||
color: #12a764;
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
margin: 2px 0 3px;
|
||||
}
|
||||
|
||||
/* Wynajem = fioletowy akcent naglowka, sprzedaz = zielony (domyslny wyzej). */
|
||||
.add-selected-option.rent .add-selected-badge {
|
||||
background: #eee9fd;
|
||||
color: #6f5be0;
|
||||
}
|
||||
|
||||
.add-selected-option.rent .add-selected-text h2 {
|
||||
color: #6f5be0;
|
||||
}
|
||||
|
||||
.add-selected-text p {
|
||||
color: #6f7f92;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Dolny pasek nawigacji kreatora: Wstecz | Dalej */
|
||||
.add-wizard-nav {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.add-nav-back {
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border: 1px solid #d7dfea;
|
||||
border-radius: 10px;
|
||||
color: #45566d;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
gap: 6px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.add-nav-back svg {
|
||||
height: 15px;
|
||||
transform: rotate(90deg);
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
.add-nav-back:hover {
|
||||
background: #f3f6fa;
|
||||
border-color: #c2cddb;
|
||||
}
|
||||
|
||||
.add-nav-next {
|
||||
align-items: center;
|
||||
background: #12a764;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
color: #ffffff;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-size: 14px;
|
||||
font-weight: 850;
|
||||
gap: 8px;
|
||||
padding: 13px 24px;
|
||||
}
|
||||
|
||||
.add-nav-next svg { height: 16px; width: 16px; }
|
||||
.add-nav-next:hover { background: #0f8f55; }
|
||||
|
||||
@media (max-width: 1240px) {
|
||||
.add-layout.add-layout-wizard {
|
||||
grid-template-columns: 210px minmax(0, 1fr) 300px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Podglad trzyma sie prawej kolumny dluzej (waskie kolumny) - dzieki temu jest widoczny takze
|
||||
w Operze, ktora zabiera czesc szerokosci na wbudowany panel boczny. */
|
||||
@media (max-width: 1040px) {
|
||||
.add-layout.add-layout-wizard {
|
||||
grid-template-columns: 180px minmax(0, 1fr) 265px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Dopiero ponizej 860px podglad schodzi pod formularz (na pelna szerokosc). */
|
||||
@media (max-width: 860px) {
|
||||
.add-layout.add-layout-wizard {
|
||||
grid-template-columns: 170px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.add-layout-wizard .add-sidebar {
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.add-layout.add-layout-wizard {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.add-steps {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
/* Wyśrodkowany kontener ekranu wyboru (hero + dwie karty) - jak na makiecie. */
|
||||
.add-chooser-view {
|
||||
margin: 0 auto;
|
||||
max-width: 1060px;
|
||||
}
|
||||
|
||||
/* Hero ekranu wyboru: tytul z podkresleniem/iskra, podtytul, plakietki i strzalki. */
|
||||
.add-hero {
|
||||
padding: 6px 0 4px;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.add-hero-title {
|
||||
color: #14223a;
|
||||
font-size: 40px;
|
||||
font-weight: 950;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.1;
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
|
||||
.add-hero-hl {
|
||||
color: #12a764;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.add-hero-underline {
|
||||
color: #16b866;
|
||||
height: 16px;
|
||||
left: -1%;
|
||||
position: absolute;
|
||||
top: 96%;
|
||||
width: 102%;
|
||||
}
|
||||
|
||||
.add-hero-spark {
|
||||
color: #16b866;
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
right: -30px;
|
||||
top: -12px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.add-hero-sub {
|
||||
color: #64748b;
|
||||
font-size: 15px;
|
||||
font-weight: 650;
|
||||
margin: 24px auto 0;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.add-hero-badges {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px;
|
||||
justify-content: center;
|
||||
margin: 24px 0 4px;
|
||||
}
|
||||
|
||||
.add-hero-badge {
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border: 1px solid #edf1f6;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 6px 18px rgba(24, 35, 57, 0.05);
|
||||
display: flex;
|
||||
gap: 11px;
|
||||
padding: 12px 18px;
|
||||
}
|
||||
|
||||
.add-hero-badge-ic {
|
||||
flex: 0 0 auto;
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
.add-hero-badge-ic svg {
|
||||
height: 26px;
|
||||
width: 26px;
|
||||
}
|
||||
|
||||
.add-hero-badge strong {
|
||||
color: #14223a;
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 850;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.add-hero-badge small {
|
||||
color: #8794a6;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 650;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.add-hero-arrow {
|
||||
height: 92px;
|
||||
position: absolute;
|
||||
top: 74px;
|
||||
width: 116px;
|
||||
}
|
||||
|
||||
.add-hero-arrow.left { color: #16b866; left: 4px; }
|
||||
.add-hero-arrow.right { color: #7c6cf0; right: 4px; }
|
||||
|
||||
/* Ekran startowy dodawania oferty: dwie karty "Chcę sprzedać / wynająć" z listą kategorii. */
|
||||
.add-chooser {
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.add-chooser-card {
|
||||
background: #ffffff;
|
||||
border: 1px solid #edf1f6;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 14px 36px rgba(24, 35, 57, 0.06);
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.add-chooser-head {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.add-chooser-badge {
|
||||
align-items: center;
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
height: 46px;
|
||||
justify-content: center;
|
||||
width: 46px;
|
||||
}
|
||||
|
||||
.add-chooser-badge svg {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.add-chooser-card.sell .add-chooser-badge {
|
||||
background: #e6f8ef;
|
||||
color: #0f9257;
|
||||
}
|
||||
|
||||
.add-chooser-card.rent .add-chooser-badge {
|
||||
background: #eee9fd;
|
||||
color: #6f5be0;
|
||||
}
|
||||
|
||||
.add-chooser-scene {
|
||||
align-self: flex-start;
|
||||
flex: 0 0 auto;
|
||||
height: auto;
|
||||
margin-left: auto;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
.add-chooser-card.sell .add-chooser-scene { color: #7cc6a1; }
|
||||
.add-chooser-card.rent .add-chooser-scene { color: #a99cea; }
|
||||
|
||||
.add-chooser-head h2 {
|
||||
color: #14223a;
|
||||
font-size: 20px;
|
||||
margin: 2px 0 4px;
|
||||
}
|
||||
|
||||
.add-chooser-head p {
|
||||
color: #6f7f92;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.add-chooser-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.add-chooser-item {
|
||||
align-items: center;
|
||||
background: #ffffff;
|
||||
border: 1px solid #e3e9f1;
|
||||
border-radius: 10px;
|
||||
color: #22314a;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
text-align: left;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease, transform 0.15s ease;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.add-chooser-item:hover {
|
||||
border-color: #b9c6d6;
|
||||
box-shadow: 0 6px 16px rgba(24, 35, 57, 0.08);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.add-chooser-card.sell .add-chooser-item:hover { border-color: #7fd3aa; }
|
||||
.add-chooser-card.rent .add-chooser-item:hover { border-color: #b3a6f2; }
|
||||
|
||||
.add-chooser-item-icon {
|
||||
align-items: center;
|
||||
color: #8793a3;
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.add-chooser-card.sell .add-chooser-item-icon { color: #0f9257; }
|
||||
.add-chooser-card.rent .add-chooser-item-icon { color: #6f5be0; }
|
||||
|
||||
.add-chooser-item-icon svg { height: 18px; width: 18px; }
|
||||
|
||||
.add-chooser-item-label {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Strzalka wiersza wskazuje w prawo (chevron domyslnie w dol). */
|
||||
.add-chooser-item > svg:last-child {
|
||||
color: #aab4c2;
|
||||
flex: 0 0 auto;
|
||||
height: 16px;
|
||||
transform: rotate(-90deg);
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.add-change-category {
|
||||
align-items: center;
|
||||
background: none;
|
||||
border: 0;
|
||||
color: #0f9257;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
gap: 4px;
|
||||
margin-top: 10px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.add-change-category svg {
|
||||
height: 14px;
|
||||
transform: rotate(90deg);
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.add-change-category:hover { text-decoration: underline; }
|
||||
|
||||
@media (max-width: 1040px) {
|
||||
.add-hero-arrow {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.add-chooser {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.add-hero-title {
|
||||
font-size: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.add-chooser-scene {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.add-form-card,
|
||||
.listing-preview-card,
|
||||
.add-side-card,
|
||||
@@ -4664,13 +5214,6 @@ a.city-card:hover {
|
||||
padding-bottom: 18px;
|
||||
}
|
||||
|
||||
.add-location-subtitle {
|
||||
color: #5f6f83;
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
margin: -7px 0 18px;
|
||||
}
|
||||
|
||||
.add-location-grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
@@ -4737,6 +5280,52 @@ a.city-card:hover {
|
||||
width: 256px;
|
||||
}
|
||||
|
||||
/* Przelacznik mapa/satelita w rogu podgladu lokalizacji. */
|
||||
.real-location-maptype {
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(24, 35, 57, 0.18);
|
||||
display: inline-flex;
|
||||
left: 12px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.real-location-maptype button {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
color: #45566d;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
padding: 7px 14px;
|
||||
}
|
||||
|
||||
.real-location-maptype button.active {
|
||||
background: #12a764;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* "Plama" przyblizonej lokalizacji - obszar zamiast pinezki. Rozmiar animuje sie przy zmianie zoomu. */
|
||||
.add-location-area {
|
||||
animation: addLocationAreaIn 0.35s ease;
|
||||
background: rgba(18, 167, 100, 0.2);
|
||||
border: 2px solid rgba(18, 167, 100, 0.8);
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.3s ease, height 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@keyframes addLocationAreaIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.real-location-pin {
|
||||
cursor: grab;
|
||||
pointer-events: auto;
|
||||
@@ -4894,6 +5483,25 @@ a.city-card:hover {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Naglowek "Dodaj zdjecia" wewnatrz karty (w ramce razem z zawartoscia). */
|
||||
.add-card-heading {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.add-card-heading h2 {
|
||||
color: #14223a;
|
||||
font-size: 22px;
|
||||
font-weight: 900;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.add-card-heading p {
|
||||
color: #6f7f92;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
margin: 5px 0 0;
|
||||
}
|
||||
|
||||
.photo-upload-box {
|
||||
align-items: center;
|
||||
border: 1px dashed #cbd6e2;
|
||||
@@ -4930,6 +5538,13 @@ a.city-card:hover {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.photo-upload-box .photo-upload-or {
|
||||
color: #6f7f92;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.photo-tip-note {
|
||||
align-items: center;
|
||||
background: #f3fbf7;
|
||||
@@ -5021,7 +5636,13 @@ a.city-card:hover {
|
||||
}
|
||||
|
||||
.photo-tour-input small {
|
||||
bottom: auto;
|
||||
color: #5f748a;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
margin-top: 6px;
|
||||
position: static;
|
||||
right: auto;
|
||||
}
|
||||
|
||||
.photo-tour-actions {
|
||||
@@ -5169,25 +5790,40 @@ a.city-card:hover {
|
||||
.empty-photos-box {
|
||||
align-items: center;
|
||||
background: #f8fbfd;
|
||||
border: 1px solid #e0e8f0;
|
||||
border: 1px dashed #d4deea;
|
||||
border-radius: 8px;
|
||||
color: #526171;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
min-height: 64px;
|
||||
padding: 14px;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
justify-content: center;
|
||||
min-height: 150px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-photos-box svg {
|
||||
color: #10a15e;
|
||||
font-size: 22px;
|
||||
color: #8fc4a8;
|
||||
font-size: 30px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.empty-photos-box p {
|
||||
.empty-photos-graphic {
|
||||
height: auto;
|
||||
margin-bottom: 6px;
|
||||
width: 118px;
|
||||
}
|
||||
|
||||
.empty-photos-box strong {
|
||||
color: #14223a;
|
||||
font-size: 14px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.empty-photos-box small {
|
||||
color: #6f7f92;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.sort-photos-button {
|
||||
@@ -11814,19 +12450,6 @@ a.city-card:hover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.add-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.add-sidebar {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
position: static;
|
||||
}
|
||||
|
||||
.listing-preview-card {
|
||||
grid-row: span 2;
|
||||
}
|
||||
|
||||
.add-footer-benefits > div {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -33649,11 +34272,13 @@ a.listing-detail-back {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.add-phone-row select {
|
||||
flex: 0 0 auto;
|
||||
max-width: 152px;
|
||||
padding-left: 10px;
|
||||
padding-right: 6px;
|
||||
/* Picker kraju (flaga + kod) - kompaktowy, rownej wysokosci z polem numeru. */
|
||||
.add-phone-row .phone-country-picker {
|
||||
flex: 0 0 118px;
|
||||
}
|
||||
|
||||
.add-phone-row .phone-country-trigger {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.add-phone-row input {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 56 KiB |
Reference in New Issue
Block a user