@zuii/booking 0.1.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,11 +18,12 @@
18
18
 
19
19
  .booking__calendar-column {
20
20
  flex: 1;
21
- max-width: 100%;
21
+ width: 100%;
22
22
  }
23
- @media (max-width: 450px) {
23
+ @media screen and (min-width: 640px) {
24
24
  .booking__calendar-column {
25
- width: 100%;
25
+ flex: none;
26
+ width: 400px;
26
27
  }
27
28
  }
28
29
 
@@ -121,4 +122,10 @@
121
122
  color: var(--booking-text);
122
123
  }
123
124
 
125
+ .booking-form {
126
+ display: grid;
127
+ gap: var(--spacing-sm);
128
+ margin: 1rem 0;
129
+ }
130
+
124
131
  /*# sourceMappingURL=booking.css.map */
@@ -1 +1 @@
1
- {"version":3,"sourceRoot":"","sources":["../../src/style/_booking.scss","../../src/style/_booking-empty-message.scss","../../src/style/_booking-slots.scss","../../src/style/_booking-slot.scss","../../src/style/_booking-footer.scss","../../src/style/_booking-empty.scss","../../src/style/_booking-title.scss"],"names":[],"mappings":"AAAA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAKD;EACC;EAEA;;AAEA;EALD;IAME;;;;AAIF;EACC;;;AAKD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACvDD;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;;AAGD;EACC;EACA;;;ACdF;EACC;EACA;EACA;;;ACHD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;;;AAIF;EACC;EACA;;;AAGD;EACC;EACA;;;ACxBD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACRD;EACC;EACA;;;ACFD;EACC;EACA","file":"booking.css"}
1
+ {"version":3,"sourceRoot":"","sources":["../../src/style/_booking.scss","../../src/style/_booking-empty-message.scss","../../src/style/_booking-slots.scss","../../src/style/_booking-slot.scss","../../src/style/_booking-footer.scss","../../src/style/_booking-empty.scss","../../src/style/_booking-title.scss","../../src/style/_booking-form.scss"],"names":[],"mappings":"AAAA;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAMD;EACC;EACA;;AAEA;EAJD;IAKE;IACD;;;;AAMD;EACC;;;AAKD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AC1DD;EACC;EACA;EACA;;AAEA;EACC;EACA;EACA;EACA;;AAGD;EACC;EACA;;;ACdF;EACC;EACA;EACA;;;ACHD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;;;AAIF;EACC;EACA;;;AAGD;EACC;EACA;;;ACxBD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;ACRD;EACC;EACA;;;ACFD;EACC;EACA;;;ACFD;EACC;EACA;EACA","file":"booking.css"}
@@ -1 +1 @@
1
- :root{--booking-bg: var(--white);--booking-color: var(--primary);--booking-text: var(--booking-color);--booking-selected-bg: #3c4e62;--booking-selected-text: #fff;--booking-footer-bg: #3c4e62;--booking-footer-text: #fff;--booking-radius: var(--radius-base);--booking-gap: 1rem}.booking-wrapper{display:flex;gap:var(--booking-gap);flex-wrap:wrap}.booking__calendar-column{flex:1;max-width:100%}@media(max-width: 450px){.booking__calendar-column{width:100%}}.booking__slots-column{flex:1}.booking{width:100%;border-radius:var(--booking-radius);overflow:hidden;color:var(--booking-text);user-select:none;display:flex;flex-direction:column;height:100%}.booking--empty{display:flex;flex-direction:column;height:100%;justify-content:center;align-items:center;background:var(--booking-bg);border:1px dashed var(--booking-color);border-radius:var(--booking-radius)}.booking__empty-message{text-align:center;color:#94a3b8;padding:40px}.booking__empty-message svg{width:48px;height:48px;margin-bottom:16px;opacity:.5}.booking__empty-message p{font-size:16px;margin:0}.booking__slots{display:grid;grid-template-columns:repeat(auto-fill, minmax(130px, 1fr));gap:15px}.booking__slot{background:var(--booking-bg);padding:20px 10px;border:none;border-radius:4px;text-align:center;cursor:pointer;font-weight:700;font-size:20px;color:var(--booking-text);transition:all .2s}.booking__slot:hover{background:color-mix(in srgb, var(--booking-bg), #000 5%)}.booking__slot--selected{background:var(--booking-selected-bg);color:var(--booking-selected-text)}.booking__slot--range{background:color-mix(in srgb, var(--booking-selected-bg), transparent 60%);color:var(--booking-selected-text)}.booking__footer{background:var(--booking-footer-bg);color:var(--booking-footer-text);padding:25px;border-radius:4px;text-align:left;font-size:22px;font-weight:400;line-height:1.4}.booking__empty{color:#6c757d;font-style:italic}.booking__title{font-size:1.2rem;color:var(--booking-text)}/*# sourceMappingURL=booking.min.css.map */
1
+ :root{--booking-bg: var(--white);--booking-color: var(--primary);--booking-text: var(--booking-color);--booking-selected-bg: #3c4e62;--booking-selected-text: #fff;--booking-footer-bg: #3c4e62;--booking-footer-text: #fff;--booking-radius: var(--radius-base);--booking-gap: 1rem}.booking-wrapper{display:flex;gap:var(--booking-gap);flex-wrap:wrap}.booking__calendar-column{flex:1;width:100%}@media screen and (min-width: 640px){.booking__calendar-column{flex:none;width:400px}}.booking__slots-column{flex:1}.booking{width:100%;border-radius:var(--booking-radius);overflow:hidden;color:var(--booking-text);user-select:none;display:flex;flex-direction:column;height:100%}.booking--empty{display:flex;flex-direction:column;height:100%;justify-content:center;align-items:center;background:var(--booking-bg);border:1px dashed var(--booking-color);border-radius:var(--booking-radius)}.booking__empty-message{text-align:center;color:#94a3b8;padding:40px}.booking__empty-message svg{width:48px;height:48px;margin-bottom:16px;opacity:.5}.booking__empty-message p{font-size:16px;margin:0}.booking__slots{display:grid;grid-template-columns:repeat(auto-fill, minmax(130px, 1fr));gap:15px}.booking__slot{background:var(--booking-bg);padding:20px 10px;border:none;border-radius:4px;text-align:center;cursor:pointer;font-weight:700;font-size:20px;color:var(--booking-text);transition:all .2s}.booking__slot:hover{background:color-mix(in srgb, var(--booking-bg), #000 5%)}.booking__slot--selected{background:var(--booking-selected-bg);color:var(--booking-selected-text)}.booking__slot--range{background:color-mix(in srgb, var(--booking-selected-bg), transparent 60%);color:var(--booking-selected-text)}.booking__footer{background:var(--booking-footer-bg);color:var(--booking-footer-text);padding:25px;border-radius:4px;text-align:left;font-size:22px;font-weight:400;line-height:1.4}.booking__empty{color:#6c757d;font-style:italic}.booking__title{font-size:1.2rem;color:var(--booking-text)}.booking-form{display:grid;gap:var(--spacing-sm);margin:1rem 0}/*# sourceMappingURL=booking.min.css.map */
@@ -1 +1 @@
1
- {"version":3,"sourceRoot":"","sources":["../../src/style/_booking.scss","../../src/style/_booking-empty-message.scss","../../src/style/_booking-slots.scss","../../src/style/_booking-slot.scss","../../src/style/_booking-footer.scss","../../src/style/_booking-empty.scss","../../src/style/_booking-title.scss"],"names":[],"mappings":"AAAA,MACC,2BACA,gCACA,qCACA,+BACA,8BACA,6BACA,4BACA,qCACA,oBAGD,iBACC,aACA,uBACA,eAKD,0BACC,OAEA,eAEA,yBALD,0BAME,YAIF,uBACC,OAKD,SACC,WACA,oCACA,gBACA,0BACA,iBACA,aACA,sBACA,YAGD,gBACC,aACA,sBACA,YACA,uBACA,mBACA,6BACA,uCACA,oCCvDD,wBACC,kBACA,cACA,aAEA,4BACC,WACA,YACA,mBACA,WAGD,0BACC,eACA,SCdF,gBACC,aACA,4DACA,SCHD,eACC,6BACA,kBACA,YACA,kBACA,kBACA,eACA,gBACA,eACA,0BACA,mBAEA,qBACC,0DAIF,yBACC,sCACA,mCAGD,sBACC,2EACA,mCCxBD,iBACC,oCACA,iCACA,aACA,kBACA,gBACA,eACA,gBACA,gBCRD,gBACC,cACA,kBCFD,gBACC,iBACA","file":"booking.min.css"}
1
+ {"version":3,"sourceRoot":"","sources":["../../src/style/_booking.scss","../../src/style/_booking-empty-message.scss","../../src/style/_booking-slots.scss","../../src/style/_booking-slot.scss","../../src/style/_booking-footer.scss","../../src/style/_booking-empty.scss","../../src/style/_booking-title.scss","../../src/style/_booking-form.scss"],"names":[],"mappings":"AAAA,MACC,2BACA,gCACA,qCACA,+BACA,8BACA,6BACA,4BACA,qCACA,oBAGD,iBACC,aACA,uBACA,eAMD,0BACC,OACA,WAEA,qCAJD,0BAKE,UACD,aAMD,uBACC,OAKD,SACC,WACA,oCACA,gBACA,0BACA,iBACA,aACA,sBACA,YAGD,gBACC,aACA,sBACA,YACA,uBACA,mBACA,6BACA,uCACA,oCC1DD,wBACC,kBACA,cACA,aAEA,4BACC,WACA,YACA,mBACA,WAGD,0BACC,eACA,SCdF,gBACC,aACA,4DACA,SCHD,eACC,6BACA,kBACA,YACA,kBACA,kBACA,eACA,gBACA,eACA,0BACA,mBAEA,qBACC,0DAIF,yBACC,sCACA,mCAGD,sBACC,2EACA,mCCxBD,iBACC,oCACA,iCACA,aACA,kBACA,gBACA,eACA,gBACA,gBCRD,gBACC,cACA,kBCFD,gBACC,iBACA,0BCFD,cACC,aACA,sBACA","file":"booking.min.css"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zuii/booking",
3
- "version": "0.1.1",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "Composant Booking pour zuii",
6
6
  "main": "./dist/index.cjs",
@@ -8,7 +8,7 @@
8
8
  "types": "./dist/index.d.ts",
9
9
  "files": [
10
10
  "dist",
11
- "src/symfony"
11
+ "src"
12
12
  ],
13
13
  "exports": {
14
14
  ".": {
@@ -26,11 +26,12 @@
26
26
  },
27
27
  "dependencies": {
28
28
  "date-fns": "^4.1.0",
29
- "@zuii/calendar": "0.1.1"
29
+ "@zuii/calendar": "1.0.0",
30
+ "@zuii/modal": "0.1.2"
30
31
  },
31
32
  "peerDependencies": {
32
33
  "react": ">=16.8.0",
33
- "@zuii/core": "1.0.0"
34
+ "@zuii/core": "1.2.0"
34
35
  },
35
36
  "keywords": [
36
37
  "zuii",
@@ -0,0 +1,289 @@
1
+ import { format } from 'date-fns';
2
+ import { fr as localeFr, enUS as localeEn } from 'date-fns/locale';
3
+ import { Calendar, type CalendarOptions } from '@zuii/calendar';
4
+ import { Modal } from '@zuii/modal';
5
+
6
+
7
+ const trads: any = {
8
+ fr: {
9
+ selectSlot: 'Vous avez sélectionné le',
10
+ noSlots: 'Pas de créneaux disponibles pour cette date.',
11
+ selectDay: 'Sélectionnez un jour dans le calendrier',
12
+ selectTime: 'Sélectionner un horaire pour le',
13
+ confirmTitle: 'Confirmer la réservation',
14
+ confirmBody: 'Vous avez sélectionné le <strong>{date}</strong> {prep} <strong>{slot}</strong>.',
15
+ confirmBtn: 'Réserver ce créneau',
16
+ cancelBtn: 'Annuler'
17
+ },
18
+ en: {
19
+ selectSlot: 'You have selected',
20
+ noSlots: 'No slots available for this date.',
21
+ selectDay: 'Please select a day in the calendar',
22
+ selectTime: 'Select a time slot for',
23
+ confirmTitle: 'Confirm Booking',
24
+ confirmBody: 'You selected <strong>{date}</strong> {prep} <strong>{slot}</strong>.',
25
+ confirmBtn: 'Book this slot',
26
+ cancelBtn: 'Cancel'
27
+ }
28
+ };
29
+
30
+ export interface BookingField {
31
+ name: string;
32
+ label: string;
33
+ type: 'text' | 'email' | 'tel' | 'textarea' | 'number' | 'date';
34
+ required?: boolean;
35
+ placeholder?: string;
36
+ }
37
+
38
+ export interface BookingOptions extends Omit<CalendarOptions, 'onDateSelect'> {
39
+ lang?: 'fr' | 'en';
40
+ availability: Record<string, string[]>;
41
+ selectedDate?: Date | null;
42
+ inputName?: string;
43
+ fields?: BookingField[];
44
+ onSlotSelect?: (date: Date, slot: string, formData: Record<string, any>) => void;
45
+ }
46
+
47
+ /**
48
+ * Composant Réservation (Booking) Vanilla JS.
49
+ * Englobe le calendrier et la sélection de créneaux.
50
+ */
51
+ export class Booking {
52
+ private container: HTMLElement;
53
+ private calendarContainer: HTMLElement | null = null;
54
+ private slotsContainer: HTMLElement | null = null;
55
+ private calendarInstance: Calendar | null = null;
56
+ private selectedDate: Date | null = null;
57
+ private selectedSlot: string | null = null;
58
+ private options: Required<BookingOptions>;
59
+ private currentTrads: any;
60
+
61
+ /**
62
+ * @param {HTMLElement} container - Élément DOM où injecter le booking.
63
+ * @param {BookingOptions} options - Options de configuration.
64
+ */
65
+ constructor(container: HTMLElement, options: BookingOptions) {
66
+ this.container = container;
67
+ this.options = {
68
+ lang: 'fr',
69
+ selectedDate: null,
70
+ inputName: 'booking_datetime',
71
+ onSlotSelect: () => {},
72
+ mode: 'single',
73
+ disablePast: false,
74
+ onRangeSelect: () => {},
75
+ initialDate: new Date(),
76
+ ...options
77
+ } as Required<BookingOptions>;
78
+
79
+ if (!this.options.fields) {
80
+ this.options.fields = [
81
+ { name: 'firstname', label: this.options.lang === 'en' ? 'Firstname' : 'Prénom', type: 'text', required: true },
82
+ { name: 'lastname', label: this.options.lang === 'en' ? 'Lastname' : 'Nom', type: 'text', required: true },
83
+ { name: 'email', label: 'Email', type: 'email', required: true }
84
+ ];
85
+ }
86
+
87
+
88
+ this.selectedDate = this.options.selectedDate;
89
+ this.currentTrads = trads[this.options.lang];
90
+ this.initLayout();
91
+ this.render();
92
+ }
93
+
94
+ /**
95
+ * Initialise la structure de base (deux colonnes).
96
+ */
97
+ private initLayout(): void {
98
+ this.container.innerHTML = `
99
+ <div class="booking-wrapper">
100
+ <div class="booking__calendar-column"></div>
101
+ <div class="booking__slots-column"></div>
102
+ </div>
103
+ `;
104
+ this.calendarContainer = this.container.querySelector('.booking__calendar-column');
105
+ this.slotsContainer = this.container.querySelector('.booking__slots-column');
106
+
107
+ if (this.calendarContainer) {
108
+ this.calendarInstance = new Calendar(this.calendarContainer, {
109
+ lang: this.options.lang,
110
+ mode: this.options.mode,
111
+ disablePast: this.options.disablePast,
112
+ availability: this.options.availability,
113
+ initialDate: this.options.initialDate,
114
+ onDateSelect: (date: Date) => {
115
+ this.updateDate(date);
116
+ }
117
+ });
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Met à jour la date sélectionnée.
123
+ * @param {Date} date - La nouvelle date sélectionnée.
124
+ */
125
+ public updateDate(date: Date): void {
126
+ this.selectedDate = date;
127
+ this.selectedSlot = null;
128
+ this.render();
129
+ }
130
+
131
+ /**
132
+ * Change la langue du composant.
133
+ * @param {string} lang - 'fr' ou 'en'.
134
+ */
135
+ public setLanguage(lang: 'fr' | 'en'): void {
136
+ this.options.lang = lang;
137
+ this.currentTrads = trads[lang];
138
+ if (this.calendarInstance) {
139
+ this.calendarInstance.setLanguage(lang);
140
+ }
141
+ this.render();
142
+ }
143
+
144
+ /**
145
+ * Rendu HTML de la partie créneaux (booking).
146
+ */
147
+ private render(): void {
148
+ if (!this.slotsContainer) return;
149
+
150
+ if (!this.selectedDate) {
151
+ this.slotsContainer.innerHTML = `
152
+ <div class="booking--empty">
153
+ <div class="booking__empty-message">
154
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="4" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
155
+ <p>${this.currentTrads.selectDay}</p>
156
+ </div>
157
+ </div>
158
+ `;
159
+ return;
160
+ }
161
+
162
+ const dateStr = format(this.selectedDate, 'yyyy-MM-dd');
163
+ const slots = this.options.availability[dateStr] || [];
164
+ const locale = this.options.lang === 'fr' ? localeFr : localeEn;
165
+ const formattedDate = format(this.selectedDate, 'EEEE d MMMM yyyy', { locale });
166
+
167
+ this.slotsContainer.innerHTML = `
168
+ <div class="booking">
169
+ <h3 class="booking__title">${this.currentTrads.selectTime} ${formattedDate}</h3>
170
+ <div class="booking__slots">
171
+ ${slots.map(slot => `
172
+ <button class="booking__slot ${this.selectedSlot === slot ? 'booking__slot--selected' : ''}" data-slot="${slot}">
173
+ ${slot.includes(':') ? slot.replace(':', 'h') : slot}
174
+ </button>
175
+ `).join('')}
176
+ ${slots.length === 0 ? `<p class="booking__empty">${this.currentTrads.noSlots}</p>` : ''}
177
+ </div>
178
+
179
+ <input type="hidden" name="${this.options.inputName}[date]" value="${this.selectedSlot ? dateStr : ''}" />
180
+ <input type="hidden" name="${this.options.inputName}[slot]" value="${this.selectedSlot || ''}" />
181
+ </div>
182
+ `;
183
+
184
+ this.bindEvents();
185
+ }
186
+
187
+ /**
188
+ * Attache les événements DOM.
189
+ */
190
+ private bindEvents(): void {
191
+ if (!this.slotsContainer) return;
192
+
193
+ const slotBtns = this.slotsContainer.querySelectorAll('.booking__slot');
194
+ slotBtns.forEach(btn => {
195
+ btn.addEventListener('click', (e) => {
196
+ e.preventDefault();
197
+ const slot = btn.getAttribute('data-slot');
198
+ if (slot && this.selectedDate) {
199
+ this.openConfirmationModal(this.selectedDate, slot);
200
+ }
201
+ });
202
+ });
203
+ }
204
+
205
+ /**
206
+ * Ouvre la modale de confirmation pour un créneau donné.
207
+ * @param {Date} date - La date sélectionnée.
208
+ * @param {string} slot - Le créneau horaire.
209
+ */
210
+ private openConfirmationModal(date: Date, slot: string): void {
211
+ const locale = this.options.lang === 'fr' ? localeFr : localeEn;
212
+ const formattedDate = format(date, 'EEEE d MMMM yyyy', { locale });
213
+ const displaySlot = slot.includes(':') ? slot.replace(':', 'h') : slot;
214
+
215
+ const prep = this.options.lang === 'fr' ? (slot.includes(':') ? 'à' : 'le') : (slot.includes(':') ? 'at' : 'on');
216
+ const body = `
217
+ <p class="booking-modal__text">${this.currentTrads.confirmBody.replace('{date}', formattedDate).replace('{slot}', displaySlot).replace('{prep}', prep)}</p>
218
+ <form id="booking-confirmation-form" class="form booking-form">
219
+ ${this.options.fields.map(field => `
220
+ <div class="form__group booking-form-group booking-form-group--${field.name}">
221
+ <label for="field-${field.name}" class="form-label">
222
+ ${field.label}${field.required ? ' <span class="text-danger">*</span>' : ''}
223
+ </label>
224
+ <div class="form__input">
225
+ ${field.type === 'textarea' ? `
226
+ <textarea
227
+ id="field-${field.name}"
228
+ name="${field.name}"
229
+ class="form-control"
230
+ ${field.required ? 'required' : ''}
231
+ placeholder="${field.placeholder || ''}"
232
+ ></textarea>
233
+ ` : `
234
+ <input
235
+ type="${field.type}"
236
+ id="field-${field.name}"
237
+ name="${field.name}"
238
+ class="form-control"
239
+ ${field.required ? 'required' : ''}
240
+ placeholder="${field.placeholder || ''}"
241
+ />
242
+ `}
243
+ </div>
244
+ </div>
245
+ `).join('')}
246
+ </form>
247
+ `;
248
+
249
+ const footer = `
250
+ <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
251
+ ${this.currentTrads.cancelBtn}
252
+ </button>
253
+ <button type="submit" form="booking-confirmation-form" class="btn btn-primary" id="confirm-booking-btn">
254
+ ${this.currentTrads.confirmBtn}
255
+ </button>
256
+ `;
257
+
258
+ const modal = Modal.open({
259
+ title: this.currentTrads.confirmTitle,
260
+ body: body,
261
+ footer: footer,
262
+ centered: true
263
+ });
264
+
265
+ // Gérer la soumission du formulaire
266
+ setTimeout(() => {
267
+ const form = document.getElementById('booking-confirmation-form') as HTMLFormElement;
268
+ if (form) {
269
+ form.addEventListener('submit', (e) => {
270
+ e.preventDefault();
271
+
272
+ const formDataRaw = new FormData(form);
273
+ const formData: Record<string, any> = {};
274
+ formDataRaw.forEach((value, key) => {
275
+ formData[key] = value;
276
+ });
277
+
278
+ this.selectedSlot = slot;
279
+ this.render();
280
+ modal.hide();
281
+
282
+ if (this.options.onSlotSelect) {
283
+ this.options.onSlotSelect(date, slot, formData);
284
+ }
285
+ });
286
+ }
287
+ }, 0);
288
+ }
289
+ }
@@ -0,0 +1 @@
1
+ export * from './Booking';
@@ -0,0 +1,64 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { Booking as VanillaBooking, type BookingOptions } from '../js/Booking';
3
+
4
+ export interface BookingProps extends BookingOptions {
5
+ className?: string;
6
+ }
7
+
8
+ /**
9
+ * Wrapper React pour le composant Booking (Calendrier + Créneaux).
10
+ * @param {BookingProps} props - Les propriétés de la réservation.
11
+ * @returns {JSX.Element}
12
+ */
13
+ export const Booking = ({
14
+ lang = 'fr',
15
+ availability = {},
16
+ onSlotSelect,
17
+ selectedDate = null,
18
+ inputName = 'booking_datetime',
19
+ className = '',
20
+ mode = 'single',
21
+ disablePast = false,
22
+ initialDate = new Date(),
23
+ onRangeSelect,
24
+ fields,
25
+ }: BookingProps) => {
26
+ const containerRef = useRef<HTMLDivElement>(null);
27
+ const bookingInstance = useRef<VanillaBooking | null>(null);
28
+
29
+ useEffect(() => {
30
+ if (containerRef.current && !bookingInstance.current) {
31
+ bookingInstance.current = new VanillaBooking(containerRef.current, {
32
+ lang,
33
+ availability,
34
+ selectedDate,
35
+ inputName,
36
+ onSlotSelect,
37
+ mode,
38
+ disablePast,
39
+ initialDate,
40
+ onRangeSelect,
41
+ fields,
42
+ });
43
+ }
44
+ }, []);
45
+
46
+ useEffect(() => {
47
+ if (bookingInstance.current) {
48
+ bookingInstance.current.setLanguage(lang);
49
+ }
50
+ }, [lang]);
51
+
52
+ useEffect(() => {
53
+ if (bookingInstance.current) {
54
+ if (selectedDate) {
55
+ bookingInstance.current.updateDate(selectedDate);
56
+ } else if (selectedDate === null) {
57
+ // Gérer le cas où on repasse à null
58
+ bookingInstance.current.updateDate(null as any);
59
+ }
60
+ }
61
+ }, [selectedDate]);
62
+
63
+ return <div ref={containerRef} className={`booking ${className}`} />;
64
+ };
@@ -0,0 +1 @@
1
+ export * from './Booking';
@@ -0,0 +1,17 @@
1
+ .booking__empty-message {
2
+ text-align: center;
3
+ color: #94a3b8;
4
+ padding: 40px;
5
+
6
+ svg {
7
+ width: 48px;
8
+ height: 48px;
9
+ margin-bottom: 16px;
10
+ opacity: 0.5;
11
+ }
12
+
13
+ p {
14
+ font-size: 16px;
15
+ margin: 0;
16
+ }
17
+ }
@@ -0,0 +1,4 @@
1
+ .booking__empty {
2
+ color: #6c757d;
3
+ font-style: italic;
4
+ }
@@ -0,0 +1,10 @@
1
+ .booking__footer {
2
+ background: var(--booking-footer-bg);
3
+ color: var(--booking-footer-text);
4
+ padding: 25px;
5
+ border-radius: 4px;
6
+ text-align: left;
7
+ font-size: 22px;
8
+ font-weight: 400;
9
+ line-height: 1.4;
10
+ }
@@ -0,0 +1,6 @@
1
+ .booking-form{
2
+ display: grid;
3
+ gap: var(--spacing-sm);
4
+ margin: 1rem 0;
5
+
6
+ }
@@ -0,0 +1,26 @@
1
+ .booking__slot {
2
+ background: var(--booking-bg);
3
+ padding: 20px 10px;
4
+ border: none;
5
+ border-radius: 4px;
6
+ text-align: center;
7
+ cursor: pointer;
8
+ font-weight: 700;
9
+ font-size: 20px;
10
+ color: var(--booking-text);
11
+ transition: all 0.2s;
12
+
13
+ &:hover {
14
+ background: color-mix(in srgb, var(--booking-bg), #000 5%);
15
+ }
16
+ }
17
+
18
+ .booking__slot--selected {
19
+ background: var(--booking-selected-bg);
20
+ color: var(--booking-selected-text);
21
+ }
22
+
23
+ .booking__slot--range {
24
+ background: color-mix(in srgb, var(--booking-selected-bg), transparent 60%);
25
+ color: var(--booking-selected-text);
26
+ }
@@ -0,0 +1,5 @@
1
+ .booking__slots {
2
+ display: grid;
3
+ grid-template-columns: repeat(auto-fill, minmax(130px, 1fr));
4
+ gap: 15px;
5
+ }
@@ -0,0 +1,4 @@
1
+ .booking__title {
2
+ font-size: 1.2rem;
3
+ color: var(--booking-text);
4
+ }
@@ -0,0 +1,60 @@
1
+ :root {
2
+ --booking-bg: var(--white);
3
+ --booking-color: var(--primary);
4
+ --booking-text: var(--booking-color);
5
+ --booking-selected-bg: #3c4e62;
6
+ --booking-selected-text: #fff;
7
+ --booking-footer-bg: #3c4e62;
8
+ --booking-footer-text: #fff;
9
+ --booking-radius: var(--radius-base);
10
+ --booking-gap: 1rem;
11
+ }
12
+
13
+ .booking-wrapper {
14
+ display: flex;
15
+ gap: var(--booking-gap);
16
+ flex-wrap: wrap;
17
+ // align-items: flex-start;
18
+ // width: 100%;
19
+
20
+ }
21
+
22
+ .booking__calendar-column {
23
+ flex: 1;
24
+ width: 100%;
25
+
26
+ @media screen and (min-width: 640px){
27
+ flex: none;
28
+ width: 400px;
29
+ }
30
+
31
+
32
+ }
33
+
34
+ .booking__slots-column {
35
+ flex: 1;
36
+ // min-width: 300px;
37
+ // max-width: 100%;
38
+ }
39
+
40
+ .booking {
41
+ width: 100%;
42
+ border-radius: var(--booking-radius);
43
+ overflow: hidden;
44
+ color: var(--booking-text);
45
+ user-select: none;
46
+ display: flex;
47
+ flex-direction: column;
48
+ height: 100%;
49
+ }
50
+
51
+ .booking--empty {
52
+ display: flex;
53
+ flex-direction: column;
54
+ height: 100%;
55
+ justify-content: center;
56
+ align-items: center;
57
+ background: var(--booking-bg);
58
+ border: 1px dashed var(--booking-color);
59
+ border-radius: var(--booking-radius);
60
+ }
@@ -0,0 +1,8 @@
1
+ @use 'booking';
2
+ @use 'booking-empty-message';
3
+ @use 'booking-slots';
4
+ @use 'booking-slot';
5
+ @use 'booking-footer';
6
+ @use 'booking-empty';
7
+ @use 'booking-title';
8
+ @use 'booking-form';