fix: widget embed — full-width support + one-liner script tags via hosted loader
- Standalone page: ?width=full removes max-w-lg container constraint - iframeSnippet: passes &width=full to URL when 100% width selected - public/widget-loader.js: hosted script, reads data-* attrs, no inline code needed - data-mode="floating" → styled FAB button bottom-right - data-mode="trigger" → listens for .hotelsync-book class clicks - Proper modal with close button and backdrop click - Code tab snippets for floating/trigger are now single <script src> lines Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
105
public/widget-loader.js
Normal file
105
public/widget-loader.js
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
/* HotelSync Widget Loader — https://hotelsync.ru */
|
||||||
|
(function () {
|
||||||
|
var s = document.currentScript || (function () {
|
||||||
|
var t = document.getElementsByTagName('script');
|
||||||
|
return t[t.length - 1];
|
||||||
|
})();
|
||||||
|
|
||||||
|
var url = s.getAttribute('data-url') || '';
|
||||||
|
var mode = s.getAttribute('data-mode') || 'floating'; // 'floating' | 'trigger'
|
||||||
|
var btn = s.getAttribute('data-btn') || 'Забронировать';
|
||||||
|
var color = s.getAttribute('data-color') || '#4F46E5';
|
||||||
|
var cls = s.getAttribute('data-class') || 'hotelsync-book';
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
/* ── Modal ── */
|
||||||
|
var modal = document.createElement('div');
|
||||||
|
modal.style.cssText = [
|
||||||
|
'display:none', 'position:fixed', 'inset:0',
|
||||||
|
'background:rgba(0,0,0,.65)', 'z-index:99999',
|
||||||
|
'align-items:center', 'justify-content:center',
|
||||||
|
'padding:16px', 'box-sizing:border-box',
|
||||||
|
].join(';');
|
||||||
|
|
||||||
|
var inner = document.createElement('div');
|
||||||
|
inner.style.cssText = [
|
||||||
|
'position:relative', 'width:100%', 'max-width:520px',
|
||||||
|
'border-radius:20px', 'overflow:hidden',
|
||||||
|
'box-shadow:0 24px 80px rgba(0,0,0,.45)',
|
||||||
|
].join(';');
|
||||||
|
|
||||||
|
var closeBtn = document.createElement('button');
|
||||||
|
closeBtn.innerHTML = '×';
|
||||||
|
closeBtn.style.cssText = [
|
||||||
|
'position:absolute', 'top:10px', 'right:10px', 'z-index:1',
|
||||||
|
'width:32px', 'height:32px', 'border-radius:50%',
|
||||||
|
'background:rgba(0,0,0,.55)', 'color:#fff',
|
||||||
|
'border:none', 'cursor:pointer', 'font-size:20px',
|
||||||
|
'font-weight:700', 'line-height:1',
|
||||||
|
'display:flex', 'align-items:center', 'justify-content:center',
|
||||||
|
].join(';');
|
||||||
|
closeBtn.onclick = function () { modal.style.display = 'none'; };
|
||||||
|
|
||||||
|
var iframe = document.createElement('iframe');
|
||||||
|
iframe.src = url;
|
||||||
|
iframe.setAttribute('allow', 'payment');
|
||||||
|
iframe.style.cssText = 'width:100%;height:680px;border:none;display:block;';
|
||||||
|
|
||||||
|
inner.appendChild(closeBtn);
|
||||||
|
inner.appendChild(iframe);
|
||||||
|
modal.appendChild(inner);
|
||||||
|
modal.addEventListener('click', function (e) {
|
||||||
|
if (e.target === modal) modal.style.display = 'none';
|
||||||
|
});
|
||||||
|
document.body.appendChild(modal);
|
||||||
|
|
||||||
|
window.hsOpenWidget = function () { modal.style.display = 'flex'; };
|
||||||
|
|
||||||
|
/* ── Floating button ── */
|
||||||
|
if (mode === 'floating') {
|
||||||
|
var fab = document.createElement('button');
|
||||||
|
fab.textContent = btn;
|
||||||
|
fab.style.cssText = [
|
||||||
|
'position:fixed', 'bottom:24px', 'right:24px',
|
||||||
|
'background:' + color, 'color:#fff',
|
||||||
|
'border:none', 'padding:14px 26px',
|
||||||
|
'border-radius:50px', 'font-size:15px',
|
||||||
|
'font-family:inherit', 'font-weight:700',
|
||||||
|
'cursor:pointer', 'z-index:99997',
|
||||||
|
'box-shadow:0 4px 20px rgba(0,0,0,.28)',
|
||||||
|
'transition:transform .15s,box-shadow .15s',
|
||||||
|
].join(';');
|
||||||
|
fab.onmouseenter = function () {
|
||||||
|
fab.style.transform = 'scale(1.06)';
|
||||||
|
fab.style.boxShadow = '0 8px 28px rgba(0,0,0,.38)';
|
||||||
|
};
|
||||||
|
fab.onmouseleave = function () {
|
||||||
|
fab.style.transform = 'scale(1)';
|
||||||
|
fab.style.boxShadow = '0 4px 20px rgba(0,0,0,.28)';
|
||||||
|
};
|
||||||
|
fab.onclick = function () { window.hsOpenWidget(); };
|
||||||
|
document.body.appendChild(fab);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Class trigger ── */
|
||||||
|
if (mode === 'trigger') {
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
var el = e.target;
|
||||||
|
while (el) {
|
||||||
|
if (el.classList && el.classList.contains(cls)) {
|
||||||
|
e.preventDefault();
|
||||||
|
window.hsOpenWidget();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el = el.parentElement;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
|
} else {
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -1495,45 +1495,19 @@ export function BookingWidgetPage() {
|
|||||||
formFields: prev.formFields.filter(f => f.id !== id),
|
formFields: prev.formFields.filter(f => f.id !== id),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const widgetUrl = `https://app.hotelsync.ru/${slug || 'grand-palace'}/book?color=${encodeURIComponent(settings.primaryColor)}&lang=${settings.language}&mode=${settings.roomDisplayMode}&min-nights=${settings.minNights}&rental=${settings.showRental}&promo=${settings.showPromo}&extra-beds=${settings.allowExtraBeds}&children=${settings.allowChildren}`
|
const widgetUrl = (fullW = false) => `https://app.hotelsync.ru/${slug || 'grand-palace'}/book?color=${encodeURIComponent(settings.primaryColor)}&lang=${settings.language}&mode=${settings.roomDisplayMode}&min-nights=${settings.minNights}&rental=${settings.showRental}&promo=${settings.showPromo}&extra-beds=${settings.allowExtraBeds}&children=${settings.allowChildren}${fullW ? '&width=full' : ''}`
|
||||||
|
|
||||||
|
const iframeUrl = widgetUrl(widgetMaxWidth === '100%')
|
||||||
|
const modalUrl = widgetUrl(false)
|
||||||
|
const loaderBase = `https://app.hotelsync.ru/widget-loader.js`
|
||||||
|
|
||||||
const iframeSnippet = widgetMaxWidth === '100%'
|
const iframeSnippet = widgetMaxWidth === '100%'
|
||||||
? `<iframe\n src="${widgetUrl}"\n width="100%"\n height="680"\n frameborder="0"\n style="border-radius:16px;border:none;box-shadow:0 4px 24px rgba(0,0,0,0.10);display:block;"\n allow="payment"\n loading="lazy"\n></iframe>`
|
? `<iframe src="${iframeUrl}" width="100%" height="680" frameborder="0" style="border:none;display:block;" allow="payment" loading="lazy"></iframe>`
|
||||||
: `<div style="max-width:${widgetMaxWidth};margin:0 auto;">\n <iframe\n src="${widgetUrl}"\n width="100%"\n height="680"\n frameborder="0"\n style="border-radius:16px;border:none;box-shadow:0 4px 24px rgba(0,0,0,0.10);"\n allow="payment"\n loading="lazy"\n ></iframe>\n</div>`
|
: `<div style="max-width:${widgetMaxWidth};margin:0 auto;">\n <iframe src="${iframeUrl}" width="100%" height="680" frameborder="0" style="border-radius:16px;border:none;box-shadow:0 4px 24px rgba(0,0,0,0.10);" allow="payment" loading="lazy"></iframe>\n</div>`
|
||||||
|
|
||||||
const modalScript = `(function(){
|
const floatingSnippet = `<script src="${loaderBase}" data-url="${modalUrl}" data-mode="floating" data-btn="${floatBtnText}" data-color="${settings.primaryColor}" async></script>`
|
||||||
var W='${widgetUrl}';
|
|
||||||
var m=document.createElement('div');
|
|
||||||
m.id='hs-modal';
|
|
||||||
m.style.cssText='display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:99998;align-items:center;justify-content:center;padding:16px;box-sizing:border-box;';
|
|
||||||
m.innerHTML='<div style="position:relative;width:100%;max-width:500px;max-height:90vh;overflow-y:auto;border-radius:16px;">'
|
|
||||||
+'<button onclick="document.getElementById(\\'hs-modal\\').style.display=\\'none\\'" style="position:absolute;top:8px;right:8px;z-index:1;width:32px;height:32px;border-radius:50%;background:rgba(0,0,0,.5);color:#fff;border:none;cursor:pointer;font-size:20px;line-height:1;display:flex;align-items:center;justify-content:center;">×</button>'
|
|
||||||
+'<iframe src="'+W+'" style="width:100%;height:680px;border:none;border-radius:16px;display:block;" allow="payment"></iframe>'
|
|
||||||
+'</div>';
|
|
||||||
m.addEventListener('click',function(e){if(e.target===m)m.style.display='none';});
|
|
||||||
document.body.appendChild(m);
|
|
||||||
window.hsOpenWidget=function(){m.style.display='flex';};
|
|
||||||
})()`
|
|
||||||
|
|
||||||
const floatingSnippet = `<script>
|
const triggerSnippet = `<script src="${loaderBase}" data-url="${modalUrl}" data-mode="trigger" data-class="${triggerClass || 'hotelsync-book'}" data-color="${settings.primaryColor}" async></script>`
|
||||||
${modalScript}
|
|
||||||
;(function(){
|
|
||||||
var b=document.createElement('button');
|
|
||||||
b.textContent='${floatBtnText}';
|
|
||||||
b.style.cssText='position:fixed;bottom:24px;right:24px;background:${settings.primaryColor};color:#fff;border:none;padding:14px 24px;border-radius:50px;font-size:15px;font-weight:600;cursor:pointer;box-shadow:0 4px 20px rgba(0,0,0,.25);z-index:99997;transition:transform .15s,box-shadow .15s;';
|
|
||||||
b.onmouseenter=function(){b.style.transform='scale(1.05)';b.style.boxShadow='0 6px 28px rgba(0,0,0,.35)';};
|
|
||||||
b.onmouseleave=function(){b.style.transform='scale(1)';b.style.boxShadow='0 4px 20px rgba(0,0,0,.25)';};
|
|
||||||
b.onclick=function(){window.hsOpenWidget();};
|
|
||||||
document.body.appendChild(b);
|
|
||||||
})();
|
|
||||||
</script>`
|
|
||||||
|
|
||||||
const triggerSnippet = `<script>
|
|
||||||
${modalScript}
|
|
||||||
;document.addEventListener('click',function(e){
|
|
||||||
if(e.target.closest('.${triggerClass}')){e.preventDefault();window.hsOpenWidget();}
|
|
||||||
});
|
|
||||||
</script>`
|
|
||||||
|
|
||||||
const activeSnippet = codeMode === 'iframe' ? iframeSnippet : codeMode === 'floating' ? floatingSnippet : triggerSnippet
|
const activeSnippet = codeMode === 'iframe' ? iframeSnippet : codeMode === 'floating' ? floatingSnippet : triggerSnippet
|
||||||
|
|
||||||
|
|||||||
@@ -74,9 +74,11 @@ export function BookingWidgetStandalonePage() {
|
|||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
}, [slug])
|
}, [slug])
|
||||||
|
|
||||||
|
const isFullWidth = searchParams.get('width') === 'full'
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-50 flex items-start justify-center py-6 px-3">
|
<div className={isFullWidth ? 'min-h-screen bg-slate-50' : 'min-h-screen bg-slate-50 flex items-start justify-center py-6 px-3'}>
|
||||||
<div className="w-full max-w-lg">
|
<div className={isFullWidth ? 'w-full' : 'w-full max-w-lg'}>
|
||||||
<WidgetPreview
|
<WidgetPreview
|
||||||
settings={settings}
|
settings={settings}
|
||||||
slug={slug}
|
slug={slug}
|
||||||
|
|||||||
Reference in New Issue
Block a user