property
(async () => { const anchor = document.get Element By Id('vue-widget-anchor'); if (!anchor) { console.error('Vue widget: anchor missing'); return; } // 0) Ensure #app exists and render a detailed first-paint skeleton immediately let app El = document.get Element By Id('app'); if (!app El) { app El = document.create Element('div'); app El.id = 'app'; app El.set Attribute('data-usercentrics', 'essential'); app El.style.position = 'relative'; app El.style.z Index = '1'; app El.style.min Height = '100vh'; // Prevent layout shift app El.style.background Color = '#ffffff'; // Prevent white flash anchor.append Child(app El); } else { // Ensure existing app has background app El.style.background Color = '#ffffff'; } // Create detailed skeleton matching Content Loading Skeleton structure const sk = document.create Element('div'); sk.id = 'app-skeleton'; sk.set Attribute('data-usercentrics', 'essential'); sk.inner HTML = ` ${Array.from({ length: 6 }, () => ` `).join('')} ${Array.from({ length: 5 }, () => ` ${Array.from({ length: 8 }, () => ` `).join('')} `).join('')} `; app El.append Child(sk); // Inline CSS so the skeleton shows without waiting for app CSS const sk Style = document.create Element('style'); sk Style.set Attribute('data-usercentrics', 'essential'); sk Style.text Content = ` /* Match Content Loading Skeleton container */ .pre-skel.container-search-page { background-color: #ffffff; display: flex; flex-direction: column; min-height: 100vh; font-family: 'Poppins', sans-serif; padding: 0; margin: 0 auto; } @media screen and (max-width: 1200px) { .pre-skel.container-search-page { padding: 0; } } /* Search bar skeleton */ .pre-skel__search-bar { position: sticky; top: 0; z-index: 1; background-color: #ffffff; padding: 0.75rem; border-bottom: 1px solid #f5f3ef; } .pre-skel__search-row { display: grid; grid-template-columns: 1fr repeat(4, max-content); gap: 1rem; align-items: center; } .pre-skel__searchbox { height: 40px; border-radius: 10px; background: linear-gradient(90deg, #e9e9e9 25%, #f3f3f3 37%, #e9e9e9 63%); background-size: 400% 100%; animation: pre-skel-shimmer 1.2s infinite linear; } .pre-skel__chip { width: 90px; height: 36px; border-radius: 10px; background: linear-gradient(90deg, #e9e9e9 25%, #f3f3f3 37%, #e9e9e9 63%); background-size: 400% 100%; animation: pre-skel-shimmer 1.2s infinite linear; } .pre-skel__tag-row { display: flex; gap: 0.5rem; margin-top: 0.5rem; } .pre-skel__tag { width: 100px; height: 26px; border-radius: 8px; background: linear-gradient(90deg, #e9e9e9 25%, #f3f3f3 37%, #e9e9e9 63%); background-size: 400% 100%; animation: pre-skel-shimmer 1.2s infinite linear; } /* Main content: list + map */ .pre-skel__main-content { flex: 1; display: flex; gap: 0; height: calc(100vh - 180px); position: sticky; padding: 0; width: 100%; } @media (max-width: 1200px) { .pre-skel__main-content { height: auto; top: 140px; padding: 0; } } /* Property list section */ .pre-skel__property-list-section { overflow-y: auto; background-color: #ffffff; border-radius: 8px; height: 100%; width: 50%; overflow-x: hidden; overflow-y: hidden; min-width: 0; padding: 0.5rem; z-index: 1000; box-shadow: 0 5px 10px -2px rgba(0, 0, 0, 0.1); display: flex; flex-direction: column; align-items: center; gap: 1rem; } @media (max-width: 1200px) { .pre-skel__property-list-section { width: 100%; padding: 1rem; } } .pre-skel__card { background-color: white; border-radius: 1rem; box-shadow: rgba(0, 0, 0, 0.16) 0px 1px 4px; overflow: hidden; display: flex; flex-direction: column; min-width: 0; width: 100%; margin-bottom: 0.5rem; } .pre-skel__card-image-container { display: grid; grid-template-columns: 1fr; gap: 0.2rem; } @media (min-width: 1400px) { .pre-skel__card-image-container { grid-template-columns: 3fr 1fr; } } .pre-skel__card-main-image { position: relative; height: 280px; border-top-left-radius: 10px; border-top-right-radius: 10px; background: linear-gradient(90deg, #e9e9e9 25%, #f3f3f3 37%, #e9e9e9 63%); background-size: 400% 100%; animation: pre-skel-shimmer 1.2s infinite linear; overflow: hidden; } @media (min-width: 1400px) { .pre-skel__card-main-image { border-top-right-radius: 0; } } .pre-skel__card-price-tag { position: absolute; bottom: 8px; left: 8px; height: 28px; width: 100px; border-radius: 4px; background: rgba(255, 255, 255, 0.9); } .pre-skel__card-thumbs { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.2rem; } @media (min-width: 1400px) { .pre-skel__card-thumbs { grid-template-columns: 1fr; grid-template-rows: repeat(3, 1fr); } } .pre-skel__card-thumb { height: 90px; border-radius: 4px; background: linear-gradient(90deg, #e9e9e9 25%, #f3f3f3 37%, #e9e9e9 63%); background-size: 400% 100%; animation: pre-skel-shimmer 1.2s infinite linear; } @media (min-width: 1400px) { .pre-skel__card-thumb { height: 100px; } } .pre-skel__card-content { padding: 0.8rem; display: grid; gap: 0.5rem; } .pre-skel__card-title { height: 20px; width: 70%; border-radius: 6px; background: linear-gradient(90deg, #e9e9e9 25%, #f3f3f3 37%, #e9e9e9 63%); background-size: 400% 100%; animation: pre-skel-shimmer 1.2s infinite linear; } .pre-skel__card-desc { height: 16px; width: 90%; border-radius: 6px; background: linear-gradient(90deg, #e9e9e9 25%, #f3f3f3 37%, #e9e9e9 63%); background-size: 400% 100%; animation: pre-skel-shimmer 1.2s infinite linear; } .pre-skel__card-footer { display: grid; grid-template-columns: 2fr 1fr; gap: 1rem; align-items: start; padding: 0.4rem; } .pre-skel__card-features { display: grid; grid-template-columns: 1fr; gap: 0.6rem; padding: 0.5rem; } .pre-skel__card-feature { height: 14px; width: 40%; border-radius: 6px; background: linear-gradient(90deg, #e9e9e9 25%, #f3f3f3 37%, #e9e9e9 63%); background-size: 400% 100%; animation: pre-skel-shimmer 1.2s infinite linear; } .pre-skel__card-location { height: 14px; width: 60%; border-radius: 6px; background: linear-gradient(90deg, #e9e9e9 25%, #f3f3f3 37%, #e9e9e9 63%); background-size: 400% 100%; animation: pre-skel-shimmer 1.2s infinite linear; margin-top: 0.5rem; } .pre-skel__card-logo { width: 130px; height: 100px; border-radius: 8px; background: linear-gradient(90deg, #e9e9e9 25%, #f3f3f3 37%, #e9e9e9 63%); background-size: 400% 100%; animation: pre-skel-shimmer 1.2s infinite linear; justify-self: center; align-self: center; } /* Map section */ .pre-skel__map-section { position: sticky; top: 80px; height: calc(100vh); background-color: #ffffff; border-radius: 8px; box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); overflow: hidden; width: 65%; min-width: 400px; display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 0.5rem; } @media (max-width: 1200px) { .pre-skel__map-section { display: none; } } /* Map grid skeleton */ .pre-skel__map-skeleton { width: 100%; height: 100%; display: grid; grid-template-rows: repeat(5, 1fr); gap: 6px; border-radius: 8px; overflow: hidden; } .pre-skel__map-row { display: grid; grid-template-columns: repeat(8, 1fr); gap: 6px; } .pre-skel__map-tile { border-radius: 6px; background: #f0f2f5; position: relative; overflow: hidden; } .pre-skel__map-tile::after { content: ''; position: absolute; inset: 0; transform: translate X(-100%); background: linear-gradient( 90deg, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.6) 50%, rgba(255, 255, 255, 0) 100% ); animation: pre-skel-map-shimmer 1.4s infinite; } .pre-skel__map-footer { width: 100%; display: flex; gap: 1rem; margin-top: 0.5rem; padding: 0 0.25rem; } .pre-skel__legend { height: 14px; border-radius: 6px; flex: 1; background: linear-gradient(90deg, #e9e9e9 25%, #f3f3f3 37%, #e9e9e9 63%); background-size: 400% 100%; animation: pre-skel-shimmer 1.2s infinite linear; } @keyframes pre-skel-shimmer { 0% { background-position: 100% 0; } 100% { background-position: 0 0; } } @keyframes pre-skel-map-shimmer { 100% { transform: translate X(100%); } } /* Smooth transition styles */ .pre-skel { transition: opacity 0.6s ease-out; opacity: 1; will-change: opacity; position: relative; z-index: 1; background-color: #ffffff; /* Ensure white background */ } @media (max-width: 1200px) { .pre-skel { transition: opacity 0.7s ease-out; } } .pre-skel.fade-out { opacity: 0; pointer-events: none; position: absolute; top: 0; left: 0; right: 0; z-index: 10; background-color: #ffffff !important; /* Force white background during fade */ min-height: 100vh; width: 100%; } @media (max-width: 1200px) { .pre-skel.fade-out { min-height: 100vh; min-height: -webkit-fill-available; /* i OS fix */ } } /* Ensure skeleton container stays in place during transition */ #app { position: relative; min-height: 100vh; background-color: #ffffff; /* Prevent white flash */ } /* Ensure Vue content has background during transition */ #app > *:not(#app-skeleton) { background-color: #ffffff; } `; document.head.append Child(sk Style); // Helper: Wait for CSS to load const wait For CSS = (link) => { return new Promise((resolve, reject) => { if (link.sheet) { resolve(); // Already loaded return; } link.onload = () => resolve(); link.onerror = () => { console.warn('CSS load warning (non-fatal):', link.href); resolve(); // Resolve anyway to not block mounting }; // Timeout after 10s (increased from 5s) set Timeout(() => { console.warn('CSS load timeout (non-fatal):', link.href); resolve(); // Resolve anyway to not block mounting }, 10000); }); }; // Helper: Normalize file paths from manifest // Manifest paths might be relative, absolute, or already include assets/ const normalize Path = (file Path, base) => { if (!file Path) return null; // If it's already an absolute URL, return as-is (but log a warning if it's the wrong domain) if (file Path.starts With('http://') || file Path.starts With('https://')) { // Check if it's pointing to wrong domain const url = new URL(file Path); const base Url = new URL(base); if (url.origin !== base Url.origin) { console.warn(`Manifest contains absolute URL from different domain: ${file Path}, expected: ${base Url.origin}`); // Try to fix it by replacing the domain return file Path.replace(url.origin, base Url.origin); } return file Path; } // If path starts with /, it's absolute from root - prepend base if (file Path.starts With('/')) { return base + file Path.slice(1); } // If path already starts with assets/, just prepend base if (file Path.starts With('assets/')) { return base + file Path; } // Otherwise, assume it needs assets/ prefix return base + 'assets/' + file Path; }; // Helper: Wait for DOM to be stable const wait For DOMReady = () => { if (document.ready State === 'complete' || document.ready State === 'interactive') { return Promise.resolve(); } return new Promise(resolve => { if (document.ready State === 'loading') { document.add Event Listener('DOMContent Loaded', resolve, { once: true }); } else { resolve(); } }); }; try { // 1) Load manifest // https://frontend.production.platform.bostello.com/assets/ // const base = 'https://bostello.com/vue/'; const base = 'https://frontend.production.platform.bostello.com/'; const manifest Res = await fetch(base + 'assets/manifest.json', { cache: 'no-store' }); if (!manifest Res.ok) { throw new Error(`Failed to load manifest: ${manifest Res.status} ${manifest Res.status Text}`); } const manifest = await manifest Res.json(); console.log('Manifest loaded, base URL:', base); console.log('Sample manifest entry:', Object.keys(manifest)[0], manifest[Object.keys(manifest)[0]]); // Vite often uses 'index.html' or 'src/main.ts' as the manifest entry key const entry = manifest['index.html'] || manifest['src/main.ts']; if (!entry || !entry.file) { throw new Error('Vue widget: no entry found in manifest'); } console.log('Entry file from manifest:', entry.file); console.log('Entry imports:', entry.imports?.length || 0); console.log('Entry dynamic Imports:', entry.dynamic Imports?.length || 0); // 2) Load entry CSS first and wait for it to load const css Promises = []; if (entry.css && entry.css.length > 0) { entry.css.for Each(css File => { const link = document.create Element('link'); link.rel = 'stylesheet'; link.href = normalize Path(css File, base); link.cross Origin = 'anonymous'; link.set Attribute('data-usercentrics', 'essential'); document.head.append Child(link); css Promises.push(wait For CSS(link)); }); } // 3) Preload static imports (eager chunks) if (entry.imports && entry.imports.length > 0) { entry.imports.for Each(key => { const chunk = manifest