209 lines
5.2 KiB
Svelte
209 lines
5.2 KiB
Svelte
<script lang="ts">
|
|
import { onMount, tick } from 'svelte';
|
|
import { afterNavigate } from '$app/navigation';
|
|
import { page } from '$app/stores';
|
|
import Icon from '$lib/components/Icon.svelte';
|
|
import { isMobileCtaButtonEnabled } from '$lib/feature-flags';
|
|
|
|
/*
|
|
* Sticky bottom CTA shown on mobile only.
|
|
*
|
|
* Pattern is the Airbnb-style "soft container, scroll-triggered" —
|
|
* - white card sits flush against the bottom edge with a thin top
|
|
* hairline and a soft shadow so it reads as a tray, not a banner;
|
|
* - the brand-yellow pill CTA lives inside the card so the action
|
|
* is unmistakable but the surrounding chrome stays calm;
|
|
* - the bar only appears after the user has scrolled roughly one
|
|
* viewport (~hero out of view), so it doesn't compete with the
|
|
* in-page hero CTA.
|
|
*
|
|
* Hidden on the contact / booking flows (no point reminding someone
|
|
* to book while they're already on the form).
|
|
*/
|
|
|
|
const mobileCtaButtonEnabled = isMobileCtaButtonEnabled();
|
|
|
|
$: pathname = $page.url.pathname;
|
|
$: hidden =
|
|
pathname === '/contact-us' ||
|
|
pathname === '/booking' ||
|
|
$page.url.hostname === 'onboarding.goodwalk.co.nz' ||
|
|
$page.url.searchParams.get('preview') === 'onboarding';
|
|
|
|
let visible = false;
|
|
let triggerPassed = false;
|
|
let bookingInView = false;
|
|
let triggerObserver: IntersectionObserver | null = null;
|
|
let bookingObserver: IntersectionObserver | null = null;
|
|
|
|
function refreshVisibility() {
|
|
visible = !hidden && triggerPassed && !bookingInView;
|
|
}
|
|
|
|
function cleanupObservers() {
|
|
triggerObserver?.disconnect();
|
|
bookingObserver?.disconnect();
|
|
triggerObserver = null;
|
|
bookingObserver = null;
|
|
}
|
|
|
|
async function setupObservers() {
|
|
if (!mobileCtaButtonEnabled || typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
await tick();
|
|
|
|
cleanupObservers();
|
|
|
|
const triggerEl =
|
|
document.getElementById('hero') ?? document.querySelector('main section, section');
|
|
const bookingEl = document.getElementById('newlead');
|
|
|
|
triggerPassed = !triggerEl;
|
|
bookingInView = false;
|
|
|
|
if (triggerEl) {
|
|
triggerObserver = new IntersectionObserver(
|
|
([entry]) => {
|
|
triggerPassed = !entry.isIntersecting && entry.boundingClientRect.top < 0;
|
|
refreshVisibility();
|
|
},
|
|
{ threshold: 0.2 }
|
|
);
|
|
|
|
triggerObserver.observe(triggerEl);
|
|
}
|
|
|
|
if (bookingEl) {
|
|
bookingObserver = new IntersectionObserver(
|
|
([entry]) => {
|
|
bookingInView = entry.isIntersecting;
|
|
refreshVisibility();
|
|
},
|
|
{ threshold: 0.2 }
|
|
);
|
|
|
|
bookingObserver.observe(bookingEl);
|
|
}
|
|
|
|
refreshVisibility();
|
|
}
|
|
|
|
afterNavigate(() => {
|
|
if (!mobileCtaButtonEnabled) {
|
|
return;
|
|
}
|
|
|
|
visible = false;
|
|
triggerPassed = false;
|
|
bookingInView = false;
|
|
void setupObservers();
|
|
});
|
|
|
|
onMount(() => {
|
|
if (!mobileCtaButtonEnabled) {
|
|
return;
|
|
}
|
|
|
|
void setupObservers();
|
|
|
|
return () => {
|
|
cleanupObservers();
|
|
};
|
|
});
|
|
</script>
|
|
|
|
{#if mobileCtaButtonEnabled && !hidden}
|
|
<div
|
|
class="mobile-book-bar"
|
|
class:mobile-book-bar-visible={visible}
|
|
aria-hidden={!visible}
|
|
>
|
|
<a class="mobile-book-bar-cta" href="/contact-us" tabindex={visible ? 0 : -1}>
|
|
<Icon name="fas fa-paw" />
|
|
<span>Book a free Meet & Greet</span>
|
|
<Icon name="fas fa-arrow-right" className="mobile-book-bar-arrow" />
|
|
</a>
|
|
</div>
|
|
{/if}
|
|
|
|
<style>
|
|
.mobile-book-bar {
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.mobile-book-bar {
|
|
position: fixed;
|
|
bottom: 0;
|
|
left: 0;
|
|
right: 0;
|
|
z-index: 50;
|
|
display: flex;
|
|
justify-content: center;
|
|
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
|
|
background: rgba(255, 255, 255, 0.96);
|
|
backdrop-filter: blur(10px);
|
|
border-top: 1px solid rgba(17, 20, 24, 0.08);
|
|
box-shadow: 0 -10px 28px rgba(17, 20, 24, 0.1);
|
|
|
|
opacity: 0;
|
|
transform: translateY(110%);
|
|
pointer-events: none;
|
|
transition:
|
|
opacity 0.22s ease,
|
|
transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
|
|
.mobile-book-bar-visible {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.mobile-book-bar-cta {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
width: 100%;
|
|
max-width: 460px;
|
|
padding: 13px 22px;
|
|
border-radius: 999px;
|
|
background: var(--yellow);
|
|
color: #000;
|
|
font-family: var(--font-head);
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.01em;
|
|
text-decoration: none;
|
|
box-shadow: 0 8px 18px rgba(255, 209, 0, 0.4);
|
|
transition:
|
|
background 0.18s ease,
|
|
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
|
|
.mobile-book-bar-cta:active {
|
|
transform: translateY(1px) scale(0.995);
|
|
background: #e6bb00;
|
|
}
|
|
|
|
:global(.mobile-book-bar-cta .icon) {
|
|
font-size: 13px;
|
|
}
|
|
|
|
:global(.mobile-book-bar-cta .mobile-book-bar-arrow) {
|
|
font-size: 12px;
|
|
opacity: 0.75;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.mobile-book-bar {
|
|
transition: opacity 0.22s ease;
|
|
transform: none;
|
|
}
|
|
}
|
|
}
|
|
</style>
|