feat(07-hero-search-form): Mini formularz rezerwacji w hero z pre-fill do modala
Phase 7 complete: - Nowy widget Elementor "Carei Search Form" do osadzenia w hero - Pola: segment, daty od/do, lokalizacja, checkbox zwrotu - Po kliknięciu przycisku otwiera modal z pre-wypełnionymi danymi - Design zgodny z Figmą (tło #EDEDF3, przycisk czerwony, tytuł fioletowy) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,9 @@ Plugin Elementor do rezerwacji samochodu na stronie carei.pagedev.pl, zintegrowa
|
|||||||
- ✓ Accessibility: ARIA dialog, focus trap, aria-live — Phase 4
|
- ✓ Accessibility: ARIA dialog, focus trap, aria-live — Phase 4
|
||||||
- ✓ Admin panel: CPT carei_reservation, lista, szczegóły, statusy — Phase 5
|
- ✓ Admin panel: CPT carei_reservation, lista, szczegóły, statusy — Phase 5
|
||||||
|
|
||||||
|
## Validated Requirements (Milestone v0.2)
|
||||||
|
- ✓ Hero Search Form: mini formularz w hero z pre-fill do modala — Phase 7
|
||||||
|
|
||||||
## Key Decisions
|
## Key Decisions
|
||||||
| Decision | Phase | Rationale |
|
| Decision | Phase | Rationale |
|
||||||
|----------|-------|-----------|
|
|----------|-------|-----------|
|
||||||
@@ -41,6 +44,8 @@ Plugin Elementor do rezerwacji samochodu na stronie carei.pagedev.pl, zintegrowa
|
|||||||
| Meta-based status (nie taxonomy) | 5 | Prosty 3-wartościowy enum |
|
| Meta-based status (nie taxonomy) | 5 | Prosty 3-wartościowy enum |
|
||||||
| Token retry on 401/403 | 4 | Automatyczny re-auth bez interwencji usera |
|
| Token retry on 401/403 | 4 | Automatyczny re-auth bez interwencji usera |
|
||||||
| Inline display:none for steps | 4 | CSS class conflict resolution |
|
| Inline display:none for steps | 4 | CSS class conflict resolution |
|
||||||
|
| Calendar picker opacity:0 stretch | 7 | Ukrycie natywnej ikonki z zachowaniem kliknięcia |
|
||||||
|
| Search form niezależne API loading | 7 | Dane dostępne od razu, niezależnie od modala |
|
||||||
|
|
||||||
## Out of Scope (backlog)
|
## Out of Scope (backlog)
|
||||||
- Ubezpieczenie (pakiet Soft/Premium) — czeka na API Softra
|
- Ubezpieczenie (pakiet Soft/Premium) — czeka na API Softra
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ CPT `carei_reservation` z automatycznym zapisem po booking, lista z kolumnami i
|
|||||||
### Phase 6: Wyjazd zagraniczny — sekcja + wyszukiwarka krajów 🔄 Planning
|
### Phase 6: Wyjazd zagraniczny — sekcja + wyszukiwarka krajów 🔄 Planning
|
||||||
Sekcja "Wyjazd zagraniczny" z checkboxem toggle, wyszukiwarką krajów z flagami/cenami, dodawanie/usuwanie krajów, integracja z API submit. Design z Figmy (node 32-397, 122:1054, 122:1091, 123:1195).
|
Sekcja "Wyjazd zagraniczny" z checkboxem toggle, wyszukiwarką krajów z flagami/cenami, dodawanie/usuwanie krajów, integracja z API submit. Design z Figmy (node 32-397, 122:1054, 122:1091, 123:1195).
|
||||||
|
|
||||||
|
### Phase 7: Hero Search Form — mini formularz w hero ✅ Complete
|
||||||
|
Kompaktowy widget Elementor "Carei Search Form" osadzany w sekcji hero. Pola: segment, daty od/do, lokalizacja, checkbox zwrotu. Po kliknięciu przycisku otwiera istniejący modal rezerwacji z pre-wypełnionymi danymi. Design z Figmy (Form.svg).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### Backlog (do realizacji gdy API będzie gotowe)
|
### Backlog (do realizacji gdy API będzie gotowe)
|
||||||
|
|||||||
@@ -3,32 +3,40 @@
|
|||||||
## Current Position
|
## Current Position
|
||||||
|
|
||||||
Milestone: v0.2 Wyjazd Zagraniczny
|
Milestone: v0.2 Wyjazd Zagraniczny
|
||||||
Phase: 6 of 6 (Wyjazd zagraniczny — sekcja + wyszukiwarka krajów) — Planning
|
Phase: 7 of 7 (Hero Search Form) — Complete
|
||||||
Plan: 06-01 created, awaiting approval
|
Plan: 07-01 complete
|
||||||
Status: PLAN created, ready for APPLY
|
Status: Phase 7 complete, Phase 6 plan awaiting execution
|
||||||
Last activity: 2026-03-30 — Created .paul/phases/06-wyjazd-zagraniczny/06-01-PLAN.md
|
Last activity: 2026-04-01 — Phase 7 complete, transitioned
|
||||||
|
|
||||||
Progress:
|
Progress:
|
||||||
- Milestone v0.1: [██████████] 100% ✅
|
- Milestone v0.1: [██████████] 100% ✅
|
||||||
- Milestone v0.2: [░░░░░░░░░░] 0%
|
- Milestone v0.2: [█████░░░░░] 50%
|
||||||
- Phase 6: [░░░░░░░░░░] 0%
|
- Phase 6 (Wyjazd zagraniczny): Plan created, not applied
|
||||||
|
- Phase 7 (Hero Search Form): [██████████] 100% ✅
|
||||||
|
|
||||||
## Loop Position
|
## Loop Position
|
||||||
|
|
||||||
Current loop state:
|
Current loop state:
|
||||||
```
|
```
|
||||||
PLAN ──▶ APPLY ──▶ UNIFY
|
PLAN ──▶ APPLY ──▶ UNIFY
|
||||||
✓ ○ ○ [Plan created, awaiting approval]
|
✓ ✓ ✓ [Loop complete — Phase 7 done]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Accumulated Context
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
| Decision | Phase | Impact |
|
||||||
|
|----------|-------|--------|
|
||||||
|
| Calendar picker opacity:0 stretch | 7 | Natywna ikonka ukryta, kliknięcie zachowane |
|
||||||
|
| Search form niezależne API loading | 7 | Dwa requesty API na page load |
|
||||||
|
|
||||||
|
### Git State
|
||||||
|
Branch: main
|
||||||
|
Feature branches merged: none
|
||||||
|
|
||||||
## Session Continuity
|
## Session Continuity
|
||||||
|
|
||||||
Last session: 2026-03-30
|
Last session: 2026-04-01
|
||||||
Stopped at: Plan 06-01 created
|
Stopped at: Phase 7 complete
|
||||||
Next action: Review and approve plan, then run /paul:apply .paul/phases/06-wyjazd-zagraniczny/06-01-PLAN.md
|
Next action: Execute Phase 6 (/paul:apply .paul/phases/06-wyjazd-zagraniczny/06-01-PLAN.md) or plan new work
|
||||||
Resume file: .paul/phases/06-wyjazd-zagraniczny/06-01-PLAN.md
|
Resume file: .paul/ROADMAP.md
|
||||||
Resume context:
|
|
||||||
- v0.1 complete (5 phases)
|
|
||||||
- v0.2 Phase 6: wyjazd zagraniczny — wydzielenie WYJAZD ZA GRANICĘ z extras do dedykowanej sekcji z wyszukiwarką krajów
|
|
||||||
- Dane krajów z istniejącego pricelist API (additionalItems z nazwą WYJAZD ZA GRANIC...)
|
|
||||||
- Design z Figmy: checkbox toggle + wyszukiwarka z flagami/cenami + karty krajów
|
|
||||||
|
|||||||
263
.paul/phases/07-hero-search-form/07-01-PLAN.md
Normal file
263
.paul/phases/07-hero-search-form/07-01-PLAN.md
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
---
|
||||||
|
phase: 07-hero-search-form
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- wp-content/plugins/carei-reservation/includes/class-search-widget.php
|
||||||
|
- wp-content/plugins/carei-reservation/assets/css/carei-reservation.css
|
||||||
|
- wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
|
||||||
|
- wp-content/plugins/carei-reservation/carei-reservation.php
|
||||||
|
autonomous: false
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Nowy widget Elementor "Carei Search Form" — kompaktowy mini formularz rezerwacji do osadzenia w sekcji hero. Po wypełnieniu i kliknięciu przycisku otwiera istniejący popup formularza rezerwacji z automatycznie uzupełnionymi danymi (segment, daty, lokalizacja).
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
Użytkownik widzi formularz już na hero — nie musi szukać przycisku rezerwacji. Skraca ścieżkę konwersji i daje natychmiastowe CTA.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
- Nowy plik: `class-search-widget.php` (widget Elementor)
|
||||||
|
- Rozszerzony CSS: style mini formularza
|
||||||
|
- Rozszerzony JS: logika prefill + otwarcie modala z danymi z mini formularza
|
||||||
|
- Rejestracja widgetu w `carei-reservation.php`
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@wp-content/plugins/carei-reservation/carei-reservation.php
|
||||||
|
@wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
|
||||||
|
@wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
|
||||||
|
@wp-content/plugins/carei-reservation/assets/css/carei-reservation.css
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Widget renderuje mini formularz
|
||||||
|
```gherkin
|
||||||
|
Given strona z osadzonym widgetem "Carei Search Form" w Elementorze
|
||||||
|
When strona się ładuje
|
||||||
|
Then widoczny jest kompaktowy formularz z polami: segment (select), daty od/do, miejsce odbioru (select), checkbox "Zwrot w tej samej lokalizacji", przycisk "Złóż zapytanie o rezerwację"
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Selecty ładują dane z API
|
||||||
|
```gherkin
|
||||||
|
Given mini formularz jest widoczny na stronie
|
||||||
|
When strona się załadowała
|
||||||
|
Then select segmentu zawiera segmenty z API (segments-branches-map)
|
||||||
|
And select lokalizacji filtruje się po wybranym segmencie
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Przycisk otwiera modal z pre-wypełnionymi danymi
|
||||||
|
```gherkin
|
||||||
|
Given użytkownik wypełnił mini formularz (segment, daty, lokalizacja)
|
||||||
|
When kliknie "Złóż zapytanie o rezerwację"
|
||||||
|
Then otwiera się istniejący modal rezerwacji
|
||||||
|
And pola segment, data od, data do, miejsce odbioru są automatycznie wypełnione wartościami z mini formularza
|
||||||
|
And checkbox zwrotu jest zsynchronizowany
|
||||||
|
And extras/pricelist ładują się automatycznie (jak po ręcznym wypełnieniu)
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Design zgodny z Figmą
|
||||||
|
```gherkin
|
||||||
|
Given mini formularz jest renderowany
|
||||||
|
When wyświetla się na desktop
|
||||||
|
Then tło #EDEDF3, zaokrąglone rogi 14px, border #2F2482/10%
|
||||||
|
And pola mają białe tło, zaokrąglone rogi
|
||||||
|
And przycisk jest czerwony (#FF0000) z białym tekstem, pełna szerokość
|
||||||
|
And font Albert Sans, tytuł bold fioletowy (#2F2482)
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Widget Elementor + HTML mini formularza</name>
|
||||||
|
<files>wp-content/plugins/carei-reservation/includes/class-search-widget.php, wp-content/plugins/carei-reservation/carei-reservation.php</files>
|
||||||
|
<action>
|
||||||
|
1. Utworzyć `class-search-widget.php` z klasą `Carei_Search_Widget extends \Elementor\Widget_Base`:
|
||||||
|
- name: `carei-search-form`
|
||||||
|
- title: `Carei Search Form`
|
||||||
|
- icon: `eicon-search`
|
||||||
|
- categories: `['general']`
|
||||||
|
- style/script depends: te same co główny widget (carei-reservation-css, carei-reservation-js)
|
||||||
|
- Brak kontrolek Elementor (formularz jest statyczny)
|
||||||
|
|
||||||
|
2. Metoda `render()` generuje HTML:
|
||||||
|
- Container `div.carei-search-form` z tłem
|
||||||
|
- Tytuł: `<h2 class="carei-search-form__title">Wypełnij formularz rezerwacji<span>.</span></h2>`
|
||||||
|
- Select segmentu: `<select id="carei-search-segment">` z placeholder "Wybierz segment" i ikoną strzałki
|
||||||
|
- Wiersz z dwoma polami daty: `<input type="datetime-local" id="carei-search-date-from">` i `id="carei-search-date-to"` z ikonkami kalendarza i labelami "Od kiedy?" / "Do kiedy?"
|
||||||
|
- Select lokalizacji: `<select id="carei-search-pickup">` z ikoną pinu i placeholder "Miejsce odbioru"
|
||||||
|
- Checkbox: `<input type="checkbox" id="carei-search-same-return" checked>` z label "Zwrot w tej samej lokalizacji"
|
||||||
|
- Przycisk: `<button type="button" class="carei-search-form__submit" id="carei-search-submit">` z tekstem "Złóż zapytanie o rezerwację"
|
||||||
|
|
||||||
|
3. Zarejestrować widget w `carei-reservation.php`:
|
||||||
|
- Dodać `require_once` w callbacku `elementor/widgets/register`
|
||||||
|
- `$widgets_manager->register( new Carei_Search_Widget() );`
|
||||||
|
|
||||||
|
Avoid: Duplikowanie modala — mini formularz NIE zawiera modala. Modal jest renderowany przez istniejący widget Carei Reservation, który musi być obecny na tej samej stronie.
|
||||||
|
</action>
|
||||||
|
<verify>Widget "Carei Search Form" widoczny w panelu Elementor, po osadzeniu renderuje HTML mini formularza</verify>
|
||||||
|
<done>AC-1 satisfied: Mini formularz renderuje się z wszystkimi polami</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: JavaScript — ładowanie danych + prefill + otwarcie modala</name>
|
||||||
|
<files>wp-content/plugins/carei-reservation/assets/js/carei-reservation.js</files>
|
||||||
|
<action>
|
||||||
|
1. W sekcji inicjalizacji (DOMContentLoaded) dodać wykrywanie mini formularza:
|
||||||
|
```
|
||||||
|
var searchForm = document.querySelector('.carei-search-form');
|
||||||
|
```
|
||||||
|
Jeśli istnieje, zainicjalizować `initSearchForm()`.
|
||||||
|
|
||||||
|
2. Funkcja `initSearchForm()`:
|
||||||
|
- Pobrać referencje do elementów: `carei-search-segment`, `carei-search-date-from`, `carei-search-date-to`, `carei-search-pickup`, `carei-search-same-return`
|
||||||
|
- Załadować dane API (segments-branches-map) — REUŻYĆ istniejącą logikę `loadInitialData`, ale populować selecty mini formularza
|
||||||
|
- Podpiąć event na zmianę segmentu → filtrowanie lokalizacji (jak w głównym formularzu)
|
||||||
|
- Podpiąć event na `#carei-search-submit` click:
|
||||||
|
a) Zebrać wartości z mini formularza
|
||||||
|
b) Znaleźć overlay modala: `document.querySelector('[data-carei-modal]')`
|
||||||
|
c) Wywołać `openModal()` (istniejąca funkcja)
|
||||||
|
d) Po otwarciu modala (setTimeout ~100ms po openModal) ustawić wartości w głównym formularzu:
|
||||||
|
- `segmentSelect.value = searchSegment` + dispatch 'change' event
|
||||||
|
- `dateFrom.value = searchDateFrom` + dispatch 'change'
|
||||||
|
- `dateTo.value = searchDateTo` + dispatch 'change'
|
||||||
|
- `pickupSelect` — poczekać aż lokalizacje się załadują po change segmentu, potem ustawić wartość
|
||||||
|
- `sameReturnCheck.checked = searchSameReturn`
|
||||||
|
e) Triggerować `loadExtras()` jeśli wszystkie wymagane pola wypełnione
|
||||||
|
|
||||||
|
3. Problem z timingiem: po ustawieniu segmentu, lokalizacje ładują się asynchronicznie. Rozwiązanie:
|
||||||
|
- Dodać callback/promise lub event `carei:branches-loaded` który dispatch-uje się po załadowaniu lokalizacji
|
||||||
|
- W prefill poczekać na ten event, potem ustawić pickupSelect.value
|
||||||
|
|
||||||
|
4. Osobna inicjalizacja dat w mini formularzu — labelki "Od kiedy?"/"Do kiedy?" zachowują się jak placeholdery (widoczne gdy puste).
|
||||||
|
|
||||||
|
Avoid:
|
||||||
|
- Nie duplikować fetchowania API — jeśli dane już załadowane (dataLoaded=true), reużyć cache
|
||||||
|
- Nie łamać istniejącej logiki modala — openModal() musi działać normalnie gdy wywoływana z przycisku `data-carei-open-modal`
|
||||||
|
</action>
|
||||||
|
<verify>Po wypełnieniu mini formularza i kliknięciu przycisku: modal się otwiera, pola segment/daty/lokalizacja są wypełnione, extras się ładują</verify>
|
||||||
|
<done>AC-2, AC-3 satisfied: Selecty ładują dane, przycisk otwiera modal z pre-fill</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: CSS — stylowanie mini formularza zgodne z Figmą</name>
|
||||||
|
<files>wp-content/plugins/carei-reservation/assets/css/carei-reservation.css</files>
|
||||||
|
<action>
|
||||||
|
Dodać style dla `.carei-search-form` na końcu pliku CSS:
|
||||||
|
|
||||||
|
1. Container `.carei-search-form`:
|
||||||
|
- background: #EDEDF3
|
||||||
|
- border-radius: 14px
|
||||||
|
- border: 1px solid rgba(47, 36, 130, 0.1)
|
||||||
|
- padding: ~24px 28px
|
||||||
|
- max-width: 422px (z Figmy: width 422px)
|
||||||
|
- box-shadow: subtletny drop shadow
|
||||||
|
|
||||||
|
2. Tytuł `.carei-search-form__title`:
|
||||||
|
- font-family: 'Albert Sans', sans-serif
|
||||||
|
- font-weight: 700
|
||||||
|
- color: #2F2482
|
||||||
|
- text-align: center
|
||||||
|
- span (kropka): color: #FF0000
|
||||||
|
|
||||||
|
3. Pola formularza `.carei-search-form__field`:
|
||||||
|
- background: #fff
|
||||||
|
- border-radius: 8px
|
||||||
|
- border: 1px solid #E0E0E0
|
||||||
|
- padding: 12px 16px
|
||||||
|
- font-size: 14px
|
||||||
|
- Selecty: appearance: none + custom arrow icon
|
||||||
|
- Daty: dwa pola obok siebie (flex row, gap)
|
||||||
|
|
||||||
|
4. Checkbox `.carei-search-form__checkbox`:
|
||||||
|
- Styl jak w głównym formularzu (custom checkbox box z SVG checkmark)
|
||||||
|
- Kolor checked: #2F2482
|
||||||
|
|
||||||
|
5. Przycisk `.carei-search-form__submit`:
|
||||||
|
- background: #FF0000
|
||||||
|
- color: #fff
|
||||||
|
- border-radius: 8px
|
||||||
|
- width: 100%
|
||||||
|
- padding: 14px
|
||||||
|
- font-weight: 600
|
||||||
|
- ikona strzałki przed tekstem
|
||||||
|
- hover: lekkie przyciemnienie
|
||||||
|
|
||||||
|
6. Responsive:
|
||||||
|
- Na mobile (< 768px): max-width: 100%, padding zmniejszony
|
||||||
|
- Pola dat: mogą być w kolumnie na bardzo wąskich ekranach (< 400px)
|
||||||
|
|
||||||
|
Avoid: Nie modyfikować istniejących styli modala/formularza — dodać TYLKO nowe reguły z prefiksem `.carei-search-form`
|
||||||
|
</action>
|
||||||
|
<verify>Mini formularz wygląda zgodnie z designem z Figmy: tło szare, pola białe, przycisk czerwony, tytuł fioletowy</verify>
|
||||||
|
<done>AC-4 satisfied: Design zgodny z Figmą</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="checkpoint:human-verify" gate="blocking">
|
||||||
|
<what-built>Mini formularz rezerwacji w hero z pre-fill do modala</what-built>
|
||||||
|
<how-to-verify>
|
||||||
|
1. Otwórz stronę w Elementorze, osadź widget "Carei Search Form" w sekcji hero
|
||||||
|
2. Na tej samej stronie musi być osadzony widget "Carei Reservation" (renderuje modal)
|
||||||
|
3. Podgląd strony:
|
||||||
|
- Mini formularz widoczny z polami segment, daty, lokalizacja, checkbox
|
||||||
|
- Wybierz segment → lokalizacje się filtrują
|
||||||
|
- Wypełnij daty i lokalizację
|
||||||
|
- Kliknij "Złóż zapytanie o rezerwację"
|
||||||
|
- Modal się otwiera z wypełnionymi danymi
|
||||||
|
- Extras ładują się automatycznie
|
||||||
|
4. Sprawdź design: tło szare, pola białe, przycisk czerwony
|
||||||
|
5. Sprawdź mobile: formularz responsywny
|
||||||
|
</how-to-verify>
|
||||||
|
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- includes/class-elementor-widget.php — HTML modala pozostaje bez zmian
|
||||||
|
- includes/class-softra-api.php — logika API bez zmian
|
||||||
|
- includes/class-rest-proxy.php — endpointy REST bez zmian
|
||||||
|
- includes/class-admin-panel.php — panel admina bez zmian
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- Mini formularz NIE ma własnego modala — korzysta z modala renderowanego przez istniejący widget
|
||||||
|
- Brak walidacji w mini formularzu — walidacja jest w głównym formularzu
|
||||||
|
- Brak pól danych osobowych w mini formularzu — tylko dane wynajmu
|
||||||
|
- Nie dodawać nowych zależności npm/composer
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] Widget "Carei Search Form" widoczny w panelu Elementor
|
||||||
|
- [ ] Mini formularz renderuje się na stronie z prawidłowym designem
|
||||||
|
- [ ] Selecty segmentu i lokalizacji ładują dane z API
|
||||||
|
- [ ] Kliknięcie przycisku otwiera modal z pre-wypełnionymi danymi
|
||||||
|
- [ ] Istniejący przycisk `data-carei-open-modal` nadal działa normalnie
|
||||||
|
- [ ] Responsive: formularz poprawny na mobile
|
||||||
|
- [ ] Brak błędów JS w konsoli
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Widget osadzony w Elementorze renderuje mini formularz
|
||||||
|
- Dane z mini formularza poprawnie przenoszone do modala
|
||||||
|
- Design zgodny z Figmą
|
||||||
|
- Istniejąca funkcjonalność modala nienaruszona
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/07-hero-search-form/07-01-SUMMARY.md`
|
||||||
|
</output>
|
||||||
122
.paul/phases/07-hero-search-form/07-01-SUMMARY.md
Normal file
122
.paul/phases/07-hero-search-form/07-01-SUMMARY.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
---
|
||||||
|
phase: 07-hero-search-form
|
||||||
|
plan: 01
|
||||||
|
subsystem: ui
|
||||||
|
tags: [elementor, widget, vanilla-js, css]
|
||||||
|
|
||||||
|
requires:
|
||||||
|
- phase: 01-reservation-form-plugin
|
||||||
|
provides: plugin bootstrap, API proxy, Elementor widget infrastructure
|
||||||
|
- phase: 02-form-ui-step1
|
||||||
|
provides: modal form UI, segments/branches data loading
|
||||||
|
provides:
|
||||||
|
- Carei Search Form Elementor widget (mini formularz hero)
|
||||||
|
- Pre-fill modal z danymi z mini formularza
|
||||||
|
affects: []
|
||||||
|
|
||||||
|
tech-stack:
|
||||||
|
added: []
|
||||||
|
patterns: [search-form-prefill-via-setTimeout-polling]
|
||||||
|
|
||||||
|
key-files:
|
||||||
|
created:
|
||||||
|
- wp-content/plugins/carei-reservation/includes/class-search-widget.php
|
||||||
|
modified:
|
||||||
|
- wp-content/plugins/carei-reservation/carei-reservation.php
|
||||||
|
- wp-content/plugins/carei-reservation/assets/js/carei-reservation.js
|
||||||
|
- wp-content/plugins/carei-reservation/assets/css/carei-reservation.css
|
||||||
|
|
||||||
|
key-decisions:
|
||||||
|
- "Calendar picker: opacity:0 stretched over input instead of display:none — preserves native click-to-open"
|
||||||
|
- "Search form loads API data independently — not dependent on modal being present"
|
||||||
|
|
||||||
|
patterns-established:
|
||||||
|
- "Mini form prefill: collect values → openModal() → setTimeout prefill → poll for async-loaded options"
|
||||||
|
|
||||||
|
duration: ~30min
|
||||||
|
started: 2026-04-01T13:00:00Z
|
||||||
|
completed: 2026-04-01T13:45:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Phase 7 Plan 01: Hero Search Form Summary
|
||||||
|
|
||||||
|
**Kompaktowy widget Elementor "Carei Search Form" w hero z pre-fill do istniejącego modala rezerwacji**
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
|--------|-------|
|
||||||
|
| Duration | ~30min |
|
||||||
|
| Tasks | 4 completed (3 auto + 1 checkpoint) |
|
||||||
|
| Files modified | 4 (1 created, 3 modified) |
|
||||||
|
|
||||||
|
## Acceptance Criteria Results
|
||||||
|
|
||||||
|
| Criterion | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| AC-1: Widget renderuje mini formularz | Pass | Segment, daty, lokalizacja, checkbox, przycisk |
|
||||||
|
| AC-2: Selecty ładują dane z API | Pass | Segmenty i lokalizacje filtrowane po segmencie |
|
||||||
|
| AC-3: Przycisk otwiera modal z pre-fill | Pass | Segment, daty, lokalizacja przenoszone do modala |
|
||||||
|
| AC-4: Design zgodny z Figmą | Pass | Tło #EDEDF3, pola białe, przycisk czerwony, tytuł fioletowy |
|
||||||
|
|
||||||
|
## Accomplishments
|
||||||
|
|
||||||
|
- Nowy widget Elementor "Carei Search Form" do osadzenia w hero
|
||||||
|
- Pola mini formularza automatycznie przenoszone do modala rezerwacji po kliknięciu przycisku
|
||||||
|
- Niezależne ładowanie danych API (nie wymaga wcześniejszego otwarcia modala)
|
||||||
|
|
||||||
|
## Files Created/Modified
|
||||||
|
|
||||||
|
| File | Change | Purpose |
|
||||||
|
|------|--------|---------|
|
||||||
|
| `includes/class-search-widget.php` | Created | Widget Elementor z HTML mini formularza |
|
||||||
|
| `carei-reservation.php` | Modified | Rejestracja nowego widgetu |
|
||||||
|
| `assets/js/carei-reservation.js` | Modified | initSearchForm() — ładowanie danych, prefill, otwarcie modala |
|
||||||
|
| `assets/css/carei-reservation.css` | Modified | Style .carei-search-form (tło, pola, przycisk, responsive) |
|
||||||
|
|
||||||
|
## Decisions Made
|
||||||
|
|
||||||
|
| Decision | Rationale | Impact |
|
||||||
|
|----------|-----------|--------|
|
||||||
|
| Osobne ładowanie API w search form | Niezależność od modala — dane dostępne od razu po załadowaniu strony | Dwa requesty API przy page load (zamiast lazy) |
|
||||||
|
| Polling na pickup options po prefill | Lokalizacje ładują się async po zmianie segmentu — nie ma eventa | setInterval z max 30 prób (3s timeout) |
|
||||||
|
|
||||||
|
## Deviations from Plan
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
|
||||||
|
| Type | Count | Impact |
|
||||||
|
|------|-------|--------|
|
||||||
|
| Auto-fixed | 1 | Niezbędny fix CSS |
|
||||||
|
|
||||||
|
**Total impact:** Fix CSS kalendarza — konieczny dla UX
|
||||||
|
|
||||||
|
### Auto-fixed Issues
|
||||||
|
|
||||||
|
**1. CSS calendar picker indicator**
|
||||||
|
- **Found during:** Checkpoint (human-verify)
|
||||||
|
- **Issue:** `display:none` na ikonkach kalendarza blokowało otwarcie date picker
|
||||||
|
- **Fix:** Zmiana na `opacity:0` + stretch na cały input (wzorzec z głównego formularza)
|
||||||
|
- **Files:** carei-reservation.css
|
||||||
|
- **Verification:** Kliknięcie w pole daty otwiera picker
|
||||||
|
|
||||||
|
**2. CSS datetime placeholder selectable**
|
||||||
|
- **Found during:** Checkpoint (human-verify)
|
||||||
|
- **Issue:** Placeholder "--" w pustych polach dat był zaznaczalny
|
||||||
|
- **Fix:** Dodanie `user-select:none` + `color:transparent` na `::-webkit-datetime-edit`
|
||||||
|
- **Verification:** Placeholder niewidoczny i niezaznaczalny
|
||||||
|
|
||||||
|
## Next Phase Readiness
|
||||||
|
|
||||||
|
**Ready:**
|
||||||
|
- Mini formularz działa i jest osadzony w hero
|
||||||
|
- Modal rezerwacji przyjmuje pre-fill z zewnętrznego źródła
|
||||||
|
|
||||||
|
**Concerns:**
|
||||||
|
- Dwa niezależne requesty API (search form + modal) — mogą powodować podwójne obciążenie przy jednoczesnym ładowaniu
|
||||||
|
|
||||||
|
**Blockers:** None
|
||||||
|
|
||||||
|
---
|
||||||
|
*Phase: 07-hero-search-form, Plan: 01*
|
||||||
|
*Completed: 2026-04-01*
|
||||||
@@ -23,13 +23,14 @@
|
|||||||
/* ═══════════════════════════════════════════
|
/* ═══════════════════════════════════════════
|
||||||
Trigger Button
|
Trigger Button
|
||||||
═══════════════════════════════════════════ */
|
═══════════════════════════════════════════ */
|
||||||
.carei-reservation-trigger {
|
.carei-reservation-trigger,
|
||||||
|
button.carei-reservation-trigger {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 14px 28px;
|
padding: 14px 28px;
|
||||||
background-color: var(--carei-red);
|
background-color: var(--carei-blue);
|
||||||
color: var(--carei-white);
|
color: var(--carei-white) !important;
|
||||||
font-family: var(--carei-font);
|
font-family: var(--carei-font);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
@@ -40,9 +41,10 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
.carei-reservation-trigger:hover {
|
.carei-reservation-trigger:hover,
|
||||||
background-color: var(--carei-red-hover);
|
button.carei-reservation-trigger:hover {
|
||||||
color: var(--carei-white);
|
background-color: #1e1660;
|
||||||
|
color: var(--carei-white) !important;
|
||||||
}
|
}
|
||||||
.carei-reservation-trigger svg {
|
.carei-reservation-trigger svg {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
@@ -76,13 +78,20 @@
|
|||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-height: 90vh;
|
max-height: 90vh;
|
||||||
overflow-y: auto;
|
overflow: hidden;
|
||||||
overflow-x: hidden;
|
|
||||||
padding: 40px 48px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
font-family: var(--carei-font);
|
font-family: var(--carei-font);
|
||||||
transform: scale(0.95) translateY(10px);
|
transform: scale(0.95) translateY(10px);
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.carei-modal__scroll {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
padding: 40px 48px;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
.carei-modal-overlay.is-open .carei-modal {
|
.carei-modal-overlay.is-open .carei-modal {
|
||||||
transform: scale(1) translateY(0);
|
transform: scale(1) translateY(0);
|
||||||
@@ -115,13 +124,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollbar styling */
|
/* Scrollbar styling */
|
||||||
.carei-modal::-webkit-scrollbar {
|
.carei-modal__scroll {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--carei-border) transparent;
|
||||||
|
}
|
||||||
|
.carei-modal__scroll::-webkit-scrollbar {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
}
|
}
|
||||||
.carei-modal::-webkit-scrollbar-track {
|
.carei-modal__scroll::-webkit-scrollbar-track {
|
||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
.carei-modal::-webkit-scrollbar-thumb {
|
.carei-modal__scroll::-webkit-scrollbar-thumb {
|
||||||
background: var(--carei-border);
|
background: var(--carei-border);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
@@ -575,11 +588,14 @@
|
|||||||
.carei-form__extra-price {
|
.carei-form__extra-price {
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--carei-gray);
|
color: #505050;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.carei-form__extra-price strong {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
.carei-form__checkbox-label--abroad {
|
.carei-form__checkbox-label--abroad {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -763,7 +779,7 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
padding: 16px 32px;
|
padding: 16px 48px;
|
||||||
background-color: var(--carei-red);
|
background-color: var(--carei-red);
|
||||||
color: var(--carei-white);
|
color: var(--carei-white);
|
||||||
font-family: var(--carei-font);
|
font-family: var(--carei-font);
|
||||||
@@ -894,10 +910,11 @@
|
|||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: var(--carei-radius-lg) var(--carei-radius-lg) 0 0;
|
|
||||||
padding: 32px 24px;
|
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
.carei-modal__scroll {
|
||||||
|
padding: 32px 24px;
|
||||||
|
}
|
||||||
.carei-form__row {
|
.carei-form__row {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -930,7 +947,7 @@
|
|||||||
.carei-form__row--top {
|
.carei-form__row--top {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
.carei-modal {
|
.carei-modal__scroll {
|
||||||
padding: 24px 16px;
|
padding: 24px 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1156,3 +1173,233 @@
|
|||||||
.carei-success__close:hover {
|
.carei-success__close:hover {
|
||||||
background: var(--carei-red-hover);
|
background: var(--carei-red-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════
|
||||||
|
Search Form (Hero Mini Form)
|
||||||
|
═══════════════════════════════════════════ */
|
||||||
|
.carei-search-form {
|
||||||
|
background: var(--carei-bg);
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid rgba(47, 36, 130, 0.1);
|
||||||
|
padding: 28px 24px 24px;
|
||||||
|
max-width: 422px;
|
||||||
|
font-family: var(--carei-font);
|
||||||
|
box-shadow: 0 4px 24px rgba(47, 36, 130, 0.08);
|
||||||
|
}
|
||||||
|
.carei-search-form__title {
|
||||||
|
font-family: var(--carei-font);
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--carei-blue);
|
||||||
|
text-align: center;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
.carei-search-form__title span {
|
||||||
|
color: var(--carei-red);
|
||||||
|
}
|
||||||
|
.carei-search-form__fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
.carei-search-form__row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.carei-search-form__row > .carei-search-form__field {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.carei-search-form__field--full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select wrap */
|
||||||
|
.carei-search-form__select-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--carei-white);
|
||||||
|
border-radius: var(--carei-radius);
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
height: var(--carei-input-h);
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.carei-search-form__select-wrap:focus-within {
|
||||||
|
border-color: var(--carei-blue);
|
||||||
|
}
|
||||||
|
.carei-search-form__select-wrap select {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-family: var(--carei-font);
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--carei-gray);
|
||||||
|
padding: 0 32px 0 16px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.carei-search-form__select-wrap--icon select {
|
||||||
|
padding-left: 38px;
|
||||||
|
}
|
||||||
|
.carei-search-form__select-arrow {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--carei-blue);
|
||||||
|
}
|
||||||
|
.carei-search-form__pin-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--carei-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Date wrap */
|
||||||
|
.carei-search-form__date-wrap {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--carei-white);
|
||||||
|
border-radius: var(--carei-radius);
|
||||||
|
border: 1px solid #E0E0E0;
|
||||||
|
height: var(--carei-input-h);
|
||||||
|
transition: border-color 0.2s;
|
||||||
|
}
|
||||||
|
.carei-search-form__date-wrap:focus-within {
|
||||||
|
border-color: var(--carei-blue);
|
||||||
|
}
|
||||||
|
.carei-search-form__date-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 14px;
|
||||||
|
pointer-events: none;
|
||||||
|
color: var(--carei-gray);
|
||||||
|
}
|
||||||
|
.carei-search-form__date-label {
|
||||||
|
position: absolute;
|
||||||
|
left: 38px;
|
||||||
|
font-family: var(--carei-font);
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--carei-placeholder);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
.carei-search-form__date-wrap.has-value .carei-search-form__date-label {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.carei-search-form__input--date {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
font-family: var(--carei-font);
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--carei-gray);
|
||||||
|
padding: 0 12px 0 38px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.carei-search-form__date-wrap:not(.has-value) .carei-search-form__input--date {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
.carei-search-form__date-wrap:not(.has-value) .carei-search-form__input--date::-webkit-datetime-edit {
|
||||||
|
color: transparent;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.carei-search-form__input--date::-webkit-calendar-picker-indicator {
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Checkbox */
|
||||||
|
.carei-search-form__checkbox-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: var(--carei-font);
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--carei-gray);
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.carei-search-form__checkbox-label input[type="checkbox"] {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
.carei-search-form__checkbox-box {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 2px solid #D0D0D0;
|
||||||
|
background: var(--carei-white);
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: background-color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
.carei-search-form__checkbox-box svg {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.carei-search-form__checkbox-label input[type="checkbox"]:checked + .carei-search-form__checkbox-box {
|
||||||
|
background: var(--carei-blue);
|
||||||
|
border-color: var(--carei-blue);
|
||||||
|
}
|
||||||
|
.carei-search-form__checkbox-label input[type="checkbox"]:checked + .carei-search-form__checkbox-box svg {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submit button */
|
||||||
|
.carei-search-form__submit {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 20px;
|
||||||
|
background: var(--carei-red);
|
||||||
|
color: var(--carei-white);
|
||||||
|
font-family: var(--carei-font);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--carei-radius);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.carei-search-form__submit:hover {
|
||||||
|
background: var(--carei-red-hover);
|
||||||
|
}
|
||||||
|
.carei-search-form__submit svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.carei-search-form {
|
||||||
|
max-width: 100%;
|
||||||
|
padding: 20px 16px 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 400px) {
|
||||||
|
.carei-search-form__row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -388,11 +388,11 @@
|
|||||||
card.className = 'carei-form__extra-card';
|
card.className = 'carei-form__extra-card';
|
||||||
card.innerHTML =
|
card.innerHTML =
|
||||||
'<label class="carei-form__checkbox-label carei-form__checkbox-label--card">' +
|
'<label class="carei-form__checkbox-label carei-form__checkbox-label--card">' +
|
||||||
'<input type="checkbox" name="extras[]" value="' + escAttr(item.id || item.code) + '" data-price="' + price + '" data-name="' + escAttr(item.name) + '" data-unit="' + escAttr(item.unit || 'szt.') + '">' +
|
'<input type="checkbox" name="extras[]" value="' + escAttr(item.id || item.code) + '" data-price="' + price + '" data-name="' + escAttr(toSentenceCase(item.name)) + '" data-unit="' + escAttr(item.unit || 'szt.') + '">' +
|
||||||
'<span class="carei-form__checkbox-box"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7l3.5 3.5L12 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>' +
|
'<span class="carei-form__checkbox-box"><svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7l3.5 3.5L12 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span>' +
|
||||||
'<span class="carei-form__extra-content"><strong>' + escHtml(item.name) + '</strong>' +
|
'<span class="carei-form__extra-content"><strong>' + escHtml(toSentenceCase(item.name)) + '</strong>' +
|
||||||
(item.description ? '<span class="carei-form__extra-desc">' + escHtml(item.description) + '</span>' : '') +
|
(item.description ? '<span class="carei-form__extra-desc">' + escHtml(toSentenceCase(item.description)) + '</span>' : '') +
|
||||||
'<span class="carei-form__extra-price">' + escHtml(priceLabel) + '</span></span></label>';
|
'<span class="carei-form__extra-price"><strong>' + escHtml(priceLabel) + '</strong></span></span></label>';
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -881,7 +881,11 @@
|
|||||||
if (selectedExtras.length > 0) {
|
if (selectedExtras.length > 0) {
|
||||||
html += '<div style="margin-top:8px"><strong>Wybrane opcje:</strong></div><ul style="margin:4px 0 0 16px;padding:0;list-style:disc;">';
|
html += '<div style="margin-top:8px"><strong>Wybrane opcje:</strong></div><ul style="margin:4px 0 0 16px;padding:0;list-style:disc;">';
|
||||||
selectedExtras.forEach(function (ex) {
|
selectedExtras.forEach(function (ex) {
|
||||||
html += '<li>' + escHtml(ex.name) + ' — ' + fmtPrice(ex.priceAfterDiscount) + ' zł</li>';
|
var totalPrice = ex.priceAfterDiscount * (ex.amount || 1);
|
||||||
|
var priceInfo = ex.unit === 'doba' && ex.amount > 1
|
||||||
|
? fmtPrice(ex.priceAfterDiscount) + ' zł/doba × ' + ex.amount + ' = ' + fmtPrice(totalPrice) + ' zł'
|
||||||
|
: fmtPrice(totalPrice) + ' zł';
|
||||||
|
html += '<li>' + escHtml(toSentenceCase(ex.name)) + ' — ' + priceInfo + '</li>';
|
||||||
});
|
});
|
||||||
html += '</ul>';
|
html += '</ul>';
|
||||||
}
|
}
|
||||||
@@ -899,7 +903,7 @@
|
|||||||
summary.pricelist.forEach(function (item) {
|
summary.pricelist.forEach(function (item) {
|
||||||
var rowClass = item.addedBySystem ? ' class="carei-summary__auto-item"' : '';
|
var rowClass = item.addedBySystem ? ' class="carei-summary__auto-item"' : '';
|
||||||
html += '<tr' + rowClass + '>' +
|
html += '<tr' + rowClass + '>' +
|
||||||
'<td>' + escHtml(item.name) + (item.addedBySystem ? ' <small>(auto)</small>' : '') + '</td>' +
|
'<td>' + escHtml(toSentenceCase(item.name)) + (item.addedBySystem ? ' <small>(auto)</small>' : '') + '</td>' +
|
||||||
'<td>' + (item.amount || 1) + ' ' + escHtml(item.unit || '') + '</td>' +
|
'<td>' + (item.amount || 1) + ' ' + escHtml(item.unit || '') + '</td>' +
|
||||||
'<td>' + fmtPrice(item.netValue) + '</td>' +
|
'<td>' + fmtPrice(item.netValue) + '</td>' +
|
||||||
'<td>' + fmtPrice(item.grossValue) + '</td></tr>';
|
'<td>' + fmtPrice(item.grossValue) + '</td></tr>';
|
||||||
@@ -1078,11 +1082,160 @@
|
|||||||
|
|
||||||
function escHtml(str) { var d = document.createElement('div'); d.textContent = str || ''; return d.innerHTML; }
|
function escHtml(str) { var d = document.createElement('div'); d.textContent = str || ''; return d.innerHTML; }
|
||||||
function escAttr(str) { return (str || '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>'); }
|
function escAttr(str) { return (str || '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>'); }
|
||||||
|
function toSentenceCase(str) { if (!str) return ''; var s = str.toLowerCase(); return s.charAt(0).toUpperCase() + s.slice(1); }
|
||||||
|
|
||||||
|
// ─── Search Form (Hero Mini Form) ──────────────────────────────
|
||||||
|
|
||||||
|
function initSearchForm() {
|
||||||
|
var searchForm = document.querySelector('.carei-search-form');
|
||||||
|
if (!searchForm) return;
|
||||||
|
|
||||||
|
var searchSegment = document.getElementById('carei-search-segment');
|
||||||
|
var searchDateFrom = document.getElementById('carei-search-date-from');
|
||||||
|
var searchDateTo = document.getElementById('carei-search-date-to');
|
||||||
|
var searchPickup = document.getElementById('carei-search-pickup');
|
||||||
|
var searchSameReturn = document.getElementById('carei-search-same-return');
|
||||||
|
var searchSubmit = document.getElementById('carei-search-submit');
|
||||||
|
|
||||||
|
var searchMapData = null;
|
||||||
|
|
||||||
|
// Ładowanie danych do mini formularza
|
||||||
|
function loadSearchData() {
|
||||||
|
Promise.all([
|
||||||
|
apiGet('car-classes-all'),
|
||||||
|
apiGet('segments-branches-map')
|
||||||
|
]).then(function (results) {
|
||||||
|
var classes = results[0];
|
||||||
|
searchMapData = results[1];
|
||||||
|
|
||||||
|
if (Array.isArray(classes) && classes.length > 0) {
|
||||||
|
var segments = classes.map(function (c) {
|
||||||
|
var val = typeof c === 'string' ? c : (c.name || c);
|
||||||
|
var label = typeof c === 'string' ? ('Segment ' + c) : (c.description || c.name || c);
|
||||||
|
return { value: val, label: label };
|
||||||
|
});
|
||||||
|
populateSelect(searchSegment, segments, 'Wybierz segment');
|
||||||
|
}
|
||||||
|
if (searchPickup) {
|
||||||
|
populateSelect(searchPickup, [], 'Najpierw wybierz segment');
|
||||||
|
searchPickup.disabled = true;
|
||||||
|
}
|
||||||
|
}).catch(function (err) {
|
||||||
|
console.error('Search form: failed to load data:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zmiana segmentu → filtr lokalizacji
|
||||||
|
if (searchSegment) {
|
||||||
|
searchSegment.addEventListener('change', function () {
|
||||||
|
var sel = searchSegment.value;
|
||||||
|
if (!sel || !searchMapData || !searchPickup) return;
|
||||||
|
var segBranches = searchMapData.segmentToBranches[sel] || [];
|
||||||
|
var allBranches = searchMapData.branches || [];
|
||||||
|
var opts = [];
|
||||||
|
allBranches.forEach(function (b) {
|
||||||
|
if (segBranches.indexOf(b.name || '') !== -1) {
|
||||||
|
var label = b.description || b.name;
|
||||||
|
if (b.city) label += ' — ' + b.city;
|
||||||
|
opts.push({ value: b.name, label: label });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (opts.length > 0) {
|
||||||
|
populateSelect(searchPickup, opts, 'Miejsce odbioru');
|
||||||
|
searchPickup.disabled = false;
|
||||||
|
} else {
|
||||||
|
populateSelect(searchPickup, [], 'Brak lokalizacji');
|
||||||
|
searchPickup.disabled = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Date label behavior
|
||||||
|
[searchDateFrom, searchDateTo].forEach(function (input) {
|
||||||
|
if (!input) return;
|
||||||
|
var wrap = input.closest('.carei-search-form__date-wrap');
|
||||||
|
function updateLabel() {
|
||||||
|
if (wrap) wrap.classList.toggle('has-value', !!input.value);
|
||||||
|
}
|
||||||
|
updateLabel();
|
||||||
|
input.addEventListener('change', updateLabel);
|
||||||
|
input.addEventListener('input', updateLabel);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Submit → otwórz modal z pre-fill
|
||||||
|
if (searchSubmit) {
|
||||||
|
searchSubmit.addEventListener('click', function () {
|
||||||
|
if (!overlay) return;
|
||||||
|
|
||||||
|
var valSegment = searchSegment ? searchSegment.value : '';
|
||||||
|
var valDateFrom = searchDateFrom ? searchDateFrom.value : '';
|
||||||
|
var valDateTo = searchDateTo ? searchDateTo.value : '';
|
||||||
|
var valPickup = searchPickup ? searchPickup.value : '';
|
||||||
|
var valSameReturn = searchSameReturn ? searchSameReturn.checked : true;
|
||||||
|
|
||||||
|
// Otwórz modal
|
||||||
|
openModal(searchSubmit);
|
||||||
|
|
||||||
|
// Pre-fill po załadowaniu danych modala
|
||||||
|
function prefillModal() {
|
||||||
|
// Segment
|
||||||
|
if (segmentSelect && valSegment) {
|
||||||
|
segmentSelect.value = valSegment;
|
||||||
|
segmentSelect.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daty
|
||||||
|
if (dateFrom && valDateFrom) {
|
||||||
|
dateFrom.value = valDateFrom;
|
||||||
|
dateFrom.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
if (dateTo && valDateTo) {
|
||||||
|
dateTo.value = valDateTo;
|
||||||
|
dateTo.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checkbox zwrotu
|
||||||
|
if (sameReturnCheck) {
|
||||||
|
sameReturnCheck.checked = valSameReturn;
|
||||||
|
sameReturnCheck.dispatchEvent(new Event('change'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pickup — poczekaj aż lokalizacje się załadują po change segmentu
|
||||||
|
if (valPickup && pickupSelect) {
|
||||||
|
var attempts = 0;
|
||||||
|
var pickupInterval = setInterval(function () {
|
||||||
|
attempts++;
|
||||||
|
// Sprawdź czy opcja jest dostępna
|
||||||
|
var optExists = Array.prototype.slice.call(pickupSelect.options).some(function (o) {
|
||||||
|
return o.value === valPickup;
|
||||||
|
});
|
||||||
|
if (optExists) {
|
||||||
|
clearInterval(pickupInterval);
|
||||||
|
pickupSelect.value = valPickup;
|
||||||
|
pickupSelect.dispatchEvent(new Event('change'));
|
||||||
|
} else if (attempts > 30) {
|
||||||
|
clearInterval(pickupInterval);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Daj czas na loadInitialData w openModal
|
||||||
|
setTimeout(prefillModal, 400);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSearchData();
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Init ─────────────────────────────────────────────────────
|
// ─── Init ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
initRefs();
|
initRefs();
|
||||||
|
|
||||||
|
// Inicjalizuj search form niezależnie od modala
|
||||||
|
initSearchForm();
|
||||||
|
|
||||||
if (!overlay || !form) return;
|
if (!overlay || !form) return;
|
||||||
|
|
||||||
initModal();
|
initModal();
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ add_action( 'plugins_loaded', function () {
|
|||||||
add_action( 'elementor/widgets/register', function ( $widgets_manager ) {
|
add_action( 'elementor/widgets/register', function ( $widgets_manager ) {
|
||||||
require_once CAREI_RESERVATION_PATH . 'includes/class-elementor-widget.php';
|
require_once CAREI_RESERVATION_PATH . 'includes/class-elementor-widget.php';
|
||||||
$widgets_manager->register( new Carei_Reservation_Widget() );
|
$widgets_manager->register( new Carei_Reservation_Widget() );
|
||||||
|
|
||||||
|
require_once CAREI_RESERVATION_PATH . 'includes/class-search-widget.php';
|
||||||
|
$widgets_manager->register( new Carei_Search_Widget() );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Elementor Widget: Carei Search Form — kompaktowy mini formularz do hero.
|
||||||
|
*/
|
||||||
|
class Carei_Search_Widget extends \Elementor\Widget_Base {
|
||||||
|
|
||||||
|
public function get_name() {
|
||||||
|
return 'carei-search-form';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_title() {
|
||||||
|
return 'Carei Search Form';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_icon() {
|
||||||
|
return 'eicon-search';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_categories() {
|
||||||
|
return array( 'general' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_style_depends() {
|
||||||
|
return array( 'carei-reservation-css' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_script_depends() {
|
||||||
|
return array( 'carei-reservation-js' );
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function register_controls() {}
|
||||||
|
|
||||||
|
protected function render() {
|
||||||
|
?>
|
||||||
|
<div class="carei-search-form">
|
||||||
|
<h2 class="carei-search-form__title">Wypełnij formularz rezerwacji<span>.</span></h2>
|
||||||
|
|
||||||
|
<div class="carei-search-form__fields">
|
||||||
|
<!-- Segment -->
|
||||||
|
<div class="carei-search-form__field carei-search-form__field--full">
|
||||||
|
<div class="carei-search-form__select-wrap">
|
||||||
|
<select id="carei-search-segment">
|
||||||
|
<option value="" disabled selected>Wybierz segment</option>
|
||||||
|
</select>
|
||||||
|
<svg class="carei-search-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Daty -->
|
||||||
|
<div class="carei-search-form__row">
|
||||||
|
<div class="carei-search-form__field">
|
||||||
|
<div class="carei-search-form__date-wrap">
|
||||||
|
<svg class="carei-search-form__date-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3" width="11" height="10" rx="0.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 1.5v2M5 1.5v2M2.5 5.5h11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
<label class="carei-search-form__date-label" for="carei-search-date-from">Od kiedy?</label>
|
||||||
|
<input type="datetime-local" id="carei-search-date-from" class="carei-search-form__input carei-search-form__input--date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="carei-search-form__field">
|
||||||
|
<div class="carei-search-form__date-wrap">
|
||||||
|
<svg class="carei-search-form__date-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2.5" y="3" width="11" height="10" rx="0.5" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/><path d="M11 1.5v2M5 1.5v2M2.5 5.5h11" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
<label class="carei-search-form__date-label" for="carei-search-date-to">Do kiedy?</label>
|
||||||
|
<input type="datetime-local" id="carei-search-date-to" class="carei-search-form__input carei-search-form__input--date">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Miejsce odbioru -->
|
||||||
|
<div class="carei-search-form__field carei-search-form__field--full">
|
||||||
|
<div class="carei-search-form__select-wrap carei-search-form__select-wrap--icon">
|
||||||
|
<svg class="carei-search-form__pin-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"><path d="M8 1C5.24 1 3 3.24 3 6c0 3.75 5 9 5 9s5-5.25 5-9c0-2.76-2.24-5-5-5zm0 7a2 2 0 110-4 2 2 0 010 4z" fill="currentColor"/></svg>
|
||||||
|
<select id="carei-search-pickup">
|
||||||
|
<option value="" disabled selected>Miejsce odbioru</option>
|
||||||
|
</select>
|
||||||
|
<svg class="carei-search-form__select-arrow" width="16" height="16" viewBox="0 0 16 16"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Checkbox zwrot -->
|
||||||
|
<div class="carei-search-form__field carei-search-form__field--full">
|
||||||
|
<label class="carei-search-form__checkbox-label">
|
||||||
|
<input type="checkbox" id="carei-search-same-return" checked>
|
||||||
|
<span class="carei-search-form__checkbox-box">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M2 7l3.5 3.5L12 4" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
||||||
|
</span>
|
||||||
|
<span class="carei-search-form__checkbox-text">Zwrot w tej samej lokalizacji</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Przycisk -->
|
||||||
|
<button type="button" class="carei-search-form__submit" id="carei-search-submit">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
|
||||||
|
Złóż zapytanie o rezerwację
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user