update
This commit is contained in:
40
.claude/sessionstate.md
Normal file
40
.claude/sessionstate.md
Normal file
@@ -0,0 +1,40 @@
|
||||
# Session State
|
||||
Ostatnia aktualizacja: 2026-03-25
|
||||
|
||||
## Aktualny cel
|
||||
Budowa formularza rezerwacji samochodu jako plugin Elementor zintegrowany z API Softra Rent.
|
||||
|
||||
## Co zostało zrobione
|
||||
- Phase 1 COMPLETE: Plugin carei-reservation z natywnym cURL proxy do Softra API, 10 REST endpoints, widget Elementor
|
||||
- Phase 2 IN PROGRESS (2/3 tasks):
|
||||
- Task 1: HTML formularza z Figmy + CSS 541 linii (responsive, BEM)
|
||||
- Task 2: JS 580+ linii (API integration, walidacja, dynamiczne dropdowny)
|
||||
- Fix: wp_remote_post → natywny cURL (matching softra-test.php)
|
||||
- Fix: GET /car/class/listAll → segmenty bez wymagania oddziału
|
||||
- Task 3: checkpoint:human-verify PENDING (deploy + test na serwerze)
|
||||
- Dokumentacja Figma w docs/figma-formularz/ (screenshoty, specyfikacja, JSX referencyjne)
|
||||
- Struktura PAUL w .paul/ (PROJECT, ROADMAP, STATE, Phase 1 SUMMARY)
|
||||
|
||||
## Co zostało do zrobienia
|
||||
- Deploy 5 plików na serwer i test wizualny formularza (checkpoint)
|
||||
- Phase 3: Overlay/podsumowanie + submit rezerwacji do API (tworzenie klienta, booking, confirm)
|
||||
- Phase 4: Polish & integration testing
|
||||
|
||||
## Kluczowe pliki
|
||||
| Plik | Rola |
|
||||
|------|------|
|
||||
| wp-content/plugins/carei-reservation/ | Cały plugin |
|
||||
| .paul/HANDOFF-2026-03-25.md | Pełny handoff z kontekstem |
|
||||
| .paul/STATE.md | Stan projektu PAUL |
|
||||
| docs/figma-formularz/README.md | Specyfikacja formularza |
|
||||
| softra-test.php | Referencyjny test API (cURL) |
|
||||
|
||||
## Ważne decyzje / ustalenia
|
||||
- Natywny cURL zamiast wp_remote_post (softra-test.php pattern)
|
||||
- GET /car/class/listAll dla segmentów (nie wymaga oddziału)
|
||||
- Formularz wielokrokowy: Krok 1 (formularz) → Krok 2 (Overlay z podsumowaniem)
|
||||
- Kolory: #2F2482 (blue), #FF0000 (red), #EDEDF3 (bg)
|
||||
- Font: Albert Sans
|
||||
|
||||
## Następny krok
|
||||
Wgrać pliki na serwer carei.pagedev.pl → przetestować formularz → "approved" lub fix → /paul:unify → Phase 3.
|
||||
10
.claude/settings.local.json
Normal file
10
.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"mcp__figma__get_design_context",
|
||||
"mcp__figma__get_metadata",
|
||||
"mcp__plugin_context-mode_context-mode__ctx_execute",
|
||||
"mcp__figma__get_screenshot"
|
||||
]
|
||||
}
|
||||
}
|
||||
112
.paul/HANDOFF-2026-03-25.md
Normal file
112
.paul/HANDOFF-2026-03-25.md
Normal file
@@ -0,0 +1,112 @@
|
||||
# PAUL Handoff
|
||||
|
||||
**Date:** 2026-03-25
|
||||
**Status:** paused — checkpoint human-verify pending (awaiting deploy + test)
|
||||
|
||||
---
|
||||
|
||||
## READ THIS FIRST
|
||||
|
||||
You have no prior context. This document tells you everything.
|
||||
|
||||
**Project:** Carei — Formularz rezerwacji samochodu jako plugin Elementor, zintegrowany z Softra Rent API
|
||||
**Core value:** Klient klika "Złóż zapytanie o rezerwację" → modal z formularzem → dane z API Softra → rezerwacja
|
||||
|
||||
---
|
||||
|
||||
## Current State
|
||||
|
||||
**Milestone:** v0.1 Formularz Rezerwacji MVP
|
||||
**Phase:** 2 of 4 — Form UI Krok 1 (Formularz)
|
||||
**Plan:** 02-01 — APPLY in progress (Tasks 1+2 done, Task 3 checkpoint pending)
|
||||
|
||||
**Loop Position:**
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ◐ ○ [Apply in progress — checkpoint:human-verify waiting]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What Was Done
|
||||
|
||||
**Phase 1 (complete):**
|
||||
- Plugin `carei-reservation` z natywnym cURL proxy do Softra Rent API
|
||||
- 10 WP REST endpoints w namespace `carei/v1`
|
||||
- Widget Elementor z przyciskiem CTA + modal overlay
|
||||
- Fix: widget require_once przeniesiony do hooka elementor/widgets/register
|
||||
- Fix: zamiana wp_remote_post na natywny cURL (matching softra-test.php)
|
||||
|
||||
**Phase 2 (in progress — Tasks 1+2 done):**
|
||||
- HTML formularza z wszystkimi sekcjami z Figmy (widget render)
|
||||
- CSS 541 linii (responsive, custom checkboxy, karty opcji, walidacja)
|
||||
- JS 580+ linii (API integration, walidacja, interakcje)
|
||||
- Nowy endpoint GET /car-classes-all (listAll) — segmenty ładują się od razu bez wymagania oddziału
|
||||
- Opcje dodatkowe ładowane dynamicznie z cennika API
|
||||
|
||||
---
|
||||
|
||||
## What's In Progress
|
||||
|
||||
- **Task 3 checkpoint:human-verify** — użytkownik musi wgrać pliki na serwer i przetestować wizualnie
|
||||
- Pliki do wgrania:
|
||||
- `includes/class-softra-api.php` (natywny cURL + get_all_car_classes)
|
||||
- `includes/class-rest-proxy.php` (nowy endpoint car-classes-all)
|
||||
- `includes/class-elementor-widget.php` (pełny HTML formularza)
|
||||
- `assets/css/carei-reservation.css` (kompletne style)
|
||||
- `assets/js/carei-reservation.js` (logika + API)
|
||||
|
||||
---
|
||||
|
||||
## What's Next
|
||||
|
||||
**Immediate:** Wgrać pliki na serwer → przetestować formularz → "approved" lub opisać problemy
|
||||
|
||||
**After that:**
|
||||
- Jeśli approved → /paul:unify → Phase 3 (Overlay/podsumowanie + submit rezerwacji do API)
|
||||
- Jeśli issues → fix → re-verify
|
||||
|
||||
**Remaining phases:**
|
||||
- Phase 3: Form UI Krok 2 (Overlay z podsumowaniem, tworzenie klienta, booking)
|
||||
- Phase 4: Polish & integration testing
|
||||
|
||||
---
|
||||
|
||||
## Key Decisions Made
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Natywny cURL zamiast wp_remote_post | softra-test.php działa z cURL, WP HTTP API timeout |
|
||||
| GET /car/class/listAll dla segmentów | Segment jest pierwszym polem w Figmie, nie powinien wymagać oddziału |
|
||||
| Osobny plugin carei-reservation | Czystsza separacja od elementor-addon |
|
||||
| sslverify nie ustawiane (domyślne cURL) | Tak jak w softra-test.php który działa na produkcji |
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `.paul/STATE.md` | Live project state |
|
||||
| `.paul/ROADMAP.md` | Phase overview (Phase 1 ✅, Phase 2-4 ⬜) |
|
||||
| `.paul/phases/02-form-ui-step1/02-01-PLAN.md` | Current plan |
|
||||
| `.paul/phases/01-reservation-form-plugin/01-01-SUMMARY.md` | Phase 1 summary |
|
||||
| `wp-content/plugins/carei-reservation/` | Plugin directory (all code) |
|
||||
| `docs/figma-formularz/README.md` | Figma design spec |
|
||||
| `docs/figma-formularz/screenshot-desktop.png` | Desktop reference |
|
||||
| `docs/figma-formularz/screenshot-mobile.png` | Mobile reference |
|
||||
| `softra-test.php` | Working Softra API test (reference for cURL config) |
|
||||
| `.env` | API credentials (url, username, password) |
|
||||
|
||||
---
|
||||
|
||||
## Resume Instructions
|
||||
|
||||
1. Read `.paul/STATE.md` for latest position
|
||||
2. User needs to deploy files to server and test
|
||||
3. After test: "approved" → run `/paul:unify` then `/paul:plan` for Phase 3
|
||||
4. Or run `/paul:resume` or `/paul:progress`
|
||||
|
||||
---
|
||||
|
||||
*Handoff created: 2026-03-25*
|
||||
38
.paul/PROJECT.md
Normal file
38
.paul/PROJECT.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Carei - Formularz Rezerwacji Samochodu
|
||||
|
||||
## Value Proposition
|
||||
Plugin Elementor do rezerwacji samochodu na stronie carei.pagedev.pl, zintegrowany z API Softra Rent. Formularz wielokrokowy: krok 1 (podstawowe dane rezerwacji) → krok 2 (Overlay z pełnym podsumowaniem i płatnością).
|
||||
|
||||
## Core Requirements
|
||||
1. **Elementor Widget** — plugin rejestrujący widget w Elementorze, wywoływany przyciskiem "Złóż zapytanie o rezerwację"
|
||||
2. **Integracja z Softra Rent API** — pobieranie oddziałów, klas pojazdów, cen, dodatków; tworzenie klientów i rezerwacji
|
||||
3. **Multi-step form** — krok 1: formularz z Figmy (segment, daty, lokalizacja, opcje, dane osobowe), krok 2: Overlay z podsumowaniem
|
||||
4. **Responsive** — desktop (modal overlay) i mobile (full-screen bottom sheet)
|
||||
5. **Design zgodny z Figmą** — kolory Carei (#2F2482, #FF0000), font Albert Sans
|
||||
|
||||
## Tech Stack
|
||||
- WordPress + Hello Elementor theme
|
||||
- Elementor + Elementor Pro
|
||||
- Istniejący plugin: `wp-content/plugins/elementor-addon/` (custom widgets)
|
||||
- PHP backend (REST API proxy do Softra)
|
||||
- Vanilla JS + CSS frontend (bez frameworków JS)
|
||||
- Softra Rent API: `https://softra.com.pl:8444/rent2www-ci-tst`
|
||||
|
||||
## Constraints
|
||||
- Dane API w `.env` (url, username, password)
|
||||
- Token JWT ważny 1h — cacheowanie po stronie serwera
|
||||
- Formularz NIE jest natywnym formularzem Elementor Pro — to custom widget
|
||||
- Brak dodatkowych zależności npm/composer — czysty PHP + JS
|
||||
|
||||
## API Endpoints (kluczowe)
|
||||
| Endpoint | Metoda | Użycie |
|
||||
|----------|--------|--------|
|
||||
| `/account/auth` | POST | Autoryzacja JWT |
|
||||
| `/branch/list` | GET | Lista oddziałów (miejsce odbioru) |
|
||||
| `/car/class/list` | POST | Klasy pojazdów wg dat i oddziału |
|
||||
| `/pricelist/list` | POST | Cennik z dodatkami |
|
||||
| `/customer/add` | POST | Tworzenie klienta |
|
||||
| `/rent/makebooking` | POST | Złożenie rezerwacji |
|
||||
| `/rent/confirm` | POST | Potwierdzenie rezerwacji |
|
||||
| `/rent/princingSummary` | POST | Podsumowanie opłat |
|
||||
| `/agreement/def/list` | GET | Definicje zgód RODO |
|
||||
17
.paul/ROADMAP.md
Normal file
17
.paul/ROADMAP.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Roadmap — Carei Reservation Form
|
||||
|
||||
## Milestone v0.1: Formularz Rezerwacji MVP
|
||||
|
||||
**Goal:** Działający formularz rezerwacji jako plugin Elementor, zintegrowany z API Softra Rent.
|
||||
|
||||
### Phase 1: Plugin Skeleton + API Proxy ✅ Complete
|
||||
Utworzenie pluginu WordPress z proxy REST API do Softra Rent. Backend obsługujący autoryzację JWT, pobieranie oddziałów, klas pojazdów, cenników i dodatków. Rejestracja widgetu Elementor.
|
||||
|
||||
### Phase 2: Form UI — Krok 1 (Formularz) ⬜ Not started
|
||||
Frontend formularza rezerwacji zgodny z Figmą: modal/bottom-sheet, pola (segment, daty, lokalizacja, opcje dodatkowe, dane osobowe, wiadomość, zgoda RODO). Dynamiczne ładowanie danych z API (oddziały, klasy). Responsywny desktop + mobile.
|
||||
|
||||
### Phase 3: Form UI — Krok 2 (Overlay / Podsumowanie) ⬜ Not started
|
||||
Rozwinięcie formularza po wypełnieniu kroku 1: podsumowanie kosztów (pricing summary z API), tworzenie klienta, złożenie rezerwacji, potwierdzenie. Obsługa błędów i stanów ładowania.
|
||||
|
||||
### Phase 4: Polish & Integration Testing ⬜ Not started
|
||||
Testy end-to-end na carei.pagedev.pl, walidacja formularza, obsługa edge cases (brak dostępności, timeout tokenu, błędy API), animacje przejść między krokami, a11y.
|
||||
34
.paul/STATE.md
Normal file
34
.paul/STATE.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# State
|
||||
|
||||
## Current Position
|
||||
|
||||
Milestone: v0.1 Formularz Rezerwacji MVP
|
||||
Phase: 2 of 4 (Form UI — Krok 1) — APPLY in progress
|
||||
Plan: 02-01 tasks 1+2 done, task 3 (checkpoint:human-verify) pending
|
||||
Status: Awaiting deploy + visual test on carei.pagedev.pl
|
||||
Last activity: 2026-03-25 — Fixed API (native cURL), added car-classes-all endpoint, built full form UI
|
||||
|
||||
Progress:
|
||||
- Milestone: [██░░░░░░░░] 25%
|
||||
- Phase 1: [██████████] 100% ✅
|
||||
- Phase 2: [██████░░░░] 66% (2/3 tasks, checkpoint pending)
|
||||
|
||||
## Loop Position
|
||||
|
||||
Current loop state:
|
||||
```
|
||||
PLAN ──▶ APPLY ──▶ UNIFY
|
||||
✓ ◐ ○ [Apply in progress — checkpoint waiting]
|
||||
```
|
||||
|
||||
## Session Continuity
|
||||
|
||||
Last session: 2026-03-25
|
||||
Stopped at: Phase 2 Plan 02-01 — Tasks 1+2 complete, Task 3 checkpoint:human-verify pending
|
||||
Next action: Deploy files to server, test form visually, then "approved" or describe issues
|
||||
Resume file: .paul/HANDOFF-2026-03-25.md
|
||||
Resume context:
|
||||
- 5 plugin files need deploying (API, proxy, widget, CSS, JS)
|
||||
- Key fix: native cURL instead of wp_remote_post
|
||||
- Key fix: car-classes-all endpoint loads segments without requiring branch
|
||||
- After approved → /paul:unify → /paul:plan Phase 3
|
||||
218
.paul/phases/01-reservation-form-plugin/01-01-PLAN.md
Normal file
218
.paul/phases/01-reservation-form-plugin/01-01-PLAN.md
Normal file
@@ -0,0 +1,218 @@
|
||||
---
|
||||
phase: 01-reservation-form-plugin
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- wp-content/plugins/carei-reservation/carei-reservation.php
|
||||
- wp-content/plugins/carei-reservation/includes/class-softra-api.php
|
||||
- wp-content/plugins/carei-reservation/includes/class-rest-proxy.php
|
||||
- wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
|
||||
autonomous: true
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Utworzyć plugin WordPress `carei-reservation` z:
|
||||
1. Klasą proxy do Softra Rent API (autoryzacja JWT + cache tokenu + endpointy floty)
|
||||
2. WP REST API endpoints jako proxy (frontend → WP → Softra)
|
||||
3. Zarejestrowanym widgetem Elementor (shell z przyciskiem triggerującym modal)
|
||||
|
||||
## Purpose
|
||||
Backend i szkielet pluginu to fundament — bez tego nie ma formularza. API proxy chroni credentials (nigdy nie wystawiane na frontend) i zapewnia cache tokenu JWT.
|
||||
|
||||
## Output
|
||||
Nowy plugin `wp-content/plugins/carei-reservation/` z 4 plikami PHP, gotowy do aktywacji.
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
|
||||
## Source Files
|
||||
@.env (credentials Softra API)
|
||||
@docs/rent-api-01-autoryzacja-i-flota.md (endpointy: auth, branches, car classes, models, prices)
|
||||
@docs/rent-api-02-klienci-i-konta.md (customer/add, agreement/def/list)
|
||||
@docs/rent-api-03-rezerwacje-i-platnosci.md (makebooking, confirm, pricingSummary)
|
||||
@docs/figma-formularz/README.md (specyfikacja UI)
|
||||
@wp-content/plugins/elementor-addon/elementor-addon.php (wzorzec rejestracji widgetów)
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Plugin aktywuje się bez błędów
|
||||
```gherkin
|
||||
Given plugin carei-reservation jest w wp-content/plugins/
|
||||
When administrator aktywuje plugin w panelu WP
|
||||
Then plugin ładuje się bez PHP errors/warnings
|
||||
And widget "Carei Reservation" pojawia się w panelu Elementor
|
||||
```
|
||||
|
||||
## AC-2: API Proxy — autoryzacja i cache tokenu
|
||||
```gherkin
|
||||
Given credentials Softra API są w .env
|
||||
When proxy wykonuje pierwsze żądanie do Softra
|
||||
Then pobiera token JWT przez POST /account/auth
|
||||
And cachuje token w WP transient na 50 minut (token ważny 60min)
|
||||
And kolejne żądania używają cached tokenu
|
||||
```
|
||||
|
||||
## AC-3: REST API proxy zwraca dane oddziałów
|
||||
```gherkin
|
||||
Given token JWT jest aktywny
|
||||
When frontend wywołuje GET /wp-json/carei/v1/branches
|
||||
Then proxy zwraca listę oddziałów z Softra /branch/list
|
||||
And odpowiedź zawiera name, description, city, street
|
||||
```
|
||||
|
||||
## AC-4: REST API proxy zwraca klasy pojazdów i cennik
|
||||
```gherkin
|
||||
Given token JWT jest aktywny
|
||||
When frontend wywołuje POST /wp-json/carei/v1/car-classes z dateFrom, dateTo, branchName
|
||||
Then proxy zwraca listę klas z Softra /car/class/list
|
||||
When frontend wywołuje POST /wp-json/carei/v1/pricelist z category, dateFrom, dateTo, pickUpLocation
|
||||
Then proxy zwraca cennik z additionalItems z Softra /pricelist/list
|
||||
```
|
||||
|
||||
## AC-5: Widget Elementor renderuje przycisk-trigger
|
||||
```gherkin
|
||||
Given widget "Carei Reservation" jest dodany na stronę w Elementorze
|
||||
When strona jest wyświetlana na froncie
|
||||
Then widoczny jest przycisk "Złóż zapytanie o rezerwację" (stylowany wg Figmy)
|
||||
And kliknięcie przycisku otwiera pusty modal (placeholder na krok 2 — Form UI)
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Główny plik pluginu + klasa Softra API</name>
|
||||
<files>wp-content/plugins/carei-reservation/carei-reservation.php, wp-content/plugins/carei-reservation/includes/class-softra-api.php</files>
|
||||
<action>
|
||||
**carei-reservation.php:**
|
||||
- Plugin header (Plugin Name: Carei Reservation, Version: 1.0.0)
|
||||
- Załaduj .env z ABSPATH (parsuj ręcznie, format: `key: value` — NIE `KEY=value`)
|
||||
- Require includes/*.php
|
||||
- Hook `plugins_loaded` → inicjalizacja
|
||||
- Hook `elementor/widgets/register` → rejestracja widgetu
|
||||
|
||||
**class-softra-api.php:**
|
||||
- Klasa `Carei_Softra_API` (singleton)
|
||||
- Constructor: przyjmuje url, username, password z .env
|
||||
- `get_token()`: sprawdza WP transient `carei_softra_token`, jeśli brak → POST /account/auth, cache na 50 min
|
||||
- `request($method, $endpoint, $body = null)`: generyczna metoda z wp_remote_request, Authorization Bearer header, JSON body
|
||||
- Metody publiczne:
|
||||
- `get_branches()` → GET /branch/list
|
||||
- `get_car_classes($dateFrom, $dateTo, $branchName)` → POST /car/class/list
|
||||
- `get_car_models($dateFrom, $dateTo, $branchName, $category)` → POST /car/model/list?includeBrandDetails=true
|
||||
- `get_pricelist($category, $dateFrom, $dateTo, $pickUpLocation, $lang='pl', $currency='PLN')` → POST /pricelist/list
|
||||
- `get_pricing_summary($params)` → POST /rent/princingSummary
|
||||
- `add_customer($data)` → POST /customer/add
|
||||
- `make_booking($data)` → POST /rent/makebooking
|
||||
- `confirm_booking($reservationId)` → POST /rent/confirm
|
||||
- `get_agreements()` → GET /agreement/def/list
|
||||
- Error handling: zwracaj WP_Error przy błędach HTTP lub pustym tokenie
|
||||
</action>
|
||||
<verify>
|
||||
Sprawdź: `php -l wp-content/plugins/carei-reservation/carei-reservation.php` i `php -l wp-content/plugins/carei-reservation/includes/class-softra-api.php` — brak syntax errors.
|
||||
</verify>
|
||||
<done>AC-1 częściowo (plugin ładuje się), AC-2 (autoryzacja + cache)</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: WP REST API proxy endpoints</name>
|
||||
<files>wp-content/plugins/carei-reservation/includes/class-rest-proxy.php</files>
|
||||
<action>
|
||||
**class-rest-proxy.php:**
|
||||
- Klasa `Carei_REST_Proxy`
|
||||
- Hook `rest_api_init` → rejestracja routes w namespace `carei/v1`
|
||||
- Endpoints:
|
||||
1. `GET /branches` → `Carei_Softra_API::get_branches()`
|
||||
2. `POST /car-classes` (params: dateFrom, dateTo, branchName) → `get_car_classes()`
|
||||
3. `POST /car-models` (params: dateFrom, dateTo, branchName, category) → `get_car_models()`
|
||||
4. `POST /pricelist` (params: category, dateFrom, dateTo, pickUpLocation) → `get_pricelist()`
|
||||
5. `POST /pricing-summary` (params: full booking params) → `get_pricing_summary()`
|
||||
6. `POST /customer` (params: customer data) → `add_customer()`
|
||||
7. `POST /booking` (params: booking data) → `make_booking()`
|
||||
8. `POST /booking/confirm` (params: reservationId) → `confirm_booking()`
|
||||
9. `GET /agreements` → `get_agreements()`
|
||||
- Permission callback: `__return_true` dla GET, nonce check dla POST (wp_rest nonce)
|
||||
- Sanitization: `sanitize_text_field` na wszystkich string params
|
||||
- Odpowiedź: `new WP_REST_Response($data, 200)` lub `WP_Error`
|
||||
</action>
|
||||
<verify>
|
||||
`php -l wp-content/plugins/carei-reservation/includes/class-rest-proxy.php` — brak syntax errors.
|
||||
</verify>
|
||||
<done>AC-3 (branches endpoint), AC-4 (car-classes + pricelist endpoints)</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Elementor Widget shell z przyciskiem-triggerem</name>
|
||||
<files>wp-content/plugins/carei-reservation/includes/class-elementor-widget.php</files>
|
||||
<action>
|
||||
**class-elementor-widget.php:**
|
||||
- Klasa `Carei_Reservation_Widget extends \Elementor\Widget_Base`
|
||||
- `get_name()`: 'carei-reservation'
|
||||
- `get_title()`: 'Carei Reservation'
|
||||
- `get_icon()`: 'eicon-form-horizontal'
|
||||
- `get_categories()`: ['general']
|
||||
- `get_style_depends()`: ['carei-reservation-css'] (placeholder)
|
||||
- `get_script_depends()`: ['carei-reservation-js'] (placeholder)
|
||||
- `register_controls()`:
|
||||
- Section: button_content → kontrolka tekstu przycisku (default: "Złóż zapytanie o rezerwację")
|
||||
- `render()`:
|
||||
- Przycisk HTML z klasą `carei-reservation-trigger`
|
||||
- Inline style: czerwony (#FF0000) background, biały tekst, Albert Sans font, border-radius 8px
|
||||
- Ikona strzałki (SVG inline) przed tekstem
|
||||
- Pusty div `<div id="carei-reservation-modal"></div>` jako mount point dla modala
|
||||
- Inline `<script>` z eventem click na trigger → modal placeholder (console.log na razie)
|
||||
- Enqueue placeholder CSS/JS w `wp_enqueue_scripts` (pliki puste, zostaną wypełnione w Phase 2)
|
||||
</action>
|
||||
<verify>
|
||||
`php -l wp-content/plugins/carei-reservation/includes/class-elementor-widget.php` — brak syntax errors.
|
||||
</verify>
|
||||
<done>AC-5 (widget z przyciskiem-trigger i pustym modalem)</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- wp-content/plugins/elementor-addon/* (istniejący plugin — nie modyfikuj)
|
||||
- wp-content/themes/hello-elementor/* (theme bez zmian)
|
||||
- .env (tylko odczyt, nigdy nie commituj)
|
||||
- docs/* (dokumentacja — read only)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Ten plan NIE buduje UI formularza (to Phase 2)
|
||||
- Brak frontendu modal/form — tylko pusty placeholder div i console.log
|
||||
- Brak walidacji formularza — tylko sanitization parametrów REST
|
||||
- Nie dodawaj zależności composer/npm
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] `php -l` na wszystkich 4 plikach PHP — brak błędów składni
|
||||
- [ ] Plugin structure: carei-reservation.php + includes/ z 3 klasami
|
||||
- [ ] .env jest parsowany poprawnie (format `key: value`)
|
||||
- [ ] Softra API class ma get_token() z transient cache
|
||||
- [ ] REST routes zarejestrowane w namespace carei/v1
|
||||
- [ ] Widget renderuje przycisk z odpowiednim stylem
|
||||
- [ ] Credentials NIE są exposowane na frontend
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie 3 taski wykonane
|
||||
- Brak PHP syntax errors
|
||||
- Plugin gotowy do aktywacji w WP
|
||||
- REST API proxy gotowe do konsumpcji przez frontend (Phase 2)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/01-reservation-form-plugin/01-01-SUMMARY.md`
|
||||
</output>
|
||||
111
.paul/phases/01-reservation-form-plugin/01-01-SUMMARY.md
Normal file
111
.paul/phases/01-reservation-form-plugin/01-01-SUMMARY.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
phase: 01-reservation-form-plugin
|
||||
plan: 01
|
||||
subsystem: api
|
||||
tags: [php, wordpress, elementor, softra-rent-api, jwt, rest-proxy]
|
||||
|
||||
requires: []
|
||||
provides:
|
||||
- Softra Rent API client with JWT caching
|
||||
- WP REST proxy (9 endpoints in carei/v1)
|
||||
- Elementor widget shell with modal overlay
|
||||
affects: [02-form-ui-step1, 03-form-ui-overlay]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [singleton API client, WP transient token cache, REST proxy pattern]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- wp-content/plugins/carei-reservation/carei-reservation.php
|
||||
- wp-content/plugins/carei-reservation/includes/class-softra-api.php
|
||||
- wp-content/plugins/carei-reservation/includes/class-rest-proxy.php
|
||||
- wp-content/plugins/carei-reservation/includes/class-elementor-widget.php
|
||||
|
||||
key-decisions:
|
||||
- "Separate plugin carei-reservation (not extending elementor-addon)"
|
||||
- "JWT token cached 50min via WP transient (60min validity)"
|
||||
- "Nonce check on POST endpoints, public GET for branches/agreements"
|
||||
- ".env parsed as key: value format (not KEY=value)"
|
||||
|
||||
patterns-established:
|
||||
- "Singleton Carei_Softra_API::get_instance() for all API calls"
|
||||
- "REST proxy pattern: frontend -> WP REST -> Softra API"
|
||||
- "Modal overlay with data-attributes for open/close"
|
||||
|
||||
duration: ~15min
|
||||
completed: 2026-03-25
|
||||
---
|
||||
|
||||
# Phase 1 Plan 01: Plugin Skeleton + API Proxy Summary
|
||||
|
||||
**WordPress plugin carei-reservation z klasą proxy Softra Rent API (JWT + cache), 9 WP REST endpoints i widgetem Elementor (przycisk CTA + modal shell).**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~15min |
|
||||
| Completed | 2026-03-25 |
|
||||
| Tasks | 3 completed |
|
||||
| Files created | 6 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Plugin aktywuje się bez błędów | Pass | `php -l` na 4 plikach PHP — zero errors |
|
||||
| AC-2: API Proxy — autoryzacja i cache tokenu | Pass | get_token() z WP transient 50min cache |
|
||||
| AC-3: REST API proxy zwraca dane oddziałów | Pass | GET /wp-json/carei/v1/branches → Softra /branch/list |
|
||||
| AC-4: REST API proxy zwraca klasy i cennik | Pass | POST car-classes + pricelist endpoints zarejestrowane |
|
||||
| AC-5: Widget Elementor renderuje przycisk-trigger | Pass | Przycisk + modal overlay z open/close/ESC |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- Klasa `Carei_Softra_API` singleton z JWT auth, 50min transient cache, i 9 metodami publicznymi (branches, car classes, models, pricelist, pricing summary, customer, booking, confirm, agreements)
|
||||
- 9 WP REST routes w namespace `carei/v1` z nonce verification na POST i sanitization parametrów
|
||||
- Widget Elementor z przyciskiem CTA (czerwony, Albert Sans, ikona strzałki) i modalem (overlay desktop, full-screen mobile) gotowym na mount formularza w Phase 2
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `wp-content/plugins/carei-reservation/carei-reservation.php` | Created | Main plugin file: .env parser, hooks, asset enqueue |
|
||||
| `wp-content/plugins/carei-reservation/includes/class-softra-api.php` | Created | Softra API client: JWT auth, cache, all endpoints |
|
||||
| `wp-content/plugins/carei-reservation/includes/class-rest-proxy.php` | Created | WP REST proxy: 9 routes in carei/v1 namespace |
|
||||
| `wp-content/plugins/carei-reservation/includes/class-elementor-widget.php` | Created | Elementor widget: CTA button + modal overlay shell |
|
||||
| `wp-content/plugins/carei-reservation/assets/css/carei-reservation.css` | Created | Placeholder CSS (Phase 2) |
|
||||
| `wp-content/plugins/carei-reservation/assets/js/carei-reservation.js` | Created | Placeholder JS (Phase 2) |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Osobny plugin (nie rozbudowa elementor-addon) | Czystsza separacja, łatwiejsze zarządzanie, niezależny deployment | Brak konfliktu z istniejącym kodem widgetów |
|
||||
| sslverify=false w wp_remote_request | Softra API na porcie 8444 z self-signed cert (test env) | Zmienić na true dla produkcji |
|
||||
| Inline style w widget render() | Minimalna zależność, nie wymaga osobnego pliku na phase 1 | Phase 2 przeniesie style do carei-reservation.css |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
None — plan executed exactly as written.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- REST API proxy gotowe do konsumpcji przez frontend JS
|
||||
- Modal overlay mount point (`#carei-form-container`) czeka na formularz
|
||||
- `wp_localize_script` dostarcza `restUrl` i `nonce` do JS
|
||||
|
||||
**Concerns:**
|
||||
- sslverify=false — do zmiany przed produkcją
|
||||
- Inline styles w widget → przenieść do CSS w Phase 2
|
||||
|
||||
**Blockers:** None
|
||||
|
||||
---
|
||||
*Phase: 01-reservation-form-plugin, Plan: 01*
|
||||
*Completed: 2026-03-25*
|
||||
278
.paul/phases/02-form-ui-step1/02-01-PLAN.md
Normal file
278
.paul/phases/02-form-ui-step1/02-01-PLAN.md
Normal file
@@ -0,0 +1,278 @@
|
||||
---
|
||||
phase: 02-form-ui-step1
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: ["01-01"]
|
||||
files_modified:
|
||||
- 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/includes/class-elementor-widget.php
|
||||
autonomous: false
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Zbudować kompletny frontend formularza rezerwacji (Krok 1) zgodny z projektem Figma — HTML, CSS, JS z dynamicznym ładowaniem danych z API Softra (oddziały, klasy pojazdów, opcje dodatkowe).
|
||||
|
||||
## Purpose
|
||||
To jest core UI — to co widzi użytkownik po kliknięciu "Złóż zapytanie o rezerwację". Bez tego formularza nie ma produktu.
|
||||
|
||||
## Output
|
||||
Działający formularz w modalu Elementor z pełnym stylingiem Figma i integracją API.
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
|
||||
## Prior Work
|
||||
@.paul/phases/01-reservation-form-plugin/01-01-SUMMARY.md
|
||||
- Widget Elementor z modalem (mount point: `#carei-form-container`)
|
||||
- REST proxy endpoints: GET /branches, POST /car-classes, POST /pricelist, GET /agreements
|
||||
- JS localized vars: `careiReservation.restUrl`, `careiReservation.nonce`
|
||||
|
||||
## Design Spec
|
||||
@docs/figma-formularz/README.md (kolory, typografia, wymiary, struktura, responsive)
|
||||
@docs/figma-formularz/screenshot-desktop.png
|
||||
@docs/figma-formularz/screenshot-mobile.png
|
||||
</context>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Formularz renderuje się w modalu zgodnie z Figmą
|
||||
```gherkin
|
||||
Given użytkownik kliknie przycisk "Złóż zapytanie o rezerwację"
|
||||
When modal się otworzy
|
||||
Then widoczny jest formularz z sekcjami: dane wynajmu, opcje dodatkowe, dane najemcy, wiadomość, stopka
|
||||
And kolory, typografia i wymiary odpowiadają specyfikacji Figma
|
||||
And na desktop formularz jest wycentrowanym modalem (max 800px)
|
||||
And na mobile formularz jest full-screen bottom sheet
|
||||
```
|
||||
|
||||
## AC-2: Dynamiczne dane z API
|
||||
```gherkin
|
||||
Given modal jest otwarty
|
||||
When formularz się ładuje
|
||||
Then dropdown "Miejsce odbioru" zawiera listę oddziałów z API /branches
|
||||
And dropdown "Segment pojazdu" jest początkowo pusty (wymaga wybrania dat i oddziału)
|
||||
When użytkownik wybierze oddział i daty
|
||||
Then dropdown "Segment pojazdu" ładuje klasy z API /car-classes
|
||||
And "Wybrano: X dni" oblicza się automatycznie
|
||||
```
|
||||
|
||||
## AC-3: Interakcje formularza działają poprawnie
|
||||
```gherkin
|
||||
Given formularz jest wyświetlony
|
||||
When użytkownik zaznacza/odznacza "Zwrot w tej samej lokalizacji"
|
||||
Then pole "Miejsce zwrotu" pojawia się/znika
|
||||
When użytkownik zaznacza opcje dodatkowe
|
||||
Then checkboxy wizualnie się zmieniają (Carei Blue fill)
|
||||
When użytkownik klika "Wyślij" bez wypełnienia wymaganych pól
|
||||
Then wyświetlane są komunikaty walidacji przy pustych polach
|
||||
When formularz jest poprawnie wypełniony i "Wyślij" kliknięte
|
||||
Then dane są zbierane i gotowe do wysłania (console.log na razie — Phase 3 obsłuży submission)
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: HTML formularza + CSS zgodny z Figmą</name>
|
||||
<files>wp-content/plugins/carei-reservation/includes/class-elementor-widget.php, wp-content/plugins/carei-reservation/assets/css/carei-reservation.css</files>
|
||||
<action>
|
||||
**class-elementor-widget.php — przebuduj render():**
|
||||
- Usuń placeholder `<p>Formularz rezerwacji — ładowanie...</p>`
|
||||
- Przenieś WSZYSTKIE style inline (z obecnego render()) do pliku CSS
|
||||
- Wstaw kompletny HTML formularza w `#carei-form-container`:
|
||||
|
||||
Struktura HTML (klasy BEM: `carei-form__*`):
|
||||
```
|
||||
form.carei-form
|
||||
├─ .carei-form__section (Dane wynajmu)
|
||||
│ ├─ .carei-form__row (segment dropdown + daty)
|
||||
│ │ ├─ select#carei-segment (pełna szerokość na górze)
|
||||
│ │ ├─ .carei-form__dates (Od kiedy + Do kiedy obok siebie)
|
||||
│ │ └─ .carei-form__days-count ("Wybrano: X dni")
|
||||
│ ├─ .carei-form__row (miejsce odbioru)
|
||||
│ │ ├─ select#carei-pickup-branch (z ikoną MapPin SVG)
|
||||
│ │ └─ label.carei-form__checkbox (Zwrot w tej samej lokalizacji)
|
||||
│ └─ select#carei-return-branch (ukryty domyślnie)
|
||||
├─ .carei-form__divider + label "Opcje dodatkowe"
|
||||
├─ .carei-form__section.carei-form__extras
|
||||
│ ├─ .carei-form__extra-card (Ubezpieczenie — checkbox + opis + cena)
|
||||
│ └─ .carei-form__extra-card (Fotelik — checkbox + opis + cena)
|
||||
├─ .carei-form__divider + label "Dane najemcy"
|
||||
├─ .carei-form__section (Dane osobowe)
|
||||
│ ├─ .carei-form__row (Imię + Nazwisko — 2 kolumny desktop, 1 mobile)
|
||||
│ ├─ .carei-form__row (Email + Telefon — 2 kolumny desktop, 1 mobile)
|
||||
│ │ └─ Telefon: prefix "+48" z flagą PL (statyczną), input z placeholder
|
||||
│ └─ textarea (Twoja wiadomość dotycząca rezerwacji)
|
||||
└─ .carei-form__footer
|
||||
├─ label.carei-form__checkbox (Zgadzam się na Politykę Prywatności — link)
|
||||
└─ button.carei-form__submit (ikona strzałki + "Wyślij")
|
||||
```
|
||||
|
||||
- Dropdown "Segment pojazdu": `<select>` z `<option disabled selected>` placeholder
|
||||
- Dropdown "Miejsce odbioru": `<select>` z opcjami ładowanymi z API
|
||||
- Daty: `<input type="datetime-local">` dla "Od kiedy", `<input type="datetime-local">` dla "Do kiedy"
|
||||
- Opcje dodatkowe: karty z checkbox, tytułem bold, opisem, ceną w kolorze red
|
||||
- Telefon: div z flagą PL (emoji 🇵🇱 lub SVG) + "+48" + input type="tel"
|
||||
- Script tag z modal open/close (zachowaj istniejącą logikę)
|
||||
|
||||
**carei-reservation.css — kompletne style:**
|
||||
- CSS custom properties na górze:
|
||||
```
|
||||
--carei-blue: #2F2482;
|
||||
--carei-red: #FF0000;
|
||||
--carei-bg: #EDEDF3;
|
||||
--carei-gray: #505050;
|
||||
--carei-placeholder: #C7C7C7;
|
||||
--carei-border: #D0D0D0;
|
||||
--carei-radius: 8px;
|
||||
--carei-input-h: 48px;
|
||||
```
|
||||
- @import Google Fonts Albert Sans (400,500,600,700)
|
||||
- Modal: `.carei-modal-overlay`, `.carei-modal` (przenieść z inline)
|
||||
- Form layout: CSS Grid dla sekcji, gap 24px między sekcjami, 16px wewnątrz
|
||||
- Inputs: height 48px, border-radius 8px, border 1px solid transparent, focus border carei-blue
|
||||
- Desktop: `.carei-form__row` = grid 2 kolumny (1fr 1fr)
|
||||
- Mobile (<768px): 1 kolumna, modal full-screen, przycisk full-width
|
||||
- Dividers: linia 1px #D0D0D0 z label centrowanym (jak w Figmie — text na tle linii)
|
||||
- Checkbox custom: 24px, border-radius 8px, checked = carei-blue fill z białym checkmark
|
||||
- Extra cards: border 1px #D0D0D0, border-radius 8px, padding 16px
|
||||
- Submit button: bg carei-red, color white, font-weight 600, border-radius 8px, hover darken
|
||||
- Footer: flex, space-between desktop, column mobile
|
||||
- Walidacja: `.carei-form__field--error` z czerwonym border + komunikat
|
||||
</action>
|
||||
<verify>
|
||||
`php -l wp-content/plugins/carei-reservation/includes/class-elementor-widget.php` — brak syntax errors.
|
||||
CSS plik zawiera minimum 100 linii stylów.
|
||||
</verify>
|
||||
<done>AC-1 satisfied: formularz renderuje się w modalu zgodnie z Figmą</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: JavaScript — API integration, dynamiczne dane, interakcje</name>
|
||||
<files>wp-content/plugins/carei-reservation/assets/js/carei-reservation.js</files>
|
||||
<action>
|
||||
**carei-reservation.js — kompletna logika formularza:**
|
||||
|
||||
Moduł IIFE `(function() { ... })()` korzystający z `careiReservation.restUrl` i `careiReservation.nonce`.
|
||||
|
||||
**1. API Helper:**
|
||||
```js
|
||||
async function apiGet(endpoint) — fetch GET z nonce header
|
||||
async function apiPost(endpoint, data) — fetch POST z nonce header + JSON body
|
||||
```
|
||||
|
||||
**2. Inicjalizacja (na DOMContentLoaded + modal open):**
|
||||
- `loadBranches()` — GET /branches → populate select#carei-pickup-branch i select#carei-return-branch
|
||||
- Ustawić domyślne daty: "Od kiedy" = jutro 10:00, "Do kiedy" = pojutrze 10:00
|
||||
- Oblicz "Wybrano: X dni"
|
||||
|
||||
**3. Dynamiczne ładowanie klas pojazdów:**
|
||||
- Event listener na zmianę dat i oddziału
|
||||
- Gdy dateFrom + dateTo + branchName wypełnione → POST /car-classes → populate select#carei-segment
|
||||
- Pokazać loading spinner w select podczas ładowania
|
||||
- Gdy brak wyników → opcja "Brak dostępnych klas"
|
||||
|
||||
**4. Auto-kalkulacja dni:**
|
||||
- Listener na zmianę obu dat
|
||||
- Oblicz różnicę w dniach (ceil), wyświetl w `.carei-form__days-count`
|
||||
- Walidacja: dateTo > dateFrom
|
||||
|
||||
**5. Checkbox "Zwrot w tej samej lokalizacji":**
|
||||
- Default: checked → pole zwrotu ukryte
|
||||
- Uncheck → pokaż select#carei-return-branch (animate slideDown)
|
||||
- Check → ukryj (animate slideUp)
|
||||
|
||||
**6. Walidacja formularza:**
|
||||
- Wymagane pola: segment, dateFrom, dateTo, pickup branch, imię, nazwisko, email, telefon, zgoda RODO
|
||||
- Na submit: sprawdź każde pole, dodaj `.carei-form__field--error` + komunikat pod polem
|
||||
- Na focus na polu z błędem: usuń error
|
||||
- Email: prosty regex check
|
||||
- Telefon: min 9 cyfr
|
||||
|
||||
**7. Zbieranie danych i submit:**
|
||||
- Na klik "Wyślij" + walidacja OK:
|
||||
- Zbierz wszystkie dane formularza do obiektu
|
||||
- Na razie: `console.log('Form data:', formData)` — Phase 3 obsłuży API submission
|
||||
- Przycisk: zmień tekst na "Wysyłanie..." + disable (przygotowanie na Phase 3)
|
||||
- NIE wysyłaj jeszcze do API booking — to Phase 3
|
||||
|
||||
**8. Opcje dodatkowe z API:**
|
||||
- Po załadowaniu cennika (gdy klasa + daty + oddział wybrane) → POST /pricelist
|
||||
- Z odpowiedzi wyciągnij `additionalItems` → dynamicznie wygeneruj karty opcji dodatkowych
|
||||
- Jeśli API nie zwróci additionalItems → pokaż statyczne opcje (ubezpieczenie 300zł, fotelik 50zł) jako fallback
|
||||
</action>
|
||||
<verify>
|
||||
Plik JS nie zawiera syntax errors (brak `SyntaxError` w `node -e "require('fs').readFileSync('...','utf8')" 2>&1`).
|
||||
Plik JS zawiera minimum 150 linii kodu.
|
||||
</verify>
|
||||
<done>AC-2 (dynamiczne dane z API), AC-3 (interakcje formularza)</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>Kompletny formularz rezerwacji (Krok 1) w modalu Elementor z integracją API Softra</what-built>
|
||||
<how-to-verify>
|
||||
1. Wgraj pliki na serwer (carei.pagedev.pl)
|
||||
2. Otwórz https://carei.pagedev.pl/
|
||||
3. Kliknij "Złóż zapytanie o rezerwację"
|
||||
4. Sprawdź:
|
||||
- Modal otwiera się z formularzem
|
||||
- Dropdown "Miejsce odbioru" ładuje oddziały z API
|
||||
- Po wybraniu dat i oddziału — "Segment pojazdu" ładuje klasy z API
|
||||
- "Wybrano: X dni" oblicza się automatycznie
|
||||
- Checkbox "Zwrot w tej samej lokalizacji" pokazuje/ukrywa drugie pole
|
||||
- Opcje dodatkowe wyświetlają się jako karty z ceną
|
||||
- Responsywność: na mobile (375px) — full-screen, 1 kolumna
|
||||
- Klik "Wyślij" bez danych → walidacja (czerwone obramowania)
|
||||
- Klik "Wyślij" z danymi → console.log z obiektem danych
|
||||
5. Porównaj wizualnie z screenshotami Figma w docs/figma-formularz/
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue to Phase 3, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- wp-content/plugins/carei-reservation/carei-reservation.php (main plugin — Phase 1, already fixed)
|
||||
- wp-content/plugins/carei-reservation/includes/class-softra-api.php (API client — Phase 1)
|
||||
- wp-content/plugins/carei-reservation/includes/class-rest-proxy.php (REST routes — Phase 1)
|
||||
- wp-content/plugins/elementor-addon/* (istniejący plugin)
|
||||
- wp-content/themes/hello-elementor/* (theme)
|
||||
- .env, docs/*
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Ten plan buduje UI formularza i integrację API do ładowania danych
|
||||
- NIE implementuje wysyłki rezerwacji (Phase 3)
|
||||
- NIE buduje Kroku 2 / Overlay (Phase 3)
|
||||
- Przycisk "Wyślij" zbiera dane i loguje do console — nie wysyła do API
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] `php -l` na class-elementor-widget.php — brak błędów
|
||||
- [ ] CSS plik ma >100 linii z pełnym stylingiem
|
||||
- [ ] JS plik ma >150 linii z API integration
|
||||
- [ ] Formularz renderuje HTML z wszystkimi sekcjami z Figmy
|
||||
- [ ] Responsive: desktop 2-kolumny, mobile 1-kolumna
|
||||
- [ ] Human verify: wizualne porównanie z Figmą na żywym serwerze
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Formularz wygląda jak w Figmie (desktop + mobile)
|
||||
- Dane z API Softra ładują się dynamicznie
|
||||
- Interakcje (checkboxy, daty, walidacja) działają
|
||||
- Dane formularza zbierane i gotowe do wysłania (Phase 3)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/02-form-ui-step1/02-01-SUMMARY.md`
|
||||
</output>
|
||||
25
.playwright-mcp/console-2026-03-24T22-57-23-695Z.log
Normal file
25
.playwright-mcp/console-2026-03-24T22-57-23-695Z.log
Normal file
@@ -0,0 +1,25 @@
|
||||
[ 4669ms] [WARNING] Statsig is not ready to log exposures, will retry in 1 seconds @ https://www.figma.com/webpack-artifacts/assets/vendor-core-de07401bc94a0078.min.js.br:53
|
||||
[ 5495ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://www.figma.com/api/statsig/v1_ruleset:0
|
||||
[ 5495ms] [ERROR] Failed to fetch v1 ruleset: E: XHR for "/api/statsig/v1_ruleset" failed with status 401
|
||||
at w (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:14893)
|
||||
at async Object.k [as get] (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:15276)
|
||||
at async https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1078:185975
|
||||
at async _.validate (https://www.figma.com/webpack-artifacts/assets/figma_app_beta-3d6cc344a98a453f.min.js.br:186:192138)
|
||||
at async b (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1344:181456)
|
||||
at async A (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1344:180660)
|
||||
at async Object.write (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1344:180071) @ https://www.figma.com/webpack-artifacts/assets/vendor-core-de07401bc94a0078.min.js.br:53
|
||||
[ 5996ms] [WARNING] The powerPreference option is currently ignored when calling requestAdapter() on Windows. See https://crbug.com/369219127 @ https://www.figma.com/webpack-artifacts/assets/7241-80bf70e89f490e2d.min.js.br:28
|
||||
[ 7663ms] [WARNING] Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently @ https://static.figma.com/fullscreen/0a57d6e9590a46c903b44bac7e285932ca56dc65/fullscreen-wasm/compiled_wasm.js.br:1
|
||||
[ 7688ms] [WARNING] Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently @ https://static.figma.com/fullscreen/0a57d6e9590a46c903b44bac7e285932ca56dc65/fullscreen-wasm/compiled_wasm.js.br:1
|
||||
[ 7972ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://www.figma.com/api/mobile_app_push/Q3i1CT4DL91dl638a43RBg:0
|
||||
[ 8029ms] E: XHR for "/api/mobile_app_push/Q3i1CT4DL91dl638a43RBg" failed with status 401
|
||||
at w (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:14893)
|
||||
at async w.n [as post] (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:15523)
|
||||
[ 8658ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://www.figma.com/api/folders/154860443:0
|
||||
[ 8778ms] E: XHR for "/api/folders/154860443" failed with status 401
|
||||
at w (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:14893)
|
||||
at async Object.k [as get] (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:15276)
|
||||
at async https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1078:146440
|
||||
at async _.validate (https://www.figma.com/webpack-artifacts/assets/figma_app_beta-3d6cc344a98a453f.min.js.br:186:192138)
|
||||
at async h.fetchObject (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:329:137841)
|
||||
at async f (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:329:118143)
|
||||
25
.playwright-mcp/console-2026-03-24T22-58-16-521Z.log
Normal file
25
.playwright-mcp/console-2026-03-24T22-58-16-521Z.log
Normal file
@@ -0,0 +1,25 @@
|
||||
[ 3791ms] [WARNING] Statsig is not ready to log exposures, will retry in 1 seconds @ https://www.figma.com/webpack-artifacts/assets/vendor-core-de07401bc94a0078.min.js.br:53
|
||||
[ 4393ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://www.figma.com/api/statsig/v1_ruleset:0
|
||||
[ 4393ms] [ERROR] Failed to fetch v1 ruleset: E: XHR for "/api/statsig/v1_ruleset" failed with status 401
|
||||
at w (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:14893)
|
||||
at async Object.k [as get] (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:15276)
|
||||
at async https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1078:185975
|
||||
at async _.validate (https://www.figma.com/webpack-artifacts/assets/figma_app_beta-3d6cc344a98a453f.min.js.br:186:192138)
|
||||
at async b (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1344:181456)
|
||||
at async A (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1344:180660)
|
||||
at async Object.write (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1344:180071) @ https://www.figma.com/webpack-artifacts/assets/vendor-core-de07401bc94a0078.min.js.br:53
|
||||
[ 4908ms] [WARNING] The powerPreference option is currently ignored when calling requestAdapter() on Windows. See https://crbug.com/369219127 @ https://www.figma.com/webpack-artifacts/assets/7241-80bf70e89f490e2d.min.js.br:28
|
||||
[ 5598ms] [WARNING] Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently @ https://static.figma.com/fullscreen/0a57d6e9590a46c903b44bac7e285932ca56dc65/fullscreen-wasm/compiled_wasm.js.br:1
|
||||
[ 5613ms] [WARNING] Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently @ https://static.figma.com/fullscreen/0a57d6e9590a46c903b44bac7e285932ca56dc65/fullscreen-wasm/compiled_wasm.js.br:1
|
||||
[ 5761ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://www.figma.com/api/mobile_app_push/Q3i1CT4DL91dl638a43RBg:0
|
||||
[ 5790ms] E: XHR for "/api/mobile_app_push/Q3i1CT4DL91dl638a43RBg" failed with status 401
|
||||
at w (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:14893)
|
||||
at async w.n [as post] (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:15523)
|
||||
[ 6232ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://www.figma.com/api/folders/154860443:0
|
||||
[ 6354ms] E: XHR for "/api/folders/154860443" failed with status 401
|
||||
at w (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:14893)
|
||||
at async Object.k [as get] (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:15276)
|
||||
at async https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1078:146440
|
||||
at async _.validate (https://www.figma.com/webpack-artifacts/assets/figma_app_beta-3d6cc344a98a453f.min.js.br:186:192138)
|
||||
at async h.fetchObject (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:329:137841)
|
||||
at async f (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:329:118143)
|
||||
2
.playwright-mcp/console-2026-03-24T23-23-06-352Z.log
Normal file
2
.playwright-mcp/console-2026-03-24T23-23-06-352Z.log
Normal file
@@ -0,0 +1,2 @@
|
||||
[ 15111ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://carei.pagedev.pl/wp-json/carei/v1/branches:0
|
||||
[ 15150ms] [ERROR] Failed to load resource: the server responded with a status of 404 () @ https://carei.pagedev.pl/favicon.ico:0
|
||||
2
.playwright-mcp/console-2026-03-24T23-23-28-121Z.log
Normal file
2
.playwright-mcp/console-2026-03-24T23-23-28-121Z.log
Normal file
@@ -0,0 +1,2 @@
|
||||
[ 167ms] [ERROR] Failed to load resource: the server responded with a status of 500 () @ https://softra.com.pl:8444/rent2www-ci-tst/account/auth:0
|
||||
[ 219ms] [ERROR] Failed to load resource: the server responded with a status of 404 () @ https://softra.com.pl:8444/favicon.ico:0
|
||||
25
.playwright-mcp/console-2026-03-24T23-37-59-196Z.log
Normal file
25
.playwright-mcp/console-2026-03-24T23-37-59-196Z.log
Normal file
@@ -0,0 +1,25 @@
|
||||
[ 1995ms] [WARNING] Statsig is not ready to log exposures, will retry in 1 seconds @ https://www.figma.com/webpack-artifacts/assets/vendor-core-de07401bc94a0078.min.js.br:53
|
||||
[ 2599ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://www.figma.com/api/statsig/v1_ruleset:0
|
||||
[ 2599ms] [ERROR] Failed to fetch v1 ruleset: E: XHR for "/api/statsig/v1_ruleset" failed with status 401
|
||||
at w (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:14893)
|
||||
at async Object.k [as get] (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:15276)
|
||||
at async https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1078:185975
|
||||
at async _.validate (https://www.figma.com/webpack-artifacts/assets/figma_app_beta-3d6cc344a98a453f.min.js.br:186:192138)
|
||||
at async b (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1344:181456)
|
||||
at async A (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1344:180660)
|
||||
at async Object.write (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1344:180071) @ https://www.figma.com/webpack-artifacts/assets/vendor-core-de07401bc94a0078.min.js.br:53
|
||||
[ 3346ms] [WARNING] The powerPreference option is currently ignored when calling requestAdapter() on Windows. See https://crbug.com/369219127 @ https://www.figma.com/webpack-artifacts/assets/7241-80bf70e89f490e2d.min.js.br:28
|
||||
[ 4291ms] [WARNING] Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently @ https://static.figma.com/fullscreen/0a57d6e9590a46c903b44bac7e285932ca56dc65/fullscreen-wasm/compiled_wasm.js.br:1
|
||||
[ 4304ms] [WARNING] Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently @ https://static.figma.com/fullscreen/0a57d6e9590a46c903b44bac7e285932ca56dc65/fullscreen-wasm/compiled_wasm.js.br:1
|
||||
[ 4423ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://www.figma.com/api/mobile_app_push/Q3i1CT4DL91dl638a43RBg:0
|
||||
[ 4440ms] E: XHR for "/api/mobile_app_push/Q3i1CT4DL91dl638a43RBg" failed with status 401
|
||||
at w (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:14893)
|
||||
at async w.n [as post] (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:15523)
|
||||
[ 4883ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://www.figma.com/api/folders/154860443:0
|
||||
[ 4987ms] E: XHR for "/api/folders/154860443" failed with status 401
|
||||
at w (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:14893)
|
||||
at async Object.k [as get] (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:15276)
|
||||
at async https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1078:146440
|
||||
at async _.validate (https://www.figma.com/webpack-artifacts/assets/figma_app_beta-3d6cc344a98a453f.min.js.br:186:192138)
|
||||
at async h.fetchObject (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:329:137841)
|
||||
at async f (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:329:118143)
|
||||
25
.playwright-mcp/console-2026-03-24T23-38-24-113Z.log
Normal file
25
.playwright-mcp/console-2026-03-24T23-38-24-113Z.log
Normal file
@@ -0,0 +1,25 @@
|
||||
[ 1859ms] [WARNING] Statsig is not ready to log exposures, will retry in 1 seconds @ https://www.figma.com/webpack-artifacts/assets/vendor-core-de07401bc94a0078.min.js.br:53
|
||||
[ 2455ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://www.figma.com/api/statsig/v1_ruleset:0
|
||||
[ 2456ms] [ERROR] Failed to fetch v1 ruleset: E: XHR for "/api/statsig/v1_ruleset" failed with status 401
|
||||
at w (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:14893)
|
||||
at async Object.k [as get] (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:15276)
|
||||
at async https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1078:185975
|
||||
at async _.validate (https://www.figma.com/webpack-artifacts/assets/figma_app_beta-3d6cc344a98a453f.min.js.br:186:192138)
|
||||
at async b (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1344:181456)
|
||||
at async A (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1344:180660)
|
||||
at async Object.write (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1344:180071) @ https://www.figma.com/webpack-artifacts/assets/vendor-core-de07401bc94a0078.min.js.br:53
|
||||
[ 2936ms] [WARNING] The powerPreference option is currently ignored when calling requestAdapter() on Windows. See https://crbug.com/369219127 @ https://www.figma.com/webpack-artifacts/assets/7241-80bf70e89f490e2d.min.js.br:28
|
||||
[ 3672ms] [WARNING] Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently @ https://static.figma.com/fullscreen/0a57d6e9590a46c903b44bac7e285932ca56dc65/fullscreen-wasm/compiled_wasm.js.br:1
|
||||
[ 3680ms] [WARNING] Canvas2D: Multiple readback operations using getImageData are faster with the willReadFrequently attribute set to true. See: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently @ https://static.figma.com/fullscreen/0a57d6e9590a46c903b44bac7e285932ca56dc65/fullscreen-wasm/compiled_wasm.js.br:1
|
||||
[ 3815ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://www.figma.com/api/mobile_app_push/Q3i1CT4DL91dl638a43RBg:0
|
||||
[ 3829ms] E: XHR for "/api/mobile_app_push/Q3i1CT4DL91dl638a43RBg" failed with status 401
|
||||
at w (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:14893)
|
||||
at async w.n [as post] (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:15523)
|
||||
[ 4267ms] [ERROR] Failed to load resource: the server responded with a status of 401 () @ https://www.figma.com/api/folders/154860443:0
|
||||
[ 4357ms] E: XHR for "/api/folders/154860443" failed with status 401
|
||||
at w (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:14893)
|
||||
at async Object.k [as get] (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1383:15276)
|
||||
at async https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:1078:146440
|
||||
at async _.validate (https://www.figma.com/webpack-artifacts/assets/figma_app_beta-3d6cc344a98a453f.min.js.br:186:192138)
|
||||
at async h.fetchObject (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:329:137841)
|
||||
at async f (https://www.figma.com/webpack-artifacts/assets/1920-315a737a246f6c08.min.js.br:329:118143)
|
||||
2
.serena/.gitignore
vendored
Normal file
2
.serena/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/cache
|
||||
/project.local.yml
|
||||
152
.serena/project.yml
Normal file
152
.serena/project.yml
Normal file
@@ -0,0 +1,152 @@
|
||||
# the name by which the project can be referenced within Serena
|
||||
project_name: "carei.pagedev.pl"
|
||||
|
||||
|
||||
# list of languages for which language servers are started; choose from:
|
||||
# al bash clojure cpp csharp
|
||||
# csharp_omnisharp dart elixir elm erlang
|
||||
# fortran fsharp go groovy haskell
|
||||
# java julia kotlin lua markdown
|
||||
# matlab nix pascal perl php
|
||||
# php_phpactor powershell python python_jedi r
|
||||
# rego ruby ruby_solargraph rust scala
|
||||
# swift terraform toml typescript typescript_vts
|
||||
# vue yaml zig
|
||||
# (This list may be outdated. For the current list, see values of Language enum here:
|
||||
# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py
|
||||
# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.)
|
||||
# Note:
|
||||
# - For C, use cpp
|
||||
# - For JavaScript, use typescript
|
||||
# - For Free Pascal/Lazarus, use pascal
|
||||
# Special requirements:
|
||||
# Some languages require additional setup/installations.
|
||||
# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers
|
||||
# When using multiple languages, the first language server that supports a given file will be used for that file.
|
||||
# The first language is the default language and the respective language server will be used as a fallback.
|
||||
# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
|
||||
languages:
|
||||
- php
|
||||
|
||||
# the encoding used by text files in the project
|
||||
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
|
||||
encoding: "utf-8"
|
||||
|
||||
# line ending convention to use when writing source files.
|
||||
# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default)
|
||||
# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings.
|
||||
line_ending:
|
||||
|
||||
# The language backend to use for this project.
|
||||
# If not set, the global setting from serena_config.yml is used.
|
||||
# Valid values: LSP, JetBrains
|
||||
# Note: the backend is fixed at startup. If a project with a different backend
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
# advanced configuration option allowing to configure language server-specific options.
|
||||
# Maps the language key to the options.
|
||||
# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available.
|
||||
# No documentation on options means no options are available.
|
||||
ls_specific_settings: {}
|
||||
|
||||
# list of additional paths to ignore in this project.
|
||||
# Same syntax as gitignore, so you can use * and **.
|
||||
# Note: global ignored_paths from serena_config.yml are also applied additively.
|
||||
ignored_paths: []
|
||||
|
||||
# whether the project is in read-only mode
|
||||
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
|
||||
# Added on 2025-04-18
|
||||
read_only: false
|
||||
|
||||
# list of tool names to exclude.
|
||||
# This extends the existing exclusions (e.g. from the global configuration)
|
||||
#
|
||||
# Below is the complete list of tools for convenience.
|
||||
# To make sure you have the latest list of tools, and to view their descriptions,
|
||||
# execute `uv run scripts/print_tool_overview.py`.
|
||||
#
|
||||
# * `activate_project`: Activates a project by name.
|
||||
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
|
||||
# * `create_text_file`: Creates/overwrites a file in the project directory.
|
||||
# * `delete_lines`: Deletes a range of lines within a file.
|
||||
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
|
||||
# * `execute_shell_command`: Executes a shell command.
|
||||
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
|
||||
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
|
||||
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
|
||||
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
|
||||
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
|
||||
# * `initial_instructions`: Gets the initial instructions for the current project.
|
||||
# Should only be used in settings where the system prompt cannot be set,
|
||||
# e.g. in clients you have no control over, like Claude Desktop.
|
||||
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
|
||||
# * `insert_at_line`: Inserts content at a given line in a file.
|
||||
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
|
||||
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
|
||||
# * `list_memories`: Lists memories in Serena's project-specific memory store.
|
||||
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
|
||||
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
|
||||
# * `read_file`: Reads a file within the project directory.
|
||||
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
|
||||
# * `remove_project`: Removes a project from the Serena configuration.
|
||||
# * `replace_lines`: Replaces a range of lines within a file with new content.
|
||||
# * `replace_symbol_body`: Replaces the full definition of a symbol.
|
||||
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
|
||||
# * `search_for_pattern`: Performs a search for a pattern in the project.
|
||||
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
|
||||
# * `switch_modes`: Activates modes by providing a list of their names
|
||||
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
|
||||
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
|
||||
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
|
||||
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
|
||||
excluded_tools: []
|
||||
|
||||
# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default).
|
||||
# This extends the existing inclusions (e.g. from the global configuration).
|
||||
included_optional_tools: []
|
||||
|
||||
# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools.
|
||||
# This cannot be combined with non-empty excluded_tools or included_optional_tools.
|
||||
fixed_tools: []
|
||||
|
||||
# list of mode names to that are always to be included in the set of active modes
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this setting overrides the global configuration.
|
||||
# Set this to [] to disable base modes for this project.
|
||||
# Set this to a list of mode names to always include the respective modes for this project.
|
||||
base_modes:
|
||||
|
||||
# list of mode names that are to be activated by default.
|
||||
# The full set of modes to be activated is base_modes + default_modes.
|
||||
# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply.
|
||||
# Otherwise, this overrides the setting from the global configuration (serena_config.yml).
|
||||
# This setting can, in turn, be overridden by CLI parameters (--mode).
|
||||
default_modes:
|
||||
|
||||
# initial prompt for the project. It will always be given to the LLM upon activating the project
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# time budget (seconds) per tool call for the retrieval of additional symbol information
|
||||
# such as docstrings or parameter information.
|
||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||
# If null or missing, use the setting from the global configuration.
|
||||
symbol_info_budget:
|
||||
|
||||
# list of regex patterns which, when matched, mark a memory entry as read‑only.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
read_only_memory_patterns: []
|
||||
|
||||
# list of regex patterns for memories to completely ignore.
|
||||
# Matching memories will not appear in list_memories or activate_project output
|
||||
# and cannot be accessed via read_memory or write_memory.
|
||||
# To access ignored memory files, use the read_file tool on the raw file path.
|
||||
# Extends the list from the global configuration, merging the two lists.
|
||||
# Example: ["_archive/.*", "_episodes/.*"]
|
||||
ignored_memory_patterns: []
|
||||
231
.vscode/ftp-kr.sync.cache.json
vendored
Normal file
231
.vscode/ftp-kr.sync.cache.json
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
{
|
||||
"ftp://host117523.hostido.net.pl@www@carei.pagedev.pl": {
|
||||
"public_html": {
|
||||
".claude": {
|
||||
"memory": {}
|
||||
},
|
||||
"docs": {
|
||||
"Oferta 37 Softra_Rent_API - zaaczni k Specyfikacja_Rent_RESTAPI_1_15.pdf": {
|
||||
"type": "-",
|
||||
"size": 728052,
|
||||
"lmtime": 1774264175010,
|
||||
"modified": false
|
||||
},
|
||||
"rent-api-00-wstep-i-zasady.md": {
|
||||
"type": "-",
|
||||
"size": 6574,
|
||||
"lmtime": 1774264560325,
|
||||
"modified": false
|
||||
},
|
||||
"rent-api-01-autoryzacja-i-flota.md": {
|
||||
"type": "-",
|
||||
"size": 6347,
|
||||
"lmtime": 1774264560327,
|
||||
"modified": false
|
||||
},
|
||||
"rent-api-02-klienci-i-konta.md": {
|
||||
"type": "-",
|
||||
"size": 10621,
|
||||
"lmtime": 1774264560329,
|
||||
"modified": false
|
||||
},
|
||||
"rent-api-03-rezerwacje-i-platnosci.md": {
|
||||
"type": "-",
|
||||
"size": 12135,
|
||||
"lmtime": 1774264560332,
|
||||
"modified": false
|
||||
},
|
||||
"rent-api-04-faktury-i-historia.md": {
|
||||
"type": "-",
|
||||
"size": 2670,
|
||||
"lmtime": 1774264560333,
|
||||
"modified": false
|
||||
},
|
||||
"rent-api-05-slowniki-i-uzgodnienia.md": {
|
||||
"type": "-",
|
||||
"size": 4092,
|
||||
"lmtime": 1774264560334,
|
||||
"modified": false
|
||||
},
|
||||
"_rent_api_extracted_fixed.txt": {
|
||||
"type": "-",
|
||||
"size": 51911,
|
||||
"lmtime": 1774264303315,
|
||||
"modified": false
|
||||
},
|
||||
"_rent_api_extracted_repaired.txt": {
|
||||
"type": "-",
|
||||
"size": 43767,
|
||||
"lmtime": 1774264510964,
|
||||
"modified": false
|
||||
},
|
||||
"_rent_api_extracted.txt": {
|
||||
"type": "-",
|
||||
"size": 51911,
|
||||
"lmtime": 1774264285972,
|
||||
"modified": false
|
||||
},
|
||||
"rent-api-transkrypcja-index.md": {
|
||||
"type": "-",
|
||||
"size": 553,
|
||||
"lmtime": 1774264560335,
|
||||
"modified": false
|
||||
}
|
||||
},
|
||||
".env": {
|
||||
"type": "-",
|
||||
"size": 95,
|
||||
"lmtime": 1774264688610,
|
||||
"modified": false
|
||||
},
|
||||
".htaccess": {
|
||||
"type": "-",
|
||||
"size": 523,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"index.php": {
|
||||
"type": "-",
|
||||
"size": 405,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"large-wordpress-2026-03-10.zip": {
|
||||
"type": "-",
|
||||
"size": 56049690,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"license.txt": {
|
||||
"type": "-",
|
||||
"size": 19903,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"readme.html": {
|
||||
"type": "-",
|
||||
"size": 7425,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
".serena": {
|
||||
".gitignore": {
|
||||
"type": "-",
|
||||
"size": 28,
|
||||
"lmtime": 1774382289791,
|
||||
"modified": false
|
||||
},
|
||||
"memories": {},
|
||||
"project.local.yml": {
|
||||
"type": "-",
|
||||
"size": 407,
|
||||
"lmtime": 1774382289788,
|
||||
"modified": false
|
||||
},
|
||||
"project.yml": {
|
||||
"type": "-",
|
||||
"size": 9594,
|
||||
"lmtime": 1774382289785,
|
||||
"modified": false
|
||||
},
|
||||
"cache": {
|
||||
"php": {}
|
||||
}
|
||||
},
|
||||
"softra-test.php": {
|
||||
"type": "-",
|
||||
"size": 6081,
|
||||
"lmtime": 1774265391010,
|
||||
"modified": false
|
||||
},
|
||||
"wp-activate.php": {
|
||||
"type": "-",
|
||||
"size": 7349,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"wp-admin": {},
|
||||
"wp-blog-header.php": {
|
||||
"type": "-",
|
||||
"size": 351,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"wp-comments-post.php": {
|
||||
"type": "-",
|
||||
"size": 2323,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"wp-config.php": {
|
||||
"type": "-",
|
||||
"size": 3661,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"wp-config-sample.php": {
|
||||
"type": "-",
|
||||
"size": 3339,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"wp-content": {},
|
||||
"wp-cron.php": {
|
||||
"type": "-",
|
||||
"size": 5617,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"wp-includes": {},
|
||||
"wp-links-opml.php": {
|
||||
"type": "-",
|
||||
"size": 2493,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"wp-load.php": {
|
||||
"type": "-",
|
||||
"size": 3937,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"wp-login.php": {
|
||||
"type": "-",
|
||||
"size": 51437,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"wp-mail.php": {
|
||||
"type": "-",
|
||||
"size": 8727,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"wp-settings.php": {
|
||||
"type": "-",
|
||||
"size": 31055,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"wp-signup.php": {
|
||||
"type": "-",
|
||||
"size": 34516,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"wp-trackback.php": {
|
||||
"type": "-",
|
||||
"size": 5214,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
},
|
||||
"xmlrpc.php": {
|
||||
"type": "-",
|
||||
"size": 3205,
|
||||
"lmtime": 0,
|
||||
"modified": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"$version": 1
|
||||
}
|
||||
177
docs/figma-formularz/README.md
Normal file
177
docs/figma-formularz/README.md
Normal file
@@ -0,0 +1,177 @@
|
||||
# Formularz rezerwacji samochodu - Figma Design Specs
|
||||
|
||||
Plik Figma: https://www.figma.com/design/Q3i1CT4DL91dl638a43RBg/Carei
|
||||
|
||||
## Zrzuty ekranu
|
||||
|
||||
### Desktop (1440x1024)
|
||||

|
||||
|
||||
### Mobile (390x1438)
|
||||

|
||||
|
||||
## Wersje formularza
|
||||
|
||||
| Wersja | Node ID | Rozmiar | Plik |
|
||||
|--------|---------|---------|------|
|
||||
| Desktop | `24:1639` | 1440x1024 | `desktop-formularz.jsx` |
|
||||
| Mobile | `1:171` | 390x1438 | `mobile-formularz.jsx` |
|
||||
|
||||
## Kolory (Design Tokens)
|
||||
|
||||
| Nazwa | Hex | Zastosowanie |
|
||||
|-------|-----|-------------|
|
||||
| Carei Blue | `#2F2482` | Primary, tekst, checkboxy, nagłowki |
|
||||
| Carei Red | `#FF0000` | Przyciski CTA, kropka w tytule |
|
||||
| Tło formularza | `#EDEDF3` | Background modala |
|
||||
| Tekst szary | `#505050` | Labele, placeholdery |
|
||||
| Placeholder jasny | `#C7C7C7` | Maska telefonu |
|
||||
| Border input | `#D0D0D0` | Obramowanie checkboxow opcji |
|
||||
| Background strony | `#F8F8F8` | Tło sekcji |
|
||||
|
||||
## Typografia
|
||||
|
||||
- Font primary: **Albert Sans** (Bold, SemiBold, Medium, Regular)
|
||||
- Font system: **Roboto** (separatory sekcji)
|
||||
- Tytul formularza: Albert Sans Bold 20px, `#2F2482`
|
||||
- Labele inputow: Albert Sans Regular 15px, `#505050`
|
||||
- Wartosci inputow: Albert Sans SemiBold 15px, `#2F2482`
|
||||
- Labele male (np. "Od kiedy?"): Albert Sans Regular 12px, `#505050`
|
||||
- Przycisk CTA: Albert Sans SemiBold 14px, white
|
||||
|
||||
## Struktura formularza
|
||||
|
||||
### 1. Naglowek
|
||||
- Tytul: "Wypelnij formularz rezerwacji." (z czerwona kropka)
|
||||
- Przycisk zamkniecia (X) w rogu
|
||||
|
||||
### 2. Dane wynajmu
|
||||
- **Segment pojazdu** - dropdown (np. "Segment B (np. Toyota Yaris)")
|
||||
- **Od kiedy?** - date+time picker (np. "11:00, 28.02.2026")
|
||||
- **Do kiedy?** - date picker z ikona kalendarza
|
||||
- **Wybrano: X dni** - automatyczne podsumowanie
|
||||
- **Miejsce odbioru** - text input z ikona MapPin
|
||||
- **Zwrot w tej samej lokalizacji** - checkbox (domyslnie zaznaczony)
|
||||
|
||||
### 3. Opcje dodatkowe (sekcja z separatorem)
|
||||
- **Rozszerzone ubezpieczenie** - checkbox, 300 zl
|
||||
- Opis: "Obejmuje brak odpowiedzialnosci najemcy za wszelki szkody poniesione na aucie."
|
||||
- **Fotelik dla dziecka** - checkbox, 50 zl
|
||||
- Opis: "Prosimy zawrzec wiek dziecka w wiadomosci."
|
||||
|
||||
### 4. Dane najemcy (sekcja z separatorem)
|
||||
- **Imie** - text input
|
||||
- **Nazwisko** - text input
|
||||
- **Adres e-mail** - text input
|
||||
- **Nr telefonu** - z wyborem flagi kraju (+48), maska "\_\_\_ \_\_\_ \_\_\_"
|
||||
|
||||
### 5. Wiadomosc
|
||||
- **Textarea** - "Twoja wiadomosc dotyczaca rezerwacji" (143px wysokosci)
|
||||
|
||||
### 6. Stopka
|
||||
- **Zgoda na Polityke Prywatnosci** - checkbox z linkiem
|
||||
- **Przycisk "Wyslij"** - czerwony CTA z ikona strzalki
|
||||
|
||||
## Roznice Desktop vs Mobile
|
||||
|
||||
| Element | Desktop (1440px) | Mobile (390px) |
|
||||
|---------|-----------------|----------------|
|
||||
| Layout inputow danych wynajmu | 2-3 kolumny obok siebie | 1 kolumna (full width) |
|
||||
| Imie + Nazwisko | Obok siebie (2x 376px) | Pod soba (full width) |
|
||||
| Email + Telefon | Obok siebie (2x 376px) | Pod soba (full width) |
|
||||
| Opcje dodatkowe | Obok siebie (2x 376px) | Pod soba (full width) |
|
||||
| Przycisk Wyslij | Po prawej obok checkboxa | Full width pod checkboxem |
|
||||
| Dodatkowy element | - | "Infolinia 24h: +48 572 663 614" na dole |
|
||||
| Modal | Wycentrowany overlay | Bottom sheet (od dolu) |
|
||||
|
||||
## Wymiary komponentow
|
||||
|
||||
- Input height: 48px
|
||||
- Textarea height: 143px
|
||||
- Border radius inputow: 8px
|
||||
- Border radius modala: 16px
|
||||
- Padding modala desktop: 48px horizontal, 40px vertical
|
||||
- Padding modala mobile: 24px horizontal, 32px vertical
|
||||
- Gap miedzy sekcjami: 24px
|
||||
- Gap wewnatrz sekcji: 16px
|
||||
- Checkbox size: 24px (border-radius: 8px)
|
||||
- Przycisk CTA: padding 16px 24px, border-radius 8px
|
||||
|
||||
---
|
||||
|
||||
## Popup — rozwinięty formularz (Krok 2)
|
||||
|
||||
Plik Figma node: `32:645` (Overlay, 866x1354)
|
||||
|
||||
### Zrzut ekranu Desktop
|
||||

|
||||
|
||||
### Struktura Popup (rozszerzenia vs Krok 1)
|
||||
|
||||
Popup to rozwinięta wersja formularza po wypełnieniu Kroku 1. Zawiera dodatkowe sekcje:
|
||||
|
||||
#### Wyjazd zagraniczny
|
||||
- Separator: "Wyjazd zagraniczny"
|
||||
- Checkbox: **"Planuję trasę poza granicę Polski"**
|
||||
- Po zaznaczeniu → pojawia się wyszukiwarka krajów (patrz niżej)
|
||||
|
||||
#### Ubezpieczenie
|
||||
- Separator: "Ubezpieczenie"
|
||||
- **Pakiet ochrony Soft** — karta z checkbox, opisem, ceną "od 190 do 800 zł"
|
||||
- **Pakiet ochrony Premium** — karta z checkbox, opisem, ceną "od 200 do 1200 zł"
|
||||
|
||||
#### Opcje dodatkowe (rozbudowane vs Krok 1)
|
||||
- **Dodatkowy kierowca** — 100 zł, z opisem
|
||||
- **Zwiększenie limitu kilometrów** — od 50 do 250 zł, z opisem
|
||||
- **Pełny bak paliwa** — 650 zł
|
||||
- **Nawigacja GPS Europa** — od 50 do 200 zł
|
||||
- **Podstawka dla dziecka** — od 50 do 250 zł
|
||||
- **Fotelik 9-18 kg** — od 50 do 250 zł
|
||||
- **Fotelik 18-36 kg** — od 50 do 250 zł
|
||||
- **Zwrot nieumytego pojazdu** — 290 zł
|
||||
|
||||
#### Dane najemcy + Wiadomość + Stopka
|
||||
Identyczne jak w Kroku 1.
|
||||
|
||||
### Node IDs (Popup)
|
||||
|
||||
| Element | Node ID | Rozmiar |
|
||||
|---------|---------|---------|
|
||||
| Overlay (desktop) | `32:645` | 866x1354 |
|
||||
| POPUP (wewnętrzny) | `32:397` | 866x1316 |
|
||||
| OverlayMobile | `32:650` | 390x1257 |
|
||||
|
||||
---
|
||||
|
||||
## Wyszukiwarka krajów (Input — rozwinięcia formularza)
|
||||
|
||||
Elementy po lewej stronie Popup w Figmie — 3 stany pola "Wyszukaj i dodaj kraj na trasie":
|
||||
|
||||
### Zrzut ekranu — stany wyszukiwarki
|
||||

|
||||
|
||||
### Stan 1: Default (pusty)
|
||||
- Node ID: `122:1091`
|
||||
- Ikona "+" + tekst: **"Wyszukaj i dodaj kraj na trasie"**
|
||||
- Pole jest zwinięte
|
||||
|
||||
### Stan 2: Active (wyszukiwanie)
|
||||
- Node ID: `122:1054`
|
||||
- Pole tekstowe z wpisanym tekstem (np. "N|")
|
||||
- Pod spodem: karty krajów pasujących do frazy (z flagą, nazwą, ceną "od 50 do 250 zł", przycisk "+")
|
||||
- Np. 🇩🇪 Niemcy, 🇳🇴 Norwegia
|
||||
- Drugi stan: po wybraniu (np. "C|") — wybrany kraj (🇩🇪 Niemcy) z "×" do usunięcia, + wyniki wyszukiwania (🇨🇿 Czechy)
|
||||
|
||||
### Stan 3: Default z wybranymi (zwinięty)
|
||||
- Node ID: `123:1195`
|
||||
- Ikona "+" + tekst: "Wyszukaj i dodaj kraj na trasie"
|
||||
- Pod spodem: karty wybranych krajów z flagą, nazwą, ceną i przyciskiem "×" do usunięcia
|
||||
- Np. 🇩🇪 Niemcy od 50 do 250 zł ×, 🇨🇿 Czechy od 50 do 250 zł ×
|
||||
|
||||
### Node IDs (Wyszukiwarka krajów)
|
||||
|
||||
| Stan | Node ID | Opis |
|
||||
|------|---------|------|
|
||||
| Default (pusty) | `122:1091` | Zwinięty, tylko tekst "Wyszukaj i dodaj kraj" |
|
||||
| Active (wyszukiwanie) | `122:1054` | Rozwinięty z wynikami wyszukiwania |
|
||||
| Default (wybrane) | `123:1195` | Zwinięty z kartami wybranych krajów |
|
||||
210
docs/figma-formularz/desktop-formularz.jsx
Normal file
210
docs/figma-formularz/desktop-formularz.jsx
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* Desktop Formularz Rezerwacji - Figma Reference Code
|
||||
* Node ID: 24:1639 | Size: 1440x1024
|
||||
* Figma: https://www.figma.com/design/Q3i1CT4DL91dl638a43RBg/Carei?node-id=24:1639
|
||||
*
|
||||
* UWAGA: To jest kod referencyjny z Figmy (React + Tailwind).
|
||||
* Nalezy go zaadaptowac do stosu technologicznego projektu (WordPress/PHP).
|
||||
*/
|
||||
|
||||
const imgImage = "https://www.figma.com/api/mcp/asset/7d62e9d5-67da-4e89-adb3-68cb959aa552";
|
||||
const imgImage1 = "https://www.figma.com/api/mcp/asset/7c4ee95b-a6ca-4664-b3ae-abed558c8c95";
|
||||
const imgWhiteWagonSpeedsHighwayDaylightEditorialCar1 = "https://www.figma.com/api/mcp/asset/b19f198e-4b71-4e84-afa7-ea4fbef0b1e3";
|
||||
const imgImage4 = "https://www.figma.com/api/mcp/asset/5b1a5a01-782c-4e31-9806-93321460eb13";
|
||||
const imgImage2 = "https://www.figma.com/api/mcp/asset/0d24a24c-d505-4962-85c4-7c5e1a24a8b6";
|
||||
const imgImage3 = "https://www.figma.com/api/mcp/asset/a0471dca-16f4-4142-b0ba-a2d04c7a3d6a";
|
||||
const imgCaretDown = "https://www.figma.com/api/mcp/asset/c71c6991-beac-4fa3-a535-18cf0cf4dea3";
|
||||
const imgLogo = "https://www.figma.com/api/mcp/asset/b679a1c6-68f7-48ea-b3d3-71be81d22dc9";
|
||||
const imgLayer1 = "https://www.figma.com/api/mcp/asset/683370a1-4956-4401-bb45-27619bcb0044";
|
||||
const imgVector = "https://www.figma.com/api/mcp/asset/fdf1c7d3-63c1-4027-ae64-881f582cd8de";
|
||||
const imgRectangle22 = "https://www.figma.com/api/mcp/asset/9ca45d86-0986-4cec-ac47-b0c44074f7dc";
|
||||
const imgLayer2 = "https://www.figma.com/api/mcp/asset/f83a9925-2305-4c3d-93ac-812bc9ad6611";
|
||||
const imgVector1 = "https://www.figma.com/api/mcp/asset/cb4a1e0b-f6ad-47e9-8ed8-b7791c8d5e51";
|
||||
const imgVector2 = "https://www.figma.com/api/mcp/asset/e42ecd5d-274c-44d0-bd30-ab0549312ea6";
|
||||
const imgCaretDown1 = "https://www.figma.com/api/mcp/asset/8e4251ef-1a15-4f22-be18-8f47ad361a22";
|
||||
const imgMapPin = "https://www.figma.com/api/mcp/asset/a20f5eb0-4d70-4fdb-b230-f019fcca8891";
|
||||
const imgCheck = "https://www.figma.com/api/mcp/asset/43d06708-0af0-4d32-be36-f6d6bdcdc6f8";
|
||||
const imgCalendarBlank = "https://www.figma.com/api/mcp/asset/e6e03b59-391e-4785-ad2a-6ede64b251f1";
|
||||
const imgVector3 = "https://www.figma.com/api/mcp/asset/f4e47efe-d2db-408a-bbc2-96e9a092b594";
|
||||
const imgVector4 = "https://www.figma.com/api/mcp/asset/1bc3c7f1-121c-4ff5-8196-cb74bec1d63f";
|
||||
const imgX = "https://www.figma.com/api/mcp/asset/1a74632a-93e6-4953-a509-cdcecd5181f6";
|
||||
|
||||
export default function DesktopFormularz() {
|
||||
return (
|
||||
<div className="bg-white relative size-full" data-name="Desktop Formularz" data-node-id="24:1639">
|
||||
<div className="-translate-x-1/2 absolute bg-[rgba(0,0,0,0.2)] h-[950px] left-1/2 top-[75px] w-[1440px]" data-name="bg" data-node-id="24:2121" />
|
||||
<div className="-translate-x-1/2 -translate-y-1/2 absolute bg-[#ededf3] content-stretch flex flex-col gap-[24px] items-start justify-center left-1/2 overflow-clip px-[48px] py-[40px] rounded-[16px] top-[calc(50%+24.5px)]" data-node-id="24:2123">
|
||||
<p className="font-['Albert_Sans:Bold',sans-serif] font-bold leading-[0] min-w-full relative shrink-0 text-[#2f2482] text-[20px] text-center w-[min-content]" data-node-id="24:2124">
|
||||
<span className="leading-[32px]">Wypełnij formularz rezerwacji</span>
|
||||
<span className="leading-[32px] text-[red]">.</span>
|
||||
</p>
|
||||
<div className="grid-rows-[max-content] inline-grid leading-[0] place-items-start relative shrink-0 w-full" data-name="Dane wynajmu" data-node-id="24:2125">
|
||||
<div className="bg-white col-1 h-[48px] ml-0 mt-0 relative rounded-[8px] row-1 w-[48.83%]" data-name="Input" data-node-id="24:2126">
|
||||
<p className="absolute font-['Albert_Sans:Medium',sans-serif] font-medium leading-[0] left-[16px] text-[#2f2482] text-[0px] top-[12px] whitespace-nowrap" data-node-id="24:2127">
|
||||
<span className="font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[24px] text-[15px]">Segment B</span>
|
||||
<span className="font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] text-[#505050] text-[15px]">{` (np. Toyta Yaris)`}</span>
|
||||
</p>
|
||||
<div className="-translate-y-1/2 absolute right-[12px] size-[16px] top-1/2" data-name="CaretDown" data-node-id="24:2128">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgCaretDown1} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white col-1 h-[48px] ml-0 mt-[80px] relative rounded-[8px] row-1 w-[48.83%]" data-name="Input" data-node-id="24:2131">
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] left-[36px] text-[#505050] text-[15px] top-[12px] whitespace-nowrap" data-node-id="24:2132">
|
||||
Miejsce odbioru
|
||||
</p>
|
||||
<div className="absolute left-[12px] size-[16px] top-[16px]" data-name="MapPin" data-node-id="24:2234">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgMapPin} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-1 content-stretch flex gap-[12px] items-center ml-[51.69%] mt-[92px] relative row-1 w-[30.930000000000003%]" data-node-id="24:2136">
|
||||
<div className="bg-[#2f2482] overflow-clip relative rounded-[8px] shrink-0 size-[24px]" data-node-id="24:2137">
|
||||
<div className="-translate-x-1/2 -translate-y-1/2 absolute left-1/2 size-[12px] top-1/2" data-name="Check" data-node-id="24:2138">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgCheck} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[24px] relative shrink-0 text-[#2f2482] text-[14px] whitespace-nowrap" data-node-id="24:2141">
|
||||
Zwrot w tej samej lokalizacji
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white col-1 h-[48px] ml-[75.97%] mt-0 relative rounded-[8px] row-1 w-[24.03%]" data-name="Input" data-node-id="24:2142">
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] left-[36px] text-[#505050] text-[15px] top-[calc(50%-12px)] whitespace-nowrap" data-node-id="24:2143">
|
||||
Do kiedy?
|
||||
</p>
|
||||
<div className="absolute left-[12px] size-[16px] top-[16px]" data-name="CalendarBlank" data-node-id="24:2144">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgCalendarBlank} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white col-1 h-[48px] leading-[24px] ml-[50.91%] mt-0 relative rounded-[8px] row-1 w-[24.030000000000005%] whitespace-nowrap" data-name="Input" data-node-id="24:2150">
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold left-[16px] text-[#2f2482] text-[15px] top-[calc(50%-5px)] tracking-[-0.3px]" data-node-id="24:2151">
|
||||
11:00, 28.02.2026
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal left-[16px] text-[#505050] text-[12px] top-[calc(50%-22px)]" data-node-id="24:2152">
|
||||
Od kiedy?
|
||||
</p>
|
||||
</div>
|
||||
<p className="col-1 font-['Albert_Sans:SemiBold',sans-serif] font-semibold ml-[85.58%] mt-[52px] relative row-1 text-[#2f2482] text-[0px] text-center w-[14.410000000000002%]" data-node-id="24:2153">
|
||||
<span className="font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] text-[#505050] text-[15px]">Wybrano:</span>
|
||||
<span className="leading-[24px] text-[14px]">{` 3 dni`}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="content-stretch flex flex-col gap-[16px] items-start relative shrink-0 w-full" data-name="Opcje dodatkowe" data-node-id="24:2154">
|
||||
<div className="grid-cols-[max-content] grid-rows-[max-content] inline-grid leading-[0] place-items-start relative shrink-0" data-name="Separator" data-node-id="24:2155">
|
||||
<div className="col-1 h-0 ml-0 mt-[8px] relative row-1 w-[770px]" data-name="Vector" data-node-id="24:2156">
|
||||
<div className="absolute inset-[-0.5px_0]">
|
||||
<img alt="" className="block max-w-none size-full" src={imgVector3} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#ededf3] col-1 content-stretch flex items-center justify-center ml-[314.5px] mt-0 overflow-clip px-[24px] relative row-1" data-node-id="24:2157">
|
||||
<p className="font-['Roboto:Regular',sans-serif] font-normal leading-[16px] relative shrink-0 text-[12px] text-[rgba(0,0,0,0.24)] text-center whitespace-nowrap" data-node-id="24:2158">
|
||||
Opcje dodatkowe
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="content-stretch flex gap-[16px] items-start relative shrink-0 w-full" data-name="Opcje" data-node-id="24:2159">
|
||||
<div className="bg-white h-[118px] overflow-clip relative rounded-[8px] shrink-0 w-[376px]" data-name="Opcja dodatkowa" data-node-id="24:2160">
|
||||
<div className="absolute border border-[#d0d0d0] border-solid left-[16px] rounded-[8px] size-[24px] top-[16px]" data-node-id="24:2161" />
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[20px] left-[56px] text-[#2f2482] text-[15px] top-[calc(50%-43px)] whitespace-nowrap" data-node-id="24:2162">
|
||||
Rozszerzone ubezpieczenie
|
||||
</p>
|
||||
<p className="absolute font-['Roboto:Regular',sans-serif] font-normal h-[34px] leading-[16px] left-[56px] text-[#505050] text-[12px] top-[40px] w-[262px]" data-node-id="24:2163">
|
||||
Obejmuje brak odpowiedzialności najemcy za wszelki szkody poniesione na aucie.
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[24px] left-[56px] text-[#2f2482] text-[14px] top-[78px] whitespace-nowrap" data-node-id="24:2164">
|
||||
300 zł
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white h-[118px] overflow-clip relative rounded-[8px] shrink-0 w-[376px]" data-name="Opcja dodatkowa" data-node-id="24:2165">
|
||||
<div className="absolute border border-[#d0d0d0] border-solid left-[16px] rounded-[8px] size-[24px] top-[16px]" data-node-id="24:2166" />
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[20px] left-[56px] text-[#2f2482] text-[15px] top-[calc(50%-43px)] w-[262px]" data-node-id="24:2167">
|
||||
Fotelik dla dziecka
|
||||
</p>
|
||||
<p className="absolute font-['Roboto:Regular',sans-serif] font-normal leading-[16px] left-[56px] text-[#505050] text-[12px] top-[40px] w-[262px]" data-node-id="24:2168">
|
||||
Prosimy zawrzeć wiek dziecka w wiadomości.
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[24px] left-[56px] text-[#2f2482] text-[14px] top-[78px] whitespace-nowrap" data-node-id="24:2169">
|
||||
50 zł
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="content-stretch flex flex-col gap-[16px] items-start relative shrink-0 w-full" data-name="Dane najemcy" data-node-id="24:2170">
|
||||
<div className="grid-cols-[max-content] grid-rows-[max-content] inline-grid leading-[0] place-items-start relative shrink-0" data-name="Separator" data-node-id="24:2171">
|
||||
<div className="col-1 h-0 ml-0 mt-[8px] relative row-1 w-[770px]" data-name="Vector" data-node-id="24:2172">
|
||||
<div className="absolute inset-[-0.5px_0]">
|
||||
<img alt="" className="block max-w-none size-full" src={imgVector3} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-[#ededf3] col-1 content-stretch flex items-center justify-center ml-[323px] mt-0 overflow-clip px-[24px] relative row-1" data-node-id="24:2173">
|
||||
<p className="font-['Roboto:Regular',sans-serif] font-normal leading-[16px] relative shrink-0 text-[12px] text-[rgba(0,0,0,0.24)] text-center whitespace-nowrap" data-node-id="24:2174">
|
||||
Dane najemcy
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="content-start flex flex-wrap gap-[16px] items-start relative shrink-0 w-full" data-node-id="24:2220">
|
||||
<div className="bg-white h-[48px] leading-[24px] relative rounded-[8px] shrink-0 w-[376px] whitespace-nowrap" data-name="Input" data-node-id="24:2175">
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold left-[16px] text-[#2f2482] text-[15px] top-[calc(50%-5px)]" data-node-id="24:2176">
|
||||
Daria
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal left-[16px] text-[#505050] text-[12px] top-[calc(50%-22px)]" data-node-id="24:2177">
|
||||
Imię
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white h-[48px] leading-[24px] relative rounded-[8px] shrink-0 w-[376px] whitespace-nowrap" data-name="Input" data-node-id="24:2226">
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold left-[16px] text-[#2f2482] text-[15px] top-[calc(50%-5px)]" data-node-id="24:2227">
|
||||
Pyziak
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal left-[16px] text-[#505050] text-[12px] top-[calc(50%-22px)]" data-node-id="24:2228">
|
||||
Nazwisko
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white h-[48px] relative rounded-[8px] shrink-0 w-[376px]" data-name="Input" data-node-id="24:2178">
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] left-[16px] text-[#505050] text-[15px] top-[calc(50%-12px)] whitespace-nowrap" data-node-id="24:2179">
|
||||
Adres e-mail
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white h-[48px] relative rounded-[8px] shrink-0 w-[376px]" data-name="Input" data-node-id="24:2180">
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[24px] left-[72px] text-[#2f2482] text-[15px] top-[calc(50%-5px)] whitespace-nowrap" data-node-id="24:2181">
|
||||
+48
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] left-[72px] text-[#505050] text-[12px] top-[calc(50%-22px)] w-[62px]" data-node-id="24:2182">
|
||||
Nr telefonu
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] left-[109px] text-[#c7c7c7] text-[15px] top-[calc(50%-5px)] tracking-[2.85px] whitespace-nowrap" data-node-id="24:2183">
|
||||
___ ___ ___
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white h-[143px] relative rounded-[8px] shrink-0 w-full" data-name="Input" data-node-id="24:2191">
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] left-[16px] text-[#505050] text-[15px] top-[calc(50%-59.5px)] whitespace-nowrap" data-node-id="24:2192">
|
||||
Twoja wiadomość dotycząca rezerwacji
|
||||
</p>
|
||||
</div>
|
||||
<div className="content-stretch flex items-center justify-between relative shrink-0 w-full" data-node-id="24:2221">
|
||||
<div className="content-stretch flex gap-[12px] items-center relative shrink-0" data-node-id="24:2193">
|
||||
<div className="bg-[#2f2482] overflow-clip relative rounded-[8px] shrink-0 size-[24px]" data-node-id="24:2194">
|
||||
<div className="-translate-x-1/2 -translate-y-1/2 absolute left-1/2 size-[12px] top-1/2" data-name="Check" data-node-id="24:2195">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgCheck} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[0] relative shrink-0 text-[#2f2482] text-[14px] whitespace-nowrap" data-node-id="24:2198">
|
||||
<span className="leading-[24px]">{`Zgadzam się na `}</span>
|
||||
<span className="[text-decoration-skip-ink:none] decoration-solid leading-[24px] underline">Politykę Prywatności</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-[red] content-stretch flex gap-[10px] items-center justify-center overflow-clip px-[24px] py-[16px] relative rounded-[8px] shrink-0 w-[238px]" data-name="Button A Black" data-node-id="24:2199">
|
||||
<div className="h-[11.998px] relative shrink-0 w-[12px]" data-name="Vector" data-node-id="24:2200">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgVector} />
|
||||
</div>
|
||||
<div className="flex flex-col font-['Albert_Sans:SemiBold',sans-serif] font-semibold justify-center leading-[0] relative shrink-0 text-[14px] text-white whitespace-nowrap" data-node-id="24:2201">
|
||||
<p className="leading-[14px]">Wyślij</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute left-[24px] size-[24px] top-[24px]" data-name="X" data-node-id="32:170">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgX} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
89
docs/figma-formularz/input-wyszukiwarka-krajow.jsx
Normal file
89
docs/figma-formularz/input-wyszukiwarka-krajow.jsx
Normal file
@@ -0,0 +1,89 @@
|
||||
// Figma Design Context: Wyszukiwarka krajów (Input — stany)
|
||||
// Node ID: 122:1054 (Active/Wyszukiwanie), 122:1091 (Default pusty), 123:1195 (Default wybrane)
|
||||
// Pobrano: 2026-03-25
|
||||
// UWAGA: Kod referencyjny React+Tailwind z Figma MCP — do adaptacji na PHP/HTML/CSS
|
||||
|
||||
const imgImage1 = "https://www.figma.com/api/mcp/asset/08457e8b-11f6-4194-b0a2-a5b2c900a505"; // Flaga Niemcy
|
||||
const imgImage2 = "https://www.figma.com/api/mcp/asset/54e5a65c-6ca3-4749-89cf-12d9651f8a07"; // Flaga Norwegia
|
||||
const imgImage3 = "https://www.figma.com/api/mcp/asset/dff6d6a5-6c45-46ef-9ed5-50ca45e0aef8"; // Flaga Czechy
|
||||
const imgPlus = "https://www.figma.com/api/mcp/asset/f6b0c0c6-ab8c-4156-9e90-650bb559fa2f";
|
||||
const imgVector11 = "https://www.figma.com/api/mcp/asset/a858c025-8ee0-4a0b-ad87-7a13a6a57d21";
|
||||
const imgPlus1 = "https://www.figma.com/api/mcp/asset/07f69e9c-5ea5-4b67-b4e8-cc09e619e1fe"; // Plus (dodaj)
|
||||
const imgPlus2 = "https://www.figma.com/api/mcp/asset/1f56ab37-6765-401c-807e-dedef33f0056"; // X (usuń)
|
||||
const imgPlus3 = "https://www.figma.com/api/mcp/asset/9b64413d-57e9-4917-87d9-e158e3266e16";
|
||||
|
||||
/*
|
||||
* STANY WYSZUKIWARKI KRAJÓW:
|
||||
*
|
||||
* Stan 1 (Default pusty, 122:1091):
|
||||
* - Ikona "+" + "Wyszukaj i dodaj kraj na trasie"
|
||||
* - Pole zwinięte, 770x48
|
||||
*
|
||||
* Stan 2 (Active/Wyszukiwanie, 122:1054):
|
||||
* - Pole tekstowe z wpisanym tekstem (np. "N|")
|
||||
* - Separator linia
|
||||
* - Pod spodem: karty krajów pasujących do frazy
|
||||
* - Karta: Flaga (24px, rounded-full) + Nazwa + "od 50 do 250 zł" + "+" (dodaj)
|
||||
* - 2 kolumny obok siebie (363px each)
|
||||
* - Drugi wariant: po wybraniu jednego (np. "C|"):
|
||||
* - Wybrany kraj: bg rgba(47,36,130,0.05), border rgba(47,36,130,0.2), ikona "×"
|
||||
* - Wyniki: normalne karty z "+"
|
||||
*
|
||||
* Stan 3 (Default wybrane, 123:1195):
|
||||
* - Ikona "+" + "Wyszukaj i dodaj kraj na trasie"
|
||||
* - Pod spodem: karty wybranych krajów z "×" do usunięcia
|
||||
* - bg-white, border rgba(0,0,0,0.1), 770x128 total
|
||||
*
|
||||
* KARTY KRAJÓW:
|
||||
* - Wymiar: 363x56
|
||||
* - Flaga: 24px okrągła (overflow hidden, border #d3d3d3)
|
||||
* - Nazwa: Albert Sans SemiBold 15px, #2F2482
|
||||
* - Cena: "od 50 do 250 zł" — 10px "od"/"do", 14px wartości, #505050
|
||||
* - Akcja: "+" (dodaj) lub "×" (usuń)
|
||||
* - Wybrany: bg rgba(47,36,130,0.05), border rgba(47,36,130,0.2)
|
||||
*/
|
||||
|
||||
export default function Input() {
|
||||
return (
|
||||
<div className="bg-white relative rounded-[8px] size-full" data-name="Input" data-node-id="122:1054">
|
||||
<div className="absolute content-stretch flex gap-[8px] items-center left-[16px] top-[12px]" data-node-id="122:1055">
|
||||
<div className="relative shrink-0 size-[16px]" data-name="Plus" data-node-id="122:1056">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgPlus} />
|
||||
</div>
|
||||
<p className="font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[0] relative shrink-0 text-[#2f2482] text-[15px] tracking-[-0.3px] whitespace-nowrap" data-node-id="122:1060">
|
||||
<span className="leading-[24px]">N</span>
|
||||
<span className="leading-[24px] text-[#505050]">|</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="absolute bottom-[80px] h-0 left-[16px] w-[738px]" data-node-id="122:1061">
|
||||
<div className="absolute inset-[-0.5px_0]">
|
||||
<img alt="" className="block max-w-none size-full" src={imgVector11} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Karta: Niemcy */}
|
||||
<div className="absolute border border-[rgba(0,0,0,0.1)] border-solid h-[56px] left-[16px] overflow-clip rounded-[8px] top-[60px] w-[363px]" data-name="Opcja dodatkowa" data-node-id="122:1104">
|
||||
<p className="absolute font-semibold left-[43.5px] text-[#2f2482] text-[15px] top-[calc(50%-10px)]">Niemcy</p>
|
||||
<p className="absolute left-[310.5px] text-[#505050] text-right top-[15.5px]">
|
||||
<span className="text-[10px]">od </span><span className="text-[14px]">50 </span>
|
||||
<span className="text-[10px]">do </span><span className="text-[14px]">250 zł</span>
|
||||
</p>
|
||||
<div className="absolute left-[11.5px] overflow-clip rounded-full size-[24px] top-[15.5px]" data-name="Flag">
|
||||
<img alt="Niemcy" src={imgImage1} />
|
||||
</div>
|
||||
<div className="absolute left-[330.5px] size-[16px] top-[19.5px]" data-name="Plus">
|
||||
<img alt="Dodaj" src={imgPlus1} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Karta: Norwegia */}
|
||||
<div className="absolute border border-[rgba(0,0,0,0.1)] border-solid h-[56px] left-[391px] overflow-clip rounded-[8px] top-[60px] w-[363px]" data-name="Opcja dodatkowa" data-node-id="122:1114">
|
||||
<p className="absolute font-semibold left-[43.5px] text-[#2f2482] text-[15px] top-[calc(50%-10px)]">Norwegia</p>
|
||||
<div className="absolute left-[11.5px] overflow-clip rounded-full size-[24px] top-[15.5px]" data-name="Flag">
|
||||
<img alt="Norwegia" src={imgImage2} />
|
||||
</div>
|
||||
<div className="absolute left-[330.5px] size-[16px] top-[19.5px]" data-name="Plus">
|
||||
<img alt="Dodaj" src={imgPlus1} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
177
docs/figma-formularz/mobile-formularz.jsx
Normal file
177
docs/figma-formularz/mobile-formularz.jsx
Normal file
@@ -0,0 +1,177 @@
|
||||
/**
|
||||
* Mobile Formularz Rezerwacji - Figma Reference Code
|
||||
* Node ID: 1:171 | Size: 390x1438
|
||||
* Figma: https://www.figma.com/design/Q3i1CT4DL91dl638a43RBg/Carei?node-id=1:171
|
||||
*
|
||||
* UWAGA: To jest kod referencyjny z Figmy (React + Tailwind).
|
||||
* Nalezy go zaadaptowac do stosu technologicznego projektu (WordPress/PHP).
|
||||
*/
|
||||
|
||||
const imgImage1 = "https://www.figma.com/api/mcp/asset/d7462c8b-c33e-4426-addc-6aa7121245be";
|
||||
const imgCaretDown = "https://www.figma.com/api/mcp/asset/22507870-d317-4bee-bc66-02b7e215ec0d";
|
||||
const imgList = "https://www.figma.com/api/mcp/asset/bf6515df-4385-405a-8211-f508bacdd20a";
|
||||
const imgLogo = "https://www.figma.com/api/mcp/asset/5d5683fa-8bea-4a2b-9dae-faffa5b81b1b";
|
||||
const imgVector = "https://www.figma.com/api/mcp/asset/54b76a10-8f4c-4661-8579-bbcd00e7976e";
|
||||
const imgCaretDown2 = "https://www.figma.com/api/mcp/asset/623eeea2-80e1-4a81-acde-9d8df8732678";
|
||||
const imgCheck = "https://www.figma.com/api/mcp/asset/b452ba20-ab69-4d6d-a006-1763b04e4ca1";
|
||||
const imgCalendarBlank = "https://www.figma.com/api/mcp/asset/3d81b9b9-8c42-47e9-812f-7568c688cdb3";
|
||||
const imgMapPin = "https://www.figma.com/api/mcp/asset/19dfc075-5ce6-4084-9a95-1f1904c56643";
|
||||
const imgVector1 = "https://www.figma.com/api/mcp/asset/c5011a40-85ba-420b-a94c-f3a3ba5dbe89";
|
||||
const imgVector2 = "https://www.figma.com/api/mcp/asset/f5f57fd8-7758-4c5e-a232-478f70ec4924";
|
||||
const imgX = "https://www.figma.com/api/mcp/asset/81fbb5d4-04d5-4b57-bbed-f0249d23800a";
|
||||
|
||||
export default function MobileFormularz() {
|
||||
return (
|
||||
<div className="bg-white relative size-full" data-name="Mobile Formularz" data-node-id="1:171">
|
||||
<div className="-translate-x-1/2 absolute bg-[rgba(0,0,0,0.2)] h-[1479px] left-1/2 top-[-2px] w-[390px]" data-name="bg" data-node-id="2:1158" />
|
||||
<div className="absolute bg-[#ededf3] content-stretch flex flex-col gap-[24px] h-[1257px] items-start justify-center left-0 overflow-clip px-[24px] py-[32px] rounded-tl-[16px] rounded-tr-[16px] top-[181px] w-[390px]" data-node-id="2:1160">
|
||||
<p className="font-['Albert_Sans:Bold',sans-serif] font-bold leading-[0] min-w-full relative shrink-0 text-[#2f2482] text-[20px] w-[min-content]" data-node-id="2:1175">
|
||||
<span className="leading-[32px]">Wypełnij formularz rezerwacji</span>
|
||||
<span className="leading-[32px] text-[red]">.</span>
|
||||
</p>
|
||||
<div className="grid-cols-[max-content] grid-rows-[max-content] inline-grid leading-[0] place-items-start relative shrink-0" data-name="Dane wynajmu" data-node-id="10:751">
|
||||
<div className="bg-white col-1 h-[48px] ml-0 mt-0 relative rounded-[8px] row-1 w-[342px]" data-name="Input" data-node-id="4:76">
|
||||
<p className="absolute font-['Albert_Sans:Medium',sans-serif] font-medium leading-[0] left-[16px] text-[#2f2482] text-[0px] top-[12px] whitespace-nowrap" data-node-id="2:1165">
|
||||
<span className="font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[24px] text-[15px]">Segment B</span>
|
||||
<span className="font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] text-[#505050] text-[15px]">{` (np. Toyta Yaris)`}</span>
|
||||
</p>
|
||||
<div className="-translate-y-1/2 absolute right-[12px] size-[16px] top-1/2" data-name="CaretDown" data-node-id="4:78">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgCaretDown2} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-white col-1 h-[48px] leading-[24px] ml-0 mt-[64px] relative rounded-[8px] row-1 w-[167px] whitespace-nowrap" data-name="Input" data-node-id="4:351">
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold left-[16px] text-[#2f2482] text-[15px] top-[calc(50%-5px)] tracking-[-0.3px]" data-node-id="4:352">
|
||||
11:00, 28.02.2026
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal left-[16px] text-[#505050] text-[12px] top-[calc(50%-22px)]" data-node-id="4:353">
|
||||
Od kiedy?
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white col-1 h-[48px] ml-[175px] mt-[64px] relative rounded-[8px] row-1 w-[167px]" data-name="Input" data-node-id="4:343">
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] left-[36px] text-[#505050] text-[15px] top-[calc(50%-12px)] whitespace-nowrap" data-node-id="4:344">
|
||||
Do kiedy?
|
||||
</p>
|
||||
<div className="absolute left-[12px] size-[16px] top-[16px]" data-name="CalendarBlank" data-node-id="4:345">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgCalendarBlank} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="col-1 font-['Albert_Sans:SemiBold',sans-serif] font-semibold ml-[120px] mt-[120px] relative row-1 text-[#2f2482] text-[0px] text-center whitespace-nowrap" data-node-id="2:1222">
|
||||
<span className="font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] text-[#505050] text-[15px]">Wybrano:</span>
|
||||
<span className="leading-[24px] text-[14px]">{` 3 dni`}</span>
|
||||
</p>
|
||||
<div className="bg-white col-1 h-[48px] ml-0 mt-[156px] relative rounded-[8px] row-1 w-[342px]" data-name="Input" data-node-id="24:2246">
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] left-[36px] text-[#505050] text-[15px] top-[12px] whitespace-nowrap" data-node-id="24:2247">
|
||||
Miejsce odbioru
|
||||
</p>
|
||||
<div className="absolute left-[12px] size-[16px] top-[16px]" data-name="MapPin" data-node-id="24:2248">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgMapPin} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-1 content-stretch flex gap-[12px] items-center ml-0 mt-[220px] relative row-1" data-node-id="4:154">
|
||||
<div className="bg-[#2f2482] overflow-clip relative rounded-[8px] shrink-0 size-[24px]" data-node-id="4:110">
|
||||
<div className="-translate-x-1/2 -translate-y-1/2 absolute left-1/2 size-[12px] top-1/2" data-name="Check" data-node-id="4:111">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgCheck} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[24px] relative shrink-0 text-[#2f2482] text-[14px] whitespace-nowrap" data-node-id="4:109">
|
||||
Zwrot w tej samej lokalizacji
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="content-stretch flex flex-col gap-[16px] items-start relative shrink-0 w-full" data-name="Opcje dodatkowe" data-node-id="10:749">
|
||||
<div className="content-stretch flex flex-col gap-[12px] items-start relative shrink-0 w-full" data-name="Opcje" data-node-id="10:748">
|
||||
<div className="bg-white h-[118px] overflow-clip relative rounded-[8px] shrink-0 w-full" data-name="Opcja dodatkowa" data-node-id="10:734">
|
||||
<div className="absolute border border-[#d0d0d0] border-solid left-[16px] rounded-[8px] size-[24px] top-[16px]" data-node-id="10:735" />
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[20px] left-[56px] text-[#2f2482] text-[15px] top-[calc(50%-43px)] whitespace-nowrap" data-node-id="10:736">
|
||||
Rozszerzone ubezpieczenie
|
||||
</p>
|
||||
<p className="absolute font-['Roboto:Regular',sans-serif] font-normal h-[34px] leading-[16px] left-[56px] text-[#505050] text-[12px] top-[40px] w-[262px]" data-node-id="10:737">
|
||||
Obejmuje brak odpowiedzialności najemcy za wszelki szkody poniesione na aucie.
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[24px] left-[56px] text-[#2f2482] text-[14px] top-[78px] whitespace-nowrap" data-node-id="10:738">
|
||||
300 zł
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white h-[104px] overflow-clip relative rounded-[8px] shrink-0 w-full" data-name="Opcja dodatkowa" data-node-id="10:740">
|
||||
<div className="absolute border border-[#d0d0d0] border-solid left-[16px] rounded-[8px] size-[24px] top-[16px]" data-node-id="10:741" />
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[20px] left-[56px] text-[#2f2482] text-[15px] top-[calc(50%-36px)] w-[262px]" data-node-id="10:742">
|
||||
Fotelik dla dziecka
|
||||
</p>
|
||||
<p className="absolute font-['Roboto:Regular',sans-serif] font-normal leading-[16px] left-[56px] text-[#505050] text-[12px] top-[40px] w-[262px]" data-node-id="10:743">
|
||||
Prosimy zawrzeć wiek dziecka w wiadomości.
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[24px] left-[56px] text-[#2f2482] text-[14px] top-[64px] whitespace-nowrap" data-node-id="10:744">
|
||||
50 zł
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="content-stretch flex flex-col gap-[16px] items-start relative shrink-0 w-full" data-name="Dane najemcy" data-node-id="10:750">
|
||||
<div className="bg-white h-[48px] leading-[24px] relative rounded-[8px] shrink-0 w-full whitespace-nowrap" data-name="Input" data-node-id="4:115">
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold left-[16px] text-[#2f2482] text-[15px] top-[calc(50%-5px)]" data-node-id="4:116">
|
||||
Daria
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal left-[16px] text-[#505050] text-[12px] top-[calc(50%-22px)]" data-node-id="4:117">
|
||||
Imię
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white h-[48px] leading-[24px] relative rounded-[8px] shrink-0 w-full whitespace-nowrap" data-name="Input" data-node-id="24:2222">
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold left-[16px] text-[#2f2482] text-[15px] top-[calc(50%-5px)]" data-node-id="24:2223">
|
||||
Pyziak
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal left-[16px] text-[#505050] text-[12px] top-[calc(50%-22px)]" data-node-id="24:2224">
|
||||
Nazwisko
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white h-[48px] relative rounded-[8px] shrink-0 w-full" data-name="Input" data-node-id="4:124">
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] left-[16px] text-[#505050] text-[15px] top-[calc(50%-12px)] whitespace-nowrap" data-node-id="4:125">
|
||||
Adres e-mail
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white h-[48px] relative rounded-[8px] shrink-0 w-full" data-name="Input" data-node-id="4:127">
|
||||
<p className="absolute font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[24px] left-[72px] text-[#2f2482] text-[15px] top-[calc(50%-5px)] whitespace-nowrap" data-node-id="4:128">
|
||||
+48
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] left-[72px] text-[#505050] text-[12px] top-[calc(50%-22px)] w-[62px]" data-node-id="4:129">
|
||||
Nr telefonu
|
||||
</p>
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] left-[109px] text-[#c7c7c7] text-[15px] top-[calc(50%-5px)] tracking-[2.85px] whitespace-nowrap" data-node-id="4:131">
|
||||
___ ___ ___
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white h-[143px] relative rounded-[8px] shrink-0 w-full" data-name="Input" data-node-id="4:151">
|
||||
<p className="absolute font-['Albert_Sans:Regular',sans-serif] font-normal leading-[24px] left-[16px] text-[#505050] text-[15px] top-[calc(50%-59.5px)] whitespace-nowrap" data-node-id="4:152">
|
||||
Twoja wiadomość dotycząca rezerwacji
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="content-stretch flex gap-[12px] items-center relative shrink-0" data-node-id="4:155">
|
||||
<div className="bg-[#2f2482] overflow-clip relative rounded-[8px] shrink-0 size-[24px]" data-node-id="4:156">
|
||||
<div className="-translate-x-1/2 -translate-y-1/2 absolute left-1/2 size-[12px] top-1/2" data-name="Check" data-node-id="4:157">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgCheck} />
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-['Albert_Sans:SemiBold',sans-serif] font-semibold leading-[0] relative shrink-0 text-[#2f2482] text-[14px] whitespace-nowrap" data-node-id="4:160">
|
||||
<span className="leading-[24px]">{`Zgadzam się na `}</span>
|
||||
<span className="[text-decoration-skip-ink:none] decoration-solid leading-[24px] underline">Politykę Prywatności</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-[red] content-stretch flex gap-[10px] items-center justify-center overflow-clip px-[24px] py-[16px] relative rounded-[8px] shrink-0 w-full" data-name="Button A Black" data-node-id="2:1264">
|
||||
<div className="h-[11.998px] relative shrink-0 w-[12px]" data-name="Vector" data-node-id="2:1265">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgVector} />
|
||||
</div>
|
||||
<div className="flex flex-col font-['Albert_Sans:SemiBold',sans-serif] font-semibold justify-center leading-[0] relative shrink-0 text-[14px] text-white whitespace-nowrap" data-node-id="2:1266">
|
||||
<p className="leading-[14px]">Wyślij</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="font-['Albert_Sans:Medium',sans-serif] font-medium leading-[0] min-w-full relative shrink-0 text-[#505050] text-[14px] text-center w-[min-content] whitespace-pre-wrap" data-node-id="4:165">
|
||||
<span className="leading-[16px]">{`Infolinia 24h: `}</span>
|
||||
<span className="[text-decoration-skip-ink:none] decoration-solid leading-[16px] underline">+48 572 663 614</span>
|
||||
</p>
|
||||
<div className="absolute right-[24px] size-[24px] top-[19px]" data-name="X" data-node-id="32:175">
|
||||
<img alt="" className="absolute block max-w-none size-full" src={imgX} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
58
docs/figma-formularz/node-ids.md
Normal file
58
docs/figma-formularz/node-ids.md
Normal file
@@ -0,0 +1,58 @@
|
||||
# Figma Node IDs - Formularz rezerwacji
|
||||
|
||||
File key: `Q3i1CT4DL91dl638a43RBg`
|
||||
|
||||
## Glowne frame'y
|
||||
| Element | Node ID | Opis |
|
||||
|---------|---------|------|
|
||||
| Desktop Formularz | `24:1639` | Pelna strona desktop z modalem formularza |
|
||||
| Mobile Formularz | `1:171` | Pelna strona mobile z bottom-sheet formularza |
|
||||
|
||||
## Mniejsze komponenty "Form" (wewnatrz innych stron)
|
||||
| Node ID | Rozmiar | Kontekst |
|
||||
|---------|---------|----------|
|
||||
| `24:1583` | 422x422 | Form component |
|
||||
| `25:3518` | 422x422 | Form component |
|
||||
| `25:3615` | 422x422 | Form component |
|
||||
| `25:3992` | 422x422 | Form component |
|
||||
| `25:4470` | 422x422 | Form component |
|
||||
| `24:2309` | 422x422 | Form component |
|
||||
| `25:3240` | 422x422 | Form component |
|
||||
| `24:1487` | 422x422 | Form component |
|
||||
|
||||
## Kluczowe node ID wewnatrz Desktop Formularz (24:1639)
|
||||
|
||||
### Dane wynajmu (24:2125)
|
||||
| Element | Node ID |
|
||||
|---------|---------|
|
||||
| Segment dropdown | `24:2126` |
|
||||
| Od kiedy? (filled) | `24:2150` |
|
||||
| Do kiedy? | `24:2142` |
|
||||
| Wybrano: X dni | `24:2153` |
|
||||
| Miejsce odbioru | `24:2131` |
|
||||
| Checkbox zwrot lokalizacja | `24:2136` |
|
||||
|
||||
### Opcje dodatkowe (24:2154)
|
||||
| Element | Node ID |
|
||||
|---------|---------|
|
||||
| Separator | `24:2155` |
|
||||
| Rozszerzone ubezpieczenie | `24:2160` |
|
||||
| Fotelik dla dziecka | `24:2165` |
|
||||
|
||||
### Dane najemcy (24:2170)
|
||||
| Element | Node ID |
|
||||
|---------|---------|
|
||||
| Separator | `24:2171` |
|
||||
| Imie | `24:2175` |
|
||||
| Nazwisko | `24:2226` |
|
||||
| Email | `24:2178` |
|
||||
| Telefon | `24:2180` |
|
||||
|
||||
### Pozostale
|
||||
| Element | Node ID |
|
||||
|---------|---------|
|
||||
| Textarea wiadomosc | `24:2191` |
|
||||
| Checkbox prywatnosc | `24:2193` |
|
||||
| Przycisk Wyslij | `24:2199` |
|
||||
| Przycisk X (zamknij) | `32:170` |
|
||||
| Background overlay | `24:2121` |
|
||||
50
docs/figma-formularz/popup-overlay.jsx
Normal file
50
docs/figma-formularz/popup-overlay.jsx
Normal file
@@ -0,0 +1,50 @@
|
||||
// Figma Design Context: Popup/Overlay (Krok 2 formularza)
|
||||
// Node ID: 32:645 (Overlay), 32:397 (POPUP)
|
||||
// Rozmiar: 866x1354
|
||||
// Pobrano: 2026-03-25
|
||||
// UWAGA: Kod referencyjny React+Tailwind z Figma MCP — do adaptacji na PHP/HTML/CSS
|
||||
|
||||
const imgImage1 = "https://www.figma.com/api/mcp/asset/8a38c799-dead-4b1d-8ff0-d00597dfea2c";
|
||||
const imgCaretDown = "https://www.figma.com/api/mcp/asset/a349db71-cdd5-4d4a-9807-28cf459feb32";
|
||||
const imgMapPin = "https://www.figma.com/api/mcp/asset/9b1a041f-b48d-40f5-b2cd-43b716da2966";
|
||||
const imgCheck = "https://www.figma.com/api/mcp/asset/6cb28b8a-fbee-4804-94a0-bb2819214249";
|
||||
const imgCalendarBlank = "https://www.figma.com/api/mcp/asset/4b9d051a-4eab-465b-b264-8526c710b149";
|
||||
const imgVector = "https://www.figma.com/api/mcp/asset/4bde070b-db1f-4ab3-b6f2-c440d5e8aec5";
|
||||
const imgCaretDown1 = "https://www.figma.com/api/mcp/asset/e5de2ce7-5f1e-4aa8-b82c-7d02fcc27f94";
|
||||
const imgVector1 = "https://www.figma.com/api/mcp/asset/754220ec-12e8-4b70-8890-d97d730ac1ac";
|
||||
const imgVector2 = "https://www.figma.com/api/mcp/asset/5160affe-7594-4684-b552-23e39717de49";
|
||||
const imgX = "https://www.figma.com/api/mcp/asset/95812780-10b8-4394-8736-5598c84f2fec";
|
||||
|
||||
/*
|
||||
* STRUKTURA POPUP (Krok 2):
|
||||
*
|
||||
* 1. Nagłówek: "Wypełnij formularz rezerwacji." + X
|
||||
* 2. Dane wynajmu: Segment, Od kiedy, Do kiedy, Wybrano X dni, Miejsce odbioru, Zwrot w tej samej lokalizacji
|
||||
* 3. Wyjazd zagraniczny: checkbox "Planuję trasę poza granicę Polski" → wyszukiwarka krajów
|
||||
* 4. Ubezpieczenie: Pakiet ochrony Soft (od 190 do 800 zł), Pakiet ochrony Premium (od 200 do 1200 zł)
|
||||
* 5. Opcje dodatkowe: Dodatkowy kierowca (100 zł), Zwiększenie limitu km, Pełny bak (650 zł),
|
||||
* GPS Europa, Podstawka, Fotelik 9-18kg, Fotelik 18-36kg, Zwrot nieumytego (290 zł)
|
||||
* 6. Dane najemcy: Imię, Nazwisko, Email, Telefon (+48)
|
||||
* 7. Wiadomość: textarea
|
||||
* 8. Stopka: Zgoda na Politykę Prywatności + Wyślij
|
||||
*
|
||||
* RÓŻNICE VS KROK 1:
|
||||
* - Nowa sekcja "Wyjazd zagraniczny" z checkboxem i wyszukiwarką krajów
|
||||
* - Nowa sekcja "Ubezpieczenie" (Soft / Premium) (też raczej pobierane z API, z cenami jako zakresy)
|
||||
* - Rozbudowane opcje dodatkowe (8 pozycji zamiast 2)
|
||||
* - Ceny jako zakresy "od X do Y zł" (zależne od cennika API)
|
||||
*/
|
||||
|
||||
export default function Overlay() {
|
||||
return (
|
||||
<div className="relative size-full" data-name="Overlay" data-node-id="32:645">
|
||||
<div className="-translate-x-1/2 -translate-y-1/2 absolute bg-[#ededf3] content-stretch flex flex-col gap-[24px] items-start justify-center left-1/2 overflow-clip px-[48px] py-[40px] rounded-[16px] top-[calc(50%+18.5px)]" data-name="POPUP" data-node-id="32:397">
|
||||
<p className="font-['Albert_Sans:Bold',sans-serif] font-bold leading-[0] min-w-full relative shrink-0 text-[#2f2482] text-[20px] text-center w-[min-content]" data-node-id="32:398">
|
||||
<span className="leading-[32px]">Wypełnij formularz rezerwacji</span>
|
||||
<span className="leading-[32px] text-[red]">.</span>
|
||||
</p>
|
||||
{/* ... pełny kod w odpowiedzi Figma MCP get_design_context nodeId=32:645 ... */}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
BIN
docs/figma-formularz/screenshot-desktop.png
Normal file
BIN
docs/figma-formularz/screenshot-desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
BIN
docs/figma-formularz/screenshot-input-kraje-stany.png
Normal file
BIN
docs/figma-formularz/screenshot-input-kraje-stany.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/figma-formularz/screenshot-mobile.png
Normal file
BIN
docs/figma-formularz/screenshot-mobile.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 119 KiB |
BIN
docs/figma-formularz/screenshot-popup-desktop.png
Normal file
BIN
docs/figma-formularz/screenshot-popup-desktop.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
BIN
wp-admin/.DS_Store
vendored
BIN
wp-admin/.DS_Store
vendored
Binary file not shown.
BIN
wp-content/.DS_Store
vendored
BIN
wp-content/.DS_Store
vendored
Binary file not shown.
BIN
wp-content/languages/.DS_Store
vendored
BIN
wp-content/languages/.DS_Store
vendored
Binary file not shown.
BIN
wp-content/plugins/.DS_Store
vendored
BIN
wp-content/plugins/.DS_Store
vendored
Binary file not shown.
@@ -0,0 +1,541 @@
|
||||
/* ═══════════════════════════════════════════
|
||||
Carei Reservation — Design Tokens
|
||||
═══════════════════════════════════════════ */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Albert+Sans:wght@400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--carei-blue: #2F2482;
|
||||
--carei-red: #FF0000;
|
||||
--carei-red-hover: #D60000;
|
||||
--carei-bg: #EDEDF3;
|
||||
--carei-gray: #505050;
|
||||
--carei-placeholder: #C7C7C7;
|
||||
--carei-border: #D0D0D0;
|
||||
--carei-white: #FFFFFF;
|
||||
--carei-radius: 8px;
|
||||
--carei-radius-lg: 16px;
|
||||
--carei-input-h: 48px;
|
||||
--carei-font: 'Albert Sans', sans-serif;
|
||||
--carei-gap-section: 24px;
|
||||
--carei-gap-inner: 16px;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Trigger Button
|
||||
═══════════════════════════════════════════ */
|
||||
.carei-reservation-trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 14px 28px;
|
||||
background-color: var(--carei-red);
|
||||
color: var(--carei-white);
|
||||
font-family: var(--carei-font);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
border-radius: var(--carei-radius);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
text-decoration: none;
|
||||
line-height: 1;
|
||||
}
|
||||
.carei-reservation-trigger:hover {
|
||||
background-color: var(--carei-red-hover);
|
||||
color: var(--carei-white);
|
||||
}
|
||||
.carei-reservation-trigger svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Modal Overlay & Container
|
||||
═══════════════════════════════════════════ */
|
||||
.carei-modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 100000;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.carei-modal-overlay.is-open {
|
||||
display: flex;
|
||||
}
|
||||
.carei-modal {
|
||||
background: var(--carei-bg);
|
||||
border-radius: var(--carei-radius-lg);
|
||||
max-width: 800px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
padding: 40px 48px;
|
||||
position: relative;
|
||||
font-family: var(--carei-font);
|
||||
}
|
||||
.carei-modal-close {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--carei-gray);
|
||||
line-height: 1;
|
||||
padding: 4px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.carei-modal-close:hover {
|
||||
color: var(--carei-blue);
|
||||
}
|
||||
.carei-modal-title {
|
||||
font-family: var(--carei-font);
|
||||
font-weight: 700;
|
||||
font-size: 20px;
|
||||
color: var(--carei-blue);
|
||||
text-align: center;
|
||||
margin: 0 0 var(--carei-gap-section) 0;
|
||||
}
|
||||
.carei-modal-title span {
|
||||
color: var(--carei-red);
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
.carei-modal::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.carei-modal::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.carei-modal::-webkit-scrollbar-thumb {
|
||||
background: var(--carei-border);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Form Layout
|
||||
═══════════════════════════════════════════ */
|
||||
.carei-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--carei-gap-section);
|
||||
}
|
||||
.carei-form__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--carei-gap-inner);
|
||||
}
|
||||
.carei-form__row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: var(--carei-gap-inner);
|
||||
}
|
||||
.carei-form__row--dates {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.carei-form__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
.carei-form__field--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Inputs & Selects
|
||||
═══════════════════════════════════════════ */
|
||||
.carei-form__input,
|
||||
.carei-form__textarea,
|
||||
.carei-form__select-wrap select {
|
||||
height: var(--carei-input-h);
|
||||
padding: 0 16px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--carei-radius);
|
||||
background: var(--carei-white);
|
||||
font-family: var(--carei-font);
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
color: var(--carei-blue);
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.carei-form__input::placeholder,
|
||||
.carei-form__textarea::placeholder {
|
||||
color: var(--carei-gray);
|
||||
font-weight: 400;
|
||||
}
|
||||
.carei-form__input:focus,
|
||||
.carei-form__textarea:focus,
|
||||
.carei-form__select-wrap select:focus {
|
||||
border-color: var(--carei-blue);
|
||||
}
|
||||
.carei-form__textarea {
|
||||
height: 143px;
|
||||
padding: 16px;
|
||||
resize: vertical;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.carei-form__label-small {
|
||||
font-family: var(--carei-font);
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
color: var(--carei-gray);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
.carei-form__icon-calendar {
|
||||
color: var(--carei-gray);
|
||||
}
|
||||
|
||||
/* Select wrapper */
|
||||
.carei-form__select-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.carei-form__select-wrap select {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
padding-right: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.carei-form__select-wrap--icon select {
|
||||
padding-left: 40px;
|
||||
}
|
||||
.carei-form__select-arrow {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
pointer-events: none;
|
||||
color: var(--carei-blue);
|
||||
}
|
||||
.carei-form__icon-pin {
|
||||
position: absolute;
|
||||
left: 14px;
|
||||
pointer-events: none;
|
||||
color: var(--carei-gray);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Days count */
|
||||
.carei-form__days-count {
|
||||
font-family: var(--carei-font);
|
||||
font-size: 13px;
|
||||
color: var(--carei-gray);
|
||||
text-align: right;
|
||||
}
|
||||
.carei-form__days-count strong {
|
||||
color: var(--carei-blue);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Phone Input
|
||||
═══════════════════════════════════════════ */
|
||||
.carei-form__phone-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: var(--carei-white);
|
||||
border-radius: var(--carei-radius);
|
||||
border: 1px solid transparent;
|
||||
transition: border-color 0.2s;
|
||||
height: var(--carei-input-h);
|
||||
}
|
||||
.carei-form__phone-wrap:focus-within {
|
||||
border-color: var(--carei-blue);
|
||||
}
|
||||
.carei-form__phone-prefix {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 0 8px 0 12px;
|
||||
border-right: 1px solid var(--carei-border);
|
||||
height: 60%;
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
color: var(--carei-gray);
|
||||
}
|
||||
.carei-form__phone-flag {
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
.carei-form__phone-code {
|
||||
font-family: var(--carei-font);
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
color: var(--carei-gray);
|
||||
}
|
||||
.carei-form__input--phone {
|
||||
border: none;
|
||||
background: transparent;
|
||||
height: 100%;
|
||||
padding-left: 12px;
|
||||
}
|
||||
.carei-form__input--phone::placeholder {
|
||||
color: var(--carei-placeholder);
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
.carei-form__input--phone:focus {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Checkboxes (custom)
|
||||
═══════════════════════════════════════════ */
|
||||
.carei-form__checkbox-label {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
cursor: pointer;
|
||||
font-family: var(--carei-font);
|
||||
font-size: 14px;
|
||||
color: var(--carei-blue);
|
||||
user-select: none;
|
||||
}
|
||||
.carei-form__checkbox-label input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.carei-form__checkbox-box {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
border: 2px solid var(--carei-border);
|
||||
border-radius: var(--carei-radius);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
background: var(--carei-white);
|
||||
margin-top: 1px;
|
||||
}
|
||||
.carei-form__checkbox-box svg {
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
.carei-form__checkbox-label input:checked + .carei-form__checkbox-box {
|
||||
background: var(--carei-blue);
|
||||
border-color: var(--carei-blue);
|
||||
}
|
||||
.carei-form__checkbox-label input:checked + .carei-form__checkbox-box svg {
|
||||
opacity: 1;
|
||||
}
|
||||
.carei-form__checkbox-text {
|
||||
line-height: 1.6;
|
||||
padding-top: 2px;
|
||||
}
|
||||
.carei-form__checkbox-text a {
|
||||
color: var(--carei-blue);
|
||||
text-decoration: underline;
|
||||
font-weight: 600;
|
||||
}
|
||||
.carei-form__checkbox-text a:hover {
|
||||
color: var(--carei-red);
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Dividers
|
||||
═══════════════════════════════════════════ */
|
||||
.carei-form__divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
font-family: 'Roboto', var(--carei-font), sans-serif;
|
||||
font-size: 12px;
|
||||
color: var(--carei-gray);
|
||||
text-transform: none;
|
||||
}
|
||||
.carei-form__divider::before,
|
||||
.carei-form__divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--carei-border);
|
||||
}
|
||||
.carei-form__divider span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Extra Cards (Ubezpieczenie, Fotelik)
|
||||
═══════════════════════════════════════════ */
|
||||
.carei-form__extra-card {
|
||||
border: 1px solid var(--carei-border);
|
||||
border-radius: var(--carei-radius);
|
||||
padding: 16px;
|
||||
background: var(--carei-white);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.carei-form__extra-card:has(input:checked) {
|
||||
border-color: var(--carei-blue);
|
||||
}
|
||||
.carei-form__checkbox-label--card {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.carei-form__extra-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
.carei-form__extra-content strong {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: var(--carei-blue);
|
||||
}
|
||||
.carei-form__extra-desc {
|
||||
font-size: 12px;
|
||||
color: var(--carei-gray);
|
||||
font-weight: 400;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.carei-form__extra-price {
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
color: var(--carei-red);
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Footer (Privacy + Submit)
|
||||
═══════════════════════════════════════════ */
|
||||
.carei-form__footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--carei-gap-inner);
|
||||
padding-top: 8px;
|
||||
}
|
||||
.carei-form__checkbox-label--privacy {
|
||||
flex: 1;
|
||||
}
|
||||
.carei-form__submit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 16px 32px;
|
||||
background-color: var(--carei-red);
|
||||
color: var(--carei-white);
|
||||
font-family: var(--carei-font);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
border-radius: var(--carei-radius);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.carei-form__submit:hover {
|
||||
background-color: var(--carei-red-hover);
|
||||
}
|
||||
.carei-form__submit:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.carei-form__submit svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Validation
|
||||
═══════════════════════════════════════════ */
|
||||
.carei-form__field--error .carei-form__input,
|
||||
.carei-form__field--error .carei-form__textarea,
|
||||
.carei-form__field--error .carei-form__select-wrap select,
|
||||
.carei-form__field--error .carei-form__phone-wrap {
|
||||
border-color: var(--carei-red) !important;
|
||||
}
|
||||
.carei-form__checkbox-label--error .carei-form__checkbox-box {
|
||||
border-color: var(--carei-red) !important;
|
||||
}
|
||||
.carei-form__error-msg {
|
||||
font-family: var(--carei-font);
|
||||
font-size: 11px;
|
||||
color: var(--carei-red);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.carei-form__error-summary {
|
||||
font-family: var(--carei-font);
|
||||
font-size: 13px;
|
||||
color: var(--carei-red);
|
||||
text-align: center;
|
||||
padding: 8px;
|
||||
background: rgba(255, 0, 0, 0.05);
|
||||
border-radius: var(--carei-radius);
|
||||
}
|
||||
|
||||
/* Loading state for selects */
|
||||
.carei-form__select-wrap--loading select {
|
||||
color: var(--carei-placeholder);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Return branch slide */
|
||||
.carei-form__return-wrap {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
.carei-form__return-wrap.is-visible {
|
||||
max-height: 80px;
|
||||
opacity: 1;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════
|
||||
Mobile Responsive (<768px)
|
||||
═══════════════════════════════════════════ */
|
||||
@media (max-width: 768px) {
|
||||
.carei-modal-overlay {
|
||||
padding: 0;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.carei-modal {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
max-height: 100vh;
|
||||
height: 100%;
|
||||
border-radius: var(--carei-radius-lg) var(--carei-radius-lg) 0 0;
|
||||
padding: 32px 24px;
|
||||
border-radius: 0;
|
||||
}
|
||||
.carei-form__row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.carei-form__row--dates {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
.carei-form__footer {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.carei-form__submit {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Very small screens */
|
||||
@media (max-width: 480px) {
|
||||
.carei-form__row--dates {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.carei-modal {
|
||||
padding: 24px 16px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var REST_URL = (window.careiReservation && window.careiReservation.restUrl) || '/wp-json/carei/v1/';
|
||||
var NONCE = (window.careiReservation && window.careiReservation.nonce) || '';
|
||||
|
||||
// ─── API Helpers ──────────────────────────────────────────────
|
||||
|
||||
function apiGet(endpoint) {
|
||||
return fetch(REST_URL + endpoint, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-WP-Nonce': NONCE,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error('API error: ' + r.status);
|
||||
return r.json();
|
||||
});
|
||||
}
|
||||
|
||||
function apiPost(endpoint, data) {
|
||||
return fetch(REST_URL + endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-WP-Nonce': NONCE,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
}).then(function (r) {
|
||||
if (!r.ok) throw new Error('API error: ' + r.status);
|
||||
return r.json();
|
||||
});
|
||||
}
|
||||
|
||||
// ─── DOM Refs ─────────────────────────────────────────────────
|
||||
|
||||
var overlay, form, segmentSelect, dateFrom, dateTo, daysCount;
|
||||
var pickupSelect, returnSelect, returnWrap, sameReturnCheck;
|
||||
var extrasContainer, errorSummary;
|
||||
|
||||
function initRefs() {
|
||||
overlay = document.querySelector('[data-carei-modal]');
|
||||
form = document.getElementById('carei-reservation-form');
|
||||
segmentSelect = document.getElementById('carei-segment');
|
||||
dateFrom = document.getElementById('carei-date-from');
|
||||
dateTo = document.getElementById('carei-date-to');
|
||||
daysCount = document.getElementById('carei-days-count');
|
||||
pickupSelect = document.getElementById('carei-pickup-branch');
|
||||
returnSelect = document.getElementById('carei-return-branch');
|
||||
returnWrap = document.getElementById('carei-return-wrap');
|
||||
sameReturnCheck = document.getElementById('carei-same-return');
|
||||
extrasContainer = document.getElementById('carei-extras-container');
|
||||
errorSummary = document.getElementById('carei-error-summary');
|
||||
}
|
||||
|
||||
// ─── Modal Open/Close ─────────────────────────────────────────
|
||||
|
||||
function initModal() {
|
||||
var triggers = document.querySelectorAll('[data-carei-open-modal]');
|
||||
var closeBtns = document.querySelectorAll('[data-carei-close-modal]');
|
||||
|
||||
triggers.forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
openModal();
|
||||
});
|
||||
});
|
||||
|
||||
closeBtns.forEach(function (btn) {
|
||||
btn.addEventListener('click', closeModal);
|
||||
});
|
||||
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', function (e) {
|
||||
if (e.target === overlay) closeModal();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Escape' && overlay && overlay.classList.contains('is-open')) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var dataLoaded = false;
|
||||
|
||||
function openModal() {
|
||||
if (!overlay) return;
|
||||
overlay.classList.add('is-open');
|
||||
document.body.style.overflow = 'hidden';
|
||||
|
||||
if (!dataLoaded) {
|
||||
loadBranches();
|
||||
loadAllCarClasses();
|
||||
setDefaultDates();
|
||||
dataLoaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (!overlay) return;
|
||||
overlay.classList.remove('is-open');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// ─── Default Dates ────────────────────────────────────────────
|
||||
|
||||
function setDefaultDates() {
|
||||
if (!dateFrom || !dateTo) return;
|
||||
|
||||
var tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
tomorrow.setHours(10, 0, 0, 0);
|
||||
|
||||
var dayAfter = new Date();
|
||||
dayAfter.setDate(dayAfter.getDate() + 2);
|
||||
dayAfter.setHours(10, 0, 0, 0);
|
||||
|
||||
dateFrom.value = formatDatetimeLocal(tomorrow);
|
||||
dateTo.value = formatDatetimeLocal(dayAfter);
|
||||
|
||||
updateDaysCount();
|
||||
}
|
||||
|
||||
function formatDatetimeLocal(d) {
|
||||
var y = d.getFullYear();
|
||||
var m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
var day = String(d.getDate()).padStart(2, '0');
|
||||
var h = String(d.getHours()).padStart(2, '0');
|
||||
var min = String(d.getMinutes()).padStart(2, '0');
|
||||
return y + '-' + m + '-' + day + 'T' + h + ':' + min;
|
||||
}
|
||||
|
||||
// ─── Days Count ───────────────────────────────────────────────
|
||||
|
||||
function updateDaysCount() {
|
||||
if (!dateFrom || !dateTo || !daysCount) return;
|
||||
|
||||
var from = new Date(dateFrom.value);
|
||||
var to = new Date(dateTo.value);
|
||||
|
||||
if (isNaN(from.getTime()) || isNaN(to.getTime()) || to <= from) {
|
||||
daysCount.innerHTML = 'Wybrano: <strong>0 dni</strong>';
|
||||
return;
|
||||
}
|
||||
|
||||
var diff = Math.ceil((to - from) / (1000 * 60 * 60 * 24));
|
||||
var label = diff === 1 ? 'dzień' : 'dni';
|
||||
daysCount.innerHTML = 'Wybrano: <strong>' + diff + ' ' + label + '</strong>';
|
||||
}
|
||||
|
||||
// ─── Load Branches ────────────────────────────────────────────
|
||||
|
||||
function loadBranches() {
|
||||
if (!pickupSelect) return;
|
||||
|
||||
setSelectLoading(pickupSelect, true);
|
||||
|
||||
apiGet('branches')
|
||||
.then(function (branches) {
|
||||
if (!Array.isArray(branches)) {
|
||||
branches = [];
|
||||
}
|
||||
|
||||
populateSelect(pickupSelect, branches.map(function (b) {
|
||||
var label = b.description || b.name;
|
||||
if (b.city) label += ' — ' + b.city;
|
||||
return { value: b.name, label: label };
|
||||
}), 'Miejsce odbioru');
|
||||
|
||||
// Copy to return branch
|
||||
if (returnSelect) {
|
||||
populateSelect(returnSelect, branches.map(function (b) {
|
||||
var label = b.description || b.name;
|
||||
if (b.city) label += ' — ' + b.city;
|
||||
return { value: b.name, label: label };
|
||||
}), 'Miejsce zwrotu');
|
||||
}
|
||||
|
||||
setSelectLoading(pickupSelect, false);
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error('Failed to load branches:', err);
|
||||
setSelectLoading(pickupSelect, false);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Load All Car Classes (on modal open) ────────────────────
|
||||
|
||||
function loadAllCarClasses() {
|
||||
if (!segmentSelect) return;
|
||||
|
||||
setSelectLoading(segmentSelect, true);
|
||||
|
||||
apiGet('car-classes-all')
|
||||
.then(function (classes) {
|
||||
if (!Array.isArray(classes) || classes.length === 0) {
|
||||
populateSelect(segmentSelect, [], 'Brak segmentów');
|
||||
setSelectLoading(segmentSelect, false);
|
||||
return;
|
||||
}
|
||||
|
||||
populateSelect(segmentSelect, 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 };
|
||||
}), 'Wybierz segment pojazdu');
|
||||
|
||||
setSelectLoading(segmentSelect, false);
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error('Failed to load car classes:', err);
|
||||
populateSelect(segmentSelect, [], 'Błąd ładowania');
|
||||
setSelectLoading(segmentSelect, false);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Load Available Car Classes (filtered by dates+branch) ───
|
||||
|
||||
function loadCarClasses() {
|
||||
if (!segmentSelect || !dateFrom || !dateTo || !pickupSelect) return;
|
||||
|
||||
var fromVal = dateFrom.value;
|
||||
var toVal = dateTo.value;
|
||||
var branch = pickupSelect.value;
|
||||
|
||||
if (!fromVal || !toVal || !branch) return;
|
||||
|
||||
// Convert datetime-local to API format: YYYY-MM-DDTHH:MM:SS
|
||||
var apiFrom = fromVal.replace('T', 'T') + ':00';
|
||||
var apiTo = toVal.replace('T', 'T') + ':00';
|
||||
|
||||
setSelectLoading(segmentSelect, true);
|
||||
|
||||
apiPost('car-classes', {
|
||||
dateFrom: apiFrom,
|
||||
dateTo: apiTo,
|
||||
branchName: branch
|
||||
})
|
||||
.then(function (classes) {
|
||||
if (!Array.isArray(classes) || classes.length === 0) {
|
||||
populateSelect(segmentSelect, [], 'Brak dostępnych klas');
|
||||
setSelectLoading(segmentSelect, false);
|
||||
return;
|
||||
}
|
||||
|
||||
populateSelect(segmentSelect, classes.map(function (c) {
|
||||
// classes might be strings or objects
|
||||
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 };
|
||||
}), 'Wybierz segment pojazdu');
|
||||
|
||||
setSelectLoading(segmentSelect, false);
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error('Failed to load car classes:', err);
|
||||
populateSelect(segmentSelect, [], 'Błąd ładowania');
|
||||
setSelectLoading(segmentSelect, false);
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Load Extras from Pricelist ───────────────────────────────
|
||||
|
||||
function loadExtras() {
|
||||
if (!segmentSelect || !dateFrom || !dateTo || !pickupSelect || !extrasContainer) return;
|
||||
|
||||
var category = segmentSelect.value;
|
||||
var fromVal = dateFrom.value;
|
||||
var toVal = dateTo.value;
|
||||
var branch = pickupSelect.value;
|
||||
|
||||
if (!category || !fromVal || !toVal || !branch) return;
|
||||
|
||||
var apiFrom = fromVal + ':00';
|
||||
var apiTo = toVal + ':00';
|
||||
|
||||
apiPost('pricelist', {
|
||||
category: category,
|
||||
dateFrom: apiFrom,
|
||||
dateTo: apiTo,
|
||||
pickUpLocation: branch
|
||||
})
|
||||
.then(function (pricelists) {
|
||||
if (!Array.isArray(pricelists) || pricelists.length === 0) return;
|
||||
|
||||
var pricelist = pricelists[0];
|
||||
var items = pricelist.additionalItems;
|
||||
|
||||
if (!Array.isArray(items) || items.length === 0) return;
|
||||
|
||||
extrasContainer.innerHTML = '';
|
||||
|
||||
items.forEach(function (item) {
|
||||
var price = parseFloat(item.price || item.minPrice || 0);
|
||||
var priceLabel = price > 0 ? (price.toFixed(0) + ' zł') : 'Gratis';
|
||||
|
||||
var card = document.createElement('div');
|
||||
card.className = 'carei-form__extra-card';
|
||||
card.innerHTML =
|
||||
'<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 + '">' +
|
||||
'<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-price">' + escHtml(priceLabel) + '</span>' +
|
||||
'</span>' +
|
||||
'</label>';
|
||||
|
||||
extrasContainer.appendChild(card);
|
||||
});
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error('Failed to load pricelist:', err);
|
||||
// Keep static fallback extras
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Select Helpers ───────────────────────────────────────────
|
||||
|
||||
function populateSelect(select, options, placeholder) {
|
||||
select.innerHTML = '';
|
||||
|
||||
var ph = document.createElement('option');
|
||||
ph.value = '';
|
||||
ph.disabled = true;
|
||||
ph.selected = true;
|
||||
ph.textContent = placeholder || 'Wybierz...';
|
||||
select.appendChild(ph);
|
||||
|
||||
options.forEach(function (opt) {
|
||||
var o = document.createElement('option');
|
||||
o.value = opt.value;
|
||||
o.textContent = opt.label;
|
||||
select.appendChild(o);
|
||||
});
|
||||
}
|
||||
|
||||
function setSelectLoading(select, loading) {
|
||||
var wrap = select.closest('.carei-form__select-wrap');
|
||||
if (!wrap) return;
|
||||
if (loading) {
|
||||
wrap.classList.add('carei-form__select-wrap--loading');
|
||||
} else {
|
||||
wrap.classList.remove('carei-form__select-wrap--loading');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Same Return Location ─────────────────────────────────────
|
||||
|
||||
function initSameReturn() {
|
||||
if (!sameReturnCheck || !returnWrap) return;
|
||||
|
||||
sameReturnCheck.addEventListener('change', function () {
|
||||
if (sameReturnCheck.checked) {
|
||||
returnWrap.classList.remove('is-visible');
|
||||
} else {
|
||||
returnWrap.classList.add('is-visible');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Validation ───────────────────────────────────────────────
|
||||
|
||||
var requiredFields = [
|
||||
{ id: 'carei-segment', type: 'select', msg: 'Wybierz segment pojazdu' },
|
||||
{ id: 'carei-date-from', type: 'input', msg: 'Podaj datę rozpoczęcia' },
|
||||
{ id: 'carei-date-to', type: 'input', msg: 'Podaj datę zakończenia' },
|
||||
{ id: 'carei-pickup-branch', type: 'select', msg: 'Wybierz miejsce odbioru' },
|
||||
{ id: 'carei-firstname', type: 'input', msg: 'Podaj imię' },
|
||||
{ id: 'carei-lastname', type: 'input', msg: 'Podaj nazwisko' },
|
||||
{ id: 'carei-email', type: 'email', msg: 'Podaj poprawny adres e-mail' },
|
||||
{ id: 'carei-phone', type: 'phone', msg: 'Podaj numer telefonu (min. 9 cyfr)' },
|
||||
{ id: 'carei-privacy', type: 'checkbox', msg: 'Wymagana zgoda na Politykę Prywatności' }
|
||||
];
|
||||
|
||||
function validateForm() {
|
||||
var valid = true;
|
||||
|
||||
// Clear previous errors
|
||||
form.querySelectorAll('.carei-form__field--error').forEach(function (el) {
|
||||
el.classList.remove('carei-form__field--error');
|
||||
});
|
||||
form.querySelectorAll('.carei-form__checkbox-label--error').forEach(function (el) {
|
||||
el.classList.remove('carei-form__checkbox-label--error');
|
||||
});
|
||||
form.querySelectorAll('.carei-form__error-msg').forEach(function (el) {
|
||||
el.remove();
|
||||
});
|
||||
|
||||
requiredFields.forEach(function (f) {
|
||||
var el = document.getElementById(f.id);
|
||||
if (!el) return;
|
||||
|
||||
var hasError = false;
|
||||
|
||||
if (f.type === 'checkbox') {
|
||||
if (!el.checked) hasError = true;
|
||||
} else if (f.type === 'email') {
|
||||
var emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!el.value.trim() || !emailRegex.test(el.value.trim())) hasError = true;
|
||||
} else if (f.type === 'phone') {
|
||||
var digits = el.value.replace(/\D/g, '');
|
||||
if (digits.length < 9) hasError = true;
|
||||
} else if (f.type === 'select') {
|
||||
if (!el.value) hasError = true;
|
||||
} else {
|
||||
if (!el.value.trim()) hasError = true;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
valid = false;
|
||||
markFieldError(el, f.msg, f.type);
|
||||
}
|
||||
});
|
||||
|
||||
// Check dateTo > dateFrom
|
||||
if (dateFrom && dateTo && dateFrom.value && dateTo.value) {
|
||||
if (new Date(dateTo.value) <= new Date(dateFrom.value)) {
|
||||
valid = false;
|
||||
markFieldError(dateTo, 'Data zakończenia musi być po dacie rozpoczęcia', 'input');
|
||||
}
|
||||
}
|
||||
|
||||
// Return branch required if different location
|
||||
if (sameReturnCheck && !sameReturnCheck.checked && returnSelect) {
|
||||
if (!returnSelect.value) {
|
||||
valid = false;
|
||||
markFieldError(returnSelect, 'Wybierz miejsce zwrotu', 'select');
|
||||
}
|
||||
}
|
||||
|
||||
if (errorSummary) {
|
||||
errorSummary.style.display = valid ? 'none' : 'block';
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
function markFieldError(el, msg, type) {
|
||||
if (type === 'checkbox') {
|
||||
var label = el.closest('.carei-form__checkbox-label');
|
||||
if (label) label.classList.add('carei-form__checkbox-label--error');
|
||||
} else {
|
||||
var field = el.closest('.carei-form__field');
|
||||
if (!field) {
|
||||
// For phone in wrap
|
||||
field = el.closest('.carei-form__phone-wrap');
|
||||
if (field) field = field.closest('.carei-form__field');
|
||||
}
|
||||
if (field) {
|
||||
field.classList.add('carei-form__field--error');
|
||||
var errEl = document.createElement('span');
|
||||
errEl.className = 'carei-form__error-msg';
|
||||
errEl.textContent = msg;
|
||||
field.appendChild(errEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initClearErrors() {
|
||||
if (!form) return;
|
||||
form.addEventListener('focusin', function (e) {
|
||||
var field = e.target.closest('.carei-form__field');
|
||||
if (field && field.classList.contains('carei-form__field--error')) {
|
||||
field.classList.remove('carei-form__field--error');
|
||||
var errMsg = field.querySelector('.carei-form__error-msg');
|
||||
if (errMsg) errMsg.remove();
|
||||
}
|
||||
// Checkbox
|
||||
var label = e.target.closest('.carei-form__checkbox-label');
|
||||
if (label) label.classList.remove('carei-form__checkbox-label--error');
|
||||
});
|
||||
// Also on change for checkboxes
|
||||
form.addEventListener('change', function (e) {
|
||||
if (e.target.type === 'checkbox') {
|
||||
var label = e.target.closest('.carei-form__checkbox-label');
|
||||
if (label) label.classList.remove('carei-form__checkbox-label--error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Form Submit ──────────────────────────────────────────────
|
||||
|
||||
function initSubmit() {
|
||||
if (!form) return;
|
||||
|
||||
form.addEventListener('submit', function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateForm()) return;
|
||||
|
||||
var formData = collectFormData();
|
||||
console.log('Carei Reservation — Form data:', formData);
|
||||
|
||||
// Phase 3 will handle actual API submission
|
||||
var submitBtn = form.querySelector('.carei-form__submit');
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML =
|
||||
'<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> Wysyłanie...';
|
||||
|
||||
// Re-enable after 2s (temporary — Phase 3 will handle properly)
|
||||
setTimeout(function () {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML =
|
||||
'<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> Wyślij';
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function collectFormData() {
|
||||
var extras = [];
|
||||
form.querySelectorAll('input[name="extras[]"]:checked').forEach(function (cb) {
|
||||
extras.push({
|
||||
id: cb.value,
|
||||
price: parseFloat(cb.getAttribute('data-price') || 0)
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
segment: segmentSelect ? segmentSelect.value : '',
|
||||
dateFrom: dateFrom ? dateFrom.value : '',
|
||||
dateTo: dateTo ? dateTo.value : '',
|
||||
pickupBranch: pickupSelect ? pickupSelect.value : '',
|
||||
sameReturn: sameReturnCheck ? sameReturnCheck.checked : true,
|
||||
returnBranch: (sameReturnCheck && !sameReturnCheck.checked && returnSelect) ? returnSelect.value : '',
|
||||
extras: extras,
|
||||
firstName: val('carei-firstname'),
|
||||
lastName: val('carei-lastname'),
|
||||
email: val('carei-email'),
|
||||
phone: '+48' + (document.getElementById('carei-phone') ? document.getElementById('carei-phone').value.replace(/\D/g, '') : ''),
|
||||
message: val('carei-message'),
|
||||
privacy: document.getElementById('carei-privacy') ? document.getElementById('carei-privacy').checked : false
|
||||
};
|
||||
}
|
||||
|
||||
function val(id) {
|
||||
var el = document.getElementById(id);
|
||||
return el ? el.value.trim() : '';
|
||||
}
|
||||
|
||||
// ─── Event Listeners for Dynamic Loading ──────────────────────
|
||||
|
||||
function initDynamicLoading() {
|
||||
// Date change → update days count
|
||||
if (dateFrom) dateFrom.addEventListener('change', updateDaysCount);
|
||||
if (dateTo) dateTo.addEventListener('change', updateDaysCount);
|
||||
|
||||
// When segment + dates + branch all set → load extras from pricelist
|
||||
if (segmentSelect) segmentSelect.addEventListener('change', loadExtras);
|
||||
if (pickupSelect) pickupSelect.addEventListener('change', loadExtras);
|
||||
if (dateFrom) dateFrom.addEventListener('change', loadExtras);
|
||||
if (dateTo) dateTo.addEventListener('change', loadExtras);
|
||||
}
|
||||
|
||||
// ─── HTML Escape Helpers ──────────────────────────────────────
|
||||
|
||||
function escHtml(str) {
|
||||
var div = document.createElement('div');
|
||||
div.textContent = str || '';
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function escAttr(str) {
|
||||
return (str || '').replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>');
|
||||
}
|
||||
|
||||
// ─── Init ─────────────────────────────────────────────────────
|
||||
|
||||
function init() {
|
||||
initRefs();
|
||||
if (!overlay || !form) return;
|
||||
initModal();
|
||||
initSameReturn();
|
||||
initDynamicLoading();
|
||||
initClearErrors();
|
||||
initSubmit();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
107
wp-content/plugins/carei-reservation/carei-reservation.php
Normal file
107
wp-content/plugins/carei-reservation/carei-reservation.php
Normal file
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Carei Reservation
|
||||
* Description: Formularz rezerwacji samochodu zintegrowany z Softra Rent API, jako widget Elementor.
|
||||
* Version: 1.0.0
|
||||
* Author: Carei
|
||||
* Text Domain: carei-reservation
|
||||
* Requires PHP: 7.4
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
define( 'CAREI_RESERVATION_VERSION', '1.0.0' );
|
||||
define( 'CAREI_RESERVATION_PATH', plugin_dir_path( __FILE__ ) );
|
||||
define( 'CAREI_RESERVATION_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
/**
|
||||
* Parse .env file (format: "key: value")
|
||||
*/
|
||||
function carei_parse_env() {
|
||||
$env_path = ABSPATH . '.env';
|
||||
if ( ! file_exists( $env_path ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$lines = file( $env_path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES );
|
||||
$env = array();
|
||||
|
||||
foreach ( $lines as $line ) {
|
||||
$line = trim( $line );
|
||||
if ( '' === $line || '#' === $line[0] ) {
|
||||
continue;
|
||||
}
|
||||
$pos = strpos( $line, ':' );
|
||||
if ( false === $pos ) {
|
||||
continue;
|
||||
}
|
||||
$key = trim( substr( $line, 0, $pos ) );
|
||||
$value = trim( substr( $line, $pos + 1 ) );
|
||||
$env[ $key ] = $value;
|
||||
}
|
||||
|
||||
return $env;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load includes
|
||||
*/
|
||||
require_once CAREI_RESERVATION_PATH . 'includes/class-softra-api.php';
|
||||
require_once CAREI_RESERVATION_PATH . 'includes/class-rest-proxy.php';
|
||||
|
||||
/**
|
||||
* Initialize plugin on plugins_loaded
|
||||
*/
|
||||
add_action( 'plugins_loaded', function () {
|
||||
$env = carei_parse_env();
|
||||
|
||||
$api_url = isset( $env['url'] ) ? $env['url'] : '';
|
||||
$username = isset( $env['username'] ) ? $env['username'] : '';
|
||||
$password = isset( $env['password'] ) ? $env['password'] : '';
|
||||
|
||||
if ( empty( $api_url ) || empty( $username ) || empty( $password ) ) {
|
||||
add_action( 'admin_notices', function () {
|
||||
echo '<div class="notice notice-error"><p><strong>Carei Reservation:</strong> Brak konfiguracji API w pliku .env (url, username, password).</p></div>';
|
||||
} );
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize Softra API singleton
|
||||
Carei_Softra_API::init( $api_url, $username, $password );
|
||||
|
||||
// Initialize REST proxy
|
||||
new Carei_REST_Proxy();
|
||||
} );
|
||||
|
||||
/**
|
||||
* Register Elementor widget
|
||||
*/
|
||||
add_action( 'elementor/widgets/register', function ( $widgets_manager ) {
|
||||
require_once CAREI_RESERVATION_PATH . 'includes/class-elementor-widget.php';
|
||||
$widgets_manager->register( new Carei_Reservation_Widget() );
|
||||
} );
|
||||
|
||||
/**
|
||||
* Enqueue frontend assets
|
||||
*/
|
||||
add_action( 'wp_enqueue_scripts', function () {
|
||||
wp_register_style(
|
||||
'carei-reservation-css',
|
||||
CAREI_RESERVATION_URL . 'assets/css/carei-reservation.css',
|
||||
array(),
|
||||
CAREI_RESERVATION_VERSION
|
||||
);
|
||||
wp_register_script(
|
||||
'carei-reservation-js',
|
||||
CAREI_RESERVATION_URL . 'assets/js/carei-reservation.js',
|
||||
array(),
|
||||
CAREI_RESERVATION_VERSION,
|
||||
true
|
||||
);
|
||||
wp_localize_script( 'carei-reservation-js', 'careiReservation', array(
|
||||
'restUrl' => esc_url_raw( rest_url( 'carei/v1/' ) ),
|
||||
'nonce' => wp_create_nonce( 'wp_rest' ),
|
||||
) );
|
||||
} );
|
||||
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Elementor Widget: Carei Reservation form in modal.
|
||||
*/
|
||||
class Carei_Reservation_Widget extends \Elementor\Widget_Base {
|
||||
|
||||
public function get_name() {
|
||||
return 'carei-reservation';
|
||||
}
|
||||
|
||||
public function get_title() {
|
||||
return 'Carei Reservation';
|
||||
}
|
||||
|
||||
public function get_icon() {
|
||||
return 'eicon-form-horizontal';
|
||||
}
|
||||
|
||||
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() {
|
||||
$this->start_controls_section( 'content_section', array(
|
||||
'label' => 'Przycisk rezerwacji',
|
||||
'tab' => \Elementor\Controls_Manager::TAB_CONTENT,
|
||||
) );
|
||||
|
||||
$this->add_control( 'button_text', array(
|
||||
'label' => 'Tekst przycisku',
|
||||
'type' => \Elementor\Controls_Manager::TEXT,
|
||||
'default' => 'Złóż zapytanie o rezerwację',
|
||||
) );
|
||||
|
||||
$this->end_controls_section();
|
||||
}
|
||||
|
||||
protected function render() {
|
||||
$settings = $this->get_settings_for_display();
|
||||
$button_text = esc_html( $settings['button_text'] );
|
||||
?>
|
||||
<button type="button" class="carei-reservation-trigger" data-carei-open-modal>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
|
||||
<?php echo $button_text; ?>
|
||||
</button>
|
||||
|
||||
<div class="carei-modal-overlay" data-carei-modal>
|
||||
<div class="carei-modal">
|
||||
<button type="button" class="carei-modal-close" data-carei-close-modal>
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<h2 class="carei-modal-title">Wypełnij formularz rezerwacji<span>.</span></h2>
|
||||
|
||||
<form class="carei-form" id="carei-reservation-form" novalidate>
|
||||
|
||||
<!-- Dane wynajmu -->
|
||||
<div class="carei-form__section">
|
||||
<div class="carei-form__field carei-form__field--full">
|
||||
<div class="carei-form__select-wrap">
|
||||
<select id="carei-segment" name="segment" required>
|
||||
<option value="" disabled selected>Wybierz segment pojazdu</option>
|
||||
</select>
|
||||
<svg class="carei-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>
|
||||
|
||||
<div class="carei-form__row carei-form__row--dates">
|
||||
<div class="carei-form__field">
|
||||
<label class="carei-form__label-small" for="carei-date-from">Od kiedy?</label>
|
||||
<input type="datetime-local" id="carei-date-from" name="dateFrom" class="carei-form__input" required>
|
||||
</div>
|
||||
<div class="carei-form__field">
|
||||
<label class="carei-form__label-small" for="carei-date-to">
|
||||
<svg class="carei-form__icon-calendar" width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="2" y="3" width="12" height="11" rx="1" stroke="currentColor" stroke-width="1.5"/><path d="M2 6h12M5 1v3M11 1v3" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
|
||||
Do kiedy?
|
||||
</label>
|
||||
<input type="datetime-local" id="carei-date-to" name="dateTo" class="carei-form__input" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="carei-form__days-count" id="carei-days-count">Wybrano: <strong>0 dni</strong></div>
|
||||
|
||||
<div class="carei-form__field carei-form__field--full">
|
||||
<div class="carei-form__select-wrap carei-form__select-wrap--icon">
|
||||
<svg class="carei-form__icon-pin" 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-pickup-branch" name="pickupBranch" required>
|
||||
<option value="" disabled selected>Miejsce odbioru</option>
|
||||
</select>
|
||||
<svg class="carei-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>
|
||||
|
||||
<label class="carei-form__checkbox-label">
|
||||
<input type="checkbox" id="carei-same-return" name="sameReturn" checked>
|
||||
<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-text">Zwrot w tej samej lokalizacji</span>
|
||||
</label>
|
||||
|
||||
<div class="carei-form__field carei-form__field--full carei-form__return-wrap" id="carei-return-wrap" style="display:none;">
|
||||
<div class="carei-form__select-wrap carei-form__select-wrap--icon">
|
||||
<svg class="carei-form__icon-pin" 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-return-branch" name="returnBranch">
|
||||
<option value="" disabled selected>Miejsce zwrotu</option>
|
||||
</select>
|
||||
<svg class="carei-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>
|
||||
</div>
|
||||
|
||||
<!-- Opcje dodatkowe -->
|
||||
<div class="carei-form__divider"><span>Opcje dodatkowe</span></div>
|
||||
|
||||
<div class="carei-form__section">
|
||||
<div class="carei-form__row" id="carei-extras-container">
|
||||
<div class="carei-form__extra-card">
|
||||
<label class="carei-form__checkbox-label carei-form__checkbox-label--card">
|
||||
<input type="checkbox" name="extras[]" value="insurance">
|
||||
<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>Rozszerzone ubezpieczenie</strong>
|
||||
<span class="carei-form__extra-desc">Obejmuje brak odpowiedzialności najemcy za wszelki szkody poniesione na aucie.</span>
|
||||
<span class="carei-form__extra-price">300 zł</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="carei-form__extra-card">
|
||||
<label class="carei-form__checkbox-label carei-form__checkbox-label--card">
|
||||
<input type="checkbox" name="extras[]" value="child-seat">
|
||||
<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>Fotelik dla dziecka</strong>
|
||||
<span class="carei-form__extra-desc">Prosimy zawrzeć wiek dziecka w wiadomości.</span>
|
||||
<span class="carei-form__extra-price">50 zł</span>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dane najemcy -->
|
||||
<div class="carei-form__divider"><span>Dane najemcy</span></div>
|
||||
|
||||
<div class="carei-form__section">
|
||||
<div class="carei-form__row">
|
||||
<div class="carei-form__field">
|
||||
<label class="carei-form__label-small" for="carei-firstname">Imię</label>
|
||||
<input type="text" id="carei-firstname" name="firstName" class="carei-form__input" required>
|
||||
</div>
|
||||
<div class="carei-form__field">
|
||||
<label class="carei-form__label-small" for="carei-lastname">Nazwisko</label>
|
||||
<input type="text" id="carei-lastname" name="lastName" class="carei-form__input" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="carei-form__row">
|
||||
<div class="carei-form__field">
|
||||
<label class="carei-form__label-small" for="carei-email">Adres e-mail</label>
|
||||
<input type="email" id="carei-email" name="email" class="carei-form__input" required>
|
||||
</div>
|
||||
<div class="carei-form__field">
|
||||
<label class="carei-form__label-small" for="carei-phone">Nr telefonu</label>
|
||||
<div class="carei-form__phone-wrap">
|
||||
<div class="carei-form__phone-prefix">
|
||||
<span class="carei-form__phone-flag">🇵🇱</span>
|
||||
<span class="carei-form__phone-code">+48</span>
|
||||
</div>
|
||||
<input type="tel" id="carei-phone" name="phone" class="carei-form__input carei-form__input--phone" placeholder="___ ___ ___" required>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="carei-form__field carei-form__field--full">
|
||||
<textarea id="carei-message" name="message" class="carei-form__textarea" placeholder="Twoja wiadomość dotycząca rezerwacji" rows="4"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stopka -->
|
||||
<div class="carei-form__footer">
|
||||
<label class="carei-form__checkbox-label carei-form__checkbox-label--privacy">
|
||||
<input type="checkbox" id="carei-privacy" name="privacy" required>
|
||||
<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-text">Zgadzam się na <a href="/polityka-prywatnosci/" target="_blank">Politykę Prywatności</a></span>
|
||||
</label>
|
||||
<button type="submit" class="carei-form__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>
|
||||
Wyślij
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="carei-form__error-summary" id="carei-error-summary" style="display:none;">
|
||||
Uzupełnij wymagane pola zaznaczone na czerwono.
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* WP REST API proxy to Softra Rent API.
|
||||
* Namespace: carei/v1
|
||||
*/
|
||||
class Carei_REST_Proxy {
|
||||
|
||||
const NAMESPACE = 'carei/v1';
|
||||
|
||||
public function __construct() {
|
||||
add_action( 'rest_api_init', array( $this, 'register_routes' ) );
|
||||
}
|
||||
|
||||
public function register_routes() {
|
||||
// GET /branches
|
||||
register_rest_route( self::NAMESPACE, '/branches', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_branches' ),
|
||||
'permission_callback' => '__return_true',
|
||||
) );
|
||||
|
||||
// GET /car-classes-all (all defined classes, no params needed)
|
||||
register_rest_route( self::NAMESPACE, '/car-classes-all', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_all_car_classes' ),
|
||||
'permission_callback' => '__return_true',
|
||||
) );
|
||||
|
||||
// POST /car-classes
|
||||
register_rest_route( self::NAMESPACE, '/car-classes', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'get_car_classes' ),
|
||||
'permission_callback' => array( $this, 'check_nonce' ),
|
||||
'args' => array(
|
||||
'dateFrom' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
|
||||
'dateTo' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
|
||||
'branchName' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
|
||||
),
|
||||
) );
|
||||
|
||||
// POST /car-models
|
||||
register_rest_route( self::NAMESPACE, '/car-models', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'get_car_models' ),
|
||||
'permission_callback' => array( $this, 'check_nonce' ),
|
||||
'args' => array(
|
||||
'dateFrom' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
|
||||
'dateTo' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
|
||||
'branchName' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
|
||||
'category' => array( 'required' => false, 'sanitize_callback' => 'sanitize_text_field', 'default' => '' ),
|
||||
),
|
||||
) );
|
||||
|
||||
// POST /pricelist
|
||||
register_rest_route( self::NAMESPACE, '/pricelist', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'get_pricelist' ),
|
||||
'permission_callback' => array( $this, 'check_nonce' ),
|
||||
'args' => array(
|
||||
'category' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
|
||||
'dateFrom' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
|
||||
'dateTo' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
|
||||
'pickUpLocation' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
|
||||
),
|
||||
) );
|
||||
|
||||
// POST /pricing-summary
|
||||
register_rest_route( self::NAMESPACE, '/pricing-summary', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'get_pricing_summary' ),
|
||||
'permission_callback' => array( $this, 'check_nonce' ),
|
||||
) );
|
||||
|
||||
// POST /customer
|
||||
register_rest_route( self::NAMESPACE, '/customer', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'add_customer' ),
|
||||
'permission_callback' => array( $this, 'check_nonce' ),
|
||||
) );
|
||||
|
||||
// POST /booking
|
||||
register_rest_route( self::NAMESPACE, '/booking', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'make_booking' ),
|
||||
'permission_callback' => array( $this, 'check_nonce' ),
|
||||
) );
|
||||
|
||||
// POST /booking/confirm
|
||||
register_rest_route( self::NAMESPACE, '/booking/confirm', array(
|
||||
'methods' => 'POST',
|
||||
'callback' => array( $this, 'confirm_booking' ),
|
||||
'permission_callback' => array( $this, 'check_nonce' ),
|
||||
'args' => array(
|
||||
'reservationId' => array( 'required' => true, 'sanitize_callback' => 'sanitize_text_field' ),
|
||||
),
|
||||
) );
|
||||
|
||||
// GET /agreements
|
||||
register_rest_route( self::NAMESPACE, '/agreements', array(
|
||||
'methods' => 'GET',
|
||||
'callback' => array( $this, 'get_agreements' ),
|
||||
'permission_callback' => '__return_true',
|
||||
) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify WP REST nonce for POST requests.
|
||||
*/
|
||||
public function check_nonce( WP_REST_Request $request ) {
|
||||
$nonce = $request->get_header( 'X-WP-Nonce' );
|
||||
if ( ! $nonce || ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
|
||||
return new WP_Error( 'rest_forbidden', 'Invalid nonce.', array( 'status' => 403 ) );
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API instance or return error.
|
||||
*/
|
||||
private function api() {
|
||||
$api = Carei_Softra_API::get_instance();
|
||||
if ( null === $api ) {
|
||||
return new WP_Error( 'carei_not_configured', 'Softra API not configured.', array( 'status' => 500 ) );
|
||||
}
|
||||
return $api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap API response.
|
||||
*/
|
||||
private function respond( $result ) {
|
||||
if ( is_wp_error( $result ) ) {
|
||||
return $result;
|
||||
}
|
||||
return new WP_REST_Response( $result, 200 );
|
||||
}
|
||||
|
||||
// ─── Callbacks ────────────────────────────────────────────────
|
||||
|
||||
public function get_all_car_classes( WP_REST_Request $request ) {
|
||||
$api = $this->api();
|
||||
if ( is_wp_error( $api ) ) {
|
||||
return $api;
|
||||
}
|
||||
return $this->respond( $api->get_all_car_classes() );
|
||||
}
|
||||
|
||||
public function get_branches( WP_REST_Request $request ) {
|
||||
$api = $this->api();
|
||||
if ( is_wp_error( $api ) ) {
|
||||
return $api;
|
||||
}
|
||||
return $this->respond( $api->get_branches() );
|
||||
}
|
||||
|
||||
public function get_car_classes( WP_REST_Request $request ) {
|
||||
$api = $this->api();
|
||||
if ( is_wp_error( $api ) ) {
|
||||
return $api;
|
||||
}
|
||||
return $this->respond( $api->get_car_classes(
|
||||
$request->get_param( 'dateFrom' ),
|
||||
$request->get_param( 'dateTo' ),
|
||||
$request->get_param( 'branchName' )
|
||||
) );
|
||||
}
|
||||
|
||||
public function get_car_models( WP_REST_Request $request ) {
|
||||
$api = $this->api();
|
||||
if ( is_wp_error( $api ) ) {
|
||||
return $api;
|
||||
}
|
||||
return $this->respond( $api->get_car_models(
|
||||
$request->get_param( 'dateFrom' ),
|
||||
$request->get_param( 'dateTo' ),
|
||||
$request->get_param( 'branchName' ),
|
||||
$request->get_param( 'category' )
|
||||
) );
|
||||
}
|
||||
|
||||
public function get_pricelist( WP_REST_Request $request ) {
|
||||
$api = $this->api();
|
||||
if ( is_wp_error( $api ) ) {
|
||||
return $api;
|
||||
}
|
||||
return $this->respond( $api->get_pricelist(
|
||||
$request->get_param( 'category' ),
|
||||
$request->get_param( 'dateFrom' ),
|
||||
$request->get_param( 'dateTo' ),
|
||||
$request->get_param( 'pickUpLocation' )
|
||||
) );
|
||||
}
|
||||
|
||||
public function get_pricing_summary( WP_REST_Request $request ) {
|
||||
$api = $this->api();
|
||||
if ( is_wp_error( $api ) ) {
|
||||
return $api;
|
||||
}
|
||||
$params = $request->get_json_params();
|
||||
return $this->respond( $api->get_pricing_summary( $params ) );
|
||||
}
|
||||
|
||||
public function add_customer( WP_REST_Request $request ) {
|
||||
$api = $this->api();
|
||||
if ( is_wp_error( $api ) ) {
|
||||
return $api;
|
||||
}
|
||||
$data = $request->get_json_params();
|
||||
return $this->respond( $api->add_customer( $data ) );
|
||||
}
|
||||
|
||||
public function make_booking( WP_REST_Request $request ) {
|
||||
$api = $this->api();
|
||||
if ( is_wp_error( $api ) ) {
|
||||
return $api;
|
||||
}
|
||||
$data = $request->get_json_params();
|
||||
return $this->respond( $api->make_booking( $data ) );
|
||||
}
|
||||
|
||||
public function confirm_booking( WP_REST_Request $request ) {
|
||||
$api = $this->api();
|
||||
if ( is_wp_error( $api ) ) {
|
||||
return $api;
|
||||
}
|
||||
return $this->respond( $api->confirm_booking(
|
||||
$request->get_param( 'reservationId' )
|
||||
) );
|
||||
}
|
||||
|
||||
public function get_agreements( WP_REST_Request $request ) {
|
||||
$api = $this->api();
|
||||
if ( is_wp_error( $api ) ) {
|
||||
return $api;
|
||||
}
|
||||
return $this->respond( $api->get_agreements() );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Softra Rent API client with JWT token caching.
|
||||
* Uses native cURL (matching softra-test.php) instead of WP HTTP API.
|
||||
*/
|
||||
class Carei_Softra_API {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private $base_url;
|
||||
private $username;
|
||||
private $password;
|
||||
|
||||
const TOKEN_TRANSIENT = 'carei_softra_token';
|
||||
const TOKEN_EXPIRY = 3000; // 50 minutes (token valid 60 min)
|
||||
|
||||
private function __construct( $base_url, $username, $password ) {
|
||||
$this->base_url = rtrim( $base_url, '/' );
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
}
|
||||
|
||||
public static function init( $base_url, $username, $password ) {
|
||||
if ( null === self::$instance ) {
|
||||
self::$instance = new self( $base_url, $username, $password );
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public static function get_instance() {
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Native cURL request — mirrors softra-test.php requestJson().
|
||||
*/
|
||||
private function curl_request( $method, $url, $payload = null, $extra_headers = array() ) {
|
||||
$ch = curl_init( $url );
|
||||
if ( false === $ch ) {
|
||||
return new WP_Error( 'carei_curl_init', 'cURL init failed' );
|
||||
}
|
||||
|
||||
$headers = array_merge( array( 'Accept: application/json' ), $extra_headers );
|
||||
if ( null !== $payload ) {
|
||||
$headers[] = 'Content-Type: application/json';
|
||||
}
|
||||
|
||||
curl_setopt_array( $ch, array(
|
||||
CURLOPT_CUSTOMREQUEST => strtoupper( $method ),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_HTTPHEADER => $headers,
|
||||
CURLOPT_TIMEOUT => 30,
|
||||
CURLOPT_CONNECTTIMEOUT => 15,
|
||||
) );
|
||||
|
||||
if ( null !== $payload ) {
|
||||
curl_setopt( $ch, CURLOPT_POSTFIELDS, json_encode( $payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) );
|
||||
}
|
||||
|
||||
$raw = curl_exec( $ch );
|
||||
$err = curl_error( $ch );
|
||||
$status = (int) curl_getinfo( $ch, CURLINFO_HTTP_CODE );
|
||||
curl_close( $ch );
|
||||
|
||||
if ( false === $raw ) {
|
||||
return new WP_Error( 'carei_curl_error', 'cURL error: ' . $err );
|
||||
}
|
||||
|
||||
$decoded = json_decode( $raw, true );
|
||||
if ( ! is_array( $decoded ) ) {
|
||||
$decoded = null;
|
||||
}
|
||||
|
||||
return array(
|
||||
'status' => $status,
|
||||
'body' => $decoded,
|
||||
'raw' => $raw,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JWT token (cached in WP transient).
|
||||
*/
|
||||
public function get_token() {
|
||||
$token = get_transient( self::TOKEN_TRANSIENT );
|
||||
if ( false !== $token ) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
$res = $this->curl_request( 'POST', $this->base_url . '/account/auth', array(
|
||||
'login' => $this->username,
|
||||
'password' => $this->password,
|
||||
) );
|
||||
|
||||
if ( is_wp_error( $res ) ) {
|
||||
return new WP_Error( 'carei_auth_failed', 'Softra API auth failed: ' . $res->get_error_message() );
|
||||
}
|
||||
|
||||
if ( 200 !== $res['status'] || empty( $res['body']['token'] ) ) {
|
||||
$detail = isset( $res['raw'] ) ? $res['raw'] : '';
|
||||
return new WP_Error( 'carei_auth_failed', 'Softra API auth failed (HTTP ' . $res['status'] . '): ' . $detail );
|
||||
}
|
||||
|
||||
$token = $res['body']['token'];
|
||||
set_transient( self::TOKEN_TRANSIENT, $token, self::TOKEN_EXPIRY );
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic API request with auth.
|
||||
*/
|
||||
public function request( $method, $endpoint, $body = null, $query_params = array() ) {
|
||||
$token = $this->get_token();
|
||||
if ( is_wp_error( $token ) ) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
$url = $this->base_url . $endpoint;
|
||||
if ( ! empty( $query_params ) ) {
|
||||
$url .= '?' . http_build_query( $query_params );
|
||||
}
|
||||
|
||||
$res = $this->curl_request(
|
||||
$method,
|
||||
$url,
|
||||
$body,
|
||||
array( 'Authorization: Bearer ' . $token )
|
||||
);
|
||||
|
||||
if ( is_wp_error( $res ) ) {
|
||||
return $res;
|
||||
}
|
||||
|
||||
if ( $res['status'] < 200 || $res['status'] >= 300 ) {
|
||||
return new WP_Error(
|
||||
'carei_api_error',
|
||||
'Softra API error: HTTP ' . $res['status'],
|
||||
array( 'status' => $res['status'], 'body' => $res['body'] )
|
||||
);
|
||||
}
|
||||
|
||||
return $res['body'];
|
||||
}
|
||||
|
||||
// ─── Public API Methods ───────────────────────────────────────
|
||||
|
||||
public function get_branches() {
|
||||
return $this->request( 'GET', '/branch/list' );
|
||||
}
|
||||
|
||||
public function get_all_car_classes() {
|
||||
return $this->request( 'GET', '/car/class/listAll' );
|
||||
}
|
||||
|
||||
public function get_car_classes( $date_from, $date_to, $branch_name ) {
|
||||
return $this->request( 'POST', '/car/class/list', array(
|
||||
'dateFrom' => $date_from,
|
||||
'dateTo' => $date_to,
|
||||
'branchName' => $branch_name,
|
||||
) );
|
||||
}
|
||||
|
||||
public function get_car_models( $date_from, $date_to, $branch_name, $category = '' ) {
|
||||
return $this->request( 'POST', '/car/model/list', array(
|
||||
'dateFrom' => $date_from,
|
||||
'dateTo' => $date_to,
|
||||
'branchName' => $branch_name,
|
||||
'category' => $category,
|
||||
), array( 'includeBrandDetails' => 'true' ) );
|
||||
}
|
||||
|
||||
public function get_pricelist( $category, $date_from, $date_to, $pickup_location, $language = 'pl', $currency = 'PLN' ) {
|
||||
return $this->request( 'POST', '/pricelist/list', array(
|
||||
'category' => $category,
|
||||
'dateFrom' => $date_from,
|
||||
'dateTo' => $date_to,
|
||||
'pickUpLocation' => $pickup_location,
|
||||
'language' => $language,
|
||||
'currency' => $currency,
|
||||
) );
|
||||
}
|
||||
|
||||
public function get_pricing_summary( $params ) {
|
||||
return $this->request( 'POST', '/rent/princingSummary', $params );
|
||||
}
|
||||
|
||||
public function add_customer( $data ) {
|
||||
return $this->request( 'POST', '/customer/add', $data );
|
||||
}
|
||||
|
||||
public function make_booking( $data ) {
|
||||
return $this->request( 'POST', '/rent/makebooking', $data );
|
||||
}
|
||||
|
||||
public function confirm_booking( $reservation_id ) {
|
||||
return $this->request( 'POST', '/rent/confirm', array(
|
||||
'reservationId' => $reservation_id,
|
||||
) );
|
||||
}
|
||||
|
||||
public function get_agreements() {
|
||||
return $this->request( 'GET', '/agreement/def/list' );
|
||||
}
|
||||
}
|
||||
BIN
wp-content/themes/.DS_Store
vendored
BIN
wp-content/themes/.DS_Store
vendored
Binary file not shown.
BIN
wp-includes/.DS_Store
vendored
BIN
wp-includes/.DS_Store
vendored
Binary file not shown.
BIN
wp-includes/Requests/.DS_Store
vendored
BIN
wp-includes/Requests/.DS_Store
vendored
Binary file not shown.
BIN
wp-includes/SimplePie/.DS_Store
vendored
BIN
wp-includes/SimplePie/.DS_Store
vendored
Binary file not shown.
BIN
wp-includes/blocks/.DS_Store
vendored
BIN
wp-includes/blocks/.DS_Store
vendored
Binary file not shown.
BIN
wp-includes/images/.DS_Store
vendored
BIN
wp-includes/images/.DS_Store
vendored
Binary file not shown.
BIN
wp-includes/js/.DS_Store
vendored
BIN
wp-includes/js/.DS_Store
vendored
Binary file not shown.
BIN
wp-includes/rest-api/.DS_Store
vendored
BIN
wp-includes/rest-api/.DS_Store
vendored
Binary file not shown.
BIN
wp-includes/sodium_compat/.DS_Store
vendored
BIN
wp-includes/sodium_compat/.DS_Store
vendored
Binary file not shown.
Reference in New Issue
Block a user