This commit is contained in:
2026-03-25 00:41:16 +01:00
parent 1739f354d1
commit a82ec90a51
48 changed files with 4019 additions and 0 deletions

BIN
.DS_Store vendored

Binary file not shown.

40
.claude/sessionstate.md Normal file
View 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.

View 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
View 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
View 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
View 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
View 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

View 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>

View 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*

View 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>

View 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)

View 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)

View 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

View 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

View 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)

View 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
View File

@@ -0,0 +1,2 @@
/cache
/project.local.yml

152
.serena/project.yml Normal file
View 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 readonly.
# 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
View 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
}

View File

@@ -0,0 +1,177 @@
# Formularz rezerwacji samochodu - Figma Design Specs
Plik Figma: https://www.figma.com/design/Q3i1CT4DL91dl638a43RBg/Carei
## Zrzuty ekranu
### Desktop (1440x1024)
![Desktop Formularz](screenshot-desktop.png)
### Mobile (390x1438)
![Mobile Formularz](screenshot-mobile.png)
## 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
![Popup Desktop](screenshot-popup-desktop.png)
### 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
![Input Kraje Stany](screenshot-input-kraje-stany.png)
### 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 |

View 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
</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
</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>
);
}

View 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 </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>
);
}

View 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
</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
</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>
);
}

View 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` |

View 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>
);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

BIN
wp-admin/.DS_Store vendored

Binary file not shown.

BIN
wp-content/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -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;
}
}

View File

@@ -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, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// ─── Init ─────────────────────────────────────────────────────
function init() {
initRefs();
if (!overlay || !form) return;
initModal();
initSameReturn();
initDynamicLoading();
initClearErrors();
initSubmit();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View 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' ),
) );
} );

View File

@@ -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
}
}

View File

@@ -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() );
}
}

View File

@@ -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' );
}
}

Binary file not shown.

BIN
wp-includes/.DS_Store vendored

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.