pxsol-booking-search-widget
Version:
Embeddable booking engine search widget with React and Web Component builds.
207 lines (201 loc) • 14 kB
JavaScript
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