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;
|
||||
}
|
||||
|
||||
.hero-points {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
margin: 1.5rem 0 0;
|
||||
padding-left: 1.1rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.hero-panel {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
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,
|
||||
.phase-card,
|
||||
.data-card {
|
||||
.data-card,
|
||||
.content-card,
|
||||
.property-card,
|
||||
.testimonial-card {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.metric-grid,
|
||||
.phase-grid,
|
||||
.data-grid {
|
||||
.data-grid,
|
||||
.property-grid,
|
||||
.content-grid,
|
||||
.testimonial-grid,
|
||||
.card-stack,
|
||||
.content-stack {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
@@ -249,6 +299,192 @@ main {
|
||||
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 {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -264,15 +500,24 @@ main {
|
||||
@media (max-width: 900px) {
|
||||
.hero,
|
||||
.phase-grid,
|
||||
.data-grid {
|
||||
.data-grid,
|
||||
.property-grid,
|
||||
.content-grid,
|
||||
.testimonial-grid,
|
||||
.page-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.site-header,
|
||||
.site-footer {
|
||||
.site-footer,
|
||||
.cta-band {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.contact-aside {
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
@@ -287,8 +532,16 @@ main {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.page-hero {
|
||||
margin: 0.75rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.property-metrics {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
284
src/app/page.tsx
284
src/app/page.tsx
@@ -1,38 +1,23 @@
|
||||
import Link from 'next/link';
|
||||
import { Section } from '@/components/Section';
|
||||
import { site } from '@/lib/site';
|
||||
import {
|
||||
featuredProperties,
|
||||
locationHighlights,
|
||||
site,
|
||||
testimonials,
|
||||
} from '@/lib/site';
|
||||
|
||||
const phaseCards = [
|
||||
{
|
||||
title: 'Project scaffold',
|
||||
items: ['Next.js app shell', 'Bootstrap + Sass', 'Shared layout and navigation'],
|
||||
},
|
||||
{
|
||||
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 ctaPoints = [
|
||||
'Featured stays and clear summaries',
|
||||
'Location-led content for quick orientation',
|
||||
'A direct contact route for questions before booking',
|
||||
];
|
||||
|
||||
const stackCards = [
|
||||
{
|
||||
title: 'Frontend',
|
||||
items: ['Next.js App Router', 'React 19', 'Bootstrap 5', 'Sass theme layer'],
|
||||
},
|
||||
{
|
||||
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'],
|
||||
},
|
||||
const bookingFields = [
|
||||
{ label: 'Arrival', value: 'Choose a date' },
|
||||
{ label: 'Departure', value: 'Choose a date' },
|
||||
{ label: 'Guests', value: '2 adults' },
|
||||
{ label: 'Area', value: 'Coastal or rural' },
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
@@ -40,55 +25,83 @@ export default function HomePage() {
|
||||
<>
|
||||
<section className="hero" id="top">
|
||||
<div className="hero-copy">
|
||||
<p className="brand-kicker">Phase 1 foundation</p>
|
||||
<p className="brand-kicker">Public website slice</p>
|
||||
<h2>{site.tagline}</h2>
|
||||
<p>{site.description}</p>
|
||||
|
||||
<div className="hero-actions">
|
||||
<a className="btn btn-primary" href="#foundation">
|
||||
Review foundation
|
||||
</a>
|
||||
<a className="btn btn-outline-dark" href="/api/health">
|
||||
Check health
|
||||
</a>
|
||||
<Link className="btn btn-primary" href="#browse">
|
||||
Explore featured stays
|
||||
</Link>
|
||||
<Link className="btn btn-outline-dark" href="/contact">
|
||||
Contact the team
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<ul className="hero-points">
|
||||
{ctaPoints.map((point) => (
|
||||
<li key={point}>{point}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<aside className="hero-panel" aria-label="Build snapshot">
|
||||
<aside className="hero-panel" aria-label="Quick search preview">
|
||||
<div className="info-card">
|
||||
<p className="footer-label">Current state</p>
|
||||
<strong>Scaffold created</strong>
|
||||
<p className="footer-label">Search preview</p>
|
||||
<strong>Plan the right stay</strong>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div className="metric-grid">
|
||||
<div className="metric">
|
||||
<strong>5</strong>
|
||||
<span>Foundation steps queued</span>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<strong>1</strong>
|
||||
<span>Health endpoint</span>
|
||||
</div>
|
||||
</div>
|
||||
<form className="search-panel" aria-label="Availability search preview">
|
||||
{bookingFields.map((field) => (
|
||||
<label key={field.label} className="search-field">
|
||||
<span>{field.label}</span>
|
||||
<input aria-label={field.label} defaultValue={field.value} />
|
||||
</label>
|
||||
))}
|
||||
<button className="btn btn-dark" type="button">
|
||||
Check availability
|
||||
</button>
|
||||
</form>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<Section
|
||||
id="foundation"
|
||||
eyebrow="What is in place"
|
||||
title="Foundation work starts here"
|
||||
description="The approved planning docs are already complete, so the first build slice is the shared platform foundation rather than a feature page."
|
||||
id="browse"
|
||||
eyebrow="Featured stays"
|
||||
title="A few properties guests can imagine themselves in"
|
||||
description="The homepage gives the browsing experience enough shape to be useful before the dedicated listing and detail pages arrive."
|
||||
>
|
||||
<div className="phase-grid">
|
||||
{phaseCards.map((card) => (
|
||||
<article key={card.title} className="phase-card">
|
||||
<h3>{card.title}</h3>
|
||||
<ul>
|
||||
{card.items.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
<div className="property-grid">
|
||||
{featuredProperties.map((property) => (
|
||||
<article key={property.slug} className="property-card">
|
||||
<div className="property-card-top">
|
||||
<div>
|
||||
<p className="footer-label">{property.area}</p>
|
||||
<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>
|
||||
</article>
|
||||
@@ -97,57 +110,130 @@ export default function HomePage() {
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
id="stack"
|
||||
eyebrow="Technical direction"
|
||||
title="The implementation stack is locked"
|
||||
description="The app is set up to match the approved planning docs: Next.js, TypeScript, Bootstrap 5, Sass, PostgreSQL, Prisma, and Docker-based environments."
|
||||
id="story"
|
||||
eyebrow="About the business"
|
||||
title="Editorial content keeps the journey understandable"
|
||||
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">
|
||||
{stackCards.map((card) => (
|
||||
<article key={card.title} className="data-card">
|
||||
<h3>{card.title}</h3>
|
||||
<ul>
|
||||
{card.items.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="content-grid">
|
||||
<article className="content-card">
|
||||
<h3>What the site should do</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>
|
||||
<li>Present the booking business clearly</li>
|
||||
<li>Reduce uncertainty before the enquiry step</li>
|
||||
<li>Keep public content editable as the site grows</li>
|
||||
</ul>
|
||||
</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>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
id="data"
|
||||
eyebrow="Data model"
|
||||
title="The first schema pass is ready"
|
||||
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."
|
||||
id="stories"
|
||||
eyebrow="Guest feedback"
|
||||
title="Testimonials and trust signals belong on the public page"
|
||||
description="The marketing layer needs a few trust cues so the journey feels deliberate instead of empty scaffold."
|
||||
>
|
||||
<div className="data-card">
|
||||
<h3>Seeded core entities</h3>
|
||||
<ul>
|
||||
{site.highlights.map((item) => (
|
||||
<li key={item}>{item}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="testimonial-grid">
|
||||
{testimonials.map((testimonial) => (
|
||||
<blockquote key={testimonial.author} className="testimonial-card">
|
||||
<p>{testimonial.quote}</p>
|
||||
<footer>
|
||||
<strong>{testimonial.author}</strong>
|
||||
<span>{testimonial.location}</span>
|
||||
</footer>
|
||||
</blockquote>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section
|
||||
id="launch"
|
||||
eyebrow="Next move"
|
||||
title="Ready for the first implementation slice"
|
||||
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."
|
||||
id="content"
|
||||
eyebrow="Editable pages"
|
||||
title="About, FAQs, local area, and policy pages are now routable"
|
||||
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">
|
||||
<h3>Immediate follow-up</h3>
|
||||
<ul>
|
||||
<li>Connect Prisma to a real Postgres instance.</li>
|
||||
<li>Seed the first property and content records.</li>
|
||||
<li>Start the public homepage and listing screens.</li>
|
||||
</ul>
|
||||
<div className="content-grid content-grid-tight">
|
||||
<article className="content-card">
|
||||
<h3>Available pages</h3>
|
||||
<ul className="link-list">
|
||||
<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>
|
||||
</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>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import Link from 'next/link';
|
||||
import { site } from '@/lib/site';
|
||||
|
||||
export function SiteFooter() {
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
@@ -5,7 +8,7 @@ export function SiteFooter() {
|
||||
<footer className="site-footer">
|
||||
<div>
|
||||
<p className="footer-label">Status</p>
|
||||
<p>Phase 1 scaffold underway.</p>
|
||||
<p>{site.tagline}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="footer-label">Build</p>
|
||||
@@ -14,7 +17,14 @@ export function SiteFooter() {
|
||||
{process.env.NEXT_PUBLIC_BUILD_ITERATION || '0'}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="footer-label">Contact</p>
|
||||
<p>
|
||||
<Link href="/contact">{site.contact.email}</Link>
|
||||
<br />
|
||||
{site.contact.phone}
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,26 @@
|
||||
const navItems = [
|
||||
{ href: '#foundation', label: 'Foundation' },
|
||||
{ href: '#stack', label: 'Stack' },
|
||||
{ href: '#data', label: 'Data model' },
|
||||
{ href: '#launch', label: 'Launch' },
|
||||
];
|
||||
import Link from 'next/link';
|
||||
import { primaryNavigation, site } from '@/lib/site';
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="site-header">
|
||||
<div className="brand-lockup">
|
||||
<span className="brand-mark">HPB</span>
|
||||
<Link className="brand-mark" href="/" aria-label={site.name}>
|
||||
HPB
|
||||
</Link>
|
||||
<div>
|
||||
<p className="brand-kicker">Project scaffold</p>
|
||||
<h1>Holiday Property Booking</h1>
|
||||
<h1>{site.name}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav aria-label="Primary" className="site-nav">
|
||||
{navItems.map((item) => (
|
||||
<a key={item.href} href={item.href}>
|
||||
{primaryNavigation.map((item) => (
|
||||
<Link key={item.href} href={item.href}>
|
||||
{item.label}
|
||||
</a>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</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 = {
|
||||
name: 'Holiday Property Booking',
|
||||
tagline: 'Direct holiday stays with visible availability and no guesswork.',
|
||||
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: [
|
||||
'Public browsing and search',
|
||||
'Live availability and pricing rules',
|
||||
'Stripe checkout and confirmation flow',
|
||||
'Admin-ready data model',
|
||||
],
|
||||
foundationSteps: [
|
||||
'Project scaffold',
|
||||
'Environment configuration',
|
||||
'Database schema',
|
||||
'App shell',
|
||||
'Health endpoint',
|
||||
],
|
||||
foundationSteps: ['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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user