v4.0.0.1
This commit is contained in:
@@ -2,8 +2,8 @@
|
||||
import { accordion } from '$lib/actions/accordion';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import CtaCard from '$lib/components/CtaCard.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import ServiceHero from '$lib/components/ServiceHero.svelte';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import type { AboutPageContent } from '$lib/types';
|
||||
|
||||
@@ -11,33 +11,25 @@
|
||||
|
||||
$: 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">
|
||||
|
||||
<!-- ── Hero ── -->
|
||||
<PageHeader
|
||||
variant="green"
|
||||
<ServiceHero
|
||||
eyebrow="About Goodwalk"
|
||||
title={pageContent.title}
|
||||
subtitle="Small dog specialists serving Auckland Central. A team your dog knows by name."
|
||||
>
|
||||
<div class="ph-chips">
|
||||
<a
|
||||
href="https://g.page/r/CUsvrWPhkYrAEB0/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
class="ph-chip ph-chip--link"
|
||||
>
|
||||
<span class="about-chip-stars" aria-hidden="true">★★★★★</span>
|
||||
30+ five-star Google reviews
|
||||
</a>
|
||||
<span class="ph-chip">Auckland Central</span>
|
||||
<span class="ph-chip">Small dog specialists</span>
|
||||
</div>
|
||||
</PageHeader>
|
||||
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}
|
||||
@@ -50,7 +42,7 @@
|
||||
<div class="page-inner about-section-grid" class:about-section-reverse={section.reverse}>
|
||||
<div class="about-copy">
|
||||
{#if section.eyebrow}
|
||||
<span class="about-eyebrow">{section.eyebrow}</span>
|
||||
<span class="eyebrow about-eyebrow">{section.eyebrow}</span>
|
||||
{/if}
|
||||
<h2>{section.title}</h2>
|
||||
{#each section.body as paragraph}
|
||||
@@ -93,7 +85,7 @@
|
||||
<div class="about-founder-copy">
|
||||
<article class="about-founder-note">
|
||||
{#if founderSection.eyebrow}
|
||||
<span class="about-eyebrow about-founder-kicker">{founderSection.eyebrow}</span>
|
||||
<span class="eyebrow about-eyebrow about-founder-kicker">{founderSection.eyebrow}</span>
|
||||
{/if}
|
||||
<h2 class="about-founder-heading">
|
||||
<span class="about-founder-heading-desktop">
|
||||
@@ -144,7 +136,7 @@
|
||||
<section use:reveal={{ delay: 30 }} class="about-faq reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class="about-faq-header">
|
||||
<span class="about-eyebrow">FAQ</span>
|
||||
<span class="eyebrow about-eyebrow">FAQ</span>
|
||||
<h2>{pageContent.faqTitle ?? 'Common questions'}</h2>
|
||||
</div>
|
||||
<div use:accordion class="faq about-faq-list">
|
||||
@@ -187,21 +179,6 @@
|
||||
.about-eyebrow {
|
||||
display: inline-block;
|
||||
margin-bottom: 14px;
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.08);
|
||||
color: var(--gw-green);
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.about-chip-stars {
|
||||
color: var(--yellow);
|
||||
letter-spacing: 1px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ── Standard sections ── */
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import BookingWizard from '$lib/components/BookingWizard.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import InfoSection from '$lib/components/InfoSection.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import ServiceHero from '$lib/components/ServiceHero.svelte';
|
||||
import type { BookingContent, InfoContent } from '$lib/types';
|
||||
|
||||
export let booking: BookingContent;
|
||||
@@ -18,12 +18,22 @@
|
||||
</script>
|
||||
|
||||
<main class="booking-page">
|
||||
<PageHeader
|
||||
variant="green"
|
||||
<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."
|
||||
>
|
||||
<div class="booking-page-contact">
|
||||
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}
|
||||
@@ -33,7 +43,7 @@
|
||||
{phone}
|
||||
</a>
|
||||
</div>
|
||||
</PageHeader>
|
||||
</div>
|
||||
|
||||
<BookingWizard {booking} pagePath="/contact-us" />
|
||||
<InfoSection {info} />
|
||||
@@ -44,13 +54,16 @@
|
||||
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;
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
.booking-contact-link {
|
||||
@@ -59,19 +72,20 @@
|
||||
gap: 8px;
|
||||
padding: 10px 20px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border: 1px solid rgba(255, 255, 255, 0.18);
|
||||
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: #fff;
|
||||
transition: background 0.2s;
|
||||
color: var(--text-heading);
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.booking-contact-link:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
export let booking: BookingContent;
|
||||
export let pagePath = '';
|
||||
$: isCompactContactPage = pagePath === '/contact-us';
|
||||
|
||||
const defaultServices = ['Tiny Gang Pack Walks', 'Solo Walks', 'Puppy Visits', 'Other Services'];
|
||||
$: serviceOptions = booking.serviceOptions && booking.serviceOptions.length > 0
|
||||
@@ -204,6 +205,22 @@
|
||||
return Object.keys(next).length === 0;
|
||||
}
|
||||
|
||||
function validateCompactContactForm(): boolean {
|
||||
const next: Record<string, string> = {};
|
||||
if (!petName.trim()) next.petName = "Please tell us your dog's name.";
|
||||
if (selectedServices.length === 0) next.services = 'Pick at least one service.';
|
||||
if (message.trim().length < 10) {
|
||||
next.message = 'Tell us a little about your dog so we can prepare properly.';
|
||||
}
|
||||
if (!fullName.trim()) next.fullName = 'Please enter your full name';
|
||||
const emailErr = validateEmail(email);
|
||||
if (emailErr) next.email = emailErr;
|
||||
if (!phone.trim()) next.phone = 'Please enter your phone number';
|
||||
if (!location.trim()) next.location = 'Please enter your suburb';
|
||||
errors = next;
|
||||
return Object.keys(next).length === 0;
|
||||
}
|
||||
|
||||
function goNext() {
|
||||
noteInteraction();
|
||||
if (!validateStep(step)) return;
|
||||
@@ -222,7 +239,9 @@
|
||||
|
||||
async function handleSubmit() {
|
||||
noteInteraction();
|
||||
if (!validateStep(2)) return;
|
||||
if (isCompactContactPage) {
|
||||
if (!validateCompactContactForm()) return;
|
||||
} else if (!validateStep(2)) return;
|
||||
|
||||
submitting = true;
|
||||
sendClickedAt = Date.now();
|
||||
@@ -252,7 +271,7 @@
|
||||
journey,
|
||||
referrer: typeof document !== 'undefined' ? document.referrer : '',
|
||||
page: typeof window !== 'undefined' ? window.location.href : '',
|
||||
variant: 'booking-wizard'
|
||||
variant: isCompactContactPage ? 'contact-compact' : 'booking-wizard'
|
||||
})
|
||||
});
|
||||
|
||||
@@ -276,7 +295,7 @@
|
||||
</script>
|
||||
|
||||
<section id="newlead" use:reveal={{ delay: 70 }} class="wiz reveal-block">
|
||||
<div class="wiz-inner">
|
||||
<div class="wiz-inner" class:wiz-inner--compact={isCompactContactPage}>
|
||||
{#if submitted && SuccessModalComponent}
|
||||
<svelte:component
|
||||
this={SuccessModalComponent}
|
||||
@@ -298,43 +317,45 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<div class="wiz-header">
|
||||
<span class="wiz-eyebrow">Free Meet & Greet</span>
|
||||
<h2 class="wiz-title">
|
||||
<span class="wiz-title-plain">{headingParts.plain}</span>
|
||||
{#if headingParts.highlight}
|
||||
{' '}
|
||||
<span class="wiz-title-highlight">{headingParts.highlight}</span>
|
||||
{/if}
|
||||
</h2>
|
||||
<p class="wiz-lead">{leadCopy}</p>
|
||||
</div>
|
||||
|
||||
<div class="wiz-trust" aria-label="Goodwalk dog families">
|
||||
<div class="wiz-avatars" aria-hidden="true">
|
||||
{#each avatarDogs as dog}
|
||||
<span class="wiz-avatar">
|
||||
<img src={dog.image} alt="" loading="lazy" />
|
||||
</span>
|
||||
{/each}
|
||||
{#if !isCompactContactPage}
|
||||
<div class="wiz-header">
|
||||
<span class="wiz-eyebrow">Free Meet & Greet</span>
|
||||
<h2 class="wiz-title">
|
||||
<span class="wiz-title-plain">{headingParts.plain}</span>
|
||||
{#if headingParts.highlight}
|
||||
{' '}
|
||||
<span class="wiz-title-highlight">{headingParts.highlight}</span>
|
||||
{/if}
|
||||
</h2>
|
||||
<p class="wiz-lead">{leadCopy}</p>
|
||||
</div>
|
||||
<div class="wiz-trust-copy">
|
||||
<p>{trustTitle}</p>
|
||||
<span>{trustNote}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wiz-progress" role="progressbar" aria-valuemin="1" aria-valuemax="2" aria-valuenow={step}>
|
||||
<span class="wiz-step-mark" class:active={step >= 1} class:done={step > 1}>
|
||||
<span class="wiz-step-num">1</span>
|
||||
<span class="wiz-step-label">{booking.dogStepLabel || 'Your dog'}</span>
|
||||
</span>
|
||||
<span class="wiz-step-line" class:done={step > 1} aria-hidden="true"></span>
|
||||
<span class="wiz-step-mark" class:active={step === 2}>
|
||||
<span class="wiz-step-num">2</span>
|
||||
<span class="wiz-step-label">{booking.ownerStepLabel || 'Your details'}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="wiz-trust" aria-label="Goodwalk dog families">
|
||||
<div class="wiz-avatars" aria-hidden="true">
|
||||
{#each avatarDogs as dog}
|
||||
<span class="wiz-avatar">
|
||||
<img src={dog.image} alt="" loading="lazy" />
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="wiz-trust-copy">
|
||||
<p>{trustTitle}</p>
|
||||
<span>{trustNote}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wiz-progress" role="progressbar" aria-valuemin="1" aria-valuemax="2" aria-valuenow={step}>
|
||||
<span class="wiz-step-mark" class:active={step >= 1} class:done={step > 1}>
|
||||
<span class="wiz-step-num">1</span>
|
||||
<span class="wiz-step-label">{booking.dogStepLabel || 'Your dog'}</span>
|
||||
</span>
|
||||
<span class="wiz-step-line" class:done={step > 1} aria-hidden="true"></span>
|
||||
<span class="wiz-step-mark" class:active={step === 2}>
|
||||
<span class="wiz-step-num">2</span>
|
||||
<span class="wiz-step-label">{booking.ownerStepLabel || 'Your details'}</span>
|
||||
</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<form
|
||||
class="wiz-form"
|
||||
@@ -355,10 +376,189 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<article class="wiz-card">
|
||||
<article class="wiz-card" class:wiz-card--compact={isCompactContactPage}>
|
||||
{#key step}
|
||||
<div class="wiz-step" in:fade={{ duration: 200 }}>
|
||||
{#if step === 1}
|
||||
<div class="wiz-step" class:wiz-step--compact={isCompactContactPage} in:fade={{ duration: 200 }}>
|
||||
{#if isCompactContactPage}
|
||||
<span class="wiz-step-eyebrow">Start here</span>
|
||||
<h3 class="wiz-step-heading">Tell us about your dog</h3>
|
||||
<p class="wiz-step-helper">A few details now. We come back with the right next step.</p>
|
||||
|
||||
<label class="wiz-field">
|
||||
<span class="wiz-label">
|
||||
<Icon name="fas fa-dog" /> Your dog's name
|
||||
</span>
|
||||
<input
|
||||
bind:value={petName}
|
||||
on:input={() => clearError('petName')}
|
||||
type="text"
|
||||
placeholder="For example, Teddy"
|
||||
class:invalid={errors.petName}
|
||||
autocomplete="off"
|
||||
/>
|
||||
{#if errors.petName}
|
||||
<span class="wiz-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.petName}
|
||||
</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<fieldset class="wiz-fieldset">
|
||||
<legend class="wiz-label">
|
||||
<Icon name="fas fa-paw" /> Which service are you interested in?
|
||||
</legend>
|
||||
<div
|
||||
class="wiz-service-grid"
|
||||
class:wiz-service-grid--compact={isCompactContactPage}
|
||||
class:invalid={errors.services}
|
||||
role="group"
|
||||
aria-label="Service interest"
|
||||
>
|
||||
{#each serviceOptions as service}
|
||||
{@const checked = selectedServices.includes(service)}
|
||||
<button
|
||||
type="button"
|
||||
class="wiz-service"
|
||||
class:wiz-service--compact={isCompactContactPage}
|
||||
class:active={checked}
|
||||
aria-pressed={checked}
|
||||
on:click={() => toggleService(service)}
|
||||
>
|
||||
<span class="wiz-service-check" aria-hidden="true">
|
||||
{#if checked}<Icon name="fas fa-check" />{/if}
|
||||
</span>
|
||||
<span class="wiz-service-text">
|
||||
<span class="wiz-service-label">{service}</span>
|
||||
{#if serviceDescriptions[service]}
|
||||
<span class="wiz-service-desc">{serviceDescriptions[service]}</span>
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if errors.services}
|
||||
<span class="wiz-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.services}
|
||||
</span>
|
||||
{/if}
|
||||
</fieldset>
|
||||
|
||||
<div class="wiz-grid-two">
|
||||
<label class="wiz-field">
|
||||
<span class="wiz-label">
|
||||
<Icon name="fas fa-user" /> Full name
|
||||
</span>
|
||||
<input
|
||||
bind:value={fullName}
|
||||
on:input={() => clearError('fullName')}
|
||||
type="text"
|
||||
placeholder="Your full name"
|
||||
class:invalid={errors.fullName}
|
||||
autocomplete="name"
|
||||
/>
|
||||
{#if errors.fullName}
|
||||
<span class="wiz-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.fullName}
|
||||
</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<label class="wiz-field">
|
||||
<span class="wiz-label">
|
||||
<Icon name="fas fa-envelope" /> Email
|
||||
</span>
|
||||
<input
|
||||
bind:value={email}
|
||||
on:input={() => clearError('email')}
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
class:invalid={errors.email}
|
||||
autocomplete="email"
|
||||
inputmode="email"
|
||||
/>
|
||||
{#if errors.email}
|
||||
<span class="wiz-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.email}
|
||||
</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<label class="wiz-field">
|
||||
<span class="wiz-label">
|
||||
<Icon name="fas fa-phone" /> Phone
|
||||
</span>
|
||||
<input
|
||||
bind:value={phone}
|
||||
on:input={() => clearError('phone')}
|
||||
type="tel"
|
||||
placeholder="(021) 234 5678"
|
||||
class:invalid={errors.phone}
|
||||
autocomplete="tel"
|
||||
inputmode="tel"
|
||||
/>
|
||||
{#if errors.phone}
|
||||
<span class="wiz-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.phone}
|
||||
</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<label class="wiz-field">
|
||||
<span class="wiz-label">
|
||||
<Icon name="fas fa-location-dot" /> Your suburb
|
||||
</span>
|
||||
<input
|
||||
bind:value={location}
|
||||
on:input={() => clearError('location')}
|
||||
type="text"
|
||||
placeholder="For example, Grey Lynn"
|
||||
class:invalid={errors.location}
|
||||
autocomplete="address-level2"
|
||||
/>
|
||||
{#if errors.location}
|
||||
<span class="wiz-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.location}
|
||||
</span>
|
||||
{/if}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<label class="wiz-field">
|
||||
<span class="wiz-label">
|
||||
<Icon name="fas fa-comment" /> What else should we know?
|
||||
</span>
|
||||
<textarea
|
||||
bind:value={message}
|
||||
on:input={() => clearError('message')}
|
||||
rows="3"
|
||||
placeholder="Age, breed, temperament around other dogs, any health quirks, anything that helps us prepare."
|
||||
class:invalid={errors.message}
|
||||
></textarea>
|
||||
{#if errors.message}
|
||||
<span class="wiz-error">
|
||||
<Icon name="fas fa-circle-exclamation" />
|
||||
{errors.message}
|
||||
</span>
|
||||
{/if}
|
||||
</label>
|
||||
|
||||
<div class="wiz-actions">
|
||||
<button type="submit" class="wiz-btn wiz-btn-primary wiz-btn-primary--wide" disabled={submitting}>
|
||||
{#if submitting}
|
||||
Sending
|
||||
{:else}
|
||||
Send my details
|
||||
{/if}
|
||||
<Icon name="fas fa-paper-plane" />
|
||||
</button>
|
||||
</div>
|
||||
{:else if step === 1}
|
||||
<span class="wiz-step-eyebrow">Step one of two</span>
|
||||
<h3 class="wiz-step-heading">Tell us about your dog</h3>
|
||||
<p class="wiz-step-helper">Just the basics. Pick everything you are open to.</p>
|
||||
@@ -556,7 +756,7 @@
|
||||
</article>
|
||||
</form>
|
||||
|
||||
<p class="wiz-reassurance" aria-live="polite">
|
||||
<p class="wiz-reassurance" class:wiz-reassurance--compact={isCompactContactPage} aria-live="polite">
|
||||
<Icon name="fas fa-bolt" />
|
||||
A real reply within 24 hours, usually sooner.
|
||||
</p>
|
||||
@@ -575,6 +775,11 @@
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.wiz-inner--compact {
|
||||
max-width: 60rem;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.wiz-header {
|
||||
text-align: center;
|
||||
margin-bottom: 28px;
|
||||
@@ -780,12 +985,21 @@
|
||||
box-shadow: 0 30px 60px rgba(var(--ink-rgb), 0.08);
|
||||
}
|
||||
|
||||
.wiz-card--compact {
|
||||
max-width: 52rem;
|
||||
padding: clamp(22px, 3vw, 32px);
|
||||
}
|
||||
|
||||
.wiz-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.wiz-step--compact {
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.wiz-step-eyebrow {
|
||||
align-self: flex-start;
|
||||
padding: 6px 12px;
|
||||
@@ -831,19 +1045,6 @@
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
.wiz-optional {
|
||||
margin-left: 6px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(var(--brand-rgb), 0.06);
|
||||
color: var(--text-subtle);
|
||||
font-family: var(--font-body);
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.01em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.wiz-field input,
|
||||
.wiz-field textarea {
|
||||
width: 100%;
|
||||
@@ -942,6 +1143,10 @@
|
||||
border-color: rgba(var(--brand-rgb), 0.32);
|
||||
}
|
||||
|
||||
.wiz-service--compact {
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.wiz-service.active {
|
||||
border-color: var(--gw-green);
|
||||
background: rgba(var(--accent-rgb), 0.1);
|
||||
@@ -1022,6 +1227,11 @@
|
||||
background: oklch(0.88 0.18 95);
|
||||
}
|
||||
|
||||
.wiz-btn-primary--wide {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.wiz-btn-back {
|
||||
background: transparent;
|
||||
color: var(--text-subtle);
|
||||
@@ -1041,6 +1251,10 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wiz-reassurance--compact {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.wiz-reassurance :global(.icon) {
|
||||
color: var(--yellow);
|
||||
font-size: 13px;
|
||||
@@ -1056,6 +1270,10 @@
|
||||
padding-left: var(--space-container-x-mobile);
|
||||
padding-right: var(--space-container-x-mobile);
|
||||
}
|
||||
|
||||
.wiz-inner--compact {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@@ -1085,6 +1303,24 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.wiz-service-grid--compact {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.wiz-service--compact {
|
||||
min-height: 100%;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.wiz-service--compact .wiz-service-desc {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.wiz-card--compact {
|
||||
padding: 20px 18px;
|
||||
border-radius: 22px;
|
||||
}
|
||||
|
||||
.wiz-grid-two {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -387,7 +387,7 @@
|
||||
content="Sign your Goodwalk service agreement online."
|
||||
/>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="canonical" href="https://onboarding.goodwalk.co.nz/contract/" />
|
||||
<link rel="canonical" href="https://clients.goodwalk.co.nz/contract/" />
|
||||
</svelte:head>
|
||||
|
||||
<main class="contract-page">
|
||||
|
||||
@@ -139,13 +139,10 @@
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
/* Layout-only override; typography and colour live on the shared
|
||||
.eyebrow utility. */
|
||||
.founder-kicker {
|
||||
display: inline-block;
|
||||
color: var(--text-subtle);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.founder-greeting {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
const journeyChips = [
|
||||
{ icon: 'fas fa-handshake', label: 'Free Meet & Greet' },
|
||||
{ icon: 'fas fa-clipboard-check', label: 'Assessment walks' },
|
||||
{ icon: 'fas fa-calendar-check', label: 'A regular weekly rhythm' }
|
||||
{ icon: 'fas fa-calendar-check', label: 'Weekly rhythm' }
|
||||
];
|
||||
</script>
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
Book your free Meet & Greet
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</a>
|
||||
<p class="hiw-cta-note">Free, no-obligation. We reply within 24 hours.</p>
|
||||
<p class="hiw-cta-note">No obligation. We reply within 24 hours.</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -74,7 +74,7 @@
|
||||
.hiw-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 0 50px;
|
||||
padding: 0 var(--space-container-x);
|
||||
}
|
||||
|
||||
/* ── Header ── */
|
||||
@@ -82,11 +82,9 @@
|
||||
margin-bottom: 36px;
|
||||
}
|
||||
|
||||
/* Layout-only override; typography lives on the shared .eyebrow utility. */
|
||||
.hiw-eyebrow {
|
||||
padding: 7px 12px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.08);
|
||||
box-shadow: inset 0 0 0 1px rgba(17, 20, 24, 0.05);
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.hiw-intro {
|
||||
@@ -103,14 +101,22 @@
|
||||
margin-top: 28px;
|
||||
}
|
||||
|
||||
/* Connector tracks through the vertical centre of the phase pills
|
||||
(card padding 40px + half pill height ~12px). Reads as a timeline
|
||||
running through the three steps, not a floating decorative rule. */
|
||||
.hiw-steps::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 32px;
|
||||
top: 52px;
|
||||
left: 13%;
|
||||
right: 13%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, rgba(33, 48, 33, 0.16), rgba(242, 191, 47, 0.4), rgba(33, 48, 33, 0.16));
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(var(--brand-rgb), 0.16),
|
||||
rgba(var(--accent-rgb), 0.4),
|
||||
rgba(var(--brand-rgb), 0.16)
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -119,12 +125,12 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 40px 40px 36px;
|
||||
padding: clamp(28px, 2.6vw, 40px) clamp(24px, 2.6vw, 40px) clamp(26px, 2.4vw, 36px);
|
||||
background:
|
||||
radial-gradient(circle at top center, rgba(255, 209, 0, 0.12), transparent 34%),
|
||||
#fff;
|
||||
border: 1px solid rgba(17, 20, 24, 0.06);
|
||||
box-shadow: 0 4px 16px rgba(17, 20, 24, 0.04);
|
||||
radial-gradient(circle at top center, rgba(var(--accent-rgb), 0.12), transparent 34%),
|
||||
var(--surface-panel);
|
||||
border: 1px solid var(--border-soft-strong);
|
||||
box-shadow: var(--shadow-card);
|
||||
transition: box-shadow 0.22s ease, transform 0.18s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
@@ -133,7 +139,7 @@
|
||||
|
||||
@media (hover: hover) {
|
||||
.hiw-step:hover {
|
||||
box-shadow: 0 16px 40px rgba(17, 20, 24, 0.09);
|
||||
box-shadow: var(--shadow-xl);
|
||||
transform: translateY(-4px);
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -151,9 +157,9 @@
|
||||
.hiw-phase {
|
||||
display: inline-block;
|
||||
padding: 5px 13px;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--yellow);
|
||||
color: #000;
|
||||
color: var(--gw-green);
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 800;
|
||||
@@ -165,7 +171,7 @@
|
||||
font-family: var(--font-head);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: rgba(33, 48, 33, 0.28);
|
||||
color: rgba(var(--brand-rgb), 0.28);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
@@ -178,8 +184,8 @@
|
||||
height: 64px;
|
||||
margin-bottom: 22px;
|
||||
border-radius: 20px;
|
||||
background: var(--gw-green);
|
||||
box-shadow: 0 10px 28px rgba(33, 48, 33, 0.2);
|
||||
background: var(--surface-brand);
|
||||
box-shadow: var(--shadow-badge);
|
||||
}
|
||||
|
||||
.hiw-icon-wrap :global(.hiw-step-icon) {
|
||||
@@ -194,12 +200,12 @@
|
||||
font-size: var(--heading-card-size);
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
color: #0d1a0d;
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
.hiw-body {
|
||||
margin: 0 0 20px;
|
||||
color: #4c5056;
|
||||
color: var(--text-muted);
|
||||
font-size: 15px;
|
||||
line-height: 1.65;
|
||||
flex: 1;
|
||||
@@ -210,8 +216,8 @@
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
padding: 7px 14px;
|
||||
border-radius: 999px;
|
||||
background: rgba(33, 48, 33, 0.07);
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--surface-brand-muted);
|
||||
color: var(--gw-green);
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
@@ -234,7 +240,7 @@
|
||||
|
||||
.hiw-cta-note {
|
||||
margin: 0;
|
||||
color: #888;
|
||||
color: var(--text-softest);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@@ -253,12 +259,12 @@
|
||||
gap: 8px;
|
||||
min-height: 44px;
|
||||
padding: 0 16px;
|
||||
border-radius: 999px;
|
||||
background: var(--gw-green);
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--surface-brand);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.04),
|
||||
0 10px 22px rgba(17, 20, 24, 0.06);
|
||||
color: #fff;
|
||||
var(--shadow-inset-inverse),
|
||||
0 10px 22px rgba(var(--ink-rgb), 0.06);
|
||||
color: var(--text-inverse);
|
||||
font-family: var(--font-head);
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
@@ -310,8 +316,8 @@
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
padding: 28px 24px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(17, 20, 24, 0.06);
|
||||
border-radius: var(--radius-xl);
|
||||
border: 1px solid var(--border-soft-strong);
|
||||
}
|
||||
|
||||
.hiw-step-meta {
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
<!-- ── Hero ── -->
|
||||
<section class="loc-hero">
|
||||
<div class="page-inner">
|
||||
<span class="loc-hero-eyebrow">Auckland Central Dog Walking</span>
|
||||
<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">
|
||||
@@ -103,7 +103,7 @@
|
||||
<section use:reveal={{ delay: 30 }} class="loc-parks reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class="loc-section-header">
|
||||
<span class="loc-eyebrow">Where we walk</span>
|
||||
<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.
|
||||
@@ -128,7 +128,7 @@
|
||||
<section use:reveal={{ delay: 30 }} class="loc-gallery reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class="loc-section-header">
|
||||
<span class="loc-eyebrow">Local parks</span>
|
||||
<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.
|
||||
@@ -161,7 +161,7 @@
|
||||
<section use:reveal={{ delay: 30 }} class="loc-services reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class="loc-section-header">
|
||||
<span class="loc-eyebrow">What we offer</span>
|
||||
<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.
|
||||
@@ -226,27 +226,12 @@
|
||||
|
||||
/* ── Eyebrow ── */
|
||||
.loc-eyebrow,
|
||||
.loc-hero-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;
|
||||
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 {
|
||||
background: rgba(255, 209, 0, 0.12);
|
||||
box-shadow: inset 0 0 0 1px rgba(255, 209, 0, 0.2);
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
/* ── Hero ── */
|
||||
|
||||
@@ -27,7 +27,9 @@
|
||||
$: hidden =
|
||||
pathname === '/contact-us' ||
|
||||
pathname === '/booking' ||
|
||||
$page.url.hostname === 'clients.goodwalk.co.nz' ||
|
||||
$page.url.hostname === 'onboarding.goodwalk.co.nz' ||
|
||||
$page.url.searchParams.get('preview') === 'clients' ||
|
||||
$page.url.searchParams.get('preview') === 'onboarding';
|
||||
|
||||
let visible = false;
|
||||
|
||||
@@ -6,11 +6,9 @@
|
||||
$: introText =
|
||||
context === 'contract'
|
||||
? "Enter the email address you used when enquiring with Goodwalk. We'll send you a one-time code to continue your contract."
|
||||
: context === 'owner'
|
||||
? "Enter the Goodwalk owner email address. We'll send you a one-time code so you can manage onboarding welcome emails."
|
||||
: "Enter the email address you used when enquiring with Goodwalk. We'll send you a one-time code to continue your onboarding.";
|
||||
: "Enter the email address you used when enquiring with Goodwalk. We'll send you a one-time code to continue your onboarding.";
|
||||
|
||||
const dispatch = createEventDispatcher<{ authenticated: { email: string; profile: Record<string, unknown>; draft: Record<string, unknown> } }>();
|
||||
const dispatch = createEventDispatcher<{ authenticated: { email: string; profile: Record<string, unknown>; draft: Record<string, unknown>; cpAdmin?: boolean; ownerEmail?: string } }>();
|
||||
|
||||
const ownerEmail = 'info@goodwalk.co.nz';
|
||||
const ownerPhone = '(022) 642 1011';
|
||||
@@ -58,6 +56,8 @@
|
||||
try { window.localStorage.setItem('gw_onboarding_session', data.token); } catch { /* ignore */ }
|
||||
let profile: Record<string, string> = {};
|
||||
let draft: Record<string, unknown> = {};
|
||||
let cpAdmin = false;
|
||||
let verifiedOwnerEmail = '';
|
||||
try {
|
||||
const verifyRes = await fetch('/api/auth/verify', {
|
||||
headers: { Authorization: `Bearer ${data.token}` },
|
||||
@@ -66,9 +66,11 @@
|
||||
const verifyData = await verifyRes.json();
|
||||
profile = verifyData.profile ?? {};
|
||||
draft = verifyData.draft ?? {};
|
||||
cpAdmin = Boolean(verifyData.cpAdmin);
|
||||
verifiedOwnerEmail = typeof verifyData.ownerEmail === 'string' ? verifyData.ownerEmail : '';
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
dispatch('authenticated', { email: data.email, profile, draft });
|
||||
dispatch('authenticated', { email: data.email, profile, draft, cpAdmin, ownerEmail: verifiedOwnerEmail });
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Something went wrong';
|
||||
} finally {
|
||||
@@ -99,7 +101,9 @@
|
||||
|
||||
{#if stage === 'email'}
|
||||
<h2>Sign in to continue</h2>
|
||||
<p>{introText}</p>
|
||||
{#if context !== 'owner'}
|
||||
<p>{introText}</p>
|
||||
{/if}
|
||||
|
||||
<div class="auth-field">
|
||||
<label for="auth-email">Email address</label>
|
||||
@@ -136,7 +140,7 @@
|
||||
maxlength="6"
|
||||
bind:value={codeValue}
|
||||
on:keydown={handleCodeKey}
|
||||
placeholder="123456"
|
||||
placeholder="198604"
|
||||
autocomplete="one-time-code"
|
||||
disabled={loading}
|
||||
class="auth-code-input"
|
||||
@@ -156,12 +160,14 @@
|
||||
</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>
|
||||
{#if context !== 'owner'}
|
||||
<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>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,7 @@
|
||||
mobileOrder: number;
|
||||
};
|
||||
export let variant: 'pricing' | 'service' = 'service';
|
||||
export let hideCtaOnMobile = false;
|
||||
|
||||
$: featured = plan.isPopular;
|
||||
const ctaLabel = 'Book a Meet & Greet';
|
||||
@@ -48,7 +49,11 @@
|
||||
{/each}
|
||||
</ul>
|
||||
|
||||
<a class="btn btn-yellow plan-card__cta" href="#newlead">
|
||||
<a
|
||||
class="btn btn-yellow plan-card__cta"
|
||||
class:plan-card__cta--mobile-hidden={hideCtaOnMobile}
|
||||
href="#newlead"
|
||||
>
|
||||
{ctaLabel}
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</a>
|
||||
@@ -380,5 +385,9 @@
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.plan-card__cta--mobile-hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -311,4 +311,5 @@
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1344,6 +1344,15 @@
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
/* "Best on solo" kicker sits on the dark-green fit cell. The default
|
||||
pill is dark-green-on-faint-green, which renders as unreadable on this
|
||||
background. Switch to yellow text on a yellow-tinted pill so it
|
||||
matches the check icons and the brand's dark-surface eyebrow colour. */
|
||||
.service-page.service-page-dog .service-decision-col-fit .service-decision-col-kicker-fit {
|
||||
background: rgba(var(--accent-rgb), 0.16);
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
.service-page.service-page-dog .service-decision-col-not {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(var(--white-rgb), 0.98) 0%, rgba(244, 247, 243, 0.96) 100%);
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
import type { IconCard } from '$lib/types';
|
||||
|
||||
export let services: IconCard[];
|
||||
export let heading = 'Choose the walk style that suits your dog best.';
|
||||
export let heading = 'Find the walk that fits your dog.';
|
||||
export let intro =
|
||||
'Dogs are social creatures. The Tiny Gang gives them their own little friendship group: older dogs guide the younger ones, playful dogs burn energy together, and everyone comes home happy, tired, and fulfilled. All the fun of doggy daycare, without the huge groups or price tag.';
|
||||
"The Tiny Gang is your dog's friendship group. Older dogs guide the youngsters; playful pairs burn energy together. The fun of doggy daycare, without the crowd or the price tag.";
|
||||
|
||||
const sharedPromises = [
|
||||
'Familiar walkers',
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import BookingWizard from '$lib/components/BookingWizard.svelte';
|
||||
import Icon from '$lib/components/Icon.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import { getEnhancedImage } from '$lib/enhanced-images';
|
||||
import { reveal } from '$lib/actions/reveal';
|
||||
import ServiceHero from '$lib/components/ServiceHero.svelte';
|
||||
import type { SiteSharedContent, TestimonialContent } from '$lib/types';
|
||||
|
||||
export let content: SiteSharedContent;
|
||||
@@ -12,6 +13,13 @@
|
||||
...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(/^([^'’]+)/);
|
||||
@@ -44,23 +52,17 @@
|
||||
</script>
|
||||
|
||||
<main class="testimonials-page">
|
||||
<PageHeader
|
||||
variant="green"
|
||||
<ServiceHero
|
||||
eyebrow="Goodwalk reviews"
|
||||
title="Client Testimonials"
|
||||
subtitle="Read why clients say their dogs love Aless, the Tiny Gang, and getting out with their mates."
|
||||
>
|
||||
<a
|
||||
class="btn btn-yellow btn-hide-arrow-mobile testimonials-page-header-cta"
|
||||
href="#newlead"
|
||||
aria-label="Book a Meet and Greet"
|
||||
>
|
||||
<span>Book a Meet & Greet</span>
|
||||
<Icon name="fas fa-arrow-right" />
|
||||
</a>
|
||||
</PageHeader>
|
||||
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 class="testimonials-page-grid-section">
|
||||
<section use:reveal class="testimonials-page-grid-section reveal-block">
|
||||
<div class="page-inner">
|
||||
<div class="testimonials-page-grid">
|
||||
{#each testimonialCards as testimonial}
|
||||
@@ -155,16 +157,6 @@
|
||||
background: var(--off-white);
|
||||
}
|
||||
|
||||
.testimonials-page-header-cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 22px;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.testimonials-page-trust-logo {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
@@ -195,6 +187,10 @@
|
||||
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 {
|
||||
@@ -210,6 +206,7 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.42s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
.testimonials-page-card-fallback {
|
||||
@@ -290,6 +287,7 @@
|
||||
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 {
|
||||
@@ -308,6 +306,7 @@
|
||||
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 {
|
||||
@@ -384,6 +383,97 @@
|
||||
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.56s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 0.56s 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;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(1) {
|
||||
transition-delay: 60ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(2) {
|
||||
transition-delay: 140ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(3) {
|
||||
transition-delay: 220ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(4) {
|
||||
transition-delay: 300ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(5) {
|
||||
transition-delay: 380ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-card:nth-child(6) {
|
||||
transition-delay: 460ms;
|
||||
}
|
||||
|
||||
:global(.reveal-visible.reveal-block).testimonials-page-grid-section .testimonials-page-google-cta-wrap {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition:
|
||||
opacity 0.56s cubic-bezier(0.22, 1, 0.36, 1),
|
||||
transform 0.56s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
transition-delay: 240ms;
|
||||
}
|
||||
|
||||
@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) {
|
||||
@@ -412,15 +502,6 @@
|
||||
padding: 20px 0 56px;
|
||||
}
|
||||
|
||||
.testimonials-page-header-cta {
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.testimonials-page-header-cta :global(.icon) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.testimonials-page-card-copy {
|
||||
gap: 14px;
|
||||
padding: 18px 6px 8px;
|
||||
@@ -460,4 +541,18 @@
|
||||
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>
|
||||
|
||||
@@ -185,7 +185,7 @@
|
||||
<div class="testimonials-inner">
|
||||
<div class="testimonials-header">
|
||||
<div class="testimonials-header-main">
|
||||
<span class="testimonials-eyebrow">
|
||||
<span class="eyebrow testimonials-eyebrow">
|
||||
<Icon name="fas fa-star" className="testimonials-eyebrow-star" />
|
||||
{eyebrow}
|
||||
</span>
|
||||
@@ -341,19 +341,10 @@
|
||||
gap: 8px;
|
||||
width: fit-content;
|
||||
margin: 0 auto 14px;
|
||||
padding: 7px 14px;
|
||||
border-radius: 999px;
|
||||
background: var(--yellow);
|
||||
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(33, 48, 33, 0.18);
|
||||
}
|
||||
|
||||
.testimonials-eyebrow :global(.testimonials-eyebrow-star) {
|
||||
color: var(--gw-green);
|
||||
color: var(--yellow);
|
||||
font-size: 11px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -7,28 +7,28 @@
|
||||
export let values: IconCard[];
|
||||
const stakes = [
|
||||
{
|
||||
label: 'Without the right routine',
|
||||
title: 'By the end of the day, everything feels harder than it should.',
|
||||
label: 'Without Goodwalk',
|
||||
title: 'The evening pays for the day.',
|
||||
body:
|
||||
'Your dog still has energy to burn, you are carrying guilt through the workday, and home does not feel as calm as it could.',
|
||||
"The dog is wired. You're tired. Home feels like the third shift.",
|
||||
points: [
|
||||
'A dog who is restless, wired, or harder to settle at home',
|
||||
'A workday shaped by guilt, logistics, and wondering how they are doing',
|
||||
'Too much uncertainty around who is picking up your dog and what the walk will be like'
|
||||
"A dog who can't settle",
|
||||
'A workday full of guilt',
|
||||
"A walker you're never quite sure about"
|
||||
],
|
||||
footer: 'The walk is not really the point. The evening is.'
|
||||
},
|
||||
{
|
||||
label: 'With Goodwalk',
|
||||
title: 'A good walk changes you & your dogs whole evening.',
|
||||
title: 'The evening pays you back.',
|
||||
body:
|
||||
'Your dog comes home happier, the routine feels lighter, and you are not spending the day second-guessing whether they are okay.',
|
||||
"The dog is tired. The home is quiet. The workday isn't carrying guilt.",
|
||||
points: [
|
||||
'A walker your dog recognises and is happy to see at the door',
|
||||
'Small-group or one-on-one care that genuinely suits your dog',
|
||||
'Clear updates, calmer evenings, and one less thing sitting on your mind'
|
||||
'A walker your dog knows',
|
||||
'Small dogs walking with small dogs',
|
||||
'A workday with one less thing on it'
|
||||
],
|
||||
footer: 'That is what people are really buying: peace of mind, routine, and a dog who feels cared for.'
|
||||
footer: 'Peace of mind, in dog form.'
|
||||
}
|
||||
];
|
||||
const clientPhotos = [
|
||||
@@ -87,11 +87,7 @@
|
||||
<section id="values" use:reveal={{ delay: 30 }} class="reveal-block">
|
||||
<div class="values-inner">
|
||||
<div class="section-header">
|
||||
<span class="eyebrow values-eyebrow">Why people come to us</span>
|
||||
<h2 class="section-heading">Calmer dogs. Better routines. The Tiny Gang effect.</h2>
|
||||
<p class="section-intro values-intro">
|
||||
Goodwalk was created for busy owners who want reliable, relationship-led care their dog genuinely looks forward to.
|
||||
</p>
|
||||
<h2 class="section-heading">Calmer dogs. Calmer evenings.</h2>
|
||||
</div>
|
||||
|
||||
<div class="values-photo-grid" aria-label="Goodwalk client dogs">
|
||||
@@ -154,11 +150,7 @@
|
||||
</div>
|
||||
|
||||
<div class="values-points-header">
|
||||
<span class="eyebrow values-eyebrow">What we stand for</span>
|
||||
<h3 class="values-points-title">The values behind every walk</h3>
|
||||
<p class="values-points-intro">
|
||||
Kind handling, small groups, proper safety training, and honest communication — not extras, just how Goodwalk works.
|
||||
</p>
|
||||
<h3 class="values-points-title">Things we don't treat as extras</h3>
|
||||
</div>
|
||||
|
||||
<div class="values-bento values-points">
|
||||
@@ -194,18 +186,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.values-eyebrow {
|
||||
width: fit-content;
|
||||
padding: 6px 12px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--surface-brand-soft);
|
||||
box-shadow: var(--shadow-inset-strong);
|
||||
}
|
||||
|
||||
.values-intro {
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
/* ── Client photo gallery ── */
|
||||
.values-photo-grid {
|
||||
display: grid;
|
||||
@@ -462,7 +442,7 @@
|
||||
|
||||
.values-points-title {
|
||||
max-width: 19ch;
|
||||
margin: 12px auto 0;
|
||||
margin: 0 auto;
|
||||
font-family: var(--font-head);
|
||||
font-size: clamp(24px, 2.4vw, 32px);
|
||||
font-weight: 700;
|
||||
@@ -471,14 +451,6 @@
|
||||
color: var(--text-heading);
|
||||
}
|
||||
|
||||
.values-points-intro {
|
||||
max-width: 560px;
|
||||
margin: 14px auto 0;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--body-copy-size);
|
||||
line-height: 1.65;
|
||||
}
|
||||
|
||||
/* ── Values points ── */
|
||||
.values-points {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
@@ -546,16 +518,6 @@
|
||||
padding: 0 var(--space-container-x-mobile);
|
||||
}
|
||||
|
||||
.values-intro {
|
||||
max-width: 32ch;
|
||||
}
|
||||
|
||||
.values-eyebrow {
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.values-photo-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-rows: auto;
|
||||
@@ -641,12 +603,6 @@
|
||||
font-size: clamp(22px, 6.4vw, 27px);
|
||||
}
|
||||
|
||||
.values-points-intro {
|
||||
max-width: 36ch;
|
||||
font-size: var(--body-lead-size-mobile);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.values-points {
|
||||
grid-template-columns: 1fr;
|
||||
margin-top: 20px;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import type { Picture } from '@sveltejs/enhanced-img';
|
||||
|
||||
const desktop = logoDesktop as Picture;
|
||||
const ownerEmail = 'info@goodwalk.co.nz';
|
||||
const allClientsPageSize = 12;
|
||||
const birthdaysPageSize = 12;
|
||||
|
||||
@@ -54,8 +53,23 @@
|
||||
let isAuthenticated = false;
|
||||
let userEmail = '';
|
||||
let accessDenied = false;
|
||||
let ownerEmail = 'info@goodwalk.co.nz';
|
||||
|
||||
let activeTab: 'home' | 'clients' | 'birthdays' | 'messaging' = 'home';
|
||||
let activeTab: 'home' | 'clients' | 'birthdays' | 'messaging' | 'activity' = 'home';
|
||||
|
||||
type ActivityEvent = {
|
||||
id: number;
|
||||
createdAt: string | null;
|
||||
requestId: string | null;
|
||||
eventType: string;
|
||||
actorEmail: string | null;
|
||||
ip: string | null;
|
||||
status: string | null;
|
||||
detail: Record<string, unknown>;
|
||||
};
|
||||
let activityEvents: ActivityEvent[] = [];
|
||||
let activityLoading = false;
|
||||
let activityError = '';
|
||||
|
||||
let loadingHome = false;
|
||||
let homeError = '';
|
||||
@@ -256,7 +270,8 @@
|
||||
const data = await res.json();
|
||||
isAuthenticated = true;
|
||||
userEmail = data.email;
|
||||
accessDenied = data.email !== ownerEmail;
|
||||
ownerEmail = typeof data.ownerEmail === 'string' && data.ownerEmail ? data.ownerEmail : ownerEmail;
|
||||
accessDenied = !data.cpAdmin;
|
||||
if (!accessDenied) {
|
||||
await Promise.all([fetchHome(), fetchAllClients(1), fetchBirthdays(1)]);
|
||||
}
|
||||
@@ -270,10 +285,11 @@
|
||||
authChecking = false;
|
||||
}
|
||||
|
||||
function handleAuthenticated(event: CustomEvent<{ email: string }>) {
|
||||
function handleAuthenticated(event: CustomEvent<{ email: string; cpAdmin?: boolean; ownerEmail?: string }>) {
|
||||
isAuthenticated = true;
|
||||
userEmail = event.detail.email;
|
||||
accessDenied = event.detail.email !== ownerEmail;
|
||||
ownerEmail = event.detail.ownerEmail || ownerEmail;
|
||||
accessDenied = !event.detail.cpAdmin;
|
||||
if (!accessDenied) {
|
||||
void Promise.all([fetchHome(), fetchAllClients(1), fetchBirthdays(1)]);
|
||||
}
|
||||
@@ -487,13 +503,52 @@
|
||||
}
|
||||
}
|
||||
|
||||
function setTab(tab: 'home' | 'clients' | 'birthdays' | 'messaging') {
|
||||
function setTab(tab: 'home' | 'clients' | 'birthdays' | 'messaging' | 'activity') {
|
||||
activeTab = tab;
|
||||
resetComposer();
|
||||
birthdayActionError = '';
|
||||
if (tab === 'messaging' && !messageTemplates.length) {
|
||||
void fetchMessageTemplates();
|
||||
}
|
||||
if (tab === 'activity') {
|
||||
void fetchActivity();
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchActivity() {
|
||||
const token = getToken();
|
||||
if (!token) return;
|
||||
activityLoading = true;
|
||||
activityError = '';
|
||||
try {
|
||||
const res = await fetch('/api/owner/activity?limit=200', {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!res.ok) throw new Error(`Activity feed failed (${res.status}).`);
|
||||
const json = await res.json();
|
||||
activityEvents = Array.isArray(json.events) ? json.events : [];
|
||||
} catch (error) {
|
||||
activityError = error instanceof Error ? error.message : 'Could not load activity.';
|
||||
} finally {
|
||||
activityLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatEventTime(iso: string | null): string {
|
||||
if (!iso) return '—';
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString('en-NZ', { dateStyle: 'medium', timeStyle: 'short' });
|
||||
} catch {
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
function formatEventDetail(detail: Record<string, unknown>): string {
|
||||
if (!detail || Object.keys(detail).length === 0) return '';
|
||||
return Object.entries(detail)
|
||||
.map(([k, v]) => `${k}: ${Array.isArray(v) ? v.join(', ') : String(v)}`)
|
||||
.join(' · ');
|
||||
}
|
||||
|
||||
type MessageTemplate = {
|
||||
@@ -754,6 +809,10 @@
|
||||
title: 'Messaging',
|
||||
description: 'Send themed bulk or individual emails to clients via BCC.',
|
||||
},
|
||||
activity: {
|
||||
title: 'Activity',
|
||||
description: 'Audit log of contact-form submissions, logins, and owner actions.',
|
||||
},
|
||||
} as const;
|
||||
|
||||
$: header = tabHeaders[activeTab];
|
||||
@@ -804,6 +863,10 @@
|
||||
<Icon name="fas fa-envelope-open-text" />
|
||||
<span>Messaging</span>
|
||||
</button>
|
||||
<button type="button" class:owner-topbar-tab-active={activeTab === 'activity'} class="owner-topbar-tab" on:click={() => setTab('activity')}>
|
||||
<Icon name="fas fa-clock-rotate-left" />
|
||||
<span>Activity</span>
|
||||
</button>
|
||||
</nav>
|
||||
{/if}
|
||||
|
||||
@@ -829,7 +892,7 @@
|
||||
{#if accessDenied}
|
||||
<div class="owner-message-card">
|
||||
<h2>Owner access only.</h2>
|
||||
<p>This screen is restricted to <strong>{ownerEmail}</strong>.</p>
|
||||
<p>This screen is restricted to configured CP admin emails.</p>
|
||||
</div>
|
||||
{:else}
|
||||
{#if activeTab === 'home'}
|
||||
@@ -1340,6 +1403,47 @@
|
||||
</aside>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if activeTab === 'activity'}
|
||||
<div class="activity-panel">
|
||||
<div class="activity-head">
|
||||
<h2>Recent activity</h2>
|
||||
<button type="button" class="activity-refresh" on:click={() => void fetchActivity()} disabled={activityLoading}>
|
||||
<Icon name="fas fa-arrows-rotate" />
|
||||
{activityLoading ? 'Refreshing…' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
{#if activityError}
|
||||
<p class="activity-error">{activityError}</p>
|
||||
{/if}
|
||||
{#if activityLoading && !activityEvents.length}
|
||||
<p class="activity-status">Loading…</p>
|
||||
{:else if !activityEvents.length}
|
||||
<p class="activity-status">No activity recorded yet.</p>
|
||||
{:else}
|
||||
<table class="activity-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>When</th>
|
||||
<th>Event</th>
|
||||
<th>Actor</th>
|
||||
<th>Status</th>
|
||||
<th>Detail</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each activityEvents as ev (ev.id)}
|
||||
<tr>
|
||||
<td class="activity-time">{formatEventTime(ev.createdAt)}</td>
|
||||
<td class="activity-type">{ev.eventType}</td>
|
||||
<td class="activity-actor">{ev.actorEmail || '—'}</td>
|
||||
<td class="activity-status-cell" data-status={ev.status || ''}>{ev.status || '—'}</td>
|
||||
<td class="activity-detail">{formatEventDetail(ev.detail)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<nav class="owner-bottom-nav" aria-label="Owner sections">
|
||||
@@ -1359,6 +1463,10 @@
|
||||
<Icon name="fas fa-envelope-open-text" />
|
||||
<span>Messaging</span>
|
||||
</button>
|
||||
<button type="button" class:owner-bottom-tab-active={activeTab === 'activity'} class="owner-bottom-tab" on:click={() => setTab('activity')}>
|
||||
<Icon name="fas fa-clock-rotate-left" />
|
||||
<span>Activity</span>
|
||||
</button>
|
||||
</nav>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -2773,4 +2881,89 @@
|
||||
gap: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Activity tab ── */
|
||||
.activity-panel {
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
box-shadow: 0 1px 3px rgba(33, 48, 33, 0.06);
|
||||
}
|
||||
|
||||
.activity-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.activity-head h2 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: var(--gw-green);
|
||||
}
|
||||
|
||||
.activity-refresh {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(33, 48, 33, 0.14);
|
||||
background: #fff;
|
||||
color: var(--gw-green);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.activity-refresh:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.activity-error {
|
||||
background: #fde9e6;
|
||||
color: #8a2b22;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.activity-status {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.activity-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.activity-table th,
|
||||
.activity-table td {
|
||||
text-align: left;
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid #eeeee8;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.activity-table th {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: #888;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.activity-time { color: #555; white-space: nowrap; }
|
||||
.activity-type { font-family: Menlo, Consolas, monospace; color: var(--gw-green); }
|
||||
.activity-actor { color: #444; }
|
||||
.activity-status-cell { font-weight: 600; }
|
||||
.activity-status-cell[data-status="ok"] { color: #2d7a2d; }
|
||||
.activity-status-cell[data-status="ignored"] { color: #b87800; }
|
||||
.activity-status-cell[data-status="partial"] { color: #b87800; }
|
||||
.activity-detail { color: #666; max-width: 480px; word-break: break-word; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user