Build public home, contact, and content pages
This commit is contained in:
108
src/app/[slug]/page.tsx
Normal file
108
src/app/[slug]/page.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { Section } from '@/components/Section';
|
||||||
|
import { contentPages, getContentPage, site } from '@/lib/site';
|
||||||
|
|
||||||
|
type ContentPageProps = {
|
||||||
|
params: Promise<{
|
||||||
|
slug: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return contentPages.map((page) => ({ slug: page.slug }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
||||||
|
const { slug } = await params;
|
||||||
|
const page = getContentPage(slug);
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
return {
|
||||||
|
title: site.name,
|
||||||
|
description: site.description,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: page.seoTitle,
|
||||||
|
description: page.seoDescription,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ContentPage({ params }: ContentPageProps) {
|
||||||
|
const { slug } = await params;
|
||||||
|
const page = getContentPage(slug);
|
||||||
|
|
||||||
|
if (!page) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="page-hero">
|
||||||
|
<p className="brand-kicker">Content page</p>
|
||||||
|
<h2>{page.title}</h2>
|
||||||
|
<p>{page.intro}</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="page-layout">
|
||||||
|
<Section
|
||||||
|
eyebrow="Editable content"
|
||||||
|
title="Rendered from the shared content page model"
|
||||||
|
description="This route keeps editorial and policy pages consistent without making each one a special-case template."
|
||||||
|
>
|
||||||
|
<div className="content-stack">
|
||||||
|
{page.sections.map((section) => (
|
||||||
|
<article key={section.title} className="content-card">
|
||||||
|
<h3>{section.title}</h3>
|
||||||
|
{section.paragraphs.map((paragraph) => (
|
||||||
|
<p key={paragraph}>{paragraph}</p>
|
||||||
|
))}
|
||||||
|
{section.bullets ? (
|
||||||
|
<ul>
|
||||||
|
{section.bullets.map((bullet) => (
|
||||||
|
<li key={bullet}>{bullet}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<aside className="content-sidebar">
|
||||||
|
<article className="content-card">
|
||||||
|
<p className="footer-label">Next move</p>
|
||||||
|
<p className="mb-0">
|
||||||
|
This page can now be expanded with real CMS or database content later without changing the route structure.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article className="content-card">
|
||||||
|
<h3>Browse other pages</h3>
|
||||||
|
<ul className="link-list">
|
||||||
|
{contentPages
|
||||||
|
.filter((item) => item.slug !== slug)
|
||||||
|
.slice(0, 4)
|
||||||
|
.map((item) => (
|
||||||
|
<li key={item.slug}>
|
||||||
|
<Link href={`/${item.slug}`}>{item.title}</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
<article className="content-card">
|
||||||
|
<h3>Need help?</h3>
|
||||||
|
<p className="mb-0">
|
||||||
|
<Link className="inline-link" href="/contact">
|
||||||
|
Open the contact page
|
||||||
|
</Link>{' '}
|
||||||
|
if the content page does not answer your question.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
92
src/app/contact/page.tsx
Normal file
92
src/app/contact/page.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import type { Metadata } from 'next';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { Section } from '@/components/Section';
|
||||||
|
import { site } from '@/lib/site';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: `Contact | ${site.name}`,
|
||||||
|
description: 'Get in touch about a holiday stay, a booking question, or a property enquiry.',
|
||||||
|
};
|
||||||
|
|
||||||
|
const reasons = [
|
||||||
|
'Ask about a property before the booking engine is live',
|
||||||
|
'Request more details about a location or house rules',
|
||||||
|
'Raise a special requirement or accessibility question',
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ContactPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<section className="page-hero">
|
||||||
|
<p className="brand-kicker">Contact</p>
|
||||||
|
<h2>Talk to the team before you book</h2>
|
||||||
|
<p>
|
||||||
|
The contact page gives guests a direct path to the business with the core information they need in one place.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="page-layout">
|
||||||
|
<Section
|
||||||
|
eyebrow="Enquiry form"
|
||||||
|
title="Send a message"
|
||||||
|
description="This is the public contact entry point for questions that do not need a booking decision yet."
|
||||||
|
>
|
||||||
|
<form className="contact-form">
|
||||||
|
<label>
|
||||||
|
<span>Name</span>
|
||||||
|
<input type="text" name="name" placeholder="Your name" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Email</span>
|
||||||
|
<input type="email" name="email" placeholder="you@example.com" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Phone</span>
|
||||||
|
<input type="tel" name="phone" placeholder="Optional phone number" />
|
||||||
|
</label>
|
||||||
|
<label className="contact-form-message">
|
||||||
|
<span>Message</span>
|
||||||
|
<textarea name="message" rows={6} placeholder="Tell us a little about your stay or question." />
|
||||||
|
</label>
|
||||||
|
<button className="btn btn-dark" type="button">
|
||||||
|
Send enquiry
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<aside className="contact-aside">
|
||||||
|
<article className="content-card">
|
||||||
|
<p className="footer-label">Direct contact</p>
|
||||||
|
<h3>{site.contact.email}</h3>
|
||||||
|
<p>{site.contact.phone}</p>
|
||||||
|
<p className="mb-0">{site.contact.area}</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="content-card">
|
||||||
|
<h3>What to ask about</h3>
|
||||||
|
<ul>
|
||||||
|
{reasons.map((reason) => (
|
||||||
|
<li key={reason}>{reason}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="content-card">
|
||||||
|
<h3>Useful links</h3>
|
||||||
|
<ul className="link-list">
|
||||||
|
<li>
|
||||||
|
<Link href="/about">About</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/faqs">FAQs</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/local-area">Local area guide</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -182,21 +182,71 @@ main {
|
|||||||
padding-inline: 1.1rem;
|
padding-inline: 1.1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-points {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.55rem;
|
||||||
|
margin: 1.5rem 0 0;
|
||||||
|
padding-left: 1.1rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
.hero-panel {
|
.hero-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-panel {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.85rem;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 1.25rem;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border: 1px solid rgba(26, 23, 20, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-field {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-field input,
|
||||||
|
.contact-form input,
|
||||||
|
.contact-form textarea {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid rgba(26, 23, 20, 0.14);
|
||||||
|
border-radius: 0.9rem;
|
||||||
|
padding: 0.8rem 0.95rem;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
color: var(--body-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-field input:focus,
|
||||||
|
.contact-form input:focus,
|
||||||
|
.contact-form textarea:focus {
|
||||||
|
outline: 2px solid rgba(122, 84, 61, 0.28);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
.info-card,
|
.info-card,
|
||||||
.phase-card,
|
.phase-card,
|
||||||
.data-card {
|
.data-card,
|
||||||
|
.content-card,
|
||||||
|
.property-card,
|
||||||
|
.testimonial-card {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-grid,
|
.metric-grid,
|
||||||
.phase-grid,
|
.phase-grid,
|
||||||
.data-grid {
|
.data-grid,
|
||||||
|
.property-grid,
|
||||||
|
.content-grid,
|
||||||
|
.testimonial-grid,
|
||||||
|
.card-stack,
|
||||||
|
.content-stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
@@ -249,6 +299,192 @@ main {
|
|||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.property-grid {
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.9rem;
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 1.35rem;
|
||||||
|
background: rgba(255, 255, 255, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-card-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-card h3,
|
||||||
|
.content-card h3,
|
||||||
|
.testimonial-card strong {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-price {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.35rem 0.7rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(46, 102, 97, 0.12);
|
||||||
|
color: var(--accent-2);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-metrics div {
|
||||||
|
padding: 0.75rem;
|
||||||
|
border-radius: 0.95rem;
|
||||||
|
background: rgba(244, 239, 231, 0.88);
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-metrics dt {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property-metrics dd {
|
||||||
|
margin: 0.25rem 0 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list,
|
||||||
|
.link-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list li {
|
||||||
|
padding: 0.32rem 0.62rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(122, 84, 61, 0.12);
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-grid-tight {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card,
|
||||||
|
.testimonial-card {
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 1.35rem;
|
||||||
|
background: rgba(255, 255, 255, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card p:last-child,
|
||||||
|
.testimonial-card p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-link {
|
||||||
|
color: var(--accent-2);
|
||||||
|
text-decoration: underline;
|
||||||
|
text-underline-offset: 0.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.testimonial-card footer {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.15rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-band,
|
||||||
|
.page-hero {
|
||||||
|
border: 1px solid var(--panel-border);
|
||||||
|
border-radius: 1.5rem;
|
||||||
|
background: rgba(255, 255, 255, 0.82);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cta-band {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 1.1rem 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-hero {
|
||||||
|
margin: 1.5rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-hero h2 {
|
||||||
|
margin: 0 0 0.6rem;
|
||||||
|
font-size: clamp(2rem, 4vw, 3.5rem);
|
||||||
|
line-height: 0.98;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.4fr) minmax(300px, 0.75fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-layout .section-shell {
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form label {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.35rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-form-message {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-aside {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
align-content: start;
|
||||||
|
padding: 1.5rem 1.5rem 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-sidebar {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
.site-footer {
|
.site-footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -264,15 +500,24 @@ main {
|
|||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.hero,
|
.hero,
|
||||||
.phase-grid,
|
.phase-grid,
|
||||||
.data-grid {
|
.data-grid,
|
||||||
|
.property-grid,
|
||||||
|
.content-grid,
|
||||||
|
.testimonial-grid,
|
||||||
|
.page-layout {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.site-header,
|
.site-header,
|
||||||
.site-footer {
|
.site-footer,
|
||||||
|
.cta-band {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-aside {
|
||||||
|
padding: 0 1.5rem 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
@@ -287,8 +532,16 @@ main {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-hero {
|
||||||
|
margin: 0.75rem;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.metric-grid {
|
.metric-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
.property-metrics {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
278
src/app/page.tsx
278
src/app/page.tsx
@@ -1,38 +1,23 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
import { Section } from '@/components/Section';
|
import { Section } from '@/components/Section';
|
||||||
import { site } from '@/lib/site';
|
import {
|
||||||
|
featuredProperties,
|
||||||
|
locationHighlights,
|
||||||
|
site,
|
||||||
|
testimonials,
|
||||||
|
} from '@/lib/site';
|
||||||
|
|
||||||
const phaseCards = [
|
const ctaPoints = [
|
||||||
{
|
'Featured stays and clear summaries',
|
||||||
title: 'Project scaffold',
|
'Location-led content for quick orientation',
|
||||||
items: ['Next.js app shell', 'Bootstrap + Sass', 'Shared layout and navigation'],
|
'A direct contact route for questions before booking',
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Foundation data model',
|
|
||||||
items: ['Prisma schema', 'Booking and payment entities', 'Content and property records'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Runtime readiness',
|
|
||||||
items: ['Health endpoint', 'Environment variables', 'Docker entrypoint'],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const stackCards = [
|
const bookingFields = [
|
||||||
{
|
{ label: 'Arrival', value: 'Choose a date' },
|
||||||
title: 'Frontend',
|
{ label: 'Departure', value: 'Choose a date' },
|
||||||
items: ['Next.js App Router', 'React 19', 'Bootstrap 5', 'Sass theme layer'],
|
{ label: 'Guests', value: '2 adults' },
|
||||||
},
|
{ label: 'Area', value: 'Coastal or rural' },
|
||||||
{
|
|
||||||
title: 'Backend',
|
|
||||||
items: ['Next.js route handlers', 'Prisma client', 'PostgreSQL'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Platform',
|
|
||||||
items: ['Docker-based environments', 'Stripe Checkout later', 'Transactional email later'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: 'Scope control',
|
|
||||||
items: ['Docs-first planning', 'Phase gates', 'Separate booking and payment state'],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function HomePage() {
|
export default function HomePage() {
|
||||||
@@ -40,55 +25,83 @@ export default function HomePage() {
|
|||||||
<>
|
<>
|
||||||
<section className="hero" id="top">
|
<section className="hero" id="top">
|
||||||
<div className="hero-copy">
|
<div className="hero-copy">
|
||||||
<p className="brand-kicker">Phase 1 foundation</p>
|
<p className="brand-kicker">Public website slice</p>
|
||||||
<h2>{site.tagline}</h2>
|
<h2>{site.tagline}</h2>
|
||||||
<p>{site.description}</p>
|
<p>{site.description}</p>
|
||||||
|
|
||||||
<div className="hero-actions">
|
<div className="hero-actions">
|
||||||
<a className="btn btn-primary" href="#foundation">
|
<Link className="btn btn-primary" href="#browse">
|
||||||
Review foundation
|
Explore featured stays
|
||||||
</a>
|
</Link>
|
||||||
<a className="btn btn-outline-dark" href="/api/health">
|
<Link className="btn btn-outline-dark" href="/contact">
|
||||||
Check health
|
Contact the team
|
||||||
</a>
|
</Link>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside className="hero-panel" aria-label="Build snapshot">
|
<ul className="hero-points">
|
||||||
|
{ctaPoints.map((point) => (
|
||||||
|
<li key={point}>{point}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside className="hero-panel" aria-label="Quick search preview">
|
||||||
<div className="info-card">
|
<div className="info-card">
|
||||||
<p className="footer-label">Current state</p>
|
<p className="footer-label">Search preview</p>
|
||||||
<strong>Scaffold created</strong>
|
<strong>Plan the right stay</strong>
|
||||||
<p className="mb-0 text-body-secondary">
|
<p className="mb-0 text-body-secondary">
|
||||||
The project now has a repo boundary, app shell, and database foundation to build on.
|
The booking flow will later use live availability and pricing. This slice keeps the public browsing entry point clear.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="metric-grid">
|
<form className="search-panel" aria-label="Availability search preview">
|
||||||
<div className="metric">
|
{bookingFields.map((field) => (
|
||||||
<strong>5</strong>
|
<label key={field.label} className="search-field">
|
||||||
<span>Foundation steps queued</span>
|
<span>{field.label}</span>
|
||||||
</div>
|
<input aria-label={field.label} defaultValue={field.value} />
|
||||||
<div className="metric">
|
</label>
|
||||||
<strong>1</strong>
|
))}
|
||||||
<span>Health endpoint</span>
|
<button className="btn btn-dark" type="button">
|
||||||
</div>
|
Check availability
|
||||||
</div>
|
</button>
|
||||||
|
</form>
|
||||||
</aside>
|
</aside>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
id="foundation"
|
id="browse"
|
||||||
eyebrow="What is in place"
|
eyebrow="Featured stays"
|
||||||
title="Foundation work starts here"
|
title="A few properties guests can imagine themselves in"
|
||||||
description="The approved planning docs are already complete, so the first build slice is the shared platform foundation rather than a feature page."
|
description="The homepage gives the browsing experience enough shape to be useful before the dedicated listing and detail pages arrive."
|
||||||
>
|
>
|
||||||
<div className="phase-grid">
|
<div className="property-grid">
|
||||||
{phaseCards.map((card) => (
|
{featuredProperties.map((property) => (
|
||||||
<article key={card.title} className="phase-card">
|
<article key={property.slug} className="property-card">
|
||||||
<h3>{card.title}</h3>
|
<div className="property-card-top">
|
||||||
<ul>
|
<div>
|
||||||
{card.items.map((item) => (
|
<p className="footer-label">{property.area}</p>
|
||||||
<li key={item}>{item}</li>
|
<h3>{property.name}</h3>
|
||||||
|
</div>
|
||||||
|
<span className="property-price">{property.priceFrom}</span>
|
||||||
|
</div>
|
||||||
|
<p>{property.summary}</p>
|
||||||
|
<dl className="property-metrics">
|
||||||
|
<div>
|
||||||
|
<dt>Sleeps</dt>
|
||||||
|
<dd>{property.sleeps}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Bedrooms</dt>
|
||||||
|
<dd>{property.bedrooms}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt>Bathrooms</dt>
|
||||||
|
<dd>{property.bathrooms}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
<ul className="tag-list">
|
||||||
|
{property.tags.map((tag) => (
|
||||||
|
<li key={tag}>{tag}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article>
|
||||||
@@ -97,57 +110,130 @@ export default function HomePage() {
|
|||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
id="stack"
|
id="story"
|
||||||
eyebrow="Technical direction"
|
eyebrow="About the business"
|
||||||
title="The implementation stack is locked"
|
title="Editorial content keeps the journey understandable"
|
||||||
description="The app is set up to match the approved planning docs: Next.js, TypeScript, Bootstrap 5, Sass, PostgreSQL, Prisma, and Docker-based environments."
|
description="The homepage introduces the business, then lets the page hierarchy handle deeper detail. That keeps the first visit focused and low friction."
|
||||||
>
|
>
|
||||||
<div className="data-grid">
|
<div className="content-grid">
|
||||||
{stackCards.map((card) => (
|
<article className="content-card">
|
||||||
<article key={card.title} className="data-card">
|
<h3>What the site should do</h3>
|
||||||
<h3>{card.title}</h3>
|
<p>
|
||||||
|
Guests should understand the style of stay, the practical details, and the next step without having to hunt through the UI.
|
||||||
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
{card.items.map((item) => (
|
<li>Present the booking business clearly</li>
|
||||||
<li key={item}>{item}</li>
|
<li>Reduce uncertainty before the enquiry step</li>
|
||||||
))}
|
<li>Keep public content editable as the site grows</li>
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<article className="content-card">
|
||||||
|
<h3>How this slice is framed</h3>
|
||||||
|
<p>
|
||||||
|
The public website now has a real homepage, contact page, and editable content-page route so the next tickets can add richer booking behavior.
|
||||||
|
</p>
|
||||||
|
<Link className="inline-link" href="/about">
|
||||||
|
Read the About page
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section
|
||||||
|
id="places"
|
||||||
|
eyebrow="Location highlights"
|
||||||
|
title="The site can now speak about place, not just property"
|
||||||
|
description="Guests often choose by location first, so the homepage includes content that makes the area feel legible before search and pricing are wired up."
|
||||||
|
>
|
||||||
|
<div className="card-stack">
|
||||||
|
{locationHighlights.map((item) => (
|
||||||
|
<article key={item.title} className="content-card">
|
||||||
|
<h3>{item.title}</h3>
|
||||||
|
<p>{item.body}</p>
|
||||||
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
id="data"
|
id="stories"
|
||||||
eyebrow="Data model"
|
eyebrow="Guest feedback"
|
||||||
title="The first schema pass is ready"
|
title="Testimonials and trust signals belong on the public page"
|
||||||
description="The initial Prisma schema covers the core content, property, booking, payment, and site settings entities so later screens can use real records instead of placeholders."
|
description="The marketing layer needs a few trust cues so the journey feels deliberate instead of empty scaffold."
|
||||||
>
|
>
|
||||||
<div className="data-card">
|
<div className="testimonial-grid">
|
||||||
<h3>Seeded core entities</h3>
|
{testimonials.map((testimonial) => (
|
||||||
<ul>
|
<blockquote key={testimonial.author} className="testimonial-card">
|
||||||
{site.highlights.map((item) => (
|
<p>{testimonial.quote}</p>
|
||||||
<li key={item}>{item}</li>
|
<footer>
|
||||||
|
<strong>{testimonial.author}</strong>
|
||||||
|
<span>{testimonial.location}</span>
|
||||||
|
</footer>
|
||||||
|
</blockquote>
|
||||||
))}
|
))}
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
<Section
|
<Section
|
||||||
id="launch"
|
id="content"
|
||||||
eyebrow="Next move"
|
eyebrow="Editable pages"
|
||||||
title="Ready for the first implementation slice"
|
title="About, FAQs, local area, and policy pages are now routable"
|
||||||
description="From here, the next work is to connect the app to real data and start the public browsing flow. The scaffold avoids guessing at booking behavior until the later docs are turned into screens."
|
description="The content-page route can render the editorial and policy pages the public site needs without building each one as a special case."
|
||||||
>
|
>
|
||||||
<div className="data-card">
|
<div className="content-grid content-grid-tight">
|
||||||
<h3>Immediate follow-up</h3>
|
<article className="content-card">
|
||||||
<ul>
|
<h3>Available pages</h3>
|
||||||
<li>Connect Prisma to a real Postgres instance.</li>
|
<ul className="link-list">
|
||||||
<li>Seed the first property and content records.</li>
|
<li>
|
||||||
<li>Start the public homepage and listing screens.</li>
|
<Link href="/about">About</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/local-area">Local area guide</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/faqs">FAQs</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/terms-and-conditions">Terms and conditions</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/privacy-policy">Privacy policy</Link>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article className="content-card">
|
||||||
|
<h3>What comes next</h3>
|
||||||
|
<p>
|
||||||
|
The next tickets can now focus on the property listing and property detail pages while the public content layer stays reusable.
|
||||||
|
</p>
|
||||||
|
<Link className="inline-link" href="/contact">
|
||||||
|
Enquire through the contact page
|
||||||
|
</Link>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Section
|
||||||
|
id="contact"
|
||||||
|
eyebrow="Next step"
|
||||||
|
title="A clear contact route is already live"
|
||||||
|
description="If a guest is not ready to book, the site now gives them a proper contact path rather than forcing a dead end."
|
||||||
|
>
|
||||||
|
<div className="cta-band">
|
||||||
|
<div>
|
||||||
|
<p className="footer-label">Contact details</p>
|
||||||
|
<h3>{site.contact.email}</h3>
|
||||||
|
<p className="mb-0">
|
||||||
|
{site.contact.phone} • {site.contact.area}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Link className="btn btn-dark" href="/contact">
|
||||||
|
Open contact page
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
import { site } from '@/lib/site';
|
||||||
|
|
||||||
export function SiteFooter() {
|
export function SiteFooter() {
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
|
|
||||||
@@ -5,7 +8,7 @@ export function SiteFooter() {
|
|||||||
<footer className="site-footer">
|
<footer className="site-footer">
|
||||||
<div>
|
<div>
|
||||||
<p className="footer-label">Status</p>
|
<p className="footer-label">Status</p>
|
||||||
<p>Phase 1 scaffold underway.</p>
|
<p>{site.tagline}</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="footer-label">Build</p>
|
<p className="footer-label">Build</p>
|
||||||
@@ -14,7 +17,14 @@ export function SiteFooter() {
|
|||||||
{process.env.NEXT_PUBLIC_BUILD_ITERATION || '0'}
|
{process.env.NEXT_PUBLIC_BUILD_ITERATION || '0'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="footer-label">Contact</p>
|
||||||
|
<p>
|
||||||
|
<Link href="/contact">{site.contact.email}</Link>
|
||||||
|
<br />
|
||||||
|
{site.contact.phone}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,26 @@
|
|||||||
const navItems = [
|
import Link from 'next/link';
|
||||||
{ href: '#foundation', label: 'Foundation' },
|
import { primaryNavigation, site } from '@/lib/site';
|
||||||
{ href: '#stack', label: 'Stack' },
|
|
||||||
{ href: '#data', label: 'Data model' },
|
|
||||||
{ href: '#launch', label: 'Launch' },
|
|
||||||
];
|
|
||||||
|
|
||||||
export function SiteHeader() {
|
export function SiteHeader() {
|
||||||
return (
|
return (
|
||||||
<header className="site-header">
|
<header className="site-header">
|
||||||
<div className="brand-lockup">
|
<div className="brand-lockup">
|
||||||
<span className="brand-mark">HPB</span>
|
<Link className="brand-mark" href="/" aria-label={site.name}>
|
||||||
|
HPB
|
||||||
|
</Link>
|
||||||
<div>
|
<div>
|
||||||
<p className="brand-kicker">Project scaffold</p>
|
<p className="brand-kicker">Project scaffold</p>
|
||||||
<h1>Holiday Property Booking</h1>
|
<h1>{site.name}</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav aria-label="Primary" className="site-nav">
|
<nav aria-label="Primary" className="site-nav">
|
||||||
{navItems.map((item) => (
|
{primaryNavigation.map((item) => (
|
||||||
<a key={item.href} href={item.href}>
|
<Link key={item.href} href={item.href}>
|
||||||
{item.label}
|
{item.label}
|
||||||
</a>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
220
src/lib/site.ts
220
src/lib/site.ts
@@ -1,20 +1,224 @@
|
|||||||
|
export type FeaturedProperty = {
|
||||||
|
slug: string;
|
||||||
|
name: string;
|
||||||
|
area: string;
|
||||||
|
summary: string;
|
||||||
|
sleeps: number;
|
||||||
|
bedrooms: number;
|
||||||
|
bathrooms: number;
|
||||||
|
priceFrom: string;
|
||||||
|
tags: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Testimonial = {
|
||||||
|
quote: string;
|
||||||
|
author: string;
|
||||||
|
location: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ContentSection = {
|
||||||
|
title: string;
|
||||||
|
paragraphs: string[];
|
||||||
|
bullets?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ContentPage = {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
intro: string;
|
||||||
|
seoTitle: string;
|
||||||
|
seoDescription: string;
|
||||||
|
sections: ContentSection[];
|
||||||
|
};
|
||||||
|
|
||||||
export const site = {
|
export const site = {
|
||||||
name: 'Holiday Property Booking',
|
name: 'Holiday Property Booking',
|
||||||
tagline: 'Direct holiday stays with visible availability and no guesswork.',
|
tagline: 'Direct holiday stays with visible availability and no guesswork.',
|
||||||
description:
|
description:
|
||||||
'A booking platform scaffold for a multi-property holiday business. Built for browsing, booking, and admin operations.',
|
'Browse holiday homes, check the essentials, and move from inspiration to enquiry without losing the thread.',
|
||||||
|
contact: {
|
||||||
|
email: 'hello@example.com',
|
||||||
|
phone: '+44 20 7946 0123',
|
||||||
|
area: 'North Devon, UK',
|
||||||
|
},
|
||||||
highlights: [
|
highlights: [
|
||||||
'Public browsing and search',
|
'Public browsing and search',
|
||||||
'Live availability and pricing rules',
|
'Live availability and pricing rules',
|
||||||
'Stripe checkout and confirmation flow',
|
'Stripe checkout and confirmation flow',
|
||||||
'Admin-ready data model',
|
'Admin-ready data model',
|
||||||
],
|
],
|
||||||
foundationSteps: [
|
foundationSteps: ['Project scaffold', 'Environment configuration', 'Database schema', 'App shell', 'Health endpoint'],
|
||||||
'Project scaffold',
|
|
||||||
'Environment configuration',
|
|
||||||
'Database schema',
|
|
||||||
'App shell',
|
|
||||||
'Health endpoint',
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const primaryNavigation = [
|
||||||
|
{ href: '/', label: 'Home' },
|
||||||
|
{ href: '/about', label: 'About' },
|
||||||
|
{ href: '/faqs', label: 'FAQs' },
|
||||||
|
{ href: '/contact', label: 'Contact' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const featuredProperties: FeaturedProperty[] = [
|
||||||
|
{
|
||||||
|
slug: 'coastal-view-cottage',
|
||||||
|
name: 'Coastal View Cottage',
|
||||||
|
area: 'Clifftop village',
|
||||||
|
summary: 'A bright two-bedroom cottage with sea views, a private terrace, and room to slow down.',
|
||||||
|
sleeps: 4,
|
||||||
|
bedrooms: 2,
|
||||||
|
bathrooms: 2,
|
||||||
|
priceFrom: 'From £185/night',
|
||||||
|
tags: ['Sea views', 'Family-friendly', 'Short breaks'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'orchard-barn',
|
||||||
|
name: 'Orchard Barn',
|
||||||
|
area: 'Rural retreat',
|
||||||
|
summary: 'Converted barn accommodation with an open-plan living space and easy access to walking trails.',
|
||||||
|
sleeps: 6,
|
||||||
|
bedrooms: 3,
|
||||||
|
bathrooms: 2,
|
||||||
|
priceFrom: 'From £210/night',
|
||||||
|
tags: ['Pets considered', 'Hot tub', 'Long weekends'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'harbour-house',
|
||||||
|
name: 'Harbour House',
|
||||||
|
area: 'Harbour front',
|
||||||
|
summary: 'A flexible stay for couples or groups, steps from the water and close to local restaurants.',
|
||||||
|
sleeps: 5,
|
||||||
|
bedrooms: 3,
|
||||||
|
bathrooms: 1,
|
||||||
|
priceFrom: 'From £165/night',
|
||||||
|
tags: ['Walkable', 'Town stay', 'Flexible dates'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const locationHighlights = [
|
||||||
|
{
|
||||||
|
title: 'Coast and countryside',
|
||||||
|
body: 'Choose between cliff paths, harbour towns, and quiet countryside stays within a short drive of each other.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Simple trip planning',
|
||||||
|
body: 'The public journey is designed to answer the basics early: where the property is, what it sleeps, and how to enquire.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Stay-led content',
|
||||||
|
body: 'Each property page can carry the practical details guests need, including capacity, amenities, and house rules.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const testimonials: Testimonial[] = [
|
||||||
|
{
|
||||||
|
quote: 'Everything we needed was clear before we booked, which made planning the trip much easier.',
|
||||||
|
author: 'Sophie M.',
|
||||||
|
location: 'Bristol',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
quote: 'The property details answered our questions without forcing us to chase for basics.',
|
||||||
|
author: 'Daniel K.',
|
||||||
|
location: 'Manchester',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const contentPages: ContentPage[] = [
|
||||||
|
{
|
||||||
|
slug: 'about',
|
||||||
|
title: 'About Holiday Property Booking',
|
||||||
|
intro:
|
||||||
|
'Holiday Property Booking is being shaped as a direct, content-led booking site that keeps the browsing experience clear before any payment flow is added.',
|
||||||
|
seoTitle: 'About Holiday Property Booking',
|
||||||
|
seoDescription: 'Learn how the holiday booking site is structured and what the public journey will cover.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: 'What the site is for',
|
||||||
|
paragraphs: [
|
||||||
|
'The public site is intended to help guests browse properties, understand the essentials quickly, and move toward a confident enquiry or booking decision.',
|
||||||
|
'The implementation is being delivered in slices so each public page is grounded in the approved planning docs instead of being filled with placeholder booking logic too early.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'What guests should expect',
|
||||||
|
paragraphs: [
|
||||||
|
'The homepage should provide a direct route into the public journey, with feature-led content, clear calls to action, and enough detail to reduce uncertainty.',
|
||||||
|
],
|
||||||
|
bullets: ['Clear property summaries', 'Practical stay information', 'A readable route to contact and enquiry'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'faqs',
|
||||||
|
title: 'Frequently Asked Questions',
|
||||||
|
intro: 'A lightweight content page for the most common questions guests ask before they enquire or book.',
|
||||||
|
seoTitle: 'Holiday Property Booking FAQs',
|
||||||
|
seoDescription: 'Common questions about the booking journey, property information, and how to contact the business.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: 'When will live availability arrive?',
|
||||||
|
paragraphs: ['Availability and pricing will be added in the dedicated booking and pricing slices that follow this public content work.'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Can guests still enquire now?',
|
||||||
|
paragraphs: ['Yes. The public experience includes a clear contact route so guests can ask questions before the booking engine is complete.'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'What will content pages cover?',
|
||||||
|
paragraphs: ['About, local area, FAQs, terms and conditions, and privacy pages can all be rendered from the same content-page structure.'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'local-area',
|
||||||
|
title: 'Local Area Guide',
|
||||||
|
intro: 'A short editorial page that can be expanded with neighbourhood tips, transport advice, and nearby attractions.',
|
||||||
|
seoTitle: 'Local Area Guide',
|
||||||
|
seoDescription: 'Explore the local area around the featured holiday properties.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: 'Why area content matters',
|
||||||
|
paragraphs: [
|
||||||
|
'Guests often decide based on location first. The content page structure allows the business to explain the setting without overloading the homepage.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'What can be added later',
|
||||||
|
paragraphs: ['Walking routes, beaches, restaurants, transport notes, and seasonal advice can all live in this page format.'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'terms-and-conditions',
|
||||||
|
title: 'Terms and Conditions',
|
||||||
|
intro: 'A placeholder legal content page that can be replaced with the final terms text when it is approved.',
|
||||||
|
seoTitle: 'Terms and Conditions',
|
||||||
|
seoDescription: 'The terms and conditions content page for Holiday Property Booking.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: 'How this page is used',
|
||||||
|
paragraphs: [
|
||||||
|
'This route exists so the public site can render formal content pages from the same model as editorial pages.',
|
||||||
|
'The legal copy itself will be finalised later, but the page structure is now ready for it.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
slug: 'privacy-policy',
|
||||||
|
title: 'Privacy Policy',
|
||||||
|
intro: 'A structured content page for the privacy policy and future data handling notices.',
|
||||||
|
seoTitle: 'Privacy Policy',
|
||||||
|
seoDescription: 'The privacy policy content page for Holiday Property Booking.',
|
||||||
|
sections: [
|
||||||
|
{
|
||||||
|
title: 'Why it exists now',
|
||||||
|
paragraphs: [
|
||||||
|
'The public content layer needs to support policy pages from the start so the site can grow into a compliant booking flow without reworking the routing model.',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function getContentPage(slug: string) {
|
||||||
|
return contentPages.find((page) => page.slug === slug);
|
||||||
|
}
|
||||||
|
|||||||
13
tests/e2e/contact.spec.ts
Normal file
13
tests/e2e/contact.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('contact page', () => {
|
||||||
|
test('renders the enquiry form and contact details', async ({ page }) => {
|
||||||
|
await page.goto('/contact');
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'Talk to the team before you book' })).toBeVisible();
|
||||||
|
await expect(page.getByLabel('Name')).toBeVisible();
|
||||||
|
await expect(page.getByLabel('Email')).toBeVisible();
|
||||||
|
await expect(page.getByLabel('Message')).toBeVisible();
|
||||||
|
await expect(page.getByRole('heading', { name: 'hello@example.com' })).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
11
tests/e2e/content-pages.spec.ts
Normal file
11
tests/e2e/content-pages.spec.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
test.describe('content pages', () => {
|
||||||
|
test('renders editorial content pages from shared data', async ({ page }) => {
|
||||||
|
await page.goto('/about');
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: 'About Holiday Property Booking' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('heading', { name: 'What the site is for' })).toBeVisible();
|
||||||
|
await expect(page.getByRole('link', { name: 'Open the contact page' })).toHaveAttribute('href', '/contact');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,23 +1,21 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
test.describe('homepage', () => {
|
test.describe('homepage', () => {
|
||||||
test('renders the phase 1 scaffold and primary navigation', async ({ page }) => {
|
test('renders the public browsing entry point and primary navigation', async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Holiday Property Booking' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Holiday Property Booking' })).toBeVisible();
|
||||||
await expect(page.getByText('Phase 1 foundation')).toBeVisible();
|
|
||||||
await expect(page.getByRole('navigation', { name: 'Primary' })).toBeVisible();
|
await expect(page.getByRole('navigation', { name: 'Primary' })).toBeVisible();
|
||||||
await expect(page.getByRole('link', { name: 'Review foundation' })).toHaveAttribute('href', '#foundation');
|
await expect(page.getByRole('link', { name: 'Explore featured stays' })).toHaveAttribute('href', '/#browse');
|
||||||
await expect(page.getByRole('link', { name: 'Check health' })).toHaveAttribute('href', '/api/health');
|
await expect(page.getByRole('link', { name: 'Contact the team' })).toHaveAttribute('href', '/contact');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shows the core planning sections', async ({ page }) => {
|
test('shows the public content sections', async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Foundation work starts here' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'A few properties guests can imagine themselves in' })).toBeVisible();
|
||||||
await expect(page.getByRole('heading', { name: 'The implementation stack is locked' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Editorial content keeps the journey understandable' })).toBeVisible();
|
||||||
await expect(page.getByRole('heading', { name: 'The first schema pass is ready' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Location highlights' })).toBeVisible();
|
||||||
await expect(page.getByRole('heading', { name: 'Ready for the first implementation slice' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'A clear contact route is already live' })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
test.describe('responsive shell', () => {
|
test.describe('responsive shell', () => {
|
||||||
test('keeps the core content visible on mobile widths', async ({ page }) => {
|
test('keeps the public content visible on mobile widths', async ({ page }) => {
|
||||||
await page.setViewportSize({ width: 390, height: 844 });
|
await page.setViewportSize({ width: 390, height: 844 });
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
await expect(page.getByRole('heading', { name: 'Holiday Property Booking' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'Holiday Property Booking' })).toBeVisible();
|
||||||
await expect(page.getByRole('heading', { name: 'Foundation work starts here' })).toBeVisible();
|
await expect(page.getByRole('heading', { name: 'A few properties guests can imagine themselves in' })).toBeVisible();
|
||||||
await expect(page.getByRole('link', { name: 'Review foundation' })).toBeVisible();
|
await expect(page.getByRole('link', { name: 'Contact the team' })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user