v4.0.5
Component architecture - Reorganise src/lib/components into pages/, sections/, ui/ subdirectories and update all imports across routes and tests. Owner admin dashboard (+2k lines) - Scheduled welcome-pack emails: durable queue with cancel/reschedule and a background sender loop (SCHEDULED_CHECK_INTERVAL_SECONDS, default 60s). - Custom welcome-pack subject line, preview recipients, and client BCC. - Add-client, edit client profile, and reset-onboarding flows. - "View as client" onboarding preview (owner impersonation, dry-run submit). - Tabbed owner welcome route (/owner/welcome/[[tab]]). MYOB integration - New mail_api/myob.py: create new clients as MYOB AccountRight customer contacts. Disabled until all MYOB_* env vars are set (no-op otherwise). Onboarding - New "Does your dog resource guard?" Yes/No question in the Behaviour step. - Persist vetAddress, flea/tick, and pet-insurance fields server-side. Misc - vite config, new hooks.ts, responsive CSS tweaks, deploy/docker config, mail-api README and start-dev.ps1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,585 @@
|
||||
<script lang="ts">
|
||||
import { accordion } from '$lib/actions/accordion';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import CtaCard from '$lib/components/ui/CtaCard.svelte';
|
||||
import Icon from '$lib/components/ui/Icon.svelte';
|
||||
import ServiceHero from '$lib/components/sections/ServiceHero.svelte';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import type { AboutPageContent } from '$lib/types';
|
||||
|
||||
export let pageContent: AboutPageContent;
|
||||
|
||||
$: standardSections = pageContent.sections.filter((s) => s.accent !== 'founder');
|
||||
$: founderSection = pageContent.sections.find((s) => s.accent === 'founder') ?? null;
|
||||
const heroChips = [
|
||||
{ icon: 'fas fa-star', label: '30+ five-star Google reviews' },
|
||||
{ icon: 'fas fa-location-dot', label: 'Auckland Central' },
|
||||
{ icon: 'fas fa-paw', label: 'Small dog specialists' }
|
||||
];
|
||||
const founderHeadingLead = 'Meet Aless,';
|
||||
const founderHeadingHighlight = 'the heart of Goodwalk';
|
||||
</script>
|
||||
|
||||
<main class="about-page">
|
||||
<ServiceHero
|
||||
eyebrow="About Goodwalk"
|
||||
title={pageContent.title}
|
||||
subtitle="Small dog specialists serving Auckland Central. A team your dog knows by name."
|
||||
imageUrl="/images/about-good-walk.webp"
|
||||
imageAlt="Goodwalk dogs gathered together in the back of the car before a walk"
|
||||
chips={heroChips}
|
||||
cta={{ label: 'Book a Meet & Greet', href: '/contact-us', variant: 'yellow' }}
|
||||
/>
|
||||
|
||||
<!-- ── Standard sections (Who we are, Our impact) ── -->
|
||||
{#each standardSections as section}
|
||||
{@const enhanced = getEnhancedImage(section.imageUrl)}
|
||||
<section
|
||||
use:reveal={{ distance: 0 }}
|
||||
class="about-section reveal-block"
|
||||
class:about-section-gradient={section.accent === 'gradient'}
|
||||
>
|
||||
<div class="page-inner about-section-grid" class:about-section-reverse={section.reverse}>
|
||||
<div class="about-copy fade-up">
|
||||
{#if section.eyebrow}
|
||||
<span class="eyebrow about-eyebrow">{section.eyebrow}</span>
|
||||
{/if}
|
||||
<h2>{section.title}</h2>
|
||||
{#each section.body as paragraph}
|
||||
<p>{paragraph}</p>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="about-media scale-soft">
|
||||
{#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}
|
||||
|
||||
<!-- ── Founder section ── -->
|
||||
{#if founderSection}
|
||||
{@const founderEnhanced = getEnhancedImage(founderSection.imageUrl)}
|
||||
<section use:reveal={{ delay: 50, distance: 0 }} class="about-founder reveal-block">
|
||||
<div class="page-inner about-founder-grid">
|
||||
<div class="about-founder-media scale-soft">
|
||||
{#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 fade-up">
|
||||
<article class="about-founder-note">
|
||||
{#if founderSection.eyebrow}
|
||||
<span class="eyebrow about-eyebrow about-founder-kicker">{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>
|
||||
<div class="about-founder-body">
|
||||
{#each founderSection.body as paragraph}
|
||||
<p>{paragraph}</p>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="about-founder-signoff">
|
||||
<div class="about-founder-signoff-text">
|
||||
<span class="about-founder-name">Aless</span>
|
||||
<span class="about-founder-role">Goodwalk founder</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
class="about-founder-contact-note"
|
||||
href="mailto:info@goodwalk.co.nz"
|
||||
aria-label="Email Aless at Goodwalk"
|
||||
>
|
||||
<span class="about-founder-contact-icon">
|
||||
<Icon name="fas fa-envelope" />
|
||||
</span>
|
||||
<span>If you are unsure about anything, feel free to email me anytime.</span>
|
||||
</a>
|
||||
|
||||
<a href="/contact-us" class="btn btn-green btn-mobile-center about-founder-cta cta-shimmer">
|
||||
Book a free Meet & Greet
|
||||
</a>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<!-- ── FAQs ── -->
|
||||
{#if pageContent.faqs && pageContent.faqs.length}
|
||||
<section use:reveal={{ delay: 30, distance: 0 }} class="about-faq reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class="about-faq-header fade-up">
|
||||
<span class="eyebrow about-eyebrow">FAQ</span>
|
||||
<h2>{pageContent.faqTitle ?? 'Common questions'}</h2>
|
||||
</div>
|
||||
<div use:accordion class="faq about-faq-list fade-up">
|
||||
{#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="page-inner">
|
||||
<CtaCard
|
||||
title={pageContent.contact.title}
|
||||
description="Questions, pricing, or a first Meet & Greet. Email, call, or send an Instagram DM. We'll reply within 24 hours."
|
||||
ctaHref={pageContent.contact.cta.href}
|
||||
ctaLabel={pageContent.contact.cta.label}
|
||||
email={pageContent.contact.email}
|
||||
phone={pageContent.contact.phone}
|
||||
instagramHref="https://www.instagram.com/goodwalk.nz/"
|
||||
contactNote="Email, call, or send an Instagram DM. We want to be easy to reach in the way that suits you best."
|
||||
showIcons={true}
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.about-page {
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
/* ── Eyebrow ── */
|
||||
.about-eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
/* ── Standard sections ── */
|
||||
.about-section {
|
||||
padding: 88px 0;
|
||||
}
|
||||
|
||||
.about-section-gradient {
|
||||
background: linear-gradient(180deg, #f5efe6 0%, #f9f6ef 100%);
|
||||
}
|
||||
|
||||
.about-section-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 60px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.about-section-reverse .about-copy {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.about-section-reverse .about-media {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.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: 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%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
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-note {
|
||||
padding: 40px 42px 34px;
|
||||
background: linear-gradient(180deg, rgba(251, 251, 251, 0.98) 0%, rgba(247, 248, 246, 1) 100%);
|
||||
border: 1px solid rgba(17, 20, 24, 0.08);
|
||||
border-radius: 28px;
|
||||
box-shadow: var(--shadow-panel-elevated);
|
||||
}
|
||||
|
||||
.about-founder-kicker {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.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: 0;
|
||||
color: #34363a;
|
||||
font-size: 17px;
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
.about-founder-body {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.about-founder-signoff {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-top: 28px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid rgba(17, 20, 24, 0.08);
|
||||
}
|
||||
|
||||
.about-founder-signoff-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.about-founder-name {
|
||||
color: #0d1a0d;
|
||||
font-family: var(--font-head);
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.about-founder-role {
|
||||
color: var(--gray);
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.about-founder-contact-note {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 20px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 18px;
|
||||
background: rgba(33, 48, 33, 0.05);
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.06);
|
||||
color: var(--gw-green);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
text-decoration: none;
|
||||
transition: background 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease;
|
||||
}
|
||||
|
||||
.about-founder-contact-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.07);
|
||||
color: var(--gw-green);
|
||||
font-size: 13px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.about-founder-cta {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
margin: 28px 0 0;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.about-founder-contact-note:hover {
|
||||
background: rgba(33, 48, 33, 0.08);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.08),
|
||||
0 10px 22px rgba(17, 20, 24, 0.05);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 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;
|
||||
}
|
||||
|
||||
/* ── Tablet ── */
|
||||
@media (max-width: 1024px) {
|
||||
.about-section-grid,
|
||||
.about-founder-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 36px;
|
||||
}
|
||||
|
||||
.about-section-reverse .about-copy,
|
||||
.about-section-reverse .about-media {
|
||||
order: initial;
|
||||
}
|
||||
|
||||
.about-founder-media img {
|
||||
max-width: 420px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mobile ── */
|
||||
@media (max-width: 768px) {
|
||||
.about-section {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.about-eyebrow {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about-section-grid {
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.about-copy h2 {
|
||||
font-size: 28px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about-copy p {
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.about-founder {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.about-founder-grid {
|
||||
gap: 28px;
|
||||
}
|
||||
|
||||
.about-founder-note {
|
||||
padding: 26px 24px 24px;
|
||||
border-radius: 24px;
|
||||
}
|
||||
|
||||
.about-founder-copy h2 {
|
||||
font-size: 26px;
|
||||
line-height: 1.02;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about-founder-heading-desktop {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.about-founder-heading-mobile {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.about-founder-copy p {
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.about-founder-kicker {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.about-founder-contact-note {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
padding: 11px 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.about-founder-cta {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.about-faq {
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.about-contact {
|
||||
padding-bottom: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.about-founder-title-highlight::after {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Per-section choreography — copy leads, then the media settles after a
|
||||
short beat. Repeated across each about-section instance. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:global(.reveal-visible) .about-copy { transition-delay: 40ms; }
|
||||
:global(.reveal-visible) .about-media { transition-delay: 200ms; }
|
||||
:global(.reveal-visible) .about-founder-media { transition-delay: 60ms; }
|
||||
:global(.reveal-visible) .about-founder-copy { transition-delay: 220ms; }
|
||||
:global(.reveal-visible) .about-faq-header { transition-delay: 40ms; }
|
||||
:global(.reveal-visible) .about-faq-list { transition-delay: 180ms; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<script lang="ts">
|
||||
import BookingWizard from '$lib/components/sections/BookingWizard.svelte';
|
||||
import Icon from '$lib/components/ui/Icon.svelte';
|
||||
import InfoSection from '$lib/components/sections/InfoSection.svelte';
|
||||
import ServiceHero from '$lib/components/sections/ServiceHero.svelte';
|
||||
import type { BookingContent, InfoContent } from '$lib/types';
|
||||
|
||||
export let booking: BookingContent;
|
||||
export let info: InfoContent;
|
||||
// General-enquiry mode disabled site-wide for now. Accepted as a prop so
|
||||
// existing callers keep type-checking, but the value is intentionally ignored.
|
||||
export let allowGeneralEnquiry = false;
|
||||
void allowGeneralEnquiry;
|
||||
|
||||
const email = 'info@goodwalk.co.nz';
|
||||
const phone = '(022) 642 1011';
|
||||
const phoneHref = `tel:${phone.replace(/[^0-9+]/g, '')}`;
|
||||
</script>
|
||||
|
||||
<main class="booking-page">
|
||||
<ServiceHero
|
||||
eyebrow="Contact Goodwalk"
|
||||
title="Contact Us"
|
||||
subtitle="Tell us a little about your dog and we'll be in touch within 24 hours to arrange a free Meet & Greet."
|
||||
imageUrl="/images/happy-customer-anna.webp"
|
||||
imageAlt="Happy Goodwalk customer Anna with her dog in Auckland"
|
||||
chips={[
|
||||
{ icon: 'fas fa-bolt', label: 'Reply within 24 hours' },
|
||||
{ icon: 'fas fa-handshake', label: 'Free Meet & Greet' },
|
||||
{ icon: 'fas fa-location-dot', label: 'Auckland Central' }
|
||||
]}
|
||||
cta={{ label: 'Start your enquiry', href: '#newlead', variant: 'yellow' }}
|
||||
/>
|
||||
|
||||
<div class="booking-page-contact-strip">
|
||||
<div class="page-inner booking-page-contact">
|
||||
<a href="mailto:{email}" class="booking-contact-link">
|
||||
<Icon name="fas fa-envelope" />
|
||||
{email}
|
||||
</a>
|
||||
<a href={phoneHref} class="booking-contact-link">
|
||||
<Icon name="fas fa-phone" />
|
||||
{phone}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BookingWizard {booking} pagePath="/contact-us" />
|
||||
<InfoSection {info} />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.booking-page {
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
.booking-page-contact-strip {
|
||||
padding: 18px 0 0;
|
||||
}
|
||||
|
||||
.booking-page-contact {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.booking-contact-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
border: 1px solid rgba(17, 20, 24, 0.08);
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
letter-spacing: 0.01em;
|
||||
color: var(--text-heading);
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.booking-contact-link:hover {
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.booking-page-contact {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.booking-contact-link {
|
||||
font-size: 13px;
|
||||
padding: 9px 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,189 @@
|
||||
<script lang="ts">
|
||||
import type { LegalPageBlock, LegalPageContent } from '$lib/types';
|
||||
|
||||
export let pageContent: LegalPageContent;
|
||||
|
||||
function isParagraph(block: LegalPageBlock): boolean {
|
||||
return block.type === 'paragraph';
|
||||
}
|
||||
|
||||
function getParagraphContent(block: LegalPageBlock): string {
|
||||
return typeof block.content === 'string' ? block.content : block.content.join(' ');
|
||||
}
|
||||
|
||||
function getListItems(block: LegalPageBlock): string[] {
|
||||
return Array.isArray(block.content) ? block.content : [block.content];
|
||||
}
|
||||
|
||||
function isSubItem(item: string): boolean {
|
||||
return /^\([a-z]\)/.test(item.trimStart());
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="legal-page">
|
||||
<section class="legal-hero">
|
||||
<div class="legal-inner">
|
||||
<h1>{pageContent.title}</h1>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="legal-body">
|
||||
<div class="legal-inner">
|
||||
<div class="legal-card">
|
||||
{#each pageContent.sections as section}
|
||||
<section class="legal-section">
|
||||
<h2>{section.title}</h2>
|
||||
|
||||
{#each section.blocks as block}
|
||||
{#if isParagraph(block)}
|
||||
<p>{getParagraphContent(block)}</p>
|
||||
{:else}
|
||||
<ul class="legal-list">
|
||||
{#each getListItems(block) as item}
|
||||
<li class:sub-item={isSubItem(item)}>{item}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{/each}
|
||||
</section>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.legal-page {
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
.legal-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 0 50px;
|
||||
}
|
||||
|
||||
.legal-hero {
|
||||
padding: 72px 0 28px;
|
||||
}
|
||||
|
||||
.legal-hero h1 {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(34px, 4vw, 56px);
|
||||
line-height: 1.05;
|
||||
letter-spacing: -0.04em;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.legal-body {
|
||||
padding: 0 0 88px;
|
||||
}
|
||||
|
||||
.legal-card {
|
||||
padding: 40px 44px;
|
||||
border-radius: 28px;
|
||||
background: #fff;
|
||||
box-shadow: 0 14px 34px rgba(17, 20, 24, 0.05);
|
||||
}
|
||||
|
||||
.legal-section {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.legal-section + .legal-section {
|
||||
margin-top: 34px;
|
||||
padding-top: 34px;
|
||||
border-top: 1px solid rgba(17, 20, 24, 0.08);
|
||||
}
|
||||
|
||||
.legal-section h2 {
|
||||
margin: 0 0 16px;
|
||||
padding-left: 14px;
|
||||
border-left: 3px solid var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(14px, 1.4vw, 17px);
|
||||
font-weight: 700;
|
||||
line-height: 1.08;
|
||||
letter-spacing: -0.02em;
|
||||
text-wrap: balance;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.legal-section p {
|
||||
margin: 16px 0 0;
|
||||
color: #34363a;
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.legal-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.legal-list + .legal-list,
|
||||
.legal-section p + .legal-list,
|
||||
.legal-list + p {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.legal-list li {
|
||||
position: relative;
|
||||
padding-left: 18px;
|
||||
color: #34363a;
|
||||
font-size: 16px;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.legal-list li::before {
|
||||
content: '–';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--gw-green);
|
||||
font-size: 14px;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.legal-list li.sub-item {
|
||||
padding-left: 34px;
|
||||
}
|
||||
|
||||
.legal-list li + li {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.legal-inner {
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.legal-hero {
|
||||
padding: 56px 0 20px;
|
||||
}
|
||||
|
||||
.legal-hero h1 {
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
.legal-body {
|
||||
padding: 0 0 64px;
|
||||
}
|
||||
|
||||
.legal-card {
|
||||
padding: 28px 22px;
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.legal-section + .legal-section {
|
||||
margin-top: 26px;
|
||||
padding-top: 26px;
|
||||
}
|
||||
|
||||
.legal-section h2 {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,773 @@
|
||||
<script lang="ts">
|
||||
import { sharedServices } from '$lib/content/services';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import CtaCard from '$lib/components/ui/CtaCard.svelte';
|
||||
import Icon from '$lib/components/ui/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, solo 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="page-inner">
|
||||
<span class="eyebrow eyebrow--accent 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="page-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="page-inner">
|
||||
<div class="loc-section-header">
|
||||
<span class="eyebrow 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="page-inner">
|
||||
<div class="loc-section-header">
|
||||
<span class="eyebrow 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="page-inner">
|
||||
<div class="loc-section-header">
|
||||
<span class="eyebrow loc-eyebrow">What we offer</span>
|
||||
<h2>Goodwalk services in {location.suburb}</h2>
|
||||
<p class="loc-section-intro">
|
||||
We offer pack walks, solo 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="page-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="page-inner">
|
||||
<CtaCard
|
||||
title="Ready to get started in {location.suburb}?"
|
||||
description="A free Meet & Greet is the first step. No commitment, no pressure. Email, call, or send an Instagram DM to talk through what your dog needs."
|
||||
ctaHref="/contact-us"
|
||||
ctaLabel="Book a free Meet & Greet"
|
||||
email="info@goodwalk.co.nz"
|
||||
phone="(022) 642 1011"
|
||||
phoneHref="tel:+64226421011"
|
||||
instagramHref="https://www.instagram.com/goodwalk.nz/"
|
||||
contactNote="Email, call, or send an Instagram DM. We want to be easy to reach wherever you already are."
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.loc-page {
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
/* ── Eyebrow ── */
|
||||
.loc-eyebrow,
|
||||
/* Layout-only overrides; typography lives on the shared .eyebrow utility,
|
||||
colour comes from .eyebrow / .eyebrow--accent. */
|
||||
.loc-hero-eyebrow,
|
||||
.loc-eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
/* ── 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;
|
||||
}
|
||||
|
||||
/* ── 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-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-eyebrow,
|
||||
.loc-hero-eyebrow {
|
||||
display: block;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loc-section-header h2,
|
||||
.loc-section-intro {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
import { fireEvent, render, screen, waitFor, within } from '@testing-library/svelte';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import OnboardingPage from './OnboardingPage.svelte';
|
||||
|
||||
function clickService(label: string) {
|
||||
const chip = screen.getByText(label).closest('label');
|
||||
if (!chip) throw new Error(`Could not find service chip: ${label}`);
|
||||
return fireEvent.click(chip);
|
||||
}
|
||||
|
||||
async function chooseOption(groupName: RegExp | string, option: 'Yes' | 'No') {
|
||||
const group = screen.getByRole('radiogroup', { name: groupName });
|
||||
await fireEvent.click(within(group).getByRole('radio', { name: option }));
|
||||
}
|
||||
|
||||
describe('OnboardingPage', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
window.sessionStorage.clear();
|
||||
window.localStorage.setItem('gw_onboarding_session', 'test-token');
|
||||
window.scrollTo = vi.fn();
|
||||
});
|
||||
|
||||
it('progresses from behaviour to sign in the 5-step flow', async () => {
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockImplementation((input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
|
||||
if (url === '/api/auth/verify') {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
email: 'alex@example.com',
|
||||
profile: {},
|
||||
draft: {}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (url === '/api/save-draft') {
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({})
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new Error(`Unhandled fetch: ${url}`));
|
||||
})
|
||||
);
|
||||
|
||||
render(OnboardingPage);
|
||||
|
||||
await waitFor(() => expect(screen.getByPlaceholderText('First name')).toBeInTheDocument());
|
||||
|
||||
await fireEvent.input(screen.getByPlaceholderText('First name'), { target: { value: 'Alex' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Last name'), { target: { value: 'Walker' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('you@example.com'), { target: { value: 'alex@example.com' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('021 234 5678'), { target: { value: '0212345678' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Street, suburb, city'), { target: { value: '1 Test Street' } });
|
||||
await fireEvent.click(screen.getByRole('button', { name: /save and continue/i }));
|
||||
|
||||
await waitFor(() => expect(screen.getByPlaceholderText('Dog name')).toBeInTheDocument());
|
||||
await fireEvent.input(screen.getByPlaceholderText('Dog name'), { target: { value: 'Milo' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('e.g. Labrador, Cavoodle, mixed'), { target: { value: 'Spoodle' } });
|
||||
await fireEvent.input(screen.getByLabelText(/date of birth/i), { target: { value: '2020-01-01' } });
|
||||
await clickService('Tiny Gang Pack Walks');
|
||||
await fireEvent.click(screen.getByRole('button', { name: /save and continue/i }));
|
||||
|
||||
await waitFor(() => expect(screen.getByPlaceholderText('Clinic name or your usual vet')).toBeInTheDocument());
|
||||
await fireEvent.input(screen.getByPlaceholderText('Clinic name or your usual vet'), { target: { value: 'Grey Lynn Vets' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Clinic phone number'), { target: { value: '099999999' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Clinic address'), { target: { value: '2 Vet Street' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Full name'), { target: { value: 'Jamie Walker' } });
|
||||
await fireEvent.input(screen.getByPlaceholderText('Phone number'), { target: { value: '0211111111' } });
|
||||
await chooseOption(/vaccinations up to date/i, 'Yes');
|
||||
await chooseOption(/^Any food allergies\?$/i, 'No');
|
||||
await chooseOption(/^Any environmental allergies\?$/i, 'No');
|
||||
await chooseOption(/^On a special diet\?$/i, 'No');
|
||||
await chooseOption(/medication that could affect walks/i, 'No');
|
||||
await fireEvent.click(screen.getByRole('button', { name: /save and continue/i }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText(/registered with council/i)).toBeInTheDocument());
|
||||
await chooseOption(/registered with council/i, 'Yes');
|
||||
await fireEvent.click(screen.getByRole('button', { name: /save and continue/i }));
|
||||
|
||||
await waitFor(() => expect(screen.getByText(/anything else we should know/i)).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,556 @@
|
||||
<script lang="ts">
|
||||
import BookingWizard from '$lib/components/sections/BookingWizard.svelte';
|
||||
import Icon from '$lib/components/ui/Icon.svelte';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import ServiceHero from '$lib/components/sections/ServiceHero.svelte';
|
||||
import type { SiteSharedContent, TestimonialContent } from '$lib/types';
|
||||
|
||||
export let content: SiteSharedContent;
|
||||
|
||||
$: testimonials = content.testimonials;
|
||||
$: testimonialCards = testimonials.map((testimonial) => ({
|
||||
...testimonial,
|
||||
enhanced: testimonial.imageUrl ? getEnhancedImage(testimonial.imageUrl) : undefined
|
||||
}));
|
||||
const heroImageUrl = '/images/our-client-testimonials.webp';
|
||||
const heroImageAlt = 'Goodwalk client dogs featured in testimonials';
|
||||
const heroChips = [
|
||||
{ icon: 'fas fa-star', label: '30+ five-star Google reviews' },
|
||||
{ icon: 'fas fa-paw', label: 'Tiny Gang regulars' },
|
||||
{ icon: 'fas fa-camera', label: 'Real client dogs' }
|
||||
];
|
||||
|
||||
function dogNameFromDetail(detail: string) {
|
||||
const match = detail.match(/^([^'’]+)/);
|
||||
return match ? match[1].trim() : '';
|
||||
}
|
||||
|
||||
function reviewerRole(testimonial: TestimonialContent) {
|
||||
const detail = testimonial.detail.trim();
|
||||
const reviewer = testimonial.reviewer.trim();
|
||||
const detailHasRelation = /(mum|dad|father|owner)/i.test(detail);
|
||||
const reviewerHasRelation = /(mum|dad|father|owner)/i.test(reviewer);
|
||||
|
||||
if (detailHasRelation) return detail;
|
||||
if (reviewerHasRelation) return reviewer;
|
||||
return 'Goodwalk client';
|
||||
}
|
||||
|
||||
function authorLine(testimonial: TestimonialContent) {
|
||||
const reviewer = testimonial.reviewer.trim();
|
||||
const role = reviewerRole(testimonial);
|
||||
return role === reviewer ? reviewer : `${reviewer} - ${role}`;
|
||||
}
|
||||
|
||||
function reviewAlt(testimonial: TestimonialContent) {
|
||||
const detailLead = dogNameFromDetail(testimonial.detail);
|
||||
return detailLead
|
||||
? `${detailLead}, a Goodwalk dog in Auckland`
|
||||
: `${testimonial.reviewer}'s dog with Goodwalk in Auckland`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="testimonials-page">
|
||||
<ServiceHero
|
||||
eyebrow="Goodwalk reviews"
|
||||
title="Client Testimonials"
|
||||
subtitle="Calmer dogs. Easier days. The kind of care owners stop worrying about once they have it."
|
||||
imageUrl={heroImageUrl}
|
||||
imageAlt={heroImageAlt}
|
||||
chips={heroChips}
|
||||
cta={{ label: 'Book a Meet & Greet', href: '#newlead', variant: 'yellow' }}
|
||||
/>
|
||||
|
||||
<section use:reveal={{ threshold: 0.02 }} class="testimonials-page-grid-section reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class="testimonials-page-grid">
|
||||
{#each testimonialCards as testimonial}
|
||||
<article class="testimonials-page-card">
|
||||
<div class="testimonials-page-card-media">
|
||||
{#if testimonial.imageUrl && testimonial.enhanced}
|
||||
<enhanced:img
|
||||
src={testimonial.enhanced}
|
||||
alt={reviewAlt(testimonial)}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{:else if testimonial.imageUrl}
|
||||
<img
|
||||
src={testimonial.imageUrl}
|
||||
alt={reviewAlt(testimonial)}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
/>
|
||||
{:else}
|
||||
<div class="testimonials-page-card-fallback" aria-hidden="true">
|
||||
<span class="testimonials-page-card-fallback-mark">
|
||||
<span class="testimonials-page-card-fallback-mark-ring">
|
||||
<Icon name="fas fa-paw" />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="testimonials-page-card-meta">
|
||||
<span class="testimonials-page-card-dog">
|
||||
{dogNameFromDetail(testimonial.detail) || testimonial.reviewer}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="testimonials-page-card-copy">
|
||||
<div class="testimonials-page-card-top">
|
||||
<span class="testimonials-page-card-quote" aria-hidden="true">“</span>
|
||||
<div class="testimonials-page-card-review-meta">
|
||||
{#if testimonial.type === 'Google'}
|
||||
<img
|
||||
class="testimonials-page-card-source-icon"
|
||||
src="/images/google-g-logo.svg"
|
||||
alt="Google review"
|
||||
width="14"
|
||||
height="15"
|
||||
/>
|
||||
{/if}
|
||||
<span class="testimonials-page-card-stars" aria-hidden="true">
|
||||
{#each Array(5) as _}
|
||||
<Icon name="fas fa-star" />
|
||||
{/each}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<blockquote>{testimonial.quote}</blockquote>
|
||||
<div class="testimonials-page-author">
|
||||
<span class="testimonials-page-author-name">{authorLine(testimonial)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="testimonials-page-google-cta-wrap">
|
||||
<a
|
||||
class="testimonials-page-google-cta"
|
||||
href="https://g.page/r/CUsvrWPhkYrAEB0/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<img
|
||||
class="testimonials-page-trust-logo"
|
||||
src="/images/google-g-logo.svg"
|
||||
alt=""
|
||||
width="18"
|
||||
height="19"
|
||||
/>
|
||||
<span>Read all our Google reviews</span>
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<BookingWizard booking={content.booking} pagePath="/testimonials" />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
.testimonials-page {
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
.testimonials-page-trust-logo {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.testimonials-page-grid-section {
|
||||
padding: 28px 0 72px;
|
||||
}
|
||||
|
||||
.testimonials-page-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 22px;
|
||||
align-items: stretch;
|
||||
max-width: 1240px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.testimonials-page-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(33, 48, 33, 0.09);
|
||||
border-radius: 28px;
|
||||
background: #fff;
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.78),
|
||||
0 16px 34px rgba(33, 48, 33, 0.08);
|
||||
transition:
|
||||
transform 0.28s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.28s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
border-color 0.24s ease;
|
||||
}
|
||||
|
||||
.testimonials-page-card-media {
|
||||
position: relative;
|
||||
aspect-ratio: 4 / 3;
|
||||
overflow: hidden;
|
||||
border-radius: 22px;
|
||||
background: rgba(33, 48, 33, 0.12);
|
||||
}
|
||||
|
||||
.testimonials-page-card-media img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.testimonials-page-card-fallback {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
background: var(--gw-green);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.testimonials-page-card-fallback::before,
|
||||
.testimonials-page-card-fallback::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 209, 0, 0.08);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.testimonials-page-card-fallback::before {
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
.testimonials-page-card-fallback::after {
|
||||
bottom: -20px;
|
||||
left: -10px;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.testimonials-page-card-fallback-mark {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.testimonials-page-card-fallback-mark {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.testimonials-page-card-fallback-mark-ring {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(180deg, rgba(255, 244, 204, 0.96), rgba(255, 209, 0, 0.82));
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
|
||||
0 12px 28px rgba(17, 20, 24, 0.18);
|
||||
}
|
||||
|
||||
.testimonials-page-card-fallback-mark-ring :global(.icon) {
|
||||
font-size: 28px;
|
||||
color: var(--gw-green);
|
||||
}
|
||||
|
||||
.testimonials-page-card-meta {
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
bottom: 14px;
|
||||
left: 14px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-start;
|
||||
padding: 14px 16px;
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(180deg, rgba(18, 26, 18, 0.1) 0%, rgba(18, 26, 18, 0.78) 100%);
|
||||
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.testimonials-page-card-dog {
|
||||
color: #fff;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.18);
|
||||
font-family: var(--font-head);
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.03em;
|
||||
}
|
||||
|
||||
.testimonials-page-card-copy {
|
||||
display: grid;
|
||||
align-content: start;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
gap: 14px;
|
||||
padding: 18px 8px 10px;
|
||||
transition: transform 0.28s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.testimonials-page-card-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.testimonials-page-card-quote {
|
||||
color: rgba(33, 48, 33, 0.22);
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 40px;
|
||||
line-height: 0.8;
|
||||
}
|
||||
|
||||
.testimonials-page-card-review-meta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.testimonials-page-card-source-icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.testimonials-page-card-stars {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
color: #f0b72f;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.testimonials-page-card blockquote {
|
||||
margin: 0;
|
||||
align-self: start;
|
||||
color: #243024;
|
||||
font-size: 17px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.testimonials-page-author {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.testimonials-page-author-name {
|
||||
color: var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.testimonials-page-google-cta-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 36px;
|
||||
}
|
||||
|
||||
.testimonials-page-google-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 50px;
|
||||
padding: 0 18px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.78);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
|
||||
0 16px 30px rgba(67, 49, 21, 0.08);
|
||||
color: #1d281e;
|
||||
font-family: var(--font-head);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
transition:
|
||||
transform 0.22s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
box-shadow 0.22s ease,
|
||||
background 0.22s ease;
|
||||
}
|
||||
|
||||
:global(.reveal-ready.reveal-block).testimonials-page-grid-section .testimonials-page-card,
|
||||
:global(.reveal-ready.reveal-block).testimonials-page-grid-section .testimonials-page-google-cta-wrap {
|
||||
opacity: 0;
|
||||
transform: translateY(28px);
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition:
|
||||
opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(1) {
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(2) {
|
||||
transition-delay: 50ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(3) {
|
||||
transition-delay: 100ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(4) {
|
||||
transition-delay: 150ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(5) {
|
||||
transition-delay: 200ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(6) {
|
||||
transition-delay: 250ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-google-cta-wrap {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition:
|
||||
opacity 0.42s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
transition-delay: 140ms;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
.testimonials-page-card:hover {
|
||||
transform: translateY(-6px);
|
||||
border-color: rgba(33, 48, 33, 0.16);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.82),
|
||||
0 24px 48px rgba(33, 48, 33, 0.12);
|
||||
}
|
||||
|
||||
.testimonials-page-card:hover .testimonials-page-card-media img {
|
||||
transform: scale(1.035);
|
||||
}
|
||||
|
||||
.testimonials-page-card:hover .testimonials-page-card-copy {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.testimonials-page-card:hover .testimonials-page-card-meta {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.testimonials-page-google-cta:hover {
|
||||
transform: translateY(-2px);
|
||||
background: rgba(255, 255, 255, 0.92);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(17, 20, 24, 0.06),
|
||||
0 20px 34px rgba(67, 49, 21, 0.11);
|
||||
}
|
||||
}
|
||||
|
||||
.testimonials-page-card:focus-within {
|
||||
transform: translateY(-4px);
|
||||
border-color: rgba(33, 48, 33, 0.16);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.82),
|
||||
0 22px 44px rgba(33, 48, 33, 0.12);
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.testimonials-page-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1180px) {
|
||||
.testimonials-page-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
max-width: 1500px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1800px) {
|
||||
.testimonials-page-grid {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
max-width: 1760px;
|
||||
gap: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.testimonials-page-grid-section {
|
||||
padding: 20px 0 56px;
|
||||
}
|
||||
|
||||
.testimonials-page-card-copy {
|
||||
gap: 14px;
|
||||
padding: 18px 6px 8px;
|
||||
}
|
||||
|
||||
.testimonials-page-grid {
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.testimonials-page-card {
|
||||
border-radius: 26px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.testimonials-page-card-media {
|
||||
border-radius: 18px;
|
||||
}
|
||||
|
||||
.testimonials-page-card-meta {
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
left: 12px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.testimonials-page-card-dog {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.testimonials-page-card blockquote {
|
||||
font-size: 16px;
|
||||
line-height: 1.64;
|
||||
}
|
||||
|
||||
.testimonials-page-author {
|
||||
align-items: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.testimonials-page-card,
|
||||
.testimonials-page-card-media img,
|
||||
.testimonials-page-card-meta,
|
||||
.testimonials-page-card-copy,
|
||||
.testimonials-page-google-cta,
|
||||
:global(.reveal-ready.reveal-block).testimonials-page-grid-section .testimonials-page-card,
|
||||
:global(.reveal-ready.reveal-block).testimonials-page-grid-section .testimonials-page-google-cta-wrap {
|
||||
transition: none;
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user