This commit is contained in:
2026-05-07 14:57:59 +02:00
parent c4a485e530
commit 811069a25c
35 changed files with 2980 additions and 30 deletions

View File

@@ -0,0 +1,113 @@
/* Yacht Calendar (All) — wspólny widok wszystkich jachtów.
Layout, instrukcja, legenda i formularz dziedziczą z calendar.css
(klasy .yacht-inquiry-layout, .yacht-calendar-instructions, .yacht-calendar-legend,
.yacht-inquiry-form-container, .yacht-inquiry-form). Tutaj tylko nadpisania
specyficzne dla widgetu zbiorczego. */
.yacht-calendar-all-wrapper {
max-width: 1200px;
margin: 0 auto 40px;
padding: 20px;
width: 100%;
box-sizing: border-box;
}
.yacht-calendar-all {
width: 100%;
/* Ciemne granatowe tło — emuluje styl /rezerwacja-maja/ gdzie komórki bg-event
są semi-transparent (#f5f9ff @ 0.66) nad ciemnym tłem parent containera. */
background: #0e2036;
}
/* Komórki przyszłe — semi-transparent biały, daje ciemnoszary efekt nad #0e2036
(efekt identyczny jak yacht-day-available bg-event w single-yacht widget). */
.yacht-calendar-all .fc-daygrid-day {
background: rgba(245, 249, 255, 0.4);
}
/* Przeszłe — białe (jak yacht-day-available .fc-event-past w single-yacht). */
.yacht-calendar-all .fc-daygrid-day.fc-day-past {
background: #ffffff;
}
/* Dziś — lekko jaśniejszy ciemny. */
.yacht-calendar-all .fc-daygrid-day.fc-day-today {
background: rgba(255, 255, 255, 0.55);
}
/* Sąsiedni miesiąc — taki sam jak przyszłe (spójny ciemny). */
.yacht-calendar-all .fc-daygrid-day.fc-day-other {
background: rgba(245, 249, 255, 0.32);
}
/* Select w formularzu zapytania — calendar.css nie ma reguł dla <select>,
wymuszamy ten sam styl co inputy (ciemnoprzezroczysty, biały tekst, custom strzałka). */
.yacht-inquiry-form select {
width: 100%;
padding: 10px 12px;
border: 1px solid hsla(0, 0%, 100%, 0.2);
border-radius: 4px;
font-size: 14px;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
background-color: hsla(0, 0%, 100%, 0.1);
color: #fff;
box-sizing: border-box;
font-family: inherit;
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'><path d='M1 1l5 5 5-5' stroke='white' stroke-width='2' fill='none'/></svg>");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
cursor: pointer;
}
.yacht-inquiry-form select:focus {
outline: none;
border-color: #bc1834;
box-shadow: 0 0 0 3px rgba(188, 24, 52, 0.3);
background-color: hsla(0, 0%, 100%, 0.15);
}
.yacht-inquiry-form select option {
background: #021526;
color: #fff;
}
/* Event styling — pełne wypełnienie kafelka kolorem jachtu, bez kropek/czasu */
.yacht-calendar-all .fc-event {
border: none !important;
padding: 2px 4px;
font-size: 12px;
font-weight: 500;
color: #fff;
cursor: default;
}
.yacht-calendar-all .fc-event-title,
.yacht-calendar-all .fc-daygrid-event-dot,
.yacht-calendar-all .fc-event-time {
display: none !important;
}
/* Pasek eventu zawsze wyższy żeby był czytelny bez tekstu */
.yacht-calendar-all .fc-daygrid-event {
min-height: 18px;
}
/* Half-day visual: gradient wpisywany przez calendar-all.js (eventDidMount), który
liczy szerokość komórki dnia i ustawia background-image dla danego segmentu. */
/* Mobile */
@media (max-width: 600px) {
.yacht-calendar-all .fc-event {
font-size: 10px;
}
.yacht-calendar-all .fc-toolbar.fc-header-toolbar {
flex-direction: column;
gap: 8px;
}
}

View File

@@ -0,0 +1,229 @@
/**
* Yacht Calendar (All) — wspólny widok wszystkich jachtów.
*
* Inicjalizuje FullCalendar dayGridMonth dla każdego elementu .yacht-calendar-all-wrapper
* na stronie. Pobiera eventy z REST `/availability/all` (timed 12:00→12:00), renderuje
* legendę kolorów wyciągniętą z eventów i dodaje klasy half-day na pierwszym/ostatnim
* dniu każdej rezerwacji.
*/
(function ($) {
'use strict';
if (typeof window.FullCalendar === 'undefined') {
return;
}
function pad(n) {
return n < 10 ? '0' + n : '' + n;
}
function ymd(date) {
return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate());
}
function initCalendar(wrapper) {
var $wrapper = $(wrapper);
var $cal = $wrapper.find('.yacht-calendar-all');
if (!$cal.length) return;
var restUrl = $wrapper.data('rest');
if (!restUrl) return;
var heightPx = parseInt($wrapper.data('height'), 10);
if (!heightPx || heightPx < 200) heightPx = 650;
var calendar = new window.FullCalendar.Calendar($cal.get(0), {
initialView: 'dayGridMonth',
locale: 'pl',
firstDay: 1,
headerToolbar: {
left: 'prev,next today',
center: 'title',
right: ''
},
height: heightPx,
displayEventTime: false,
eventDisplay: 'block',
events: function (fetchInfo, successCallback, failureCallback) {
var url = restUrl
+ (restUrl.indexOf('?') === -1 ? '?' : '&')
+ 'start=' + ymd(fetchInfo.start)
+ '&end=' + ymd(fetchInfo.end);
$.getJSON(url)
.done(function (data) {
successCallback(data || []);
})
.fail(function () {
failureCallback(new Error('Failed to fetch /availability/all'));
});
},
eventDidMount: function (info) {
// Per-event color via CSS variable.
var color = info.event.backgroundColor || '#3498db';
info.el.style.setProperty('--yc-event-color', color);
// Half-day visual: gradient z half-cell transparent na pierwszym/ostatnim dniu.
// Liczone po renderze (wymaga znajomości szerokości komórki dnia w siatce).
// Przekładamy na requestAnimationFrame żeby DOM był ułożony.
window.requestAnimationFrame(function () {
applyHalfDayGradient(info);
});
},
eventContent: function () {
// Pasek koloru bez treści — privacy: nie pokazujemy nazwisk klientów ani nazw jachtów.
return { html: '' };
}
});
calendar.render();
// Inquiry form submission (yacht select dropdown).
var $form = $wrapper.find('.yacht-calendar-all-inquiry-form');
if ($form.length) {
$form.on('submit', function (e) {
e.preventDefault();
var $submitBtn = $form.find('.yacht-booking-submit');
var $response = $form.find('.yacht-calendar-all-inquiry-response');
var originalBtnText = $submitBtn.text();
var i18n = (window.yachtBookingData && window.yachtBookingData.i18n) || {};
var yachtId = parseInt($form.find('[name="yacht_id"]').val(), 10) || 0;
if (!yachtId) {
$response.html('<div class="booking-error" style="padding: 12px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 4px;"><strong>' +
(i18n.errorTitle || 'Błąd!') + '</strong> Wybierz jacht z listy.</div>');
return;
}
$submitBtn.prop('disabled', true).text(i18n.submitting || 'Wysyłanie...');
$response.html('');
var formData = {
yacht_id: yachtId,
customer_name: $form.find('[name="customer_name"]').val(),
customer_email: $form.find('[name="customer_email"]').val(),
customer_phone: $form.find('[name="customer_phone"]').val(),
preferred_dates: $form.find('[name="preferred_dates"]').val(),
message: $form.find('[name="message"]').val()
};
$.ajax({
url: window.yachtBookingData.apiUrl + '/inquiries',
method: 'POST',
beforeSend: function (xhr) {
xhr.setRequestHeader('X-WP-Nonce', window.yachtBookingData.nonce);
},
contentType: 'application/json',
data: JSON.stringify(formData),
success: function (resp) {
$response.html('<div class="booking-success" style="padding: 12px; background: #d4edda; color: #155724; border: 1px solid #c3e6cb; border-radius: 4px;"><strong>' +
(i18n.successTitle || 'Sukces!') + '</strong> ' +
((resp && resp.message) || i18n.inquirySuccess || 'Twoje zapytanie zostało wysłane.') +
'</div>');
$form[0].reset();
},
error: function (xhr) {
var msg = i18n.errorMessage || 'Wystąpił błąd. Spróbuj ponownie.';
if (xhr.responseJSON && xhr.responseJSON.message) {
msg = xhr.responseJSON.message;
}
$response.html('<div class="booking-error" style="padding: 12px; background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; border-radius: 4px;"><strong>' +
(i18n.errorTitle || 'Błąd!') + '</strong> ' + msg + '</div>');
},
complete: function () {
$submitBtn.prop('disabled', false).text(originalBtnText);
}
});
});
}
}
/**
* Apply half-day gradient to event bar. Pierwsze dni rezerwacji: lewa połowa pierwszej
* komórki transparentna (yacht wraca w południe). Ostatnie: prawa połowa ostatniej
* komórki transparentna (yacht wypływa w południe). Dla segmentów środkowych — pełny kolor.
*/
function applyHalfDayGradient(info) {
var el = info.el;
var rect = el.getBoundingClientRect();
if (!rect.width) return;
// Find a sibling day cell to read its width.
var dayCell = el.closest('.fc-daygrid-day') || el.closest('td');
// Fallback: scan parent for any .fc-daygrid-day sibling.
if (!dayCell) {
var grid = el.closest('.fc-daygrid');
if (grid) {
dayCell = grid.querySelector('.fc-daygrid-day');
}
}
if (!dayCell) return;
var cellRect = dayCell.getBoundingClientRect();
if (!cellRect.width) return;
var halfPct = (cellRect.width / 2) / rect.width * 100;
// Clamp to safe bounds.
if (halfPct < 1) halfPct = 1;
if (halfPct > 49) halfPct = 49;
var color = el.style.getPropertyValue('--yc-event-color') || '#3498db';
var startTrans = info.isStart;
var endTrans = info.isEnd;
// Build gradient.
var stops;
if (startTrans && endTrans) {
// Single segment containing both start and end.
stops = [
'transparent 0%',
'transparent ' + halfPct.toFixed(2) + '%',
color + ' ' + halfPct.toFixed(2) + '%',
color + ' ' + (100 - halfPct).toFixed(2) + '%',
'transparent ' + (100 - halfPct).toFixed(2) + '%',
'transparent 100%'
];
} else if (startTrans) {
stops = [
'transparent 0%',
'transparent ' + halfPct.toFixed(2) + '%',
color + ' ' + halfPct.toFixed(2) + '%',
color + ' 100%'
];
} else if (endTrans) {
stops = [
color + ' 0%',
color + ' ' + (100 - halfPct).toFixed(2) + '%',
'transparent ' + (100 - halfPct).toFixed(2) + '%',
'transparent 100%'
];
} else {
// Middle segment — pełny kolor.
el.style.backgroundColor = color;
el.style.backgroundImage = 'none';
return;
}
el.style.backgroundImage = 'linear-gradient(to right, ' + stops.join(', ') + ')';
el.style.backgroundColor = 'transparent';
}
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, function (m) {
return ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;'
})[m];
});
}
$(function () {
$('.yacht-calendar-all-wrapper').each(function () {
initCalendar(this);
});
});
})(jQuery);

View File

@@ -0,0 +1,282 @@
<?php
/**
* Yacht Calendar (All) Widget for Elementor
*
* Wspólny kalendarz pokazujący rezerwacje WSZYSTKICH publikowanych jachtów + globalne
* wydarzenia (sync_mode=global). Read-only, kolory per-jacht z auto palety, half-day
* przez timed events 12:00 → 12:00.
*
* @package YachtBooking
*/
namespace YachtBooking;
// Exit if accessed directly
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
use Elementor\Controls_Manager;
use Elementor\Widget_Base;
/**
* Yacht Calendar (All) Widget Class
*/
class Calendar_Widget_All extends Widget_Base {
/**
* Widget name (Elementor identifier).
*/
public function get_name() {
return 'yacht-calendar-all';
}
/**
* Widget title.
*/
public function get_title() {
return esc_html__( 'Kalendarz Jachtów (wszystkie)', 'yacht-booking' );
}
/**
* Widget icon.
*/
public function get_icon() {
return 'eicon-calendar';
}
/**
* Widget categories.
*/
public function get_categories() {
return array( 'basic' );
}
/**
* Widget keywords.
*/
public function get_keywords() {
return array( 'yacht', 'calendar', 'wszystkie', 'flota', 'kalendarz', 'rezerwacje' );
}
/**
* Register widget controls.
*/
protected function register_controls() {
$this->start_controls_section(
'content_section',
array(
'label' => esc_html__( 'Ustawienia Kalendarza', 'yacht-booking' ),
'tab' => Controls_Manager::TAB_CONTENT,
)
);
$this->add_control(
'calendar_height',
array(
'label' => esc_html__( 'Wysokość kalendarza', 'yacht-booking' ),
'type' => Controls_Manager::SLIDER,
'range' => array(
'px' => array(
'min' => 400,
'max' => 1000,
),
),
'default' => array(
'size' => 650,
'unit' => 'px',
),
)
);
$this->add_control(
'show_legend',
array(
'label' => esc_html__( 'Pokaż legendę kolorów', 'yacht-booking' ),
'type' => Controls_Manager::SWITCHER,
'label_on' => esc_html__( 'Tak', 'yacht-booking' ),
'label_off' => esc_html__( 'Nie', 'yacht-booking' ),
'return_value' => 'yes',
'default' => 'yes',
)
);
$this->end_controls_section();
}
/**
* Render widget output.
*/
protected function render() {
$settings = $this->get_settings_for_display();
$height = ! empty( $settings['calendar_height']['size'] ) ? (int) $settings['calendar_height']['size'] : 650;
$show_legend = ! isset( $settings['show_legend'] ) || 'yes' === $settings['show_legend'];
$dom_id = 'yacht-calendar-all-' . $this->get_id();
echo Calendar_All_View::render( $dom_id, $height, $show_legend ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
/**
* Pomocnicza klasa renderująca markup widgetu i shortcode.
*
* Wspólna dla widgetu Elementor i shortcode `[yacht_calendar_all]` żeby uniknąć duplikacji.
*/
class Calendar_All_View {
/**
* Render markup wspólnego kalendarza.
*
* @param string $dom_id Unikalny ID kontenera FullCalendar.
* @param int $height Wysokość kalendarza w px.
* @param bool $show_legend Czy renderować legendę.
* @return string HTML.
*/
public static function render( $dom_id, $height = 650, $show_legend = true ) {
$rest_url = esc_url_raw( rest_url( 'yacht-booking/v1/availability/all' ) );
// Yachts for legend + form select.
$yacht_posts = get_posts(
array(
'post_type' => 'yacht',
'post_status' => 'publish',
'posts_per_page' => -1,
'orderby' => 'ID',
'order' => 'ASC',
)
);
$yacht_ids = wp_list_pluck( $yacht_posts, 'ID' );
$color_map = \YachtBooking\Rest_Controller::get_yacht_color_palette( $yacht_ids );
$global_color = \YachtBooking\Rest_Controller::GLOBAL_EVENT_COLOR;
$sync_mode = \YachtBooking\Settings::get_ical_sync_mode();
$terms_url = \YachtBooking\Settings::get_terms_page_url();
$form_uid = preg_replace( '/[^a-z0-9_-]/i', '', $dom_id );
ob_start();
?>
<div class="yacht-calendar-all-wrapper"
data-rest="<?php echo esc_attr( $rest_url ); ?>"
data-show-legend="<?php echo $show_legend ? '1' : '0'; ?>"
data-height="<?php echo esc_attr( (int) $height ); ?>">
<div class="yacht-calendar-instructions">
<p>
<?php esc_html_e( 'Aby zarezerwować termin, wypełnij formularz po prawej stronie albo skontaktuj się z nami telefonicznie lub mailowo.', 'yacht-booking' ); ?>
</p>
</div>
<?php if ( $show_legend ) : ?>
<div class="yacht-calendar-legend yacht-calendar-all-legend" aria-label="<?php esc_attr_e( 'Legenda kalendarza', 'yacht-booking' ); ?>">
<?php if ( 'global' === $sync_mode ) : ?>
<span class="yacht-legend-item">
<span class="yacht-legend-swatch" style="background-color: <?php echo esc_attr( $global_color ); ?>;"></span>
<?php esc_html_e( 'Rezerwacja', 'yacht-booking' ); ?>
</span>
<?php else : ?>
<?php foreach ( $yacht_posts as $yacht ) : ?>
<?php $color = isset( $color_map[ $yacht->ID ] ) ? $color_map[ $yacht->ID ] : $global_color; ?>
<span class="yacht-legend-item">
<span class="yacht-legend-swatch" style="background-color: <?php echo esc_attr( $color ); ?>;"></span>
<?php echo esc_html( $yacht->post_title ); ?>
</span>
<?php endforeach; ?>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="yacht-inquiry-layout yacht-calendar-all-layout">
<div class="yacht-inquiry-calendar-col">
<div id="<?php echo esc_attr( $dom_id ); ?>"
class="yacht-calendar yacht-calendar-all"
style="height: <?php echo esc_attr( $height ); ?>px;"></div>
</div>
<div class="yacht-inquiry-form-col">
<div class="yacht-inquiry-form-container">
<h4><?php esc_html_e( 'Zapytaj o rezerwację', 'yacht-booking' ); ?></h4>
<p class="yacht-inquiry-desc">
<?php esc_html_e( 'Wybierz jacht i wypełnij formularz — odezwiemy się w sprawie dostępności i cen.', 'yacht-booking' ); ?>
</p>
<form class="yacht-inquiry-form yacht-calendar-all-inquiry-form">
<?php wp_nonce_field( 'yacht_inquiry_submit', 'yacht_inquiry_nonce' ); ?>
<div class="form-field">
<label for="all_inquiry_yacht_<?php echo esc_attr( $form_uid ); ?>">
<?php esc_html_e( 'Jacht', 'yacht-booking' ); ?> <span class="required">*</span>
</label>
<select id="all_inquiry_yacht_<?php echo esc_attr( $form_uid ); ?>" name="yacht_id" required>
<option value=""><?php esc_html_e( '— wybierz jacht —', 'yacht-booking' ); ?></option>
<?php foreach ( $yacht_posts as $yacht ) : ?>
<option value="<?php echo esc_attr( $yacht->ID ); ?>"><?php echo esc_html( $yacht->post_title ); ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="form-field">
<label for="all_inquiry_name_<?php echo esc_attr( $form_uid ); ?>">
<?php esc_html_e( 'Imię i nazwisko', 'yacht-booking' ); ?> <span class="required">*</span>
</label>
<input type="text" id="all_inquiry_name_<?php echo esc_attr( $form_uid ); ?>" name="customer_name" required>
</div>
<div class="form-field">
<label for="all_inquiry_email_<?php echo esc_attr( $form_uid ); ?>">
<?php esc_html_e( 'Email', 'yacht-booking' ); ?> <span class="required">*</span>
</label>
<input type="email" id="all_inquiry_email_<?php echo esc_attr( $form_uid ); ?>" name="customer_email" required>
</div>
<div class="form-field">
<label for="all_inquiry_phone_<?php echo esc_attr( $form_uid ); ?>">
<?php esc_html_e( 'Telefon', 'yacht-booking' ); ?> <span class="required">*</span>
</label>
<input type="tel" id="all_inquiry_phone_<?php echo esc_attr( $form_uid ); ?>" name="customer_phone" required>
</div>
<div class="form-field">
<label for="all_inquiry_dates_<?php echo esc_attr( $form_uid ); ?>">
<?php esc_html_e( 'Preferowane terminy', 'yacht-booking' ); ?>
</label>
<input type="text"
id="all_inquiry_dates_<?php echo esc_attr( $form_uid ); ?>"
name="preferred_dates"
placeholder="<?php esc_attr_e( 'np. 15-22 lipca', 'yacht-booking' ); ?>">
</div>
<div class="form-field">
<label for="all_inquiry_message_<?php echo esc_attr( $form_uid ); ?>">
<?php esc_html_e( 'Wiadomość', 'yacht-booking' ); ?>
</label>
<textarea id="all_inquiry_message_<?php echo esc_attr( $form_uid ); ?>"
name="message"
rows="3"
placeholder="<?php esc_attr_e( 'Dodatkowe pytania lub uwagi...', 'yacht-booking' ); ?>"></textarea>
</div>
<?php if ( $terms_url ) : ?>
<p class="booking-terms">
<?php
printf(
wp_kses_post( __( 'Wysyłając formularz akceptujesz %s.', 'yacht-booking' ) ),
'<a href="' . esc_url( $terms_url ) . '" target="_blank" rel="noopener">' . esc_html__( 'regulamin', 'yacht-booking' ) . '</a>'
);
?>
</p>
<?php endif; ?>
<div class="form-actions">
<button type="submit" class="yacht-booking-submit">
<?php esc_html_e( 'Wyślij zapytanie', 'yacht-booking' ); ?>
</button>
</div>
<div class="yacht-inquiry-response yacht-calendar-all-inquiry-response"></div>
</form>
</div>
</div>
</div>
</div>
<?php
return ob_get_clean();
}
}

View File

@@ -41,6 +41,39 @@ class Shortcode {
*/
private function __construct() {
add_shortcode( 'yacht_calendar', array( $this, 'render_calendar' ) );
add_shortcode( 'yacht_calendar_all', array( $this, 'render_calendar_all' ) );
}
/**
* Render shortcode `[yacht_calendar_all]` — wspólny kalendarz wszystkich jachtów.
*
* Atrybuty:
* - height: wysokość w px (default 650)
* - show_legend: yes|no (default yes)
*
* @param array $atts Shortcode attributes.
* @return string HTML.
*/
public function render_calendar_all( $atts ) {
$atts = shortcode_atts(
array(
'height' => 650,
'show_legend' => 'yes',
),
$atts,
'yacht_calendar_all'
);
// Lazy-load widget class for the View helper.
if ( ! class_exists( '\YachtBooking\Calendar_All_View' ) ) {
require_once YACHT_BOOKING_PLUGIN_DIR . 'frontend/class-calendar-widget-all.php';
}
$dom_id = 'yacht-calendar-all-' . wp_rand( 1000, 9999 );
$height = (int) $atts['height'];
$show_legend = 'yes' === strtolower( (string) $atts['show_legend'] );
return Calendar_All_View::render( $dom_id, $height, $show_legend );
}
/**