Files
gw-svelte/src/lib/components/PricingPage.svelte
T
2026-05-06 11:36:19 +12:00

717 lines
17 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts">
import { onMount } from 'svelte';
import { reveal } from '$lib/actions/reveal';
import BookingSection from '$lib/components/BookingSection.svelte';
import Icon from '$lib/components/Icon.svelte';
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte';
import type { PricingPageContent, SiteSharedContent } from '$lib/types';
export let content: SiteSharedContent;
export let pageContent: PricingPageContent;
const scrollDepthThreshold = 0.65;
const desktopPromptMediaQuery = '(min-width: 769px)';
let showMeetGreetPrompt = false;
let dismissMeetGreetPrompt = false;
let bookingInView = false;
let promptShown = false;
let canShowDesktopPrompt = false;
function revealMeetGreetPrompt() {
if (dismissMeetGreetPrompt || bookingInView || promptShown || !canShowDesktopPrompt) {
return;
}
showMeetGreetPrompt = true;
promptShown = true;
}
function closeMeetGreetPrompt() {
showMeetGreetPrompt = false;
dismissMeetGreetPrompt = true;
}
function handleMeetGreetCta() {
showMeetGreetPrompt = false;
dismissMeetGreetPrompt = true;
}
$: if ((!canShowDesktopPrompt || bookingInView) && showMeetGreetPrompt) {
showMeetGreetPrompt = false;
}
onMount(() => {
const desktopPromptQuery = window.matchMedia(desktopPromptMediaQuery);
canShowDesktopPrompt = desktopPromptQuery.matches;
const handleDesktopPromptViewportChange = (event: MediaQueryListEvent) => {
canShowDesktopPrompt = event.matches;
};
const handleScroll = () => {
if (promptShown || dismissMeetGreetPrompt || bookingInView || !canShowDesktopPrompt) {
return;
}
const scrollableHeight = document.documentElement.scrollHeight - window.innerHeight;
if (scrollableHeight <= 0) return;
const scrollPercent = window.scrollY / scrollableHeight;
if (scrollPercent >= scrollDepthThreshold) {
revealMeetGreetPrompt();
}
};
window.addEventListener('scroll', handleScroll, { passive: true });
const bookingSection = document.getElementById('newlead');
const bookingObserver = bookingSection
? new IntersectionObserver(
([entry]) => {
bookingInView = entry.isIntersecting;
},
{ threshold: 0.2 }
)
: null;
if (bookingObserver && bookingSection) {
bookingObserver.observe(bookingSection);
}
desktopPromptQuery.addEventListener('change', handleDesktopPromptViewportChange);
return () => {
window.removeEventListener('scroll', handleScroll);
desktopPromptQuery.removeEventListener('change', handleDesktopPromptViewportChange);
bookingObserver?.disconnect();
};
});
</script>
<main class="pricing-page">
<section class="pricing-page-hero">
<div class="pricing-inner">
<h1>{pageContent.title}</h1>
{#if pageContent.subtitle}
<p class="pricing-page-sub">{pageContent.subtitle}</p>
{/if}
<a
class="pricing-trust"
href="https://g.page/r/CUsvrWPhkYrAEB0/"
target="_blank"
rel="noopener"
aria-label="Read our 5-star Google reviews"
>
<img
class="pricing-trust-logo"
src="/images/google-g-logo.svg"
alt=""
width="18"
height="19"
/>
<span class="pricing-trust-stars" aria-hidden="true">
{#each Array(5) as _}
<Icon name="fas fa-star" />
{/each}
</span>
<span class="pricing-trust-label">30+ 5-star Google reviews, trusted by Auckland dog owners</span>
<Icon name="fas fa-arrow-right" className="pricing-trust-arrow" />
</a>
</div>
</section>
{#each pageContent.sections as section, index}
<section use:reveal class="pricing-section reveal-block">
<div class="pricing-inner">
<div class="pricing-section-heading">
{#if section.icon}
<div class="pricing-section-icon">
<Icon name={section.icon} />
</div>
{/if}
<h2>{section.title}</h2>
{#if section.blurb}
<p class="pricing-section-blurb">{section.blurb}</p>
{/if}
{#if section.detailCta}
<a
class={`btn pricing-section-link ${section.detailCta.variant === 'yellow' ? 'btn-yellow' : section.detailCta.variant === 'outline' ? 'btn-outline' : 'btn-green'}`}
href={section.detailCta.href}
>
<span>{section.detailCta.label}</span>
<Icon name="fas fa-arrow-right" />
</a>
{/if}
</div>
<div class:pricing-plan-grid-three={section.plans.length === 3} class="pricing-plan-grid">
{#each section.plans as plan}
<article class:pricing-plan-popular={plan.popular} class="pricing-plan-card">
{#if plan.popular}
<span class="pricing-plan-ribbon">Popular</span>
{/if}
<h3>{plan.title}</h3>
<div class="pricing-plan-price">{plan.price}</div>
<p class="pricing-plan-period">{plan.period}</p>
<ul class="pricing-plan-features">
{#each plan.features as feature}
<li>{feature}</li>
{/each}
</ul>
<a class="btn btn-yellow pricing-plan-cta" href="#newlead">Book a Meet &amp; Greet</a>
</article>
{/each}
</div>
<a class="btn btn-yellow pricing-section-mobile-cta" href="#newlead">
Book a Meet &amp; Greet
</a>
{#if index === 0}
<aside class="pricing-mobile-consult" aria-label="Need help choosing the right option?">
<span class="pricing-mobile-consult-kicker">
<Icon name="fas fa-comment-dots" />
Not sure which option fits?
</span>
<p>
Book a free Meet &amp; Greet and well help you choose the right walk or visit for your dog.
</p>
<a class="btn btn-outline btn-outline-green pricing-mobile-consult-cta" href="#newlead">
Talk it through with us
</a>
</aside>
{/if}
</div>
</section>
{/each}
<TestimonialsSection heading={pageContent.testimonialsHeading} testimonials={content.testimonials} />
<BookingSection booking={pageContent.booking} />
{#if showMeetGreetPrompt}
<aside class="meet-greet-prompt" aria-label="Free meet and greet reminder">
<button class="meet-greet-close" type="button" aria-label="Dismiss reminder" on:click={closeMeetGreetPrompt}>
<Icon name="fas fa-xmark" />
</button>
<div class="meet-greet-copy">
<span class="meet-greet-kicker">
<Icon name="fas fa-comment-dots" />
Free Meet & Greet
</span>
<p>
Not sure which option fits best? We can talk it through together and make sure your dog ends up happy.
</p>
</div>
<a class="meet-greet-cta" href="#newlead" on:click={handleMeetGreetCta}>Book a Meet &amp; Greet</a>
</aside>
{/if}
</main>
<style>
.pricing-page {
background: var(--off-white);
}
.pricing-inner {
max-width: var(--max-w);
margin: 0 auto;
padding: 0 50px;
}
.pricing-page-hero {
background: var(--green);
padding: 56px 0 64px;
text-align: center;
}
.pricing-page-hero h1 {
margin: 0 0 12px;
font-family: var(--font-head);
font-size: clamp(34px, 4vw, 56px);
line-height: 1.05;
letter-spacing: -0.04em;
color: #fff;
}
.pricing-page-sub {
margin: 0;
font-size: 16px;
line-height: 1.5;
color: rgba(255, 255, 255, 0.7);
}
.pricing-trust {
display: inline-flex;
align-items: center;
gap: 12px;
margin-top: 22px;
padding: 9px 18px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.18);
color: #fff;
font-size: 14px;
font-weight: 600;
text-decoration: none;
transition:
background 0.2s ease,
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
}
.pricing-trust-logo {
width: 18px;
height: 19px;
flex: 0 0 auto;
}
.pricing-trust:hover {
background: rgba(255, 255, 255, 0.18);
transform: translateY(-1px);
}
.pricing-trust-stars {
display: inline-flex;
align-items: center;
gap: 2px;
color: var(--yellow);
font-size: 13px;
}
.pricing-trust-label {
letter-spacing: 0.01em;
}
:global(.pricing-trust .pricing-trust-arrow) {
font-size: 12px;
opacity: 0.85;
}
.pricing-section-heading h2 {
margin: 0;
text-align: center;
font-family: var(--font-head);
font-size: clamp(24px, 2.8vw, 36px);
line-height: 1.1;
letter-spacing: -0.03em;
color: #000;
}
.pricing-section {
padding: 48px 0 72px;
}
.pricing-section-heading {
margin-bottom: 30px;
text-align: center;
}
.pricing-section-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 56px;
height: 56px;
margin-bottom: 16px;
border-radius: 16px;
background: var(--green);
color: #fff;
font-size: 22px;
}
.pricing-plan-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 22px;
}
.pricing-section-blurb {
max-width: 680px;
margin: 14px auto 0;
color: #4c5056;
font-size: 17px;
line-height: 1.65;
}
.pricing-section-link {
display: inline-flex;
align-items: center;
gap: 10px;
margin-top: 18px;
}
.pricing-plan-grid-three {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.pricing-plan-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
background: #fff;
border-radius: 28px;
padding: 30px 26px;
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
transition:
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.22s ease,
border-color 0.22s ease;
}
.pricing-plan-popular {
border: 2px solid var(--yellow);
}
.pricing-plan-ribbon {
position: absolute;
top: 0;
left: 50%;
transform: translate(-50%, -50%);
padding: 6px 12px;
border-radius: 999px;
background: var(--yellow);
color: #000;
font-family: var(--font-head);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.pricing-plan-card h3 {
margin: 0;
font-family: var(--font-head);
font-size: 22px;
line-height: 1.2;
color: #000;
}
.pricing-plan-price {
margin-top: 22px;
font-family: var(--font-head);
font-size: 52px;
line-height: 0.95;
letter-spacing: -0.05em;
color: #000;
}
.pricing-plan-period {
margin: 10px 0 0;
color: #5e6167;
font-size: 14px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.pricing-plan-features {
width: 100%;
margin: 24px 0 0;
padding: 0;
list-style: none;
}
.pricing-plan-features li {
padding: 15px 0;
border-top: 1px solid rgba(17, 20, 24, 0.08);
color: #34363a;
font-size: 16px;
line-height: 1.5;
}
.pricing-plan-cta {
margin-top: 24px;
font-family: var(--font-head);
}
.pricing-section-mobile-cta,
.pricing-mobile-consult {
display: none;
}
.meet-greet-prompt {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 30;
display: flex;
align-items: flex-end;
gap: 18px;
width: min(420px, calc(100vw - 32px));
padding: 18px 18px 18px 20px;
border-radius: 28px;
background:
linear-gradient(135deg, rgba(255, 255, 255, 0.98), rgba(247, 243, 232, 0.98));
box-shadow:
0 20px 40px rgba(17, 20, 24, 0.16),
0 0 0 1px rgba(17, 20, 24, 0.06);
animation: meet-greet-rise 0.28s cubic-bezier(0.22, 1, 0.36, 1);
backdrop-filter: blur(10px);
}
.meet-greet-copy {
min-width: 0;
flex: 1;
}
.meet-greet-kicker {
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
padding: 6px 10px;
border-radius: 999px;
background: rgba(33, 48, 33, 0.08);
color: var(--green);
font-family: var(--font-head);
font-size: 12px;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.meet-greet-kicker :global(.icon) {
font-size: 12px;
}
.meet-greet-copy p {
margin: 0;
color: #2f3134;
font-size: 15px;
line-height: 1.55;
}
.meet-greet-cta {
flex-shrink: 0;
align-self: center;
padding: 12px 18px;
border-radius: 999px;
background: var(--yellow);
color: #111;
font-family: var(--font-head);
font-size: 14px;
text-decoration: none;
box-shadow: inset 0 -2px 0 rgba(0, 0, 0, 0.08);
transition:
transform 0.16s cubic-bezier(0.22, 1, 0.36, 1),
box-shadow 0.2s ease,
background 0.2s ease;
}
.meet-greet-close {
position: absolute;
top: 12px;
right: 12px;
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border: none;
border-radius: 999px;
background: rgba(17, 20, 24, 0.05);
color: #2f3134;
cursor: pointer;
transition:
transform 0.16s cubic-bezier(0.22, 1, 0.36, 1),
background 0.2s ease;
}
@media (hover: hover) {
.meet-greet-cta:hover {
transform: translateY(-2px);
background: #ffd100;
box-shadow:
inset 0 -2px 0 rgba(0, 0, 0, 0.08),
0 12px 24px rgba(17, 20, 24, 0.12);
}
.meet-greet-close:hover {
transform: scale(1.05);
background: rgba(17, 20, 24, 0.09);
}
}
.meet-greet-cta:active,
.meet-greet-close:active {
transform: scale(0.97);
}
@keyframes meet-greet-rise {
from {
opacity: 0;
transform: translate3d(0, 16px, 0);
}
to {
opacity: 1;
transform: translate3d(0, 0, 0);
}
}
@media (hover: hover) {
.pricing-plan-card:hover {
transform: translateY(-6px) scale(1.012);
box-shadow: 0 22px 44px rgba(17, 20, 24, 0.1);
}
}
.pricing-plan-card:active {
transform: translateY(-2px) scale(0.992);
}
:global(.reveal-ready.reveal-block) {
opacity: 0;
transform: translate3d(0, var(--reveal-distance, 24px), 0);
transition:
opacity 0.55s ease,
transform 0.7s cubic-bezier(0.2, 0.8, 0.2, 1);
transition-delay: var(--reveal-delay, 0ms);
}
:global(.reveal-visible.reveal-block) {
opacity: 1;
transform: translate3d(0, 0, 0);
}
@media (max-width: 1024px) {
.pricing-plan-grid,
.pricing-plan-grid-three {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 768px) {
.pricing-inner {
padding: 0 24px;
}
.pricing-page-hero {
padding: 56px 0 20px;
}
.pricing-page-hero h1 {
font-size: 34px;
}
.pricing-trust {
gap: 10px;
padding: 10px 14px;
font-size: 13px;
}
.pricing-section-heading h2 {
font-size: 26px;
}
.pricing-section {
padding: 30px 0 52px;
}
.pricing-section-heading {
margin-bottom: 26px;
}
.pricing-section-blurb {
font-size: 15px;
line-height: 1.55;
}
.pricing-section-link {
margin-top: 22px;
margin-bottom: 8px;
}
.pricing-plan-grid,
.pricing-plan-grid-three {
grid-template-columns: 1fr;
gap: 18px;
}
.pricing-plan-popular {
order: -1;
}
.pricing-plan-card {
padding: 28px 22px;
}
.pricing-plan-price {
font-size: 46px;
}
.pricing-plan-cta {
display: none;
}
.pricing-section-mobile-cta {
display: flex;
width: fit-content;
margin: 18px auto 0;
font-family: var(--font-head);
}
.pricing-mobile-consult {
display: block;
margin-top: 18px;
padding: 22px 20px;
border-radius: 24px;
background: linear-gradient(180deg, #fffaf0 0%, #f9f4e7 100%);
box-shadow: 0 12px 28px rgba(17, 20, 24, 0.05);
text-align: left;
}
.pricing-mobile-consult-kicker {
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
color: var(--green);
font-family: var(--font-head);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.pricing-mobile-consult p {
margin: 0;
color: #34363a;
font-size: 15px;
line-height: 1.55;
}
.pricing-mobile-consult-cta {
margin-top: 16px;
}
.meet-greet-prompt {
right: 16px;
left: 16px;
bottom: 16px;
width: auto;
flex-direction: column;
align-items: stretch;
gap: 14px;
padding: 18px 18px 16px;
border-radius: 28px;
}
.meet-greet-copy p {
font-size: 14px;
line-height: 1.5;
padding-right: 26px;
}
.meet-greet-cta {
align-self: flex-start;
}
}
</style>