UNPKG

pxsol-booking-search-widget

Version:

Embeddable booking engine search widget with React and Web Component builds.

207 lines (201 loc) 14 kB
var YourWidget=(function(exports){'use strict';var S={rooms:1,adults:2,childrenAges:[],infants:0},$=";",A=".",y=(e,t=0)=>{let r=Number.parseInt(e,10);return Number.isFinite(r)&&r>=0?r:t},L=e=>{let t=Number.parseFloat(e);return Number.isFinite(t)&&t>=0?t:0},C=e=>e?e.split($).map(t=>t.trim()).filter(Boolean).map(t=>{let[r,n]=t.split(":"),[i,a="0",s="0"]=(n!=null?n:"").split(","),o=a.split(A).map(d=>d.trim()).filter(Boolean).map(d=>L(d)).filter(d=>d>=0);return {rooms:Math.max(1,y(r,1)),adults:Math.max(1,y(i,1)),childrenAges:o,infants:Math.max(0,y(s,0))}}):[S],f=e=>e.map(r=>{let i=r.childrenAges.filter(s=>typeof s=="number"&&Number.isFinite(s)&&s>=0).map(s=>{if(Number.isInteger(s))return String(s);let o=Number(s.toFixed(2));return o%1===0?String(o):o.toString()}).join(A);return `${r.rooms}:${r.adults},${i},${r.infants}`}).join($),k=e=>e.reduce((r,n)=>(r.rooms+=n.rooms,r.adults+=n.adults,r.children+=n.childrenAges.length,r.infants+=n.infants,r),{rooms:0,adults:0,children:0,infants:0}),g=e=>e.map(t=>({...t,childrenAges:[...t.childrenAges]}));var w=/\d{4}-\d{2}-\d{2}/,I=()=>{let e=new Date;e.setHours(0,0,0,0);let t=e.getFullYear(),r=String(e.getMonth()+1).padStart(2,"0"),n=String(e.getDate()).padStart(2,"0");return `${t}-${r}-${n}`},G=(e,t)=>{let[r,n,i]=e.split("-").map(Number),a=new Date(r,n-1,i);a.setDate(a.getDate()+t);let s=a.getFullYear(),o=String(a.getMonth()+1).padStart(2,"0"),d=String(a.getDate()).padStart(2,"0");return `${s}-${o}-${d}`},D=(e,t)=>e&&w.test(e)?e:t,N="en",_="USD",F="GLOBAL",W=1e3,h=e=>{var u,l,c,m;let t=D(e.initialStart,I()),r=D(e.initialEnd,G(t,1)),n=t,i=r;i<=n&&(i=G(n,1));let a=C(e.initialGroups),s=a.length?a:[S],o=(u=e.productId)!=null?u:W,d;if(typeof window!="undefined")try{let p=localStorage.getItem(`booking-widget-promo-code-${o}`);p&&(d=p);}catch(p){console.warn("[booking-search-widget] could not read promo code from localStorage",p);}return {startDate:n,endDate:i,pos:(l=e.pos)!=null?l:F,locale:(c=e.locale)!=null?c:N,currency:(m=e.currency)!=null?m:_,productId:o,groups:g(s),redirect:!!e.redirect,promoCode:d,isSearching:false,errors:{}}},M=(e,t)=>{if(!w.test(e))return "Start date is invalid.";if(!w.test(t))return "End date is invalid.";if(t<=e)return "End date must be after start date.";let r=new Date(e),i=new Date(t).getTime()-r.getTime();return Math.ceil(i/(1e3*60*60*24))<1?"Check-out must be at least 1 day after check-in.":null},P=e=>{if(!e.length)return "At least one room group is required.";for(let[t,r]of e.entries()){if(r.rooms<=0)return `Room ${t+1}: rooms must be greater than 0.`;if(r.adults<=0)return `Room ${t+1}: adults must be greater than 0.`;if(r.childrenAges.some(n=>n<0))return `Room ${t+1}: child ages must be >= 0.`;if(r.infants<0)return `Room ${t+1}: infants must be >= 0.`}return null},T=e=>{let t={},r=M(e.startDate,e.endDate);r&&(t.dates=r),(!e.productId||e.productId<=0)&&(t.productId="Product is required.");let n=P(e.groups);return n&&(t.groups=n),e.pos||(t.pos="POS is required."),e.locale||(t.locale="Language is required."),e.currency||(t.currency="Currency is required."),{valid:Object.keys(t).length===0,errors:t}},R=(e,t)=>{let r=e.groups;(t.hideBabies||t.hideChildren)&&(r=e.groups.map(a=>({...a,childrenAges:t.hideChildren?[]:a.childrenAges,infants:t.hideBabies?0:a.infants}))),console.log("[booking-search-widget][buildSearchPayload] groups to format:",JSON.stringify(r,null,2));let n=f(r);console.log("[booking-search-widget][buildSearchPayload] formatted groups_form:",n);let i={start_date:e.startDate,end_date:e.endDate,product_id:Number(e.productId)||0,groups_form:n,pos:e.pos,language:e.locale,currency:e.currency};return e.promoCode&&(i.code=e.promoCode),i};var O=` :host { all: initial; display: inline-block; font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } .yw-container { font-family: inherit; background: var(--yw-bg, #ffffff); color: var(--yw-fg, #101828); border: 1px solid rgba(16, 24, 40, 0.1); border-radius: 12px; padding: 1rem; min-width: 260px; max-width: 420px; box-sizing: border-box; box-shadow: 0 8px 24px rgba(16, 24, 40, 0.08); } .yw-container[data-theme='dark'] { background: var(--yw-bg, #101828); color: var(--yw-fg, #ffffff); border-color: rgba(255, 255, 255, 0.2); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.75); } .yw-container h2 { font-size: 1.1rem; margin: 0 0 0.75rem; } form { display: grid; gap: 0.75rem; } label { display: flex; flex-direction: column; font-size: 0.85rem; gap: 0.35rem; } input, select, button { font: inherit; } input, select { border-radius: 8px; border: 1px solid rgba(16, 24, 40, 0.18); padding: 0.5rem 0.6rem; background: var(--yw-bg, #ffffff); color: inherit; } [data-theme='dark'] input, [data-theme='dark'] select { border-color: rgba(255, 255, 255, 0.18); } .yw-row { display: grid; gap: 0.75rem; } .yw-row--inline { grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); } .yw-groups { display: grid; gap: 0.75rem; border: 1px dashed rgba(16, 24, 40, 0.15); padding: 0.75rem; border-radius: 8px; } .yw-group { display: grid; gap: 0.5rem; } .yw-group-controls { display: flex; gap: 0.5rem; flex-wrap: wrap; } .yw-group-controls label { flex: 1; min-width: 120px; } .yw-actions { display: flex; justify-content: space-between; gap: 0.75rem; align-items: center; } .yw-primary { background: var(--yw-accent, #2563eb); border: none; color: #ffffff; border-radius: 999px; padding: 0.65rem 1.25rem; cursor: pointer; transition: transform 0.15s ease, box-shadow 0.2s ease; } .yw-primary:disabled { opacity: 0.55; cursor: not-allowed; } .yw-secondary { background: transparent; border: 1px solid rgba(16, 24, 40, 0.2); color: inherit; border-radius: 999px; padding: 0.55rem 1rem; cursor: pointer; } .yw-error { color: #dc2626; font-size: 0.8rem; } [data-theme='dark'] .yw-error { color: #f87171; } .yw-summary { font-size: 0.85rem; opacity: 0.8; } small { font-size: 0.8rem; opacity: 0.7; } `,q=["token","theme","locale","currency","pos","product-id","initial-start","initial-end","initial-groups","redirect"],U=(e,t)=>{if(!e.hasAttribute(t))return;let r=e.getAttribute(t);return r===null||r===""||r.toLowerCase()==="true"?true:r.toLowerCase()!=="false"},B=e=>{if(e==null)return;let t=Number(e);return Number.isFinite(t)?t:void 0},H=(e,t)=>` <div class="yw-group" data-index="${t}"> <header class="yw-summary">Room ${t+1}</header> <div class="yw-group-controls"> <label> <span>Rooms</span> <input type="number" min="1" data-group-field="rooms" data-index="${t}" value="${e.rooms}" /> </label> <label> <span>Adults</span> <input type="number" min="1" data-group-field="adults" data-index="${t}" value="${e.adults}" /> </label> <label> <span>Children ages</span> <input type="text" placeholder="e.g. 5.8" data-group-field="children" data-index="${t}" value="${e.childrenAges.join(".")}" /> </label> <label> <span>Infants</span> <input type="number" min="0" data-group-field="infants" data-index="${t}" value="${e.infants}" /> </label> </div> ${t>0?`<button class="yw-secondary" type="button" data-remove-group="${t}" aria-label="Remove room ${t+1}">Remove room</button>`:""} </div> `,z=e=>e.map(H).join(""),j=e=>e.split(/[.,]/).map(t=>t.trim()).filter(Boolean).map(Number).filter(t=>Number.isFinite(t)&&t>=0),Y=e=>({token:e}),b=class extends HTMLElement{constructor(){var r,n;super();this.bootstrapped=false;this.attachShadow({mode:"open"}),this.config=Y((r=this.getAttribute("token"))!=null?r:"PUBLIC_TOKEN"),this.state=h({...this.config,initialGroups:(n=this.getAttribute("initial-groups"))!=null?n:void 0});}static get observedAttributes(){return q}connectedCallback(){this.config=this.readConfig(),this.state=h(this.config),this.render(),this.bootstrapped=true,this.emit("ready",{state:this.state});}attributeChangedCallback(){this.bootstrapped&&(this.config=this.readConfig(),this.state=h(this.config),this.render());}readConfig(){var r,n,i,a,s,o,d,u;return {token:(r=this.getAttribute("token"))!=null?r:"PUBLIC_TOKEN",theme:(n=this.getAttribute("theme"))!=null?n:void 0,locale:(i=this.getAttribute("locale"))!=null?i:void 0,currency:(a=this.getAttribute("currency"))!=null?a:void 0,pos:(s=this.getAttribute("pos"))!=null?s:void 0,productId:B(this.getAttribute("product-id")),initialStart:(o=this.getAttribute("initial-start"))!=null?o:void 0,initialEnd:(d=this.getAttribute("initial-end"))!=null?d:void 0,initialGroups:(u=this.getAttribute("initial-groups"))!=null?u:void 0,redirect:U(this,"redirect"),onEvent:l=>this.emit(l.type,l.payload)}}emit(r,n){let i={type:r,payload:n};this.dispatchEvent(new CustomEvent("yw:event",{detail:i,bubbles:true,composed:true}));}setState(r){this.state={...this.state,...r},this.emit("change",{state:this.state}),this.render();}render(){var E,x;if(!this.shadowRoot)return;let r=(E=this.config.theme)!=null?E:"light",{startDate:n,endDate:i,pos:a,locale:s,currency:o,productId:d,groups:u,isSearching:l,errors:c,redirect:m}=this.state,p=k(u),v=(x=Object.values(c)[0])!=null?x:"";this.shadowRoot.innerHTML=` <style>${O}</style> <div class="yw-container" data-theme="${r}" role="region" aria-live="polite"> <h2>Booking search</h2> <form> <div class="yw-row yw-row--inline"> <label> <span>Check-in</span> <input type="date" name="start" value="${n}" /> </label> <label> <span>Check-out</span> <input type="date" name="end" value="${i}" min="${n}" /> </label> </div> <div class="yw-row yw-row--inline"> <label> <span>POS</span> <input type="text" name="pos" value="${a}" /> </label> <label> <span>Language</span> <input type="text" name="locale" value="${s}" /> </label> <label> <span>Currency</span> <input type="text" name="currency" value="${o}" /> </label> </div> <label> <span>Product ID</span> <input type="number" name="product" min="1" value="${d}" /> </label> <section class="yw-groups" aria-label="Guests"> ${z(u)} <button type="button" class="yw-secondary" data-add-group>Add room</button> <p class="yw-summary">${p.rooms} rooms \xB7 ${p.adults} adults \xB7 ${p.children} children \xB7 ${p.infants} infants</p> </section> <label> <span> Redirect to booking engine <input type="checkbox" name="redirect" ${m?"checked":""} /> </span> </label> ${v?`<p class="yw-error">${v}</p>`:""} <div class="yw-actions"> <button type="submit" class="yw-primary" ${l?"disabled":""}>${l?"Searching\u2026":"Search"}</button> <small>${f(u)}</small> </div> </form> </div> `,this.bindEvents();}bindEvents(){let r=this.shadowRoot;if(!r)return;let n=r.querySelector("form");n==null||n.addEventListener("submit",o=>this.onSubmit(o));let i=(o,d)=>{let u=r.querySelector(o);u&&u.addEventListener("change",l=>d(l.target.value));};i('input[name="start"]',o=>this.setState({startDate:o})),i('input[name="end"]',o=>this.setState({endDate:o})),i('input[name="pos"]',o=>this.setState({pos:o})),i('input[name="locale"]',o=>this.setState({locale:o})),i('input[name="currency"]',o=>this.setState({currency:o})),i('input[name="product"]',o=>this.setState({productId:Number(o)||0}));let a=r.querySelector('input[name="redirect"]');a==null||a.addEventListener("change",o=>{let d=o.target;this.setState({redirect:d.checked});}),r.querySelectorAll("[data-group-field]").forEach(o=>{o.addEventListener("change",d=>{var m;let u=d.target,l=Number((m=u.dataset.index)!=null?m:"0"),c=u.dataset.groupField;this.updateGroup(l,c!=null?c:"",u.value);});}),r.querySelectorAll("[data-remove-group]").forEach(o=>{o.addEventListener("click",()=>{var u;let d=Number((u=o.dataset.removeGroup)!=null?u:"0");this.setState({groups:this.state.groups.filter((l,c)=>c!==d)});});});let s=r.querySelector("[data-add-group]");s==null||s.addEventListener("click",()=>{this.setState({groups:[...g(this.state.groups),{rooms:1,adults:2,childrenAges:[],infants:0}]});});}updateGroup(r,n,i){let a=g(this.state.groups),s=a[r];s&&(n==="rooms"&&(s.rooms=Math.max(1,Number(i)||1)),n==="adults"&&(s.adults=Math.max(1,Number(i)||1)),n==="children"&&(s.childrenAges=j(i)),n==="infants"&&(s.infants=Math.max(0,Number(i)||0)),this.setState({groups:a}));}async onSubmit(r){r.preventDefault();let n=T(this.state);if(!n.valid){this.setState({errors:n.errors}),this.emit("validate_error",n);return}let i=R({...this.state},this.config);this.setState({isSearching:true,errors:{}}),this.emit("search_start",i);try{let a=await fetch("https://gateway-prod.pxsol.com/v2/search",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${this.config.token}`},body:JSON.stringify(i)});if(!a.ok)throw new Error(`Search failed with status ${a.status}`);let s=await a.json();this.setState({isSearching:!1,lastResponse:s}),this.emit("search_success",{request:i,response:s}),s.booking_engine_url&&window.location.assign(s.booking_engine_url);}catch(a){let s=a instanceof Error?a:new Error("Unexpected error");this.setState({isSearching:false}),this.emit("search_error",{request:i,error:s}),console.error("[booking-search-widget]",s);}}};customElements.get("your-widget")||customElements.define("your-widget",b);var Z=()=>{customElements.get("your-widget")||customElements.define("your-widget",b);}; exports.YourWidgetElement=b;exports.registerYourWidget=Z;return exports;})({});//# sourceMappingURL=your-widget.iife.js.map //# sourceMappingURL=your-widget.iife.js.map