Onboarding / Deployment Scripts / Marketing updates
This commit is contained in:
+481
-106
@@ -1,68 +1,164 @@
|
||||
<script lang="ts">
|
||||
import { accordion } from '$lib/actions/accordion';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import ServicesSection from '$lib/components/ServicesSection.svelte';
|
||||
import { getImageMetadata } from '$lib/image-metadata';
|
||||
import type { AboutPageContent, SiteSharedContent } from '$lib/types';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import type { AboutPageContent } from '$lib/types';
|
||||
|
||||
export let content: SiteSharedContent;
|
||||
export let pageContent: AboutPageContent;
|
||||
|
||||
$: standardSections = pageContent.sections.filter((s) => s.accent !== 'founder');
|
||||
$: founderSection = pageContent.sections.find((s) => s.accent === 'founder') ?? null;
|
||||
const founderHeadingLead = 'Meet Aless,';
|
||||
const founderHeadingHighlight = 'the heart of Goodwalk';
|
||||
</script>
|
||||
|
||||
<main class="about-page">
|
||||
|
||||
<!-- ── Hero ── -->
|
||||
<section class="about-hero">
|
||||
<div class="about-inner">
|
||||
<span class="about-hero-eyebrow">About Goodwalk</span>
|
||||
<h1>{pageContent.title}</h1>
|
||||
<p class="about-hero-desc">Small dog specialists serving Auckland Central. A team your dog knows by name.</p>
|
||||
<div class="about-hero-chips">
|
||||
<a
|
||||
href="https://g.page/r/CUsvrWPhkYrAEB0/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="about-hero-chip about-hero-chip-link"
|
||||
>
|
||||
<span class="about-chip-stars" aria-hidden="true">★★★★★</span>
|
||||
30+ five-star Google reviews
|
||||
</a>
|
||||
<span class="about-hero-chip">Auckland Central</span>
|
||||
<span class="about-hero-chip">Small dog specialists</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#each pageContent.sections as section}
|
||||
<!-- ── Standard sections (Who we are, Our impact) ── -->
|
||||
{#each standardSections as section}
|
||||
{@const enhanced = getEnhancedImage(section.imageUrl)}
|
||||
<section
|
||||
use:reveal
|
||||
class:about-section-gradient={section.accent === 'gradient'}
|
||||
class="about-section reveal-block"
|
||||
class:about-section-gradient={section.accent === 'gradient'}
|
||||
>
|
||||
<div class:about-section-reverse={section.reverse} class="about-inner about-section-grid">
|
||||
<div class="about-inner about-section-grid" class:about-section-reverse={section.reverse}>
|
||||
<div class="about-copy">
|
||||
{#if section.eyebrow}
|
||||
<span class="about-eyebrow">{section.eyebrow}</span>
|
||||
{/if}
|
||||
<h2>{section.title}</h2>
|
||||
{#each section.body as paragraph}
|
||||
<p>{paragraph}</p>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="about-media">
|
||||
<img
|
||||
src={section.imageUrl}
|
||||
alt={section.imageAlt}
|
||||
width={getImageMetadata(section.imageUrl)?.width}
|
||||
height={getImageMetadata(section.imageUrl)?.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{#if enhanced}
|
||||
<enhanced:img src={enhanced} alt={section.imageAlt} loading="lazy" decoding="async" />
|
||||
{:else}
|
||||
<img src={section.imageUrl} alt={section.imageAlt} loading="lazy" decoding="async" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/each}
|
||||
|
||||
<ServicesSection services={content.services} heading={pageContent.servicesTitle} />
|
||||
<!-- ── Founder section ── -->
|
||||
{#if founderSection}
|
||||
{@const founderEnhanced = getEnhancedImage(founderSection.imageUrl)}
|
||||
<section use:reveal={{ delay: 50 }} class="about-founder reveal-block">
|
||||
<div class="about-inner about-founder-grid">
|
||||
<div class="about-founder-media">
|
||||
{#if founderEnhanced}
|
||||
<enhanced:img
|
||||
src={founderEnhanced}
|
||||
alt={founderSection.imageAlt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
src={founderSection.imageUrl}
|
||||
alt={founderSection.imageAlt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="about-founder-copy">
|
||||
{#if founderSection.eyebrow}
|
||||
<span class="about-eyebrow">{founderSection.eyebrow}</span>
|
||||
{/if}
|
||||
<h2 class="about-founder-heading">
|
||||
<span class="about-founder-heading-desktop">
|
||||
<span class="about-founder-title-main">{founderHeadingLead}</span>
|
||||
<br />
|
||||
<span class="about-founder-title-highlight">{founderHeadingHighlight}</span>
|
||||
</span>
|
||||
<span class="about-founder-heading-mobile">
|
||||
<span class="about-founder-title-main">{founderHeadingLead}</span>
|
||||
<span class="about-founder-title-highlight">{founderHeadingHighlight}</span>
|
||||
</span>
|
||||
</h2>
|
||||
{#each founderSection.body as paragraph}
|
||||
<p>{paragraph}</p>
|
||||
{/each}
|
||||
<a href="/contact-us" class="btn btn-green btn-mobile-center">Book a free Meet & Greet</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<section use:reveal={{ delay: 70 }} class="about-contact reveal-block">
|
||||
<!-- ── FAQs ── -->
|
||||
{#if pageContent.faqs && pageContent.faqs.length}
|
||||
<section use:reveal={{ delay: 30 }} class="about-faq reveal-block">
|
||||
<div class="about-inner">
|
||||
<div class="about-faq-header">
|
||||
<span class="about-eyebrow">FAQ</span>
|
||||
<h2>{pageContent.faqTitle ?? 'Common questions'}</h2>
|
||||
</div>
|
||||
<div use:accordion class="faq about-faq-list">
|
||||
{#each pageContent.faqs as item}
|
||||
<details>
|
||||
<summary>{item.question}</summary>
|
||||
<p>{item.answer}</p>
|
||||
</details>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- ── Contact CTA ── -->
|
||||
<section use:reveal={{ delay: 50 }} class="about-contact reveal-block">
|
||||
<div class="about-inner">
|
||||
<div class="about-contact-card">
|
||||
<span class="about-contact-eyebrow">Get in touch</span>
|
||||
<h2>{pageContent.contact.title}</h2>
|
||||
<div class="about-contact-grid">
|
||||
<p class="about-contact-desc">Questions, pricing, or your first Meet & Greet — start here and we'll reply within 24 hours.</p>
|
||||
<a class="btn btn-yellow btn-mobile-center about-contact-btn" href={pageContent.contact.cta.href}>
|
||||
{pageContent.contact.cta.label}
|
||||
</a>
|
||||
<div class="about-contact-links">
|
||||
<a class="about-contact-link" href={`mailto:${pageContent.contact.email}`}>
|
||||
<Icon name="fas fa-envelope" />
|
||||
{pageContent.contact.email}
|
||||
</a>
|
||||
<a class="btn btn-yellow" href={pageContent.contact.cta.href}>
|
||||
{pageContent.contact.cta.label}
|
||||
</a>
|
||||
<a class="about-contact-link" href={`tel:${pageContent.contact.phone.replace(/[^0-9+]/g, '')}`}>
|
||||
<a
|
||||
class="about-contact-link"
|
||||
href={`tel:${pageContent.contact.phone.replace(/[^0-9+]/g, '')}`}
|
||||
>
|
||||
<Icon name="fas fa-phone" />
|
||||
{pageContent.contact.phone}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<style>
|
||||
@@ -76,47 +172,112 @@
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
/* ── Shared eyebrow ── */
|
||||
.about-eyebrow,
|
||||
.about-hero-eyebrow,
|
||||
.about-contact-eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.about-eyebrow {
|
||||
background: rgba(33, 48, 33, 0.08);
|
||||
color: var(--gw-green);
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
.about-hero-eyebrow,
|
||||
.about-contact-eyebrow {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Hero ── */
|
||||
.about-hero {
|
||||
padding: 72px 0 40px;
|
||||
}
|
||||
|
||||
.about-hero h1,
|
||||
.about-copy h2,
|
||||
.about-contact-card h2 {
|
||||
margin: 0;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(34px, 4vw, 56px);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.04em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.about-hero h1 {
|
||||
background: var(--gw-green);
|
||||
color: #fff;
|
||||
padding: 80px 0 72px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about-hero h1 {
|
||||
margin: 0 0 16px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(40px, 5vw, 68px);
|
||||
font-weight: 800;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.04em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.about-hero-desc {
|
||||
max-width: 480px;
|
||||
margin: 0 auto 28px;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 17px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.about-hero-chips {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.about-hero-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 18px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
color: #fff;
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.about-hero-chip-link {
|
||||
text-decoration: none;
|
||||
transition: background 0.18s ease;
|
||||
}
|
||||
|
||||
.about-hero-chip-link:hover {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.about-chip-stars {
|
||||
color: var(--yellow);
|
||||
letter-spacing: 1px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── Standard sections ── */
|
||||
.about-section {
|
||||
padding: 0 0 88px;
|
||||
padding: 88px 0;
|
||||
}
|
||||
|
||||
.about-section-gradient {
|
||||
margin: 0 24px 88px;
|
||||
padding: 40px 0;
|
||||
border-radius: 28px;
|
||||
background: linear-gradient(180deg, #f5efe6 0%, #f9f6ef 100%);
|
||||
}
|
||||
|
||||
.about-section-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 44px;
|
||||
gap: 60px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.about-section-reverse {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.about-section-reverse .about-copy {
|
||||
order: 2;
|
||||
}
|
||||
@@ -126,29 +287,227 @@
|
||||
}
|
||||
|
||||
.about-copy h2 {
|
||||
margin: 0 0 16px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(28px, 3vw, 40px);
|
||||
font-weight: 800;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
color: #0d1a0d;
|
||||
}
|
||||
|
||||
.about-copy p {
|
||||
margin: 18px 0 0;
|
||||
margin: 12px 0 0;
|
||||
color: #34363a;
|
||||
font-size: 17px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.about-media {
|
||||
aspect-ratio: 4 / 3;
|
||||
overflow: hidden;
|
||||
border-radius: 28px;
|
||||
box-shadow: 0 16px 48px rgba(17, 20, 24, 0.1);
|
||||
}
|
||||
|
||||
.about-media img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 460px;
|
||||
aspect-ratio: 4 / 3;
|
||||
height: auto;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
border-radius: 28px;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 16px 40px rgba(17, 20, 24, 0.08);
|
||||
object-position: center top;
|
||||
}
|
||||
|
||||
/* ── Founder section ── */
|
||||
.about-founder {
|
||||
padding: 88px 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.about-founder-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
|
||||
gap: 64px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.about-founder-media img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: auto;
|
||||
border-radius: 28px;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 24px 56px rgba(17, 20, 24, 0.12);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.about-founder-copy h2 {
|
||||
margin: 0 0 16px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(30px, 3.5vw, 44px);
|
||||
font-weight: 800;
|
||||
line-height: 1.06;
|
||||
letter-spacing: -0.03em;
|
||||
text-wrap: balance;
|
||||
color: #0d1a0d;
|
||||
}
|
||||
|
||||
.about-founder-heading-desktop {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.about-founder-heading-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.about-founder-heading-mobile .about-founder-title-main,
|
||||
.about-founder-heading-mobile .about-founder-title-highlight {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.about-founder-title-main {
|
||||
color: #0d1a0d;
|
||||
}
|
||||
|
||||
.about-founder-title-highlight {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
color: #0d1a0d;
|
||||
}
|
||||
|
||||
.about-founder-title-highlight::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: -6px;
|
||||
bottom: -16px;
|
||||
height: 24px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 34' fill='none'%3E%3Cpath d='M4 24C67 10 131 4 198 5c43 1 82 6 118 18' stroke='%23192419' stroke-width='8' stroke-linecap='round'/%3E%3C/svg%3E")
|
||||
center/contain no-repeat;
|
||||
transform-origin: left center;
|
||||
animation: about-founder-underline-draw 0.9s cubic-bezier(0.22, 1, 0.36, 1) 0.2s both;
|
||||
}
|
||||
|
||||
@keyframes about-founder-underline-draw {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scaleX(0.2) translateY(6px) rotate(-1.5deg);
|
||||
}
|
||||
|
||||
65% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1.04) translateY(0) rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1) translateY(0) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
.about-founder-copy p {
|
||||
margin: 14px 0 0;
|
||||
color: #34363a;
|
||||
font-size: 17px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.about-founder-copy .btn {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin: 28px auto 0;
|
||||
}
|
||||
|
||||
/* ── FAQs ── */
|
||||
.about-faq {
|
||||
padding: 80px 0;
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
.about-faq-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.about-faq-header h2 {
|
||||
margin: 0;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(28px, 3vw, 40px);
|
||||
font-weight: 800;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
color: #0d1a0d;
|
||||
}
|
||||
|
||||
.about-faq-list {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* ── Contact CTA ── */
|
||||
.about-contact {
|
||||
padding: 0 0 88px;
|
||||
}
|
||||
|
||||
.about-contact-card {
|
||||
background: var(--gw-green);
|
||||
color: #fff;
|
||||
border-radius: 28px;
|
||||
padding: 56px 48px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 48px rgba(33, 48, 33, 0.18);
|
||||
}
|
||||
|
||||
.about-contact-card h2 {
|
||||
margin: 0 0 10px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(28px, 3vw, 42px);
|
||||
font-weight: 800;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.about-contact-desc {
|
||||
max-width: 440px;
|
||||
margin: 0 auto 28px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.about-contact-btn {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.about-contact-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.about-contact-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: color 0.18s ease;
|
||||
}
|
||||
|
||||
.about-contact-link:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Reveal ── */
|
||||
:global(.reveal-ready.reveal-block) {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, var(--reveal-distance, 24px), 0);
|
||||
@@ -163,41 +522,12 @@
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.about-contact {
|
||||
padding: 0 0 88px;
|
||||
}
|
||||
|
||||
.about-contact-card {
|
||||
border-radius: 28px;
|
||||
background: #fff;
|
||||
padding: 42px 48px;
|
||||
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about-contact-card h2 {
|
||||
font-size: clamp(28px, 3vw, 42px);
|
||||
}
|
||||
|
||||
.about-contact-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.about-contact-link {
|
||||
color: #34363a;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* ── Tablet ── */
|
||||
@media (max-width: 1024px) {
|
||||
.about-section-grid,
|
||||
.about-section-reverse {
|
||||
.about-founder-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 36px;
|
||||
}
|
||||
|
||||
.about-section-reverse .about-copy,
|
||||
@@ -205,38 +535,44 @@
|
||||
order: initial;
|
||||
}
|
||||
|
||||
.about-contact-grid {
|
||||
grid-template-columns: 1fr;
|
||||
.about-founder-media img {
|
||||
max-width: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
.about-inner {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.about-hero {
|
||||
padding: 56px 0 24px;
|
||||
padding: 56px 0 48px;
|
||||
}
|
||||
|
||||
.about-section,
|
||||
.about-contact {
|
||||
padding-bottom: 64px;
|
||||
.about-hero h1 {
|
||||
font-size: 38px;
|
||||
}
|
||||
|
||||
.about-section-gradient {
|
||||
margin: 0 12px 64px;
|
||||
padding: 28px 0;
|
||||
border-radius: 28px;
|
||||
.about-hero-desc {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.about-hero-chip {
|
||||
font-size: 13px;
|
||||
padding: 8px 14px;
|
||||
}
|
||||
|
||||
.about-section {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.about-section-grid {
|
||||
gap: 24px;
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.about-copy h2,
|
||||
.about-contact-card h2 {
|
||||
font-size: 30px;
|
||||
.about-copy h2 {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.about-copy p {
|
||||
@@ -244,16 +580,55 @@
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.about-founder {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.about-founder-grid {
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.about-founder-copy h2 {
|
||||
font-size: 26px;
|
||||
line-height: 1.02;
|
||||
}
|
||||
|
||||
.about-founder-heading-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.about-founder-heading-mobile {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.about-founder-copy p {
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.about-faq {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.about-contact {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.about-contact-card {
|
||||
padding: 30px 24px;
|
||||
padding: 36px 24px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.about-contact-grid {
|
||||
margin-top: 22px;
|
||||
.about-contact-links {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.about-contact-link {
|
||||
font-size: 18px;
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.about-founder-title-highlight::after {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -91,8 +91,11 @@
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.01em;
|
||||
color: #fff;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
@@ -249,11 +249,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
function sortSelectedServices(services: string[]) {
|
||||
return [...services].sort((a, b) => {
|
||||
const indexA = booking.serviceOptions.indexOf(a);
|
||||
const indexB = booking.serviceOptions.indexOf(b);
|
||||
|
||||
if (indexA === -1 && indexB === -1) return a.localeCompare(b);
|
||||
if (indexA === -1) return 1;
|
||||
if (indexB === -1) return -1;
|
||||
|
||||
return indexA - indexB;
|
||||
});
|
||||
}
|
||||
|
||||
function toggleService(service: string, checked: boolean) {
|
||||
noteInteraction();
|
||||
|
||||
if (checked) {
|
||||
selectedServices = [service, ...selectedServices.filter((item) => item !== service)];
|
||||
selectedServices = sortSelectedServices([
|
||||
...selectedServices.filter((item) => item !== service),
|
||||
service
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ async function fillDogStep() {
|
||||
await fireEvent.input(screen.getByLabelText(/Location/i), {
|
||||
target: { value: 'Kingsland' }
|
||||
});
|
||||
await fireEvent.input(screen.getByLabelText(/About Your Dog/i), {
|
||||
await fireEvent.input(screen.getByLabelText(/Pack Walks fit/i), {
|
||||
target: { value: 'Loves small group walks.' }
|
||||
});
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import type { FooterContent, LinkItem } from '$lib/types';
|
||||
import { locationPages } from '$lib/content/locations';
|
||||
|
||||
export let footer: FooterContent;
|
||||
|
||||
|
||||
const socialLinks: LinkItem[] = [
|
||||
{ label: 'Instagram', href: 'https://www.instagram.com/goodwalk.nz/', external: true },
|
||||
{ label: 'Facebook', href: 'https://facebook.com/goodwalk.nz', external: true },
|
||||
@@ -32,12 +34,10 @@
|
||||
<footer>
|
||||
<div class="footer-inner">
|
||||
<div class="footer-brand">
|
||||
<img
|
||||
src="/images/goodwalk-auckland-dog-walking-logo.png"
|
||||
<enhanced:img
|
||||
src="$lib/images/goodwalk-auckland-dog-walking-logo.png"
|
||||
alt="Goodwalk – Auckland dog walking service logo"
|
||||
class="footer-logo"
|
||||
width="241"
|
||||
height="48"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
@@ -72,6 +72,15 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-locations">
|
||||
<p class="footer-col-label">Areas we serve</p>
|
||||
<ul class="footer-nav">
|
||||
{#each locationPages as loc}
|
||||
<li><a href="/locations/{loc.slug}">{loc.suburb}</a></li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer-action footer-panel footer-panel-accent">
|
||||
<p class="footer-col-label">Get Started</p>
|
||||
<h3 class="footer-action-title">Ready when you are</h3>
|
||||
|
||||
@@ -3,6 +3,12 @@
|
||||
import { onMount } from 'svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import type { NavigationContent } from '$lib/types';
|
||||
import type { Picture } from '@sveltejs/enhanced-img';
|
||||
import logoDesktop from '$lib/images/goodwalk-auckland-dog-walking-logo.png?enhanced';
|
||||
import logoMobile from '$lib/images/goodwalk-auckland-dog-walking-logo-mobile.png?enhanced';
|
||||
|
||||
const desktop = logoDesktop as Picture;
|
||||
const mobile = logoMobile as Picture;
|
||||
|
||||
export let navigation: NavigationContent;
|
||||
|
||||
@@ -141,15 +147,18 @@
|
||||
|
||||
<a href="/" class="logo" aria-label="Goodwalk – Auckland Dog Walking, home">
|
||||
<picture>
|
||||
<source
|
||||
media="(max-width: 768px)"
|
||||
srcset="/images/goodwalk-auckland-dog-walking-logo-mobile.png"
|
||||
/>
|
||||
{#if mobile.sources?.webp}
|
||||
<source media="(max-width: 768px)" type="image/webp" srcset={mobile.sources.webp} />
|
||||
{/if}
|
||||
<source media="(max-width: 768px)" srcset={mobile.img.src} />
|
||||
{#if desktop.sources?.webp}
|
||||
<source type="image/webp" srcset={desktop.sources.webp} />
|
||||
{/if}
|
||||
<img
|
||||
src="/images/goodwalk-auckland-dog-walking-logo.png"
|
||||
src={desktop.img.src}
|
||||
alt="Goodwalk – Auckland dog walking service logo"
|
||||
width="241"
|
||||
height="48"
|
||||
width={desktop.img.w}
|
||||
height={desktop.img.h}
|
||||
decoding="async"
|
||||
/>
|
||||
</picture>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { getImageMetadata } from '$lib/image-metadata';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import type { CallToAction, HeroContent } from '$lib/types';
|
||||
|
||||
export let hero: HeroContent;
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
$: titleParts = splitTitle(hero.title);
|
||||
$: mobileTitle = hero.mobileTitle?.trim() || `${hero.title} ${hero.highlight}`.trim();
|
||||
$: heroImage = getImageMetadata(hero.imageUrl);
|
||||
$: heroEnhanced = getEnhancedImage(hero.imageUrl);
|
||||
|
||||
function splitTitle(title: string) {
|
||||
const trimmed = title.trim();
|
||||
@@ -99,15 +99,23 @@
|
||||
</div>
|
||||
|
||||
<div class="hero-img">
|
||||
<img
|
||||
src={hero.imageUrl}
|
||||
alt={hero.imageAlt}
|
||||
width={heroImage?.width}
|
||||
height={heroImage?.height}
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
/>
|
||||
{#if heroEnhanced}
|
||||
<enhanced:img
|
||||
src={heroEnhanced}
|
||||
alt={hero.imageAlt}
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
src={hero.imageUrl}
|
||||
alt={hero.imageAlt}
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1,43 +1,297 @@
|
||||
<script lang="ts">
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import type { HowItWorksContent } from '$lib/types';
|
||||
|
||||
export let content: HowItWorksContent;
|
||||
</script>
|
||||
|
||||
<section id="how-it-works" use:reveal={{ delay: 30 }} class="reveal-block">
|
||||
<div class="how-it-works-inner">
|
||||
<div class="how-it-works-header">
|
||||
<div class="hiw-inner">
|
||||
|
||||
<div class="hiw-header">
|
||||
<span class="hiw-eyebrow">Getting started</span>
|
||||
<h2 class="section-heading">{content.title}</h2>
|
||||
{#if content.intro}
|
||||
<p class="how-it-works-intro">{content.intro}</p>
|
||||
<p class="hiw-intro">{content.intro}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="how-it-works-flow" aria-label="How it works">
|
||||
<div class="hiw-steps">
|
||||
{#each content.steps as step, index}
|
||||
<article class:how-it-works-step-payoff={index === content.steps.length - 1} class="how-it-works-step">
|
||||
<div class="how-it-works-rail-node" aria-hidden="true">
|
||||
<span class="how-it-works-rail-dot"></span>
|
||||
<div class="hiw-step">
|
||||
<div class="hiw-step-meta">
|
||||
<span class="hiw-phase">{step.phase}</span>
|
||||
<span class="hiw-num">0{index + 1}</span>
|
||||
</div>
|
||||
<div class="how-it-works-step-top">
|
||||
<span class="how-it-works-count">{`0${index + 1}`}</span>
|
||||
<span class="how-it-works-phase">{step.phase || `Step ${index + 1}`}</span>
|
||||
<div class="hiw-icon-wrap">
|
||||
<Icon name={step.icon} className="hiw-step-icon" />
|
||||
</div>
|
||||
<div class="how-it-works-copy">
|
||||
<h3>{step.title}</h3>
|
||||
{#if step.benefit}
|
||||
<p class="how-it-works-benefit">{step.benefit}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="how-it-works-body">{step.body}</p>
|
||||
</article>
|
||||
<h3 class="hiw-title">{step.title}</h3>
|
||||
<p class="hiw-body">{step.body}</p>
|
||||
{#if step.benefit}
|
||||
<span class="hiw-benefit">
|
||||
<Icon name="fas fa-check" className="hiw-check-icon" />
|
||||
{step.benefit}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="hiw-cta">
|
||||
<a href="#newlead" class="btn btn-green btn-mobile-center">Book your free Meet & Greet</a>
|
||||
<p class="hiw-cta-note">Free, no-obligation. We reply within 24 hours.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
#how-it-works {
|
||||
background: var(--off-white);
|
||||
padding: 80px 0;
|
||||
}
|
||||
|
||||
.hiw-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
.hiw-header {
|
||||
text-align: center;
|
||||
margin-bottom: 56px;
|
||||
}
|
||||
|
||||
.hiw-eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.08);
|
||||
color: var(--gw-green);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
.hiw-intro {
|
||||
max-width: 580px;
|
||||
margin: 16px auto 0;
|
||||
color: #4c5056;
|
||||
font-size: 16px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* ── Steps grid ── */
|
||||
.hiw-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hiw-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 40px 40px 36px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(17, 20, 24, 0.06);
|
||||
box-shadow: 0 4px 16px rgba(17, 20, 24, 0.04);
|
||||
transition: box-shadow 0.22s ease, transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.hiw-step:first-child {
|
||||
border-radius: 28px 0 0 28px;
|
||||
}
|
||||
|
||||
.hiw-step:last-child {
|
||||
border-radius: 0 28px 28px 0;
|
||||
}
|
||||
|
||||
.hiw-step + .hiw-step {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.hiw-step:hover {
|
||||
box-shadow: 0 16px 40px rgba(17, 20, 24, 0.09);
|
||||
transform: translateY(-4px);
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Step meta (phase + number) ── */
|
||||
.hiw-step-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.hiw-phase {
|
||||
display: inline-block;
|
||||
padding: 5px 13px;
|
||||
border-radius: 999px;
|
||||
background: var(--yellow);
|
||||
color: #000;
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hiw-num {
|
||||
font-family: var(--font-head);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(33, 48, 33, 0.28);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* ── Icon ── */
|
||||
.hiw-icon-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin-bottom: 22px;
|
||||
border-radius: 20px;
|
||||
background: var(--gw-green);
|
||||
box-shadow: 0 10px 28px rgba(33, 48, 33, 0.2);
|
||||
}
|
||||
|
||||
.hiw-icon-wrap :global(.hiw-step-icon) {
|
||||
font-size: 26px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Content ── */
|
||||
.hiw-title {
|
||||
margin: 0 0 14px;
|
||||
font-family: var(--font-head);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: #0d1a0d;
|
||||
}
|
||||
|
||||
.hiw-body {
|
||||
margin: 0 0 20px;
|
||||
color: #4c5056;
|
||||
font-size: 15px;
|
||||
line-height: 1.65;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.hiw-benefit {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 7px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.07);
|
||||
color: var(--gw-green);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.hiw-benefit :global(.hiw-check-icon) {
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── CTA ── */
|
||||
.hiw-cta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 52px;
|
||||
}
|
||||
|
||||
.hiw-cta-note {
|
||||
margin: 0;
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
#how-it-works {
|
||||
padding: 64px 0;
|
||||
}
|
||||
|
||||
.hiw-inner {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.hiw-header {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.hiw-intro {
|
||||
font-size: 15px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.hiw-steps {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.hiw-step {
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
padding: 28px 24px;
|
||||
border-radius: 24px !important;
|
||||
border: 1px solid rgba(17, 20, 24, 0.06);
|
||||
}
|
||||
|
||||
.hiw-step + .hiw-step {
|
||||
border-left: 1px solid rgba(17, 20, 24, 0.06);
|
||||
}
|
||||
|
||||
.hiw-step-meta {
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hiw-icon-wrap {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.hiw-icon-wrap :global(.hiw-step-icon) {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.hiw-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.hiw-body {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.hiw-cta {
|
||||
margin-top: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Reveal ── */
|
||||
:global(.reveal-ready.reveal-block) {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, var(--reveal-distance, 24px), 0);
|
||||
@@ -51,215 +305,4 @@
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
#how-it-works {
|
||||
background: #fff;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.how-it-works-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
.how-it-works-header {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.how-it-works-intro {
|
||||
max-width: 640px;
|
||||
margin: 14px auto 0;
|
||||
color: #4c5056;
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.how-it-works-flow {
|
||||
display: grid;
|
||||
position: relative;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 28px;
|
||||
align-items: stretch;
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.how-it-works-flow::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 18px;
|
||||
right: 18px;
|
||||
top: 22px;
|
||||
height: 1px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(33, 48, 33, 0.12) 0%,
|
||||
rgba(33, 48, 33, 0.28) 50%,
|
||||
rgba(33, 48, 33, 0.12) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.how-it-works-step {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 18px 0 0;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.how-it-works-step-payoff {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.how-it-works-rail-node {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 44px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.how-it-works-rail-dot {
|
||||
display: inline-flex;
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
border-radius: 50%;
|
||||
background: var(--gw-green);
|
||||
box-shadow:
|
||||
0 0 0 7px #fff,
|
||||
0 0 0 8px rgba(33, 48, 33, 0.12);
|
||||
}
|
||||
|
||||
.how-it-works-step-top {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.how-it-works-count {
|
||||
color: rgba(33, 48, 33, 0.3);
|
||||
font-family: var(--font-head);
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
line-height: 0.9;
|
||||
}
|
||||
|
||||
.how-it-works-phase {
|
||||
color: var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.how-it-works-copy {
|
||||
padding: 22px 22px 20px;
|
||||
border-radius: 24px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 250, 240, 0.92) 0%, rgba(248, 244, 234, 0.92) 100%);
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
.how-it-works-step h3 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
line-height: 1.18;
|
||||
}
|
||||
|
||||
.how-it-works-benefit {
|
||||
margin: 10px 0 0;
|
||||
color: #6b5830;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.how-it-works-body {
|
||||
margin: 14px 0 0;
|
||||
padding: 0 4px 0 22px;
|
||||
color: #4c5056;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#how-it-works {
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.how-it-works-inner {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.how-it-works-intro {
|
||||
font-size: 15px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.how-it-works-flow {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 24px;
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.how-it-works-flow::before {
|
||||
left: 5px;
|
||||
right: auto;
|
||||
top: 22px;
|
||||
bottom: 22px;
|
||||
width: 1px;
|
||||
height: auto;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(33, 48, 33, 0.12) 0%,
|
||||
rgba(33, 48, 33, 0.28) 50%,
|
||||
rgba(33, 48, 33, 0.12) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.how-it-works-step {
|
||||
padding: 0 0 0 28px;
|
||||
}
|
||||
|
||||
.how-it-works-step-payoff {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.how-it-works-rail-node {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
min-height: 0;
|
||||
margin: 18px 0 0;
|
||||
}
|
||||
|
||||
.how-it-works-rail-dot {
|
||||
box-shadow:
|
||||
0 0 0 5px #fff,
|
||||
0 0 0 6px rgba(33, 48, 33, 0.12);
|
||||
}
|
||||
|
||||
.how-it-works-step-top {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.how-it-works-step h3 {
|
||||
font-size: 18px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.how-it-works-benefit {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.how-it-works-body {
|
||||
margin-top: 2px;
|
||||
padding: 14px 2px 0 18px;
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { accordion } from '$lib/actions/accordion';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import type { InfoContent } from '$lib/types';
|
||||
|
||||
@@ -39,7 +40,7 @@
|
||||
|
||||
<div class="info-block">
|
||||
<h2><Icon name="fas fa-circle-question" /> {info.faqTitle}</h2>
|
||||
<div class="faq">
|
||||
<div use:accordion class="faq">
|
||||
{#each info.faqs as faq}
|
||||
<details>
|
||||
<summary>{faq.question}</summary>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
|
||||
export let instagram: HomePageContent['instagram'];
|
||||
|
||||
const dogCutoutSrc = '/images/dog-cutout.png';
|
||||
</script>
|
||||
|
||||
<aside id="instagram" aria-label="Follow Goodwalk on Instagram">
|
||||
@@ -22,7 +21,7 @@
|
||||
</div>
|
||||
|
||||
<div class="instagram-dog-wrap" aria-hidden="true">
|
||||
<img class="instagram-dog" src={dogCutoutSrc} alt="" loading="lazy" decoding="async" />
|
||||
<enhanced:img src="$lib/images/dog-cutout.png" alt="" class="instagram-dog" loading="lazy" decoding="async" />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -104,8 +104,10 @@
|
||||
border-left: 3px solid var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(14px, 1.4vw, 17px);
|
||||
line-height: 1.3;
|
||||
letter-spacing: -0.01em;
|
||||
font-weight: 700;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.02em;
|
||||
text-wrap: balance;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,863 @@
|
||||
<script lang="ts">
|
||||
import { sharedServices } from '$lib/content/services';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import { getSeededTestimonialIndex } from '$lib/testimonials';
|
||||
import type { LocationPageContent, TestimonialContent } from '$lib/types';
|
||||
|
||||
export let location: LocationPageContent;
|
||||
export let testimonials: TestimonialContent[];
|
||||
|
||||
type ParkWithImage = LocationPageContent['parks'][number] & {
|
||||
image: NonNullable<LocationPageContent['parks'][number]['image']>;
|
||||
enhanced: ReturnType<typeof getEnhancedImage>;
|
||||
};
|
||||
|
||||
$: featuredTestimonial = testimonials[getSeededTestimonialIndex(testimonials, location.slug)];
|
||||
$: parksWithImages = location.parks
|
||||
.filter((park): park is LocationPageContent['parks'][number] & { image: NonNullable<LocationPageContent['parks'][number]['image']> } => Boolean(park.image))
|
||||
.map(
|
||||
(park): ParkWithImage => ({
|
||||
...park,
|
||||
enhanced: getEnhancedImage(park.image.src)
|
||||
})
|
||||
);
|
||||
$: serviceLinks = sharedServices.map((service) => ({
|
||||
label: service.title,
|
||||
href: service.href,
|
||||
desc: service.locationDescription,
|
||||
icon: service.icon
|
||||
}));
|
||||
$: locationHighlights = [
|
||||
{
|
||||
icon: 'fas fa-map-location-dot',
|
||||
label: 'Local routes',
|
||||
value: `${location.parks.length}+ parks`,
|
||||
detail: `Regular walking options in and around ${location.suburb}`
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-paw',
|
||||
label: 'Services',
|
||||
value: '3 ways to help',
|
||||
detail: 'Pack walks, 1:1 walks, and puppy visits'
|
||||
},
|
||||
{
|
||||
icon: 'fas fa-van-shuttle',
|
||||
label: 'Included',
|
||||
value: 'Free pickup',
|
||||
detail: 'Pickup and drop-off across the central suburbs'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<main class="loc-page">
|
||||
|
||||
<!-- ── Hero ── -->
|
||||
<section class="loc-hero">
|
||||
<div class="loc-inner">
|
||||
<span class="loc-hero-eyebrow">Auckland Central Dog Walking</span>
|
||||
<h1>Dog walkers in {location.suburb}</h1>
|
||||
<p class="loc-hero-desc">{location.intro}</p>
|
||||
<div class="loc-hero-actions">
|
||||
<a href="/contact-us" class="btn btn-yellow btn-mobile-center">Book a free Meet & Greet</a>
|
||||
<a href="tel:+64226421011" class="loc-hero-phone">or call (022) 642 1011</a>
|
||||
</div>
|
||||
<div class="loc-hero-chips">
|
||||
<a
|
||||
href="https://g.page/r/CUsvrWPhkYrAEB0/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="loc-chip loc-chip-link"
|
||||
>
|
||||
<span class="loc-chip-stars" aria-hidden="true">★★★★★</span>
|
||||
30+ five-star Google reviews
|
||||
</a>
|
||||
<span class="loc-chip">Small dog specialists</span>
|
||||
<span class="loc-chip">Free pickup & drop-off</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="loc-highlights" aria-label={`Goodwalk highlights in ${location.suburb}`}>
|
||||
<div class="loc-inner">
|
||||
<div class="loc-highlights-grid">
|
||||
{#each locationHighlights as highlight}
|
||||
<div class="loc-highlight-card">
|
||||
<div class="loc-highlight-top">
|
||||
<div class="loc-highlight-icon-wrap">
|
||||
<Icon name={highlight.icon} className="loc-highlight-icon" />
|
||||
</div>
|
||||
<span class="loc-highlight-label">{highlight.label}</span>
|
||||
</div>
|
||||
<strong>{highlight.value}</strong>
|
||||
<p>{highlight.detail}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Parks ── -->
|
||||
<section use:reveal={{ delay: 30 }} class="loc-parks reveal-block">
|
||||
<div class="loc-inner">
|
||||
<div class="loc-section-header">
|
||||
<span class="loc-eyebrow">Where we walk</span>
|
||||
<h2>Parks & walks in {location.suburb}</h2>
|
||||
<p class="loc-section-intro">
|
||||
These are the parks and routes we know well in {location.suburb}. Every walk is planned around your dog's pace, size, and temperament — not just the nearest green space.
|
||||
</p>
|
||||
</div>
|
||||
<div class="loc-parks-grid">
|
||||
{#each location.parks as park}
|
||||
<div class="loc-park-card">
|
||||
<div class="loc-park-icon" aria-hidden="true">🐾</div>
|
||||
<h3>{park.name}</h3>
|
||||
<p>{park.description}</p>
|
||||
{#if park.leashNote}
|
||||
<span class="loc-park-leash">{park.leashNote}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{#if parksWithImages.length > 0}
|
||||
<section use:reveal={{ delay: 30 }} class="loc-gallery reveal-block">
|
||||
<div class="loc-inner">
|
||||
<div class="loc-section-header">
|
||||
<span class="loc-eyebrow">Local parks</span>
|
||||
<h2>Park photos from {location.suburb}</h2>
|
||||
<p class="loc-section-intro">
|
||||
Real images from the parks we mention help each suburb page feel more specific and give search engines clearer local context.
|
||||
</p>
|
||||
</div>
|
||||
<div class="loc-gallery-grid">
|
||||
{#each parksWithImages as park}
|
||||
<figure class="loc-gallery-card">
|
||||
{#if park.enhanced}
|
||||
<picture>
|
||||
<img src={park.enhanced.img.src} alt={park.image.alt} loading="lazy" decoding="async" />
|
||||
</picture>
|
||||
{:else}
|
||||
<img src={park.image.src} alt={park.image.alt} loading="lazy" decoding="async" />
|
||||
{/if}
|
||||
<figcaption>
|
||||
<strong>{park.name}</strong>
|
||||
{#if park.image.caption}
|
||||
<span>{park.image.caption}</span>
|
||||
{/if}
|
||||
</figcaption>
|
||||
</figure>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- ── Services ── -->
|
||||
<section use:reveal={{ delay: 30 }} class="loc-services reveal-block">
|
||||
<div class="loc-inner">
|
||||
<div class="loc-section-header">
|
||||
<span class="loc-eyebrow">What we offer</span>
|
||||
<h2>Goodwalk services in {location.suburb}</h2>
|
||||
<p class="loc-section-intro">
|
||||
We offer pack walks, 1:1 walks, and puppy visits in {location.suburb}, with free pickup and drop-off across the central suburbs. Every service starts with a free Meet & Greet so we can understand your dog and recommend the right fit.
|
||||
</p>
|
||||
</div>
|
||||
<div class="loc-services-grid">
|
||||
{#each serviceLinks as svc}
|
||||
<a href={svc.href} class="loc-service-card">
|
||||
<div class="loc-service-icon-bubble">
|
||||
<Icon name={svc.icon} className="loc-service-icon" />
|
||||
</div>
|
||||
<h3>{svc.label}</h3>
|
||||
<p>{svc.desc}</p>
|
||||
<span class="loc-service-link">Learn more →</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Testimonial ── -->
|
||||
{#if featuredTestimonial}
|
||||
<section use:reveal={{ delay: 30 }} class="loc-review reveal-block">
|
||||
<div class="loc-inner">
|
||||
<div class="loc-review-card">
|
||||
<span class="loc-review-stars" aria-hidden="true">★★★★★</span>
|
||||
<blockquote class="loc-review-quote">"{featuredTestimonial.quote}"</blockquote>
|
||||
<cite class="loc-review-cite">
|
||||
{featuredTestimonial.reviewer}
|
||||
{#if featuredTestimonial.detail}
|
||||
<span class="loc-review-detail">— {featuredTestimonial.detail}</span>
|
||||
{/if}
|
||||
</cite>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- ── CTA ── -->
|
||||
<section use:reveal={{ delay: 30 }} class="loc-cta reveal-block">
|
||||
<div class="loc-inner">
|
||||
<div class="loc-cta-card">
|
||||
<span class="loc-cta-eyebrow">Get in touch</span>
|
||||
<h2>Ready to get started in {location.suburb}?</h2>
|
||||
<p class="loc-cta-desc">
|
||||
A free Meet & Greet is the first step — no commitment, no pressure. We meet your dog, answer your questions, and see if Goodwalk is the right fit.
|
||||
</p>
|
||||
<a class="btn btn-yellow btn-mobile-center loc-cta-btn" href="/contact-us">Book a free Meet & Greet</a>
|
||||
<div class="loc-cta-links">
|
||||
<a class="loc-cta-link" href="mailto:info@goodwalk.co.nz">info@goodwalk.co.nz</a>
|
||||
<a class="loc-cta-link" href="tel:+64226421011">(022) 642 1011</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.loc-page {
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
.loc-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
/* ── Eyebrow ── */
|
||||
.loc-eyebrow,
|
||||
.loc-hero-eyebrow,
|
||||
.loc-cta-eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.loc-eyebrow {
|
||||
background: rgba(33, 48, 33, 0.08);
|
||||
color: var(--gw-green);
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
.loc-hero-eyebrow,
|
||||
.loc-cta-eyebrow {
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Hero ── */
|
||||
.loc-hero {
|
||||
background: var(--gw-green);
|
||||
color: #fff;
|
||||
padding: 80px 0 112px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loc-hero h1 {
|
||||
margin: 0 0 16px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(36px, 5vw, 64px);
|
||||
font-weight: 800;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.04em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.loc-hero-desc {
|
||||
max-width: 640px;
|
||||
margin: 0 auto 28px;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 17px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.loc-hero-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
|
||||
.loc-hero-phone {
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 15px;
|
||||
text-decoration: none;
|
||||
transition: color 0.18s ease;
|
||||
}
|
||||
|
||||
.loc-hero-phone:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.loc-hero-chips {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.loc-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 9px 18px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||
color: #fff;
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.loc-chip-link {
|
||||
text-decoration: none;
|
||||
transition: background 0.18s ease;
|
||||
}
|
||||
|
||||
.loc-chip-link:hover {
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
.loc-chip-stars {
|
||||
color: var(--yellow);
|
||||
letter-spacing: 1px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── Highlights ── */
|
||||
.loc-highlights {
|
||||
margin-top: -56px;
|
||||
padding: 0 0 88px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.loc-highlights-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.loc-highlight-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 24px 24px 22px;
|
||||
border-radius: 22px;
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(255, 209, 71, 0.22), transparent 34%),
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.99) 0%, #f7f4ec 100%);
|
||||
border: 1px solid rgba(17, 20, 24, 0.07);
|
||||
box-shadow: 0 18px 44px rgba(13, 26, 13, 0.09);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.loc-highlight-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -18px;
|
||||
bottom: -18px;
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
border-radius: 50%;
|
||||
background: rgba(33, 48, 33, 0.05);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.loc-highlight-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.loc-highlight-icon-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(180deg, #ffe173 0%, #ffd54a 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
|
||||
0 10px 18px rgba(255, 209, 71, 0.24);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
:global(.loc-highlight-icon-wrap .loc-highlight-icon) {
|
||||
color: var(--gw-green);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.loc-highlight-label {
|
||||
display: inline-block;
|
||||
color: var(--gw-green);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.loc-highlight-card strong {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
color: #0d1a0d;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(22px, 2.5vw, 28px);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.loc-highlight-card p {
|
||||
margin: 0;
|
||||
color: #4c5056;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Section headers ── */
|
||||
.loc-section-header {
|
||||
text-align: center;
|
||||
margin-bottom: 48px;
|
||||
}
|
||||
|
||||
.loc-section-header h2 {
|
||||
margin: 0 0 12px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(26px, 3vw, 38px);
|
||||
font-weight: 700;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
text-wrap: balance;
|
||||
color: #0d1a0d;
|
||||
}
|
||||
|
||||
.loc-section-intro {
|
||||
max-width: 560px;
|
||||
margin: 0 auto;
|
||||
color: #4c5056;
|
||||
font-size: 16px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* ── Parks ── */
|
||||
.loc-parks {
|
||||
padding: 0 0 88px;
|
||||
}
|
||||
|
||||
.loc-parks-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.loc-park-card {
|
||||
background: #fff;
|
||||
border-radius: 20px;
|
||||
padding: 32px 28px;
|
||||
border: 1px solid rgba(17, 20, 24, 0.07);
|
||||
box-shadow: 0 4px 16px rgba(17, 20, 24, 0.04);
|
||||
}
|
||||
|
||||
.loc-park-icon {
|
||||
font-size: 28px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.loc-park-card h3 {
|
||||
margin: 0 0 10px;
|
||||
font-family: var(--font-head);
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: #0d1a0d;
|
||||
}
|
||||
|
||||
.loc-park-card p {
|
||||
margin: 0 0 14px;
|
||||
color: #4c5056;
|
||||
font-size: 15px;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.loc-park-leash {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.07);
|
||||
color: var(--gw-green);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Gallery ── */
|
||||
.loc-gallery {
|
||||
padding: 0 0 88px;
|
||||
}
|
||||
|
||||
.loc-gallery-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.loc-gallery-card {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 20px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(17, 20, 24, 0.07);
|
||||
box-shadow: 0 8px 28px rgba(17, 20, 24, 0.06);
|
||||
}
|
||||
|
||||
.loc-gallery-card picture,
|
||||
.loc-gallery-card img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.loc-gallery-card img {
|
||||
aspect-ratio: 4 / 3;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.loc-gallery-card figcaption {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
padding: 18px 20px 20px;
|
||||
}
|
||||
|
||||
.loc-gallery-card strong {
|
||||
color: #0d1a0d;
|
||||
font-size: 16px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.loc-gallery-card span {
|
||||
color: #4c5056;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Services ── */
|
||||
.loc-services {
|
||||
padding: 0 0 88px;
|
||||
}
|
||||
|
||||
.loc-services-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.loc-service-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 28px 24px;
|
||||
background: var(--gw-green);
|
||||
border-radius: 20px;
|
||||
text-decoration: none;
|
||||
transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.loc-highlight-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 20px 40px rgba(13, 26, 13, 0.12);
|
||||
}
|
||||
|
||||
.loc-park-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 16px 32px rgba(17, 20, 24, 0.08);
|
||||
}
|
||||
|
||||
.loc-service-card:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 16px 36px rgba(33, 48, 33, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.loc-park-card,
|
||||
.loc-highlight-card,
|
||||
.loc-gallery-card,
|
||||
.loc-service-card {
|
||||
transition: transform 0.18s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.loc-service-icon-bubble {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
margin: 0 0 20px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, #ffe173 0%, #ffd54a 100%);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
|
||||
0 10px 24px rgba(17, 20, 24, 0.16);
|
||||
}
|
||||
|
||||
:global(.loc-service-icon-bubble .loc-service-icon) {
|
||||
color: var(--gw-green);
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.loc-service-card h3 {
|
||||
margin: 0 0 8px;
|
||||
font-family: var(--font-head);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.loc-service-card p {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 14px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.loc-service-link {
|
||||
display: inline-block;
|
||||
margin-top: 18px;
|
||||
color: var(--yellow);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ── Review ── */
|
||||
.loc-review {
|
||||
padding: 0 0 88px;
|
||||
}
|
||||
|
||||
.loc-review-card {
|
||||
background: #fff;
|
||||
border-radius: 24px;
|
||||
padding: 48px 56px;
|
||||
text-align: center;
|
||||
border: 1px solid rgba(17, 20, 24, 0.06);
|
||||
box-shadow: 0 8px 32px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
.loc-review-stars {
|
||||
display: block;
|
||||
color: var(--yellow);
|
||||
font-size: 20px;
|
||||
letter-spacing: 3px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.loc-review-quote {
|
||||
margin: 0 0 20px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(18px, 2.2vw, 24px);
|
||||
font-weight: 600;
|
||||
line-height: 1.45;
|
||||
color: #0d1a0d;
|
||||
font-style: normal;
|
||||
max-width: 720px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.loc-review-cite {
|
||||
font-style: normal;
|
||||
color: var(--gw-green);
|
||||
font-weight: 700;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.loc-review-detail {
|
||||
font-weight: 400;
|
||||
color: #888;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* ── CTA ── */
|
||||
.loc-cta {
|
||||
padding: 0 0 88px;
|
||||
}
|
||||
|
||||
.loc-cta-card {
|
||||
background: var(--gw-green);
|
||||
color: #fff;
|
||||
border-radius: 28px;
|
||||
padding: 56px 48px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 48px rgba(33, 48, 33, 0.18);
|
||||
}
|
||||
|
||||
.loc-cta-card h2 {
|
||||
margin: 0 0 10px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(26px, 3vw, 40px);
|
||||
font-weight: 800;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.loc-cta-desc {
|
||||
max-width: 460px;
|
||||
margin: 0 auto 28px;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.loc-cta-btn {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loc-cta-links {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 22px;
|
||||
}
|
||||
|
||||
.loc-cta-link {
|
||||
color: rgba(255, 255, 255, 0.72);
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
transition: color 0.18s ease;
|
||||
}
|
||||
|
||||
.loc-cta-link:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* ── Reveal ── */
|
||||
: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);
|
||||
}
|
||||
|
||||
/* ── Tablet ── */
|
||||
@media (max-width: 1024px) {
|
||||
.loc-highlights-grid,
|
||||
.loc-parks-grid,
|
||||
.loc-gallery-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
.loc-inner {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.loc-hero {
|
||||
padding: 56px 0 48px;
|
||||
}
|
||||
|
||||
.loc-hero h1 {
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.loc-hero-desc {
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.loc-hero-actions {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.loc-highlights {
|
||||
margin-top: -24px;
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.loc-highlights-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.loc-highlight-card {
|
||||
padding: 20px 18px 18px;
|
||||
}
|
||||
|
||||
.loc-parks {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.loc-parks-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loc-gallery {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.loc-gallery-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loc-services {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.loc-services-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.loc-review {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.loc-review-card {
|
||||
padding: 32px 24px;
|
||||
}
|
||||
|
||||
.loc-cta {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
|
||||
.loc-cta-card {
|
||||
padding: 36px 24px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.loc-cta-links {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -24,7 +24,11 @@
|
||||
const mobileCtaButtonEnabled = isMobileCtaButtonEnabled();
|
||||
|
||||
$: pathname = $page.url.pathname;
|
||||
$: hidden = pathname === '/contact-us' || pathname === '/booking';
|
||||
$: 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;
|
||||
|
||||
@@ -0,0 +1,363 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
|
||||
export let context: 'onboarding' | 'contract' = 'onboarding';
|
||||
|
||||
const dispatch = createEventDispatcher<{ authenticated: { email: string; profile: Record<string, string>; draft: Record<string, unknown> } }>();
|
||||
|
||||
const ownerEmail = 'info@goodwalk.co.nz';
|
||||
const ownerPhone = '(022) 642 1011';
|
||||
|
||||
let stage: 'email' | 'code' = 'email';
|
||||
let emailValue = '';
|
||||
let codeValue = '';
|
||||
let loading = false;
|
||||
let error = '';
|
||||
|
||||
async function requestCode() {
|
||||
const trimmed = emailValue.trim();
|
||||
if (!trimmed) { error = 'Please enter your email address'; return; }
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const res = await fetch('/api/auth/request-code', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: trimmed }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) throw new Error(data?.detail ?? 'Failed to send code. Please try again.');
|
||||
stage = 'code';
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Something went wrong';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyCode() {
|
||||
const trimmed = codeValue.trim();
|
||||
if (!trimmed) { error = 'Please enter the code'; return; }
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
const res = await fetch('/api/auth/verify-code', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ email: emailValue.trim(), code: trimmed }),
|
||||
});
|
||||
const data = await res.json().catch(() => null);
|
||||
if (!res.ok) throw new Error(data?.detail ?? 'Incorrect code. Please try again.');
|
||||
try { window.localStorage.setItem('gw_onboarding_session', data.token); } catch { /* ignore */ }
|
||||
let profile: Record<string, string> = {};
|
||||
let draft: Record<string, unknown> = {};
|
||||
try {
|
||||
const verifyRes = await fetch('/api/auth/verify', {
|
||||
headers: { Authorization: `Bearer ${data.token}` },
|
||||
});
|
||||
if (verifyRes.ok) {
|
||||
const verifyData = await verifyRes.json();
|
||||
profile = verifyData.profile ?? {};
|
||||
draft = verifyData.draft ?? {};
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
dispatch('authenticated', { email: data.email, profile, draft });
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Something went wrong';
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleEmailKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') requestCode();
|
||||
}
|
||||
|
||||
function handleCodeKey(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') verifyCode();
|
||||
}
|
||||
|
||||
function goBack() {
|
||||
stage = 'email';
|
||||
codeValue = '';
|
||||
error = '';
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="auth-wrap">
|
||||
<div class="auth-card">
|
||||
<div class="auth-icon">
|
||||
<Icon name="fas fa-lock" />
|
||||
</div>
|
||||
|
||||
{#if stage === 'email'}
|
||||
<h2>Sign in to continue</h2>
|
||||
<p>Enter the email address you used when enquiring with Goodwalk. We'll send you a one-time code.</p>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="auth-email">Email address</label>
|
||||
<input
|
||||
id="auth-email"
|
||||
type="email"
|
||||
bind:value={emailValue}
|
||||
on:keydown={handleEmailKey}
|
||||
placeholder="you@example.com"
|
||||
autocomplete="email"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="auth-error">{error}</div>
|
||||
{/if}
|
||||
|
||||
<button class="btn btn-yellow auth-btn" on:click={requestCode} disabled={loading}>
|
||||
{#if loading}Sending…{:else}Send code <Icon name="fas fa-arrow-right" />{/if}
|
||||
</button>
|
||||
|
||||
{:else}
|
||||
<h2>Enter your code</h2>
|
||||
<p>We sent a 6-digit code to <strong>{emailValue}</strong>. It expires in 10 minutes.</p>
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="auth-code">One-time code</label>
|
||||
<input
|
||||
id="auth-code"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
pattern="[0-9]*"
|
||||
maxlength="6"
|
||||
bind:value={codeValue}
|
||||
on:keydown={handleCodeKey}
|
||||
placeholder="123456"
|
||||
autocomplete="one-time-code"
|
||||
disabled={loading}
|
||||
class="auth-code-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="auth-error">{error}</div>
|
||||
{/if}
|
||||
|
||||
<button class="btn btn-yellow auth-btn" on:click={verifyCode} disabled={loading}>
|
||||
{#if loading}Verifying…{:else}Verify code <Icon name="fas fa-arrow-right" />{/if}
|
||||
</button>
|
||||
|
||||
<button class="auth-back" on:click={goBack}>
|
||||
<Icon name="fas fa-arrow-left" /> Use a different email
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="auth-help">
|
||||
<span>Need help?</span>
|
||||
<a href="mailto:{ownerEmail}">{ownerEmail}</a>
|
||||
<span>or</span>
|
||||
<a href="tel:{ownerPhone.replace(/[^0-9+]/g, '')}">{ownerPhone}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="auth-copyright">
|
||||
<a href="https://goodwalk.co.nz">goodwalk.co.nz</a>
|
||||
<span>·</span>
|
||||
<span>© {new Date().getFullYear()} Goodwalk. All rights reserved.</span>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.auth-wrap {
|
||||
padding: 32px 28px 64px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
padding: 36px 32px;
|
||||
border-radius: 28px;
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
border: 1px solid rgba(33, 48, 33, 0.08);
|
||||
box-shadow: 0 20px 48px rgba(33, 48, 33, 0.09);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.auth-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 16px;
|
||||
background: linear-gradient(180deg, #ffe36b 0%, #ffd100 100%);
|
||||
color: #213021;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.auth-card h2 {
|
||||
margin: 0 0 10px;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(22px, 3vw, 30px);
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.03em;
|
||||
color: #213021;
|
||||
}
|
||||
|
||||
.auth-card p {
|
||||
margin: 0 0 24px;
|
||||
font-size: 15px;
|
||||
line-height: 1.65;
|
||||
color: rgba(33, 48, 33, 0.72);
|
||||
}
|
||||
|
||||
.auth-card p strong {
|
||||
color: #213021;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.auth-field {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.auth-field label {
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
color: #213021;
|
||||
}
|
||||
|
||||
.auth-field input {
|
||||
width: 100%;
|
||||
padding: 15px 16px;
|
||||
border: 1px solid rgba(33, 48, 33, 0.14);
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
font: inherit;
|
||||
font-size: 16px;
|
||||
color: #213021;
|
||||
outline: none;
|
||||
transition: border-color 0.18s ease, box-shadow 0.18s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.auth-field input:focus {
|
||||
border-color: rgba(255, 209, 0, 0.9);
|
||||
box-shadow: 0 0 0 4px rgba(255, 209, 0, 0.16);
|
||||
}
|
||||
|
||||
.auth-code-input {
|
||||
font-size: 28px !important;
|
||||
font-family: var(--font-head) !important;
|
||||
font-weight: 800 !important;
|
||||
letter-spacing: 0.22em !important;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-error {
|
||||
margin-bottom: 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 14px;
|
||||
background: #fff3ef;
|
||||
color: #a43f2c;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.auth-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.auth-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(33, 48, 33, 0.12);
|
||||
background: transparent;
|
||||
font-family: var(--font-head);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(33, 48, 33, 0.65);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
margin-bottom: 20px;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.auth-back:hover {
|
||||
background: rgba(33, 48, 33, 0.05);
|
||||
}
|
||||
|
||||
.auth-help {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid rgba(33, 48, 33, 0.07);
|
||||
font-size: 13px;
|
||||
color: rgba(33, 48, 33, 0.5);
|
||||
}
|
||||
|
||||
.auth-help a {
|
||||
color: rgba(33, 48, 33, 0.75);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.auth-help a:hover {
|
||||
color: #213021;
|
||||
}
|
||||
|
||||
.auth-copyright {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 12px 28px;
|
||||
background: #fff;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.07);
|
||||
font-size: 12px;
|
||||
color: #aaa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.auth-copyright a {
|
||||
color: #888;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.auth-copyright a:hover {
|
||||
color: #555;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.auth-wrap {
|
||||
padding: 20px 18px 32px;
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
padding: 26px 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
|
||||
export let email = '';
|
||||
|
||||
const dispatch = createEventDispatcher<{ logout: void }>();
|
||||
|
||||
let loggingOut = false;
|
||||
|
||||
async function logout() {
|
||||
loggingOut = true;
|
||||
try {
|
||||
const token = window.localStorage.getItem('gw_onboarding_session') ?? '';
|
||||
if (token) {
|
||||
await fetch('/api/auth/logout', {
|
||||
method: 'POST',
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
}).catch(() => { /* ignore network errors on logout */ });
|
||||
}
|
||||
} finally {
|
||||
try { window.localStorage.removeItem('gw_onboarding_session'); } catch { /* ignore */ }
|
||||
loggingOut = false;
|
||||
dispatch('logout');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<footer class="ob-footer">
|
||||
<div class="ob-footer-inner">
|
||||
<div class="ob-footer-identity">
|
||||
<Icon name="fas fa-circle-check" />
|
||||
<span>Signed in as <strong>{email}</strong></span>
|
||||
</div>
|
||||
<button class="ob-footer-logout" on:click={logout} disabled={loggingOut}>
|
||||
<Icon name="fas fa-right-from-bracket" />
|
||||
{loggingOut ? 'Signing out…' : 'Sign out'}
|
||||
</button>
|
||||
</div>
|
||||
<div class="ob-footer-copyright">
|
||||
<a href="https://goodwalk.co.nz">goodwalk.co.nz</a>
|
||||
<span>·</span>
|
||||
<span>© {new Date().getFullYear()} Goodwalk. All rights reserved.</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<style>
|
||||
.ob-footer {
|
||||
background: #213021;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.ob-footer-inner {
|
||||
max-width: 1120px;
|
||||
margin: 0 auto;
|
||||
padding: 0 28px;
|
||||
height: 52px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.ob-footer-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.ob-footer-identity strong {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ob-footer-logout {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 7px 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
background: transparent;
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, color 0.15s;
|
||||
}
|
||||
|
||||
.ob-footer-logout:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.ob-footer-copyright {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 10px 28px;
|
||||
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.25);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ob-footer-copyright a {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ob-footer-copyright a:hover {
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ob-footer-inner {
|
||||
padding: 0 18px;
|
||||
}
|
||||
|
||||
.ob-footer-identity span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ob-footer-copyright {
|
||||
padding: 10px 18px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,199 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
export let value = '';
|
||||
export let disabled = false;
|
||||
|
||||
let canvas: HTMLCanvasElement;
|
||||
let isDrawing = false;
|
||||
let hasSigned = false;
|
||||
let activePointerId: number | null = null;
|
||||
let lines: { x: number; y: number }[][] = [];
|
||||
|
||||
function resizeCanvas() {
|
||||
if (!canvas) return;
|
||||
|
||||
const ratio = Math.max(window.devicePixelRatio || 1, 1);
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = Math.max(1, Math.round(rect.width * ratio));
|
||||
canvas.height = Math.max(1, Math.round(rect.height * ratio));
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
||||
drawAllLines();
|
||||
syncValue();
|
||||
}
|
||||
|
||||
function getContext() {
|
||||
return canvas?.getContext('2d') ?? null;
|
||||
}
|
||||
|
||||
function drawAllLines() {
|
||||
const ctx = getContext();
|
||||
if (!ctx || !canvas) return;
|
||||
|
||||
const width = canvas.width / Math.max(window.devicePixelRatio || 1, 1);
|
||||
const height = canvas.height / Math.max(window.devicePixelRatio || 1, 1);
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.lineCap = 'round';
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.strokeStyle = '#213021';
|
||||
ctx.lineWidth = 3;
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.length) continue;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(line[0].x, line[0].y);
|
||||
|
||||
if (line.length === 1) {
|
||||
ctx.lineTo(line[0].x + 0.01, line[0].y + 0.01);
|
||||
} else {
|
||||
for (const point of line.slice(1)) {
|
||||
ctx.lineTo(point.x, point.y);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
function pointFromEvent(event: PointerEvent) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
|
||||
return {
|
||||
x: event.clientX - rect.left,
|
||||
y: event.clientY - rect.top
|
||||
};
|
||||
}
|
||||
|
||||
function syncValue() {
|
||||
value = hasSigned && canvas ? canvas.toDataURL('image/png') : '';
|
||||
}
|
||||
|
||||
function startDrawing(event: PointerEvent) {
|
||||
if (disabled) return;
|
||||
|
||||
activePointerId = event.pointerId;
|
||||
isDrawing = true;
|
||||
canvas.setPointerCapture(event.pointerId);
|
||||
const point = pointFromEvent(event);
|
||||
lines = [...lines, [point]];
|
||||
hasSigned = true;
|
||||
drawAllLines();
|
||||
syncValue();
|
||||
}
|
||||
|
||||
function continueDrawing(event: PointerEvent) {
|
||||
if (!isDrawing || disabled || activePointerId !== event.pointerId) return;
|
||||
|
||||
const point = pointFromEvent(event);
|
||||
const nextLines = [...lines];
|
||||
const currentLine = nextLines[nextLines.length - 1];
|
||||
|
||||
if (!currentLine) return;
|
||||
|
||||
currentLine.push(point);
|
||||
lines = nextLines;
|
||||
drawAllLines();
|
||||
syncValue();
|
||||
}
|
||||
|
||||
function stopDrawing(event?: PointerEvent) {
|
||||
if (event && activePointerId === event.pointerId && canvas.hasPointerCapture(event.pointerId)) {
|
||||
canvas.releasePointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
activePointerId = null;
|
||||
isDrawing = false;
|
||||
syncValue();
|
||||
}
|
||||
|
||||
export function clear() {
|
||||
lines = [];
|
||||
hasSigned = false;
|
||||
drawAllLines();
|
||||
syncValue();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
resizeCanvas();
|
||||
window.addEventListener('resize', resizeCanvas);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('resize', resizeCanvas);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class:signature-disabled={disabled} class="signature-shell">
|
||||
<canvas
|
||||
bind:this={canvas}
|
||||
class="signature-canvas"
|
||||
aria-label="Draw your signature"
|
||||
on:pointerdown={startDrawing}
|
||||
on:pointermove={continueDrawing}
|
||||
on:pointerup={stopDrawing}
|
||||
on:pointerleave={stopDrawing}
|
||||
on:pointercancel={stopDrawing}
|
||||
></canvas>
|
||||
{#if !value}
|
||||
<div class="signature-hint" aria-hidden="true">Sign here</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.signature-shell {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 180px;
|
||||
border-radius: 18px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.signature-shell.signature-disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.signature-canvas {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
touch-action: none;
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.signature-disabled .signature-canvas {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.signature-hint {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-head);
|
||||
font-size: 24px;
|
||||
color: rgba(33, 48, 33, 0.22);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.signature-shell {
|
||||
min-height: 160px;
|
||||
}
|
||||
|
||||
.signature-canvas {
|
||||
height: 160px;
|
||||
}
|
||||
|
||||
.signature-hint {
|
||||
font-size: 21px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -192,7 +192,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<a class="btn btn-yellow pricing-section-mobile-cta" href="#newlead">
|
||||
<a class="btn btn-yellow btn-mobile-center pricing-section-mobile-cta" href="#newlead">
|
||||
Book a Meet & Greet
|
||||
</a>
|
||||
|
||||
@@ -205,7 +205,7 @@
|
||||
<p>
|
||||
Book a free Meet & Greet and we’ll 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">
|
||||
<a class="btn btn-outline btn-outline-green btn-mobile-center pricing-mobile-consult-cta" href="#newlead">
|
||||
Talk it through with us
|
||||
</a>
|
||||
</aside>
|
||||
@@ -214,7 +214,11 @@
|
||||
</section>
|
||||
{/each}
|
||||
|
||||
<TestimonialsSection heading={pageContent.testimonialsHeading} testimonials={content.testimonials} />
|
||||
<TestimonialsSection
|
||||
heading={pageContent.testimonialsHeading}
|
||||
testimonials={content.testimonials}
|
||||
seedKey="/our-pricing"
|
||||
/>
|
||||
<BookingSection booking={pageContent.booking} />
|
||||
|
||||
{#if showMeetGreetPrompt}
|
||||
@@ -281,8 +285,11 @@
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
color: #fff;
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.01em;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
background 0.2s ease,
|
||||
@@ -308,10 +315,6 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.pricing-trust-label {
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
:global(.pricing-trust .pricing-trust-arrow) {
|
||||
font-size: 12px;
|
||||
opacity: 0.85;
|
||||
@@ -322,8 +325,10 @@
|
||||
text-align: center;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(24px, 2.8vw, 36px);
|
||||
line-height: 1.1;
|
||||
font-weight: 700;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
text-wrap: balance;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
@@ -658,7 +663,7 @@
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.pricing-plan-popular {
|
||||
.pricing-plan-card {
|
||||
order: var(--mobile-order, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
<script lang="ts">
|
||||
import { getImageMetadata } from '$lib/image-metadata';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import type { PromiseContent } from '$lib/types';
|
||||
|
||||
export let promise: PromiseContent;
|
||||
|
||||
$: promiseImage = getImageMetadata(promise.imageUrl);
|
||||
$: promiseEnhanced = getEnhancedImage(promise.imageUrl);
|
||||
</script>
|
||||
|
||||
<section id="promise">
|
||||
<div class="promise-inner">
|
||||
<div class="promise-text">
|
||||
<h2>
|
||||
{promise.title}<br />
|
||||
{promise.subtitle}
|
||||
<h2 class="promise-heading">
|
||||
<span class="promise-heading-desktop">
|
||||
<span class="promise-title-main">{promise.title}</span>
|
||||
<br />
|
||||
<span class="promise-title-highlight">{promise.subtitle}</span>
|
||||
</span>
|
||||
<span class="promise-heading-mobile">
|
||||
<span class="promise-title-main">{promise.title}</span>
|
||||
<span class="promise-title-highlight">{promise.subtitle}</span>
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
{#each promise.body as paragraph, idx}
|
||||
@@ -29,15 +36,88 @@
|
||||
|
||||
<div class="promise-img">
|
||||
<div class="promise-img-frame">
|
||||
<img
|
||||
src={promise.imageUrl}
|
||||
alt={promise.imageAlt}
|
||||
width={promiseImage?.width}
|
||||
height={promiseImage?.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{#if promiseEnhanced}
|
||||
<enhanced:img
|
||||
src={promiseEnhanced}
|
||||
alt={promise.imageAlt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{:else}
|
||||
<img src={promise.imageUrl} alt={promise.imageAlt} loading="lazy" decoding="async" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.promise-heading-desktop {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.promise-heading-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.promise-heading-mobile .promise-title-main,
|
||||
.promise-heading-mobile .promise-title-highlight {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.promise-title-main {
|
||||
color: #0d1a0d;
|
||||
}
|
||||
|
||||
.promise-title-highlight {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
color: #0d1a0d;
|
||||
}
|
||||
|
||||
.promise-title-highlight::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: -6px;
|
||||
bottom: -16px;
|
||||
height: 24px;
|
||||
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 320 34' fill='none'%3E%3Cpath d='M4 24C67 10 131 4 198 5c43 1 82 6 118 18' stroke='%23192419' stroke-width='8' stroke-linecap='round'/%3E%3C/svg%3E")
|
||||
center/contain no-repeat;
|
||||
transform-origin: left center;
|
||||
animation: promise-underline-draw 0.9s cubic-bezier(0.22, 1, 0.36, 1) 0.2s both;
|
||||
}
|
||||
|
||||
@keyframes promise-underline-draw {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scaleX(0.2) translateY(6px) rotate(-1.5deg);
|
||||
}
|
||||
|
||||
65% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1.04) translateY(0) rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scaleX(1) translateY(0) rotate(0deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.promise-heading-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.promise-heading-mobile {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.promise-title-highlight::after {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import BookingSection from '$lib/components/BookingSection.svelte';
|
||||
import TestimonialsSection from '$lib/components/TestimonialsSection.svelte';
|
||||
import { getImageMetadata } from '$lib/image-metadata';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import type { ServicePageContent, SiteSharedContent } from '$lib/types';
|
||||
|
||||
export let content: SiteSharedContent;
|
||||
@@ -30,12 +30,12 @@
|
||||
}));
|
||||
}
|
||||
|
||||
$: heroImage = getImageMetadata(pageContent.hero.imageUrl);
|
||||
$: highlightImage = pageContent.highlight ? getImageMetadata(pageContent.highlight.imageUrl) : null;
|
||||
$: heroEnhanced = getEnhancedImage(pageContent.hero.imageUrl);
|
||||
$: highlightEnhanced = pageContent.highlight ? getEnhancedImage(pageContent.highlight.imageUrl) : null;
|
||||
$: highlightCollageImages =
|
||||
pageContent.highlight?.collageImages?.map((image) => ({
|
||||
...image,
|
||||
meta: getImageMetadata(image.imageUrl)
|
||||
enhanced: getEnhancedImage(image.imageUrl)
|
||||
})) ?? [];
|
||||
$: relatedServices = content.services.filter((s) => s.href && s.href !== currentPath);
|
||||
$: pricingPlans = decoratePlans(pageContent.pricing.plans);
|
||||
@@ -73,15 +73,23 @@
|
||||
</div>
|
||||
|
||||
<div class="service-hero-media">
|
||||
<img
|
||||
src={pageContent.hero.imageUrl}
|
||||
alt={pageContent.hero.imageAlt}
|
||||
width={heroImage?.width}
|
||||
height={heroImage?.height}
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
/>
|
||||
{#if heroEnhanced}
|
||||
<enhanced:img
|
||||
src={heroEnhanced}
|
||||
alt={pageContent.hero.imageAlt}
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
src={pageContent.hero.imageUrl}
|
||||
alt={pageContent.hero.imageAlt}
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
decoding="async"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -98,27 +106,31 @@
|
||||
<div class="service-highlight-collage" aria-label={pageContent.highlight.title}>
|
||||
{#each highlightCollageImages as image, index}
|
||||
<figure class={`service-collage-card service-collage-card-${index + 1}`}>
|
||||
<img
|
||||
src={image.imageUrl}
|
||||
alt={image.imageAlt}
|
||||
width={image.meta?.width}
|
||||
height={image.meta?.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{#if image.enhanced}
|
||||
<enhanced:img src={image.enhanced} alt={image.imageAlt} loading="lazy" decoding="async" />
|
||||
{:else}
|
||||
<img src={image.imageUrl} alt={image.imageAlt} loading="lazy" decoding="async" />
|
||||
{/if}
|
||||
</figure>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="service-highlight-image">
|
||||
<img
|
||||
src={pageContent.highlight.imageUrl}
|
||||
alt={pageContent.highlight.imageAlt}
|
||||
width={highlightImage?.width}
|
||||
height={highlightImage?.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{#if highlightEnhanced}
|
||||
<enhanced:img
|
||||
src={highlightEnhanced}
|
||||
alt={pageContent.highlight.imageAlt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
src={pageContent.highlight.imageUrl}
|
||||
alt={pageContent.highlight.imageAlt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -172,7 +184,7 @@
|
||||
Every booking starts with a free, no-obligation Meet & Greet.
|
||||
</p>
|
||||
|
||||
<a class="btn btn-yellow service-plan-mobile-cta" href="#newlead">Book a Meet & Greet</a>
|
||||
<a class="btn btn-yellow btn-mobile-center service-plan-mobile-cta" href="#newlead">Book a Meet & Greet</a>
|
||||
|
||||
{#if pageContent.pricing.extras?.length}
|
||||
<div class="service-extras">
|
||||
@@ -246,7 +258,11 @@
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<TestimonialsSection heading={pageContent.testimonialsHeading} testimonials={content.testimonials} />
|
||||
<TestimonialsSection
|
||||
heading={pageContent.testimonialsHeading}
|
||||
testimonials={content.testimonials}
|
||||
seedKey={currentPath}
|
||||
/>
|
||||
<BookingSection booking={pageContent.booking} />
|
||||
</main>
|
||||
|
||||
@@ -276,67 +292,26 @@
|
||||
}
|
||||
|
||||
.service-related-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
padding: 28px 26px;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
gap: 0;
|
||||
padding: 34px 28px 30px;
|
||||
border-radius: 28px;
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
|
||||
background: var(--off-white);
|
||||
box-shadow: 0 10px 28px rgba(17, 20, 24, 0.05);
|
||||
color: #000;
|
||||
text-decoration: none;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
transform 0.18s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.22s ease;
|
||||
}
|
||||
|
||||
.service-related-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0 0 auto 0;
|
||||
height: 4px;
|
||||
background: var(--card-accent, var(--yellow));
|
||||
}
|
||||
|
||||
.service-related-tint-0 {
|
||||
--card-accent: var(--yellow);
|
||||
}
|
||||
.service-related-tint-0 .service-related-icon {
|
||||
background: #fff3c6;
|
||||
color: #5a4500;
|
||||
}
|
||||
|
||||
.service-related-tint-1 {
|
||||
--card-accent: var(--gw-green);
|
||||
}
|
||||
.service-related-tint-1 .service-related-icon {
|
||||
background: #dce6dc;
|
||||
color: var(--gw-green);
|
||||
}
|
||||
|
||||
.service-related-tint-2 {
|
||||
--card-accent: #c98a3f;
|
||||
}
|
||||
.service-related-tint-2 .service-related-icon {
|
||||
background: #efe4d1;
|
||||
color: var(--gw-green);
|
||||
}
|
||||
|
||||
.service-related-tint-3 {
|
||||
--card-accent: #9ca3af;
|
||||
}
|
||||
.service-related-tint-3 .service-related-icon {
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.service-related-card:hover {
|
||||
transform: translateY(-6px) scale(1.012);
|
||||
box-shadow: 0 20px 40px rgba(17, 20, 24, 0.09);
|
||||
box-shadow: 0 18px 38px rgba(17, 20, 24, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,19 +323,22 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background: #efe4d1;
|
||||
background: linear-gradient(180deg, #ffe173 0%, #ffd54a 100%);
|
||||
color: var(--gw-green);
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
font-size: 28px;
|
||||
margin-bottom: 22px;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.05),
|
||||
0 10px 24px rgba(17, 20, 24, 0.08);
|
||||
}
|
||||
|
||||
.service-related-card h3 {
|
||||
margin: 0;
|
||||
margin: 0 0 10px;
|
||||
font-family: var(--font-head);
|
||||
font-size: 22px;
|
||||
font-size: 20px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@@ -368,37 +346,44 @@
|
||||
margin: 0;
|
||||
color: #34363a;
|
||||
font-size: 15px;
|
||||
line-height: 1.55;
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
.service-related-meta {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.service-related-price {
|
||||
display: inline-block;
|
||||
padding: 6px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.06);
|
||||
font-family: var(--font-head);
|
||||
font-weight: 700;
|
||||
color: var(--gw-green);
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.service-related-pill {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
padding: 6px 14px;
|
||||
border-radius: 999px;
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
background: rgba(33, 48, 33, 0.06);
|
||||
color: var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.service-related-link {
|
||||
margin-top: auto;
|
||||
padding-top: 6px;
|
||||
margin-top: 18px;
|
||||
color: var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-weight: 700;
|
||||
@@ -418,9 +403,20 @@
|
||||
margin: 0;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(34px, 4vw, 56px);
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.service-hero-copy h1 {
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.04em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.service-section-heading h2,
|
||||
.service-highlight-copy h2 {
|
||||
font-weight: 700;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.03em;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.service-hero-copy p,
|
||||
@@ -834,7 +830,7 @@
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.service-plan-popular {
|
||||
.service-plan-card {
|
||||
order: var(--mobile-order, 0);
|
||||
}
|
||||
|
||||
|
||||
@@ -148,4 +148,5 @@
|
||||
text-underline-offset: 0.18em;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
import { onMount } from 'svelte';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import { getImageMetadata } from '$lib/image-metadata';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import { getSeededTestimonialIndex } from '$lib/testimonials';
|
||||
import type { TestimonialContent } from '$lib/types';
|
||||
|
||||
export let testimonials: TestimonialContent[];
|
||||
@@ -11,6 +12,7 @@
|
||||
export let blurb = 'Peace of mind for busy Auckland dog owners. Happier dogs, smoother routines, and a team owners trust with the important stuff.';
|
||||
export let instagramHref = 'https://www.instagram.com/goodwalk.nz/';
|
||||
export let instagramLabel = 'goodwalk.nz';
|
||||
export let seedKey = '';
|
||||
|
||||
type TestimonialSlide = TestimonialContent & { imageUrl: string };
|
||||
|
||||
@@ -50,6 +52,7 @@
|
||||
let inView = false;
|
||||
let prefersReducedMotion = false;
|
||||
let carouselEl: HTMLDivElement | undefined;
|
||||
let slideSignature = '';
|
||||
|
||||
$: slides = testimonials
|
||||
.map((testimonial) => wordpressTestimonials[testimonial.reviewer] ?? testimonial)
|
||||
@@ -59,6 +62,15 @@
|
||||
activeIndex = 0;
|
||||
}
|
||||
|
||||
$: {
|
||||
const nextSignature = `${seedKey}:${slides.map((slide) => slide.reviewer).join('|')}`;
|
||||
|
||||
if (nextSignature !== slideSignature) {
|
||||
slideSignature = nextSignature;
|
||||
activeIndex = getSeededTestimonialIndex(slides, seedKey);
|
||||
}
|
||||
}
|
||||
|
||||
function dogNameFromDetail(detail: string) {
|
||||
const match = detail.match(/^([^'’]+)/);
|
||||
return match ? match[1].trim() : '';
|
||||
@@ -163,16 +175,24 @@
|
||||
<div class="testimonial-photo-wrap">
|
||||
<div class="testimonial-photo-frame">
|
||||
{#if index === activeIndex}
|
||||
{@const imageMeta = getImageMetadata(testimonial.imageUrl)}
|
||||
<img
|
||||
class="testimonial-photo"
|
||||
src={testimonial.imageUrl}
|
||||
alt={testimonialAlt(testimonial)}
|
||||
width={imageMeta?.width}
|
||||
height={imageMeta?.height}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{@const enhancedPhoto = getEnhancedImage(testimonial.imageUrl)}
|
||||
{#if enhancedPhoto}
|
||||
<enhanced:img
|
||||
class="testimonial-photo"
|
||||
src={enhancedPhoto}
|
||||
alt={testimonialAlt(testimonial)}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
class="testimonial-photo"
|
||||
src={testimonial.imageUrl}
|
||||
alt={testimonialAlt(testimonial)}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
@@ -489,8 +509,11 @@
|
||||
border-radius: 999px;
|
||||
background: #f8f8f8;
|
||||
color: #0a304e;
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
line-height: 1.3;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.01em;
|
||||
box-shadow: 0 0 0 1px rgba(10, 48, 78, 0.06);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import { homepageContent } from '$lib/content/homepage';
|
||||
import type { TestimonialContent } from '$lib/types';
|
||||
|
||||
const expectedMappedSlides = [
|
||||
{ reviewer: 'Kate', src: '/images/archie-auckland-dog-walking-review.png' },
|
||||
{ reviewer: 'Estelle', src: '/images/monty-auckland-dog-walking-review.png' },
|
||||
{ reviewer: 'Ross', src: '/images/otis-auckland-dog-walking-review.png' },
|
||||
{ reviewer: 'Nina', src: '/images/wallace-auckland-dog-walking-review.png' }
|
||||
{ reviewer: 'Kate' },
|
||||
{ reviewer: 'Estelle' },
|
||||
{ reviewer: 'Ross' },
|
||||
{ reviewer: 'Nina' }
|
||||
];
|
||||
|
||||
function getActiveSlide(container: HTMLElement) {
|
||||
@@ -23,6 +23,14 @@ function getActiveImage(container: HTMLElement) {
|
||||
return getActiveSlide(container).querySelector('img') as HTMLImageElement;
|
||||
}
|
||||
|
||||
function getNextButton(container: HTMLElement) {
|
||||
return container.querySelector('.testimonial-arrow-right') as HTMLButtonElement;
|
||||
}
|
||||
|
||||
function getPreviousButton(container: HTMLElement) {
|
||||
return container.querySelector('.testimonial-arrow-left') as HTMLButtonElement;
|
||||
}
|
||||
|
||||
describe('TestimonialsSection', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
@@ -33,11 +41,11 @@ describe('TestimonialsSection', () => {
|
||||
testimonials: homepageContent.testimonials
|
||||
});
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: /next testimonial/i });
|
||||
const nextButton = getNextButton(container);
|
||||
|
||||
for (const [index, slide] of expectedMappedSlides.entries()) {
|
||||
expect(getActiveReviewer(container)).toBe(slide.reviewer);
|
||||
expect(getActiveImage(container).getAttribute('src')).toBe(slide.src);
|
||||
expect(getActiveImage(container)).toBeTruthy();
|
||||
|
||||
if (index < expectedMappedSlides.length - 1) {
|
||||
await fireEvent.click(nextButton);
|
||||
@@ -52,7 +60,7 @@ describe('TestimonialsSection', () => {
|
||||
testimonials: homepageContent.testimonials
|
||||
});
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: /next testimonial/i });
|
||||
const nextButton = getNextButton(container);
|
||||
|
||||
expect(getActiveReviewer(container)).toBe('Kate');
|
||||
|
||||
@@ -68,16 +76,14 @@ describe('TestimonialsSection', () => {
|
||||
testimonials: homepageContent.testimonials
|
||||
});
|
||||
|
||||
const previousButton = screen.getByRole('button', { name: /previous testimonial/i });
|
||||
const previousButton = getPreviousButton(container);
|
||||
|
||||
expect(getActiveReviewer(container)).toBe('Kate');
|
||||
|
||||
await fireEvent.click(previousButton);
|
||||
|
||||
expect(getActiveReviewer(container)).toBe('Nina');
|
||||
expect(getActiveImage(container).getAttribute('src')).toBe(
|
||||
'/images/wallace-auckland-dog-walking-review.png'
|
||||
);
|
||||
expect(getActiveImage(container)).toBeTruthy();
|
||||
});
|
||||
|
||||
it('keeps custom testimonial images and filters out testimonials with no image', async () => {
|
||||
@@ -100,7 +106,7 @@ describe('TestimonialsSection', () => {
|
||||
testimonials: customTestimonials
|
||||
});
|
||||
|
||||
const nextButton = screen.getByRole('button', { name: /next testimonial/i });
|
||||
const nextButton = getNextButton(container);
|
||||
|
||||
expect(container.querySelectorAll('.testimonial-slide')).toHaveLength(5);
|
||||
|
||||
@@ -109,7 +115,16 @@ describe('TestimonialsSection', () => {
|
||||
}
|
||||
|
||||
expect(getActiveReviewer(container)).toBe('Casey');
|
||||
expect(getActiveImage(container).getAttribute('src')).toBe('/images/custom-casey-review.png');
|
||||
expect(getActiveImage(container)).toBeTruthy();
|
||||
expect(screen.queryByText('Jordan')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can start on a different testimonial for a different page seed', () => {
|
||||
const { container } = render(TestimonialsSection, {
|
||||
testimonials: homepageContent.testimonials,
|
||||
seedKey: '/dog-walking'
|
||||
});
|
||||
|
||||
expect(getActiveReviewer(container)).not.toBe('Kate');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,9 +30,13 @@
|
||||
<div class="values-grid">
|
||||
{#each orderedValues as value}
|
||||
<div class="value-card">
|
||||
<Icon name={value.icon} className="value-card-icon" />
|
||||
<h3>{value.title}</h3>
|
||||
<p>{value.body}</p>
|
||||
<div class="value-icon-wrap">
|
||||
<Icon name={value.icon} className="value-card-icon" />
|
||||
</div>
|
||||
<div class="value-text">
|
||||
<h3>{value.title}</h3>
|
||||
<p>{value.body}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user