Design Language tweaks
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
import MobileBookBar from '$lib/components/MobileBookBar.svelte';
|
||||
import RouteSkeleton from '$lib/components/RouteSkeleton.svelte';
|
||||
import { isMobileCtaButtonEnabled } from '$lib/feature-flags';
|
||||
import readex400Woff2 from '@fontsource/readex-pro/files/readex-pro-latin-400-normal.woff2?url';
|
||||
import readex700Woff2 from '@fontsource/readex-pro/files/readex-pro-latin-700-normal.woff2?url';
|
||||
import '@fontsource/readex-pro/latin-400.css';
|
||||
import '@fontsource/readex-pro/latin-500.css';
|
||||
import '@fontsource/readex-pro/latin-600.css';
|
||||
@@ -16,6 +18,8 @@
|
||||
import '@fontsource/noto-sans/latin-400.css';
|
||||
import '@fontsource/noto-sans/latin-500.css';
|
||||
import '@fontsource/noto-sans/latin-700.css';
|
||||
import unbounded700Woff2 from '@fontsource/unbounded/files/unbounded-latin-700-normal.woff2?url';
|
||||
import unbounded800Woff2 from '@fontsource/unbounded/files/unbounded-latin-800-normal.woff2?url';
|
||||
import '@fontsource/unbounded/latin-400.css';
|
||||
import '@fontsource/unbounded/latin-600.css';
|
||||
import '@fontsource/unbounded/latin-700.css';
|
||||
@@ -36,6 +40,34 @@
|
||||
const mobileCtaButtonEnabled = isMobileCtaButtonEnabled();
|
||||
|
||||
let revealObserver: IntersectionObserver | null = null;
|
||||
const scrollStoragePrefix = 'goodwalk:scroll:';
|
||||
|
||||
function getScrollStorageKey(urlLike: string) {
|
||||
const url = new URL(urlLike, 'http://goodwalk.local');
|
||||
return `${scrollStoragePrefix}${url.pathname}${url.search}`;
|
||||
}
|
||||
|
||||
function saveScrollPosition(urlLike = window.location.href) {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
sessionStorage.setItem(getScrollStorageKey(urlLike), String(window.scrollY));
|
||||
}
|
||||
|
||||
function restoreScrollPosition(urlLike = window.location.href) {
|
||||
if (typeof window === 'undefined' || window.location.hash) return;
|
||||
|
||||
const saved = sessionStorage.getItem(getScrollStorageKey(urlLike));
|
||||
if (!saved) return;
|
||||
|
||||
const top = Number(saved);
|
||||
if (!Number.isFinite(top)) return;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
window.scrollTo({ top, left: 0, behavior: 'auto' });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function shouldAnimateAnchorBounce() {
|
||||
return typeof window !== 'undefined' && window.innerWidth > 768;
|
||||
@@ -87,12 +119,40 @@
|
||||
initClickTracking();
|
||||
requestAnimationFrame(initReveal);
|
||||
|
||||
restoreScrollPosition();
|
||||
|
||||
// Same-page hash clicks aren't caught by afterNavigate
|
||||
function onHashChange() {
|
||||
bounceSection(window.location.hash);
|
||||
}
|
||||
|
||||
function onPageHide() {
|
||||
saveScrollPosition();
|
||||
}
|
||||
|
||||
function onVisibilityChange() {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
saveScrollPosition();
|
||||
}
|
||||
}
|
||||
|
||||
function onPageShow(event: PageTransitionEvent) {
|
||||
if (event.persisted) {
|
||||
restoreScrollPosition();
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
return () => window.removeEventListener('hashchange', onHashChange);
|
||||
window.addEventListener('pagehide', onPageHide);
|
||||
window.addEventListener('pageshow', onPageShow);
|
||||
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('hashchange', onHashChange);
|
||||
window.removeEventListener('pagehide', onPageHide);
|
||||
window.removeEventListener('pageshow', onPageShow);
|
||||
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||
};
|
||||
});
|
||||
|
||||
function shouldShowSkeleton() {
|
||||
@@ -148,6 +208,13 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<link rel="preload" href={readex400Woff2} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<link rel="preload" href={readex700Woff2} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<link rel="preload" href={unbounded700Woff2} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<link rel="preload" href={unbounded800Woff2} as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
</svelte:head>
|
||||
|
||||
<svelte:body class:mobile-cta-enabled={mobileCtaButtonEnabled} />
|
||||
|
||||
<div class="layout-shell">
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import HowItWorksSection from '$lib/components/HowItWorksSection.svelte';
|
||||
import InfoSection from '$lib/components/InfoSection.svelte';
|
||||
import InstagramSection from '$lib/components/InstagramSection.svelte';
|
||||
import IntroStrip from '$lib/components/IntroStrip.svelte';
|
||||
import BookingSection from '$lib/components/BookingSection.svelte';
|
||||
import FounderStorySection from '$lib/components/FounderStorySection.svelte';
|
||||
import ServicesSection from '$lib/components/ServicesSection.svelte';
|
||||
@@ -130,13 +131,14 @@
|
||||
|
||||
<Header navigation={content.navigation} />
|
||||
<HeroSection hero={content.hero} reviewCta={content.intro.reviewCta} />
|
||||
<FounderStorySection founderStory={content.founderStory} />
|
||||
<IntroStrip intro={content.intro} />
|
||||
<ValuesSection values={content.values} />
|
||||
<ServicesSection services={content.services} />
|
||||
<HowItWorksSection content={content.howItWorks} />
|
||||
<TestimonialsSection testimonials={content.testimonials} seedKey="/" />
|
||||
<ValuesSection values={content.values} />
|
||||
<BookingSection booking={content.booking} />
|
||||
<FounderStorySection founderStory={content.founderStory} />
|
||||
<InfoSection info={content.info} />
|
||||
<BookingSection booking={content.booking} />
|
||||
<InstagramSection instagram={content.instagram} />
|
||||
<Footer footer={content.footer} />
|
||||
{/if}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import LegalPage from '$lib/components/LegalPage.svelte';
|
||||
import PageHeader from '$lib/components/PageHeader.svelte';
|
||||
import PricingPage from '$lib/components/PricingPage.svelte';
|
||||
import TestimonialsPage from '$lib/components/TestimonialsPage.svelte';
|
||||
import { aboutPageContent } from '$lib/content/about';
|
||||
import ServiceLandingPage from '$lib/components/ServiceLandingPage.svelte';
|
||||
import { dogWalkingContent } from '$lib/content/dog-walking';
|
||||
@@ -79,7 +80,7 @@
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'Service',
|
||||
name: 'Goodwalk Pack Walks',
|
||||
name: 'Goodwalk Tiny Gang Pack Walks',
|
||||
description: data.page.description,
|
||||
serviceType: 'Pack walks for small and medium dogs',
|
||||
provider: { '@id': 'https://www.goodwalk.co.nz/#business' },
|
||||
@@ -90,7 +91,7 @@
|
||||
},
|
||||
buildBreadcrumb([
|
||||
{ name: 'Home', url: siteUrl },
|
||||
{ name: 'Pack Walks', path: data.page.canonicalPath }
|
||||
{ name: 'Tiny Gang Pack Walks', path: data.page.canonicalPath }
|
||||
])
|
||||
];
|
||||
} else if (data.slug === 'dog-walking') {
|
||||
@@ -179,6 +180,23 @@
|
||||
{ name: 'About Us', path: data.page.canonicalPath }
|
||||
])
|
||||
];
|
||||
} else if (data.slug === 'testimonials') {
|
||||
seoImage = data.content.testimonials[0]?.imageUrl ?? defaultSeoImage;
|
||||
seoImageAlt = 'Happy Goodwalk client dogs in Auckland';
|
||||
pageStructuredData = [
|
||||
{
|
||||
'@context': 'https://schema.org',
|
||||
'@type': 'CollectionPage',
|
||||
name: data.page.title,
|
||||
description: data.page.description,
|
||||
url: `${siteUrl}${data.page.canonicalPath}`,
|
||||
image: absoluteUrl(seoImage)
|
||||
},
|
||||
buildBreadcrumb([
|
||||
{ name: 'Home', url: siteUrl },
|
||||
{ name: 'Testimonials', path: data.page.canonicalPath }
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -206,6 +224,8 @@
|
||||
<PricingPage content={data.content} pageContent={ourPricingContent} />
|
||||
{:else if data.slug === 'about'}
|
||||
<AboutPage pageContent={aboutPageContent} />
|
||||
{:else if data.slug === 'testimonials'}
|
||||
<TestimonialsPage content={data.content} />
|
||||
{:else if data.slug === 'terms-and-conditions'}
|
||||
<LegalPage pageContent={termsAndConditionsContent} />
|
||||
{:else if data.slug === 'privacy-policy'}
|
||||
|
||||
@@ -56,6 +56,17 @@ describe('static slug page server load', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the shared content and page metadata for the testimonials route', async () => {
|
||||
getSharedPageContent.mockResolvedValue(sharedPageContent);
|
||||
|
||||
await expect(load({ params: { slug: 'testimonials' } } as never)).resolves.toEqual({
|
||||
content: sharedPageContent,
|
||||
generalEnquiryEnabled: false,
|
||||
page: staticPages.testimonials,
|
||||
slug: 'testimonials'
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps general enquiries disabled on contact-us by default', async () => {
|
||||
getSharedPageContent.mockResolvedValue(sharedPageContent);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ describe('static slug route page', () => {
|
||||
['puppy-visits', puppyVisitsContent.hero.title],
|
||||
['our-pricing', ourPricingContent.subtitle],
|
||||
['about', aboutPageContent.sections[0].title],
|
||||
['testimonials', 'What our clients say'],
|
||||
['contact-us', "Let's meet!"],
|
||||
['terms-and-conditions', '1. Application of Terms'],
|
||||
['privacy-policy', 'How we collect your information']
|
||||
|
||||
@@ -10,9 +10,29 @@ describe('home page route', () => {
|
||||
data: createHomepageRouteData()
|
||||
});
|
||||
|
||||
const hero = document.getElementById('hero');
|
||||
const intro = document.getElementById('intro');
|
||||
const values = document.getElementById('values');
|
||||
const services = document.getElementById('services');
|
||||
const howItWorks = document.getElementById('how-it-works');
|
||||
const testimonials = document.getElementById('testimonials');
|
||||
const founderStory = document.getElementById('promise');
|
||||
const info = document.getElementById('info');
|
||||
const booking = document.getElementById('newlead');
|
||||
|
||||
expect(screen.getByText(homepageContent.hero.highlight)).toBeInTheDocument();
|
||||
expect(screen.getByText(homepageContent.intro.text)).toBeInTheDocument();
|
||||
expect(screen.getByText('Calmer dogs. Clearer routines. Less worry.')).toBeInTheDocument();
|
||||
expect(screen.getByText(homepageContent.howItWorks.title)).toBeInTheDocument();
|
||||
expect(screen.getByText(homepageContent.info.title)).toBeInTheDocument();
|
||||
expect(hero?.compareDocumentPosition(intro as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
expect(intro?.compareDocumentPosition(values as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
expect(values?.compareDocumentPosition(services as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
expect(services?.compareDocumentPosition(howItWorks as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
expect(howItWorks?.compareDocumentPosition(testimonials as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
expect(testimonials?.compareDocumentPosition(founderStory as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
expect(founderStory?.compareDocumentPosition(info as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
expect(info?.compareDocumentPosition(booking as Node)).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
||||
expect(screen.queryByLabelText(/General enquiry/i)).not.toBeInTheDocument();
|
||||
expect(document.title).toBe(homepageContent.seo.title);
|
||||
expect(document.head.innerHTML).toContain('FAQPage');
|
||||
|
||||
Reference in New Issue
Block a user