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

864 lines
20 KiB
Svelte
Raw Normal View History

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