Files
gw-svelte/src/lib/components/MobileBookBar.svelte
T

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 &amp; 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>