feat: scaffold holiday booking project

This commit is contained in:
2026-05-22 07:54:09 +00:00
commit abfc8dcf8e
39 changed files with 8941 additions and 0 deletions

10
.env.example Normal file
View File

@@ -0,0 +1,10 @@
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/holiday_property_booking?schema=public"
NEXT_PUBLIC_SITE_URL="http://localhost:3000"
NEXT_PUBLIC_BUILD_COMMIT="local"
NEXT_PUBLIC_BUILD_ITERATION="0"
BOOKING_HOLD_MINUTES="30"
STRIPE_SECRET_KEY=""
STRIPE_WEBHOOK_SECRET=""
EMAIL_PROVIDER=""
EMAIL_FROM_ADDRESS="noreply@example.com"

43
.gitignore vendored Normal file
View File

@@ -0,0 +1,43 @@
# Dependencies
node_modules
.pnp
.pnp.js
# Build outputs
.next
out
dist
# Environment variables
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Testing
coverage
# Prisma
prisma/dev.db
# Misc
.cache

69
01-intake-and-scope.md Normal file
View File

@@ -0,0 +1,69 @@
# 01. Intake and Scope
## Purpose
Define the project boundary, MVP target, and the decisions needed before design or implementation starts.
## Scope
- Confirm the project is a responsive holiday property booking website for one business with multiple properties.
- Confirm the MVP includes:
- public browsing
- property detail pages
- availability search
- booking flow
- Stripe Checkout
- confirmation/failure handling
- booking/admin management
- editable content
- email notifications
- basic SEO
- Record anything explicitly out of scope.
- Lock the approved technical direction for planning:
- Next.js + TypeScript
- Bootstrap 5 + SCSS
- Next.js server routes/API layer for the MVP backend
- PostgreSQL
- Prisma
- session-based admin auth
- Stripe Checkout + webhooks
- transactional email provider abstraction
- S3-compatible image storage
- Docker-based dev/qa/prod environments
## What Needs to Be Done
- Extract the key business goals from the spec.
- List the product areas that need separate planning.
- Identify unresolved questions and assumptions.
- Establish the delivery phase order.
- Set the approval gate for the next document.
## Proposed Phase Order
1. Intake and scope
2. Content architecture and data model
3. Public website and search
4. Booking, availability, and pricing
5. Payments and notifications
6. Admin console
7. SEO, accessibility, and performance
8. Implementation plan and launch readiness
## Acceptance Criteria
- The MVP scope is written in one place.
- The non-MVP items are clearly marked.
- Open questions are listed, not hidden inside later docs.
- The approved stack is recorded for the project.
- The phase order is explicit and reviewable.
- The next planning step is obvious and sequenced.
## Dependencies
- Functional specification review.
- Agreement on what counts as MVP.
## Notes
- This document is the start of the project plan, not a build task.

View File

@@ -0,0 +1,164 @@
# 02. Content Architecture and Data Model
## Purpose
Define the information structure behind the public site and admin area before implementation begins.
## Scope
- Public pages:
- homepage
- listing page
- property detail page
- contact page
- editable content pages
- Core business entities:
- property
- property image
- amenity
- pricing rule
- availability block
- booking
- payment
- enquiry
- testimonial
- content page
- site settings
## Proposed Content Architecture
### Public Content Layers
- Global site content:
- brand name
- logo
- contact details
- social links
- footer content
- site-wide SEO defaults
- Marketing content:
- homepage hero
- testimonials
- feature blocks
- location highlights
- FAQs
- Property content:
- title
- slug
- summary
- full description
- location text
- capacity details
- amenities
- gallery
- house rules
- check-in/check-out times
- pricing summary
- availability data
### Core Entities
- Property
- published status
- featured status
- slug
- summary
- long description
- sleeps
- bedrooms
- bathrooms
- pets allowed flag
- minimum stay rules if needed
- PropertyImage
- property relation
- alt text
- display order
- primary image flag
- Amenity
- name
- icon/label if needed
- property relation
- PricingRule
- property relation or global relation
- base price
- seasonal overrides
- weekend override
- guest-based adjustments if needed
- AvailabilityBlock
- property relation
- start date
- end date
- reason/status
- Booking
- property relation
- customer details
- dates
- guest counts
- price totals
- booking status
- payment status
- timestamps
- Payment
- booking relation
- Stripe identifiers
- amount
- currency
- status
- webhook event tracking
- Enquiry
- property relation optional
- customer details
- message
- timestamps
- Testimonial
- author name
- content
- rating if used
- publish status
- ContentPage
- slug
- title
- body content
- SEO fields
- SiteSettings
- business details
- booking rules
- default SEO
- email settings references
- social links
## What Needs to Be Done
- Define the entities and their relationships.
- Define which fields are public, admin-only, or system-managed.
- Identify which content is reusable across pages.
- Define the minimum data needed to support the booking flow and SEO pages.
- Identify any content that should be seeded versus manually managed.
## Entity Relationships
- A property can have many images.
- A property can have many amenities.
- A property can have many pricing rules.
- A property can have many availability blocks.
- A property can have many bookings.
- A booking can have one payment record.
- A property can have many enquiries.
- A content page is standalone but uses shared site settings.
- Testimonials are site-level content and may optionally be linked to a property.
## Acceptance Criteria
- The project has a clear content model.
- The admin-managed content is separated from the public-facing content.
- The model supports multiple properties without special-casing one-off pages.
- The data model is sufficient to support the later booking and admin docs.
- Relationship ownership is clear enough to implement later without reinterpretation.
## Dependencies
- Intake and scope approval.
## Notes
- If a field is uncertain, it should be marked for decision instead of guessed.

View File

@@ -0,0 +1,80 @@
# 03. Public Website and Search
## Purpose
Define the customer-facing browsing experience before any booking logic is built.
## Scope
- Homepage layout and sections.
- Property listing page.
- Property card content.
- Filters and search controls.
- Property detail page content.
- Contact page.
- Editable content pages.
## Proposed Public Experience
### Homepage
- Hero with primary booking/search CTA.
- Availability search panel.
- Featured properties.
- Business introduction content.
- Key selling points.
- Location highlights.
- Testimonials.
- Contact/enquiry CTA.
### Property Listing
- Show published properties only.
- Support cards with image, name, area, summary, sleeps, bedrooms, bathrooms, price from, key amenities, and availability state.
- Provide filters for dates, guests, bedrooms, pets, location, and amenities.
- Hidden or clearly marked unavailable properties when date filters are active.
### Property Detail
- SEO-friendly dedicated page per property.
- Gallery, summary, description, location text, capacity, amenities, rules, times, pricing summary, availability calendar, booking panel, testimonials, and optional FAQs/related properties.
- Actions for date selection, guest count, availability check, price preview, booking start, and enquiry.
### Contact and Content Pages
- Contact form with name, email, optional phone, and message.
- Editable content pages for About, Local Area, FAQs, Terms and Conditions, and Privacy Policy.
## What Needs to Be Done
- Define the public navigation and page hierarchy.
- Define the homepage sections and CTA placement.
- Define listing filters and result states.
- Define the property detail page sections and actions.
- Define the contact and content page requirements.
- Define the public messaging hierarchy so users can move from browse to booking without confusion.
## Page Flow
1. Homepage
2. Property listing
3. Property detail
4. Availability check
5. Booking start
6. Contact/enquiry if the user is not ready to book
## Acceptance Criteria
- A customer can understand how to browse, inspect, and contact the business from the plan alone.
- The listing and detail pages have clear required content.
- Filter requirements are explicit.
- The search flow is compatible with the later booking rules.
- The public journey is ordered and consistent across pages.
## Dependencies
- Content architecture and data model.
## Notes
- This step should not decide pricing or payment behavior yet.

View File

@@ -0,0 +1,96 @@
# 04. Booking, Availability, and Pricing
## Purpose
Define the booking journey and the rules that control availability and price calculation.
## Scope
- Booking flow from property selection through checkout.
- Booking form fields and validation rules.
- Booking statuses and state transitions.
- Availability checks and date handling.
- Price calculation inputs and outputs.
- Pet-related conditional logic.
## Proposed Booking Rules
### Booking Flow
1. Select property.
2. Select arrival date.
3. Select departure date.
4. Select guest counts.
5. Confirm availability.
6. Review price.
7. Enter customer details.
8. Accept terms and conditions.
9. Proceed to Stripe Checkout.
10. Complete payment.
11. Return to the site.
12. See confirmation or failure state.
13. Receive confirmation email if payment succeeds.
### Booking Fields
- First name
- Last name
- Email address
- Phone number
- Arrival date
- Departure date
- Number of adults
- Number of children
- Special requests/message
- Terms acceptance
- Number of pets when the property allows pets
- Optional address, country, estimated arrival time, and marketing consent
### Booking Status Model
- Pending Payment
- Payment Received
- Confirmed
- Cancelled
- Failed
### Availability Logic
- Availability checks happen before payment.
- Unavailable dates prevent a booking from proceeding.
- Availability must respect both published availability blocks and already confirmed bookings.
- The system must be able to show a calendar or date-state view on the property page.
### Pricing Logic
- Price is shown before checkout.
- Price must be calculated from the selected property, dates, guest count, and any relevant seasonal or override pricing rules.
- The displayed price and the charged price must match.
- Any deposit or fee rules must be explicit if introduced later.
## What Needs to Be Done
- Define the booking lifecycle.
- Define when a booking becomes pending, confirmed, cancelled, or failed.
- Define how availability is checked and reserved.
- Define what data is required before checkout.
- Define how price is calculated and displayed.
- Define the exact handoff point between the booking form and Stripe Checkout.
- Define how long a hold lasts if a booking is started but not completed.
## Acceptance Criteria
- The booking flow is explicit from first selection to final confirmation.
- The status model is clear enough to implement later without guessing.
- Availability rules and pricing rules are documented separately.
- Edge cases are called out, especially around partial completion and failed payment.
- The document makes it clear which step owns truth for booking state.
## Dependencies
- Public website and search planning.
- Content architecture and data model.
## Notes
- This document should be the source of truth for any later booking implementation.

View File

@@ -0,0 +1,67 @@
# 05. Payments and Notifications
## Purpose
Define how money is taken and how the system informs the customer and the business.
## Scope
- Stripe Checkout integration.
- Webhook handling and payment truth.
- Success and failure return flows.
- Booking confirmation email.
- Failure handling email or on-screen messaging.
- Optional post-booking notifications for admin.
## Proposed Payment and Notification Rules
### Payment Flow
- The app creates a booking record before redirecting to Stripe Checkout.
- Stripe Checkout is used for payment collection.
- The app listens for Stripe webhooks to confirm payment completion.
- The browser return page is informational only and must not be the only source of truth.
### Payment Truth
- Stripe webhook events are authoritative for payment success/failure.
- The booking record should only be marked confirmed after payment is verified.
- Payment status must be recorded separately from booking status.
### Notification Rules
- Send a booking confirmation email after successful payment verification.
- Show a failure or incomplete payment message if checkout does not complete.
- Optionally notify admins of new confirmed bookings if needed later.
### Failure Handling
- If payment fails or is abandoned, the booking should not appear confirmed.
- If the browser return fails but payment succeeds, webhook processing must still finalize the booking.
- If the webhook arrives late, the system should resolve the booking once the event is processed.
## What Needs to Be Done
- Define the payment initiation and callback flow.
- Define what Stripe events matter.
- Define which system owns final payment state.
- Define the email templates and triggers.
- Define failure recovery behavior.
- Define which booking states can trigger notifications.
- Define retry behavior for failed or delayed webhook delivery.
## Acceptance Criteria
- Payment state is not inferred from the browser alone.
- The success and failure paths are separately documented.
- Email timing and triggers are explicit.
- The booking result is consistent across website, admin, and payment provider.
- The booking state cannot be finalized incorrectly from frontend-only events.
## Dependencies
- Booking, availability, and pricing plan.
## Notes
- This step should prevent later ambiguity about whether Stripe, the browser, or the app is authoritative.

80
06-admin-console.md Normal file
View File

@@ -0,0 +1,80 @@
# 06. Admin Console
## Purpose
Define the back office screens and admin actions needed to run the business.
## Scope
- Secure admin login.
- Property management.
- Image management.
- Amenity management.
- Pricing management.
- Availability management.
- Booking management.
- Payment record view.
- Enquiry management.
- Content page management.
- Testimonial management.
- Site settings.
## Proposed Admin Structure
### Core Admin Areas
- Dashboard / overview
- Properties
- Availability and pricing
- Bookings
- Payments
- Enquiries
- Content pages
- Testimonials
- Site settings
### Admin Actions
- Create, edit, publish, unpublish, and archive properties.
- Upload, reorder, and remove property images.
- Add and assign amenities.
- Set and override pricing rules.
- Add availability blocks and exceptions.
- Review bookings and payment records.
- Read and manage enquiries.
- Edit static pages and testimonials.
- Update site-wide settings and defaults.
### Admin Guardrails
- Admin login is required for all back office actions.
- Sensitive changes should be scoped by role or privilege if roles are added later.
- The admin cannot silently break the booking/payment truth model.
## What Needs to Be Done
- Define the admin navigation and main screens.
- Define CRUD operations for each managed entity.
- Define admin permissions and safe defaults.
- Define which screens need audit or status history.
- Define what the admin can override and what they cannot.
- Define which booking/payment states are editable versus read-only.
- Define whether audit history is required for important admin changes.
## Acceptance Criteria
- Every admin requirement from the spec is mapped to a screen or action.
- The admin surface is grouped in a way that matches day-to-day usage.
- Sensitive changes have clear rules and boundaries.
- The admin plan supports future implementation without redesigning the workflow.
- The admin model does not conflict with booking, payment, or availability rules.
## Dependencies
- Content architecture and data model.
- Booking, availability, and pricing.
- Payments and notifications.
## Notes
- Any admin override needs a clear rule to avoid conflict with booking or payment state.

View File

@@ -0,0 +1,72 @@
# 07. SEO, Accessibility, and Performance
## Purpose
Define the non-functional expectations that should shape the build from the start.
## Scope
- SEO structure for property and content pages.
- Metadata and structured content needs.
- Accessibility requirements.
- Responsive behavior.
- Performance targets.
- Image handling and gallery behavior.
## Proposed Non-Functional Standards
### SEO
- Every public page should have a clear title, meta description, and canonical intent.
- Property pages should be SEO-friendly and indexable when published.
- Structured content should support search visibility for properties, location pages, and static pages.
### Accessibility
- Forms, navigation, filters, and booking flows must be keyboard usable.
- Labels, focus states, and error messages must be explicit.
- Color contrast and readable spacing should be part of the base design.
### Responsive Behavior
- The site must work on mobile, tablet, and desktop.
- Listing cards, booking panels, and galleries must reflow cleanly on smaller screens.
- Admin screens should remain usable on typical laptop widths.
### Performance
- Image delivery should be optimized for property galleries and listing cards.
- Search and filter interactions should remain responsive.
- The public pages should avoid unnecessary blocking work on initial load.
### Image Handling
- Property images should support primary and gallery presentation.
- Alt text should be mandatory where images are public-facing.
- Image ordering should be controllable from the admin.
## What Needs to Be Done
- Define the SEO fields required for each page type.
- Define the accessibility baseline for forms, navigation, and booking flows.
- Define mobile and tablet behavior expectations.
- Define performance expectations for images, page load, and search filtering.
- Define any page types that should be noindex or hidden from search engines.
- Define target performance expectations if the team wants measurable thresholds.
## Acceptance Criteria
- SEO requirements are tied to page types, not treated as an afterthought.
- The UI requirements are responsive by design.
- Key flows are usable with keyboard and assistive tech.
- Performance expectations are written in measurable terms where possible.
- Image handling and alt text rules are explicit enough for implementation.
## Dependencies
- Public website planning.
- Content architecture and data model.
## Notes
- This document should keep the eventual implementation honest about quality, not just features.

View File

@@ -0,0 +1,102 @@
# 08. Implementation Plan and Launch Readiness
## Purpose
Turn the approved planning docs into a build sequence and a launch checklist.
## Scope
- Development order.
- Milestone grouping.
- Testing plan.
- Review and approval gates.
- Launch readiness criteria.
## Proposed Implementation Plan
### Phase 1: Foundation
- Project setup
- Repository structure
- Environment configuration
- Database schema and migrations
- Core layout/system components
### Phase 2: Public Experience
- Homepage
- Listing page
- Property detail page
- Contact page
- Content page rendering
### Phase 3: Booking Core
- Availability search
- Booking form
- Price calculation
- Booking persistence
- Booking state transitions
### Phase 4: Payments and Notifications
- Stripe Checkout
- Webhooks
- Confirmation emails
- Failure handling
### Phase 5: Admin Console
- Property management
- Pricing and availability management
- Booking management
- Content management
- Site settings
### Phase 6: Quality and Launch
- SEO validation
- Accessibility review
- Performance review
- Regression testing
- Launch readiness check
## Testing Plan
- Unit tests for pricing, availability, and booking state logic.
- Integration tests for checkout and webhook handling.
- End-to-end tests for browse, book, pay, and confirm flows.
- Admin workflow tests for core management actions.
## Launch Readiness Criteria
- Core customer booking flow works end to end.
- Stripe payment and webhook confirmation are reliable.
- Admin can manage live content, bookings, and availability.
- Public pages meet the agreed SEO, accessibility, and responsiveness baseline.
- No unresolved critical questions remain from the planning docs.
## What Needs to Be Done
- Convert the approved planning docs into build phases.
- Set dependencies between phases.
- Define what must be tested before each stage is approved.
- Define what evidence is needed before launch.
- Define the handoff point for implementation work.
- Define what evidence each phase must produce before the next one starts.
## Acceptance Criteria
- The project has a clear build order.
- The implementation phases are small enough to review one at a time.
- Testing and launch criteria are explicit.
- The next action after approval is obvious.
- The planning set is complete enough to begin implementation in controlled stages.
## Dependencies
- All prior planning docs approved.
## Notes
- This is the last planning doc before actual build work begins.

15
ARCHITECTURE.md Normal file
View File

@@ -0,0 +1,15 @@
# Architecture
## Initial Shape
- `src/app` contains route-level UI and the health endpoint.
- `src/components` contains reusable layout pieces.
- `src/lib` contains shared data and infrastructure helpers.
- `prisma/` contains the initial database schema and seed entrypoint.
## Notes
- The UI starts as server-rendered pages with a small client-free shell.
- Database access will flow through Prisma once the app starts reading real records.
- Feature work should keep the booking state model and payment state model separate.

16
DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,16 @@
# Deployment
## Environment Model
The deployment model is expected to follow the shared dev, QA, and production branch flow once the project is onboarded into the pipeline.
## Runtime Expectations
- `NEXT_PUBLIC_SITE_URL` should match the deployed environment.
- `DATABASE_URL` should target the environment-specific PostgreSQL instance.
- Stripe and email provider secrets live in environment variables.
## Health Check
- `GET /api/health` returns a lightweight JSON readiness response.

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start"]

33
PROJECT.md Normal file
View File

@@ -0,0 +1,33 @@
# Holiday Property Booking Project
## Project Classification
Public holiday property booking website with a booking engine, admin management, and transactional notifications.
## Status
Phase 1 scaffold started from the approved planning docs.
## Stack
- Next.js
- TypeScript
- Bootstrap 5
- Sass
- PostgreSQL
- Prisma
- Stripe Checkout
- Session-based admin auth
## Current Build Scope
- Repo scaffold
- App shell
- Base styling
- Prisma schema
- Health endpoint
## Next Build Step
- Wire the first data-backed screens after the foundation is in place.

32
README.md Normal file
View File

@@ -0,0 +1,32 @@
# Holiday Property Booking Website
Planning workspace for the new project based on the functional specification.
## Current Status
Phase 1 scaffold has started. The repo now contains the Next.js app shell, Prisma schema, Docker entrypoint, and baseline project docs.
## Working Rule
We will not build this in one shot. Each numbered document in this folder defines one stage of work and must be reviewed before the next stage starts.
## Document Order
1. `01-intake-and-scope.md`
2. `02-content-architecture-and-data-model.md`
3. `03-public-website-and-search.md`
4. `04-booking-availability-and-pricing.md`
5. `05-payments-and-notifications.md`
6. `06-admin-console.md`
7. `07-seo-accessibility-performance.md`
8. `08-implementation-plan-and-launch-readiness.md`
## Source
Functional specification:
- `holiday-property-booking-functional-spec_1---5c2d597d-4cbf-48fb-9c94-05c8fd719627.md`
## Review Gate
Nothing in the build should start until the relevant document in this sequence is approved.

12
RELEASES.md Normal file
View File

@@ -0,0 +1,12 @@
# Releases
## Current State
No releases yet.
## First Milestone
- Phase 1 scaffold complete
- Baseline health endpoint available
- Prisma schema established

12
VIKUNJA.md Normal file
View File

@@ -0,0 +1,12 @@
# Vikunja
## Notes
The project has not been onboarded to a board yet.
## Expected Workflow
- Planning docs are approved before build work.
- Phase 1 foundation work lands first.
- Feature work should be tracked as separate tickets once the board exists.

12
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,12 @@
services:
web:
command: npm run dev
volumes:
- .:/app
- /app/node_modules
environment:
NODE_ENV: development
NEXT_PUBLIC_SITE_URL: http://localhost:3000
ports:
- "${WEB_PORT:-3000}:3000"

8
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,8 @@
services:
web:
environment:
NODE_ENV: production
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-https://example.com}
ports:
- "${WEB_PORT:-3002}:3000"

8
docker-compose.qa.yml Normal file
View File

@@ -0,0 +1,8 @@
services:
web:
environment:
NODE_ENV: production
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3001}
ports:
- "${WEB_PORT:-3001}:3000"

29
docker-compose.yml Normal file
View File

@@ -0,0 +1,29 @@
services:
web:
build:
context: .
dockerfile: Dockerfile
environment:
NODE_ENV: production
NEXT_PUBLIC_SITE_URL: ${NEXT_PUBLIC_SITE_URL:-http://localhost:3000}
DATABASE_URL: ${DATABASE_URL:-postgresql://postgres:postgres@db:5432/holiday_property_booking?schema=public}
depends_on:
db:
condition: service_healthy
ports:
- "${WEB_PORT:-3000}:3000"
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: holiday_property_booking
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "${DB_PORT:-5432}:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d holiday_property_booking"]
interval: 5s
timeout: 5s
retries: 10

20
eslint.config.mjs Normal file
View File

@@ -0,0 +1,20 @@
import { dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { FlatCompat } from '@eslint/eslintrc';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
});
const config = [
...compat.extends('next/core-web-vitals'),
{
ignores: ['.next/**', 'node_modules/**'],
},
];
export default config;

6
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

15
next.config.js Normal file
View File

@@ -0,0 +1,15 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
distDir: '.next',
eslint: {
ignoreDuringBuilds: true,
},
webpack: (config) => {
config.resolve.alias['@'] = require('path').resolve(__dirname, 'src');
return config;
},
};
module.exports = nextConfig;

6816
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

33
package.json Normal file
View File

@@ -0,0 +1,33 @@
{
"name": "holiday-property-booking",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint .",
"prisma:generate": "prisma generate",
"prisma:migrate:dev": "prisma migrate dev",
"prisma:seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@prisma/client": "^6.10.1",
"bootstrap": "^5.3.3",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@types/node": "20.19.37",
"@types/react": "19.2.14",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.0.0",
"prisma": "^6.10.1",
"sass": "^1.89.2",
"tsx": "^4.20.3",
"typescript": "5.9.3"
}
}

View File

@@ -0,0 +1,199 @@
CREATE TYPE "BookingStatus" AS ENUM ('PENDING_PAYMENT', 'PAYMENT_RECEIVED', 'CONFIRMED', 'CANCELLED', 'FAILED');
CREATE TYPE "PaymentStatus" AS ENUM ('REQUIRES_PAYMENT', 'COMPLETED', 'FAILED', 'REFUNDED');
CREATE TYPE "AvailabilityBlockReason" AS ENUM ('MAINTENANCE', 'OWNER_BLOCKED', 'BASE_RULE', 'OTHER');
CREATE TYPE "ContentPageStatus" AS ENUM ('DRAFT', 'PUBLISHED', 'ARCHIVED');
CREATE TABLE "SiteSettings" (
"id" TEXT NOT NULL,
"businessName" TEXT NOT NULL,
"tagline" TEXT,
"contactEmail" TEXT,
"contactPhone" TEXT,
"defaultSeoTitle" TEXT,
"defaultSeoDescription" TEXT,
"bookingHoldMinutes" INTEGER NOT NULL DEFAULT 30,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "SiteSettings_pkey" PRIMARY KEY ("id")
);
CREATE TABLE "Property" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"title" TEXT NOT NULL,
"summary" TEXT NOT NULL,
"longDescription" TEXT NOT NULL,
"locationText" TEXT NOT NULL,
"sleeps" INTEGER NOT NULL,
"bedrooms" INTEGER NOT NULL,
"bathrooms" INTEGER NOT NULL,
"petsAllowed" BOOLEAN NOT NULL DEFAULT false,
"published" BOOLEAN NOT NULL DEFAULT false,
"featured" BOOLEAN NOT NULL DEFAULT false,
"minStayNights" INTEGER,
"checkInTime" TEXT,
"checkOutTime" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Property_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "Property_slug_key" ON "Property"("slug");
CREATE TABLE "PropertyImage" (
"id" TEXT NOT NULL,
"propertyId" TEXT NOT NULL,
"url" TEXT NOT NULL,
"altText" TEXT NOT NULL,
"displayOrder" INTEGER NOT NULL DEFAULT 0,
"primaryImage" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "PropertyImage_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "PropertyImage_propertyId_displayOrder_idx" ON "PropertyImage"("propertyId", "displayOrder");
CREATE TABLE "Amenity" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"name" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Amenity_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "Amenity_slug_key" ON "Amenity"("slug");
CREATE TABLE "PropertyAmenity" (
"propertyId" TEXT NOT NULL,
"amenityId" TEXT NOT NULL
);
CREATE UNIQUE INDEX "PropertyAmenity_propertyId_amenityId_key" ON "PropertyAmenity"("propertyId", "amenityId");
CREATE INDEX "PropertyAmenity_amenityId_idx" ON "PropertyAmenity"("amenityId");
CREATE TABLE "PricingRule" (
"id" TEXT NOT NULL,
"propertyId" TEXT NOT NULL,
"label" TEXT,
"basePriceCents" INTEGER NOT NULL,
"weekendPriceCents" INTEGER,
"guestDeltaCents" INTEGER,
"validFrom" TIMESTAMP(3),
"validTo" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "PricingRule_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "PricingRule_propertyId_validFrom_validTo_idx" ON "PricingRule"("propertyId", "validFrom", "validTo");
CREATE TABLE "AvailabilityBlock" (
"id" TEXT NOT NULL,
"propertyId" TEXT NOT NULL,
"startDate" TIMESTAMP(3) NOT NULL,
"endDate" TIMESTAMP(3) NOT NULL,
"reason" "AvailabilityBlockReason" NOT NULL DEFAULT 'OTHER',
"notes" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "AvailabilityBlock_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "AvailabilityBlock_propertyId_startDate_endDate_idx" ON "AvailabilityBlock"("propertyId", "startDate", "endDate");
CREATE TABLE "Booking" (
"id" TEXT NOT NULL,
"propertyId" TEXT NOT NULL,
"firstName" TEXT NOT NULL,
"lastName" TEXT NOT NULL,
"email" TEXT NOT NULL,
"phone" TEXT,
"arrivalDate" TIMESTAMP(3) NOT NULL,
"departureDate" TIMESTAMP(3) NOT NULL,
"adults" INTEGER NOT NULL,
"children" INTEGER NOT NULL DEFAULT 0,
"pets" INTEGER NOT NULL DEFAULT 0,
"specialRequests" TEXT,
"termsAccepted" BOOLEAN NOT NULL DEFAULT false,
"holdExpiresAt" TIMESTAMP(3),
"totalCents" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'GBP',
"status" "BookingStatus" NOT NULL DEFAULT 'PENDING_PAYMENT',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Booking_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Booking_propertyId_arrivalDate_departureDate_idx" ON "Booking"("propertyId", "arrivalDate", "departureDate");
CREATE INDEX "Booking_status_idx" ON "Booking"("status");
CREATE TABLE "Payment" (
"id" TEXT NOT NULL,
"bookingId" TEXT NOT NULL,
"stripeCheckoutSessionId" TEXT,
"stripePaymentIntentId" TEXT,
"stripeEventId" TEXT,
"amountCents" INTEGER NOT NULL,
"currency" TEXT NOT NULL DEFAULT 'GBP',
"status" "PaymentStatus" NOT NULL DEFAULT 'REQUIRES_PAYMENT',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "Payment_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "Payment_bookingId_key" ON "Payment"("bookingId");
CREATE UNIQUE INDEX "Payment_stripeCheckoutSessionId_key" ON "Payment"("stripeCheckoutSessionId");
CREATE UNIQUE INDEX "Payment_stripePaymentIntentId_key" ON "Payment"("stripePaymentIntentId");
CREATE UNIQUE INDEX "Payment_stripeEventId_key" ON "Payment"("stripeEventId");
CREATE TABLE "Enquiry" (
"id" TEXT NOT NULL,
"propertyId" TEXT,
"name" TEXT NOT NULL,
"email" TEXT NOT NULL,
"phone" TEXT,
"message" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Enquiry_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Enquiry_propertyId_createdAt_idx" ON "Enquiry"("propertyId", "createdAt");
CREATE TABLE "Testimonial" (
"id" TEXT NOT NULL,
"propertyId" TEXT,
"authorName" TEXT NOT NULL,
"content" TEXT NOT NULL,
"rating" INTEGER,
"published" BOOLEAN NOT NULL DEFAULT false,
"displayOrder" INTEGER NOT NULL DEFAULT 0,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "Testimonial_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "Testimonial_published_displayOrder_idx" ON "Testimonial"("published", "displayOrder");
CREATE TABLE "ContentPage" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"title" TEXT NOT NULL,
"body" TEXT NOT NULL,
"status" "ContentPageStatus" NOT NULL DEFAULT 'DRAFT',
"seoTitle" TEXT,
"seoDescription" TEXT,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "ContentPage_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "ContentPage_slug_key" ON "ContentPage"("slug");
ALTER TABLE "PropertyImage" ADD CONSTRAINT "PropertyImage_propertyId_fkey" FOREIGN KEY ("propertyId") REFERENCES "Property"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "PropertyAmenity" ADD CONSTRAINT "PropertyAmenity_propertyId_fkey" FOREIGN KEY ("propertyId") REFERENCES "Property"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "PropertyAmenity" ADD CONSTRAINT "PropertyAmenity_amenityId_fkey" FOREIGN KEY ("amenityId") REFERENCES "Amenity"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "PricingRule" ADD CONSTRAINT "PricingRule_propertyId_fkey" FOREIGN KEY ("propertyId") REFERENCES "Property"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "AvailabilityBlock" ADD CONSTRAINT "AvailabilityBlock_propertyId_fkey" FOREIGN KEY ("propertyId") REFERENCES "Property"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Booking" ADD CONSTRAINT "Booking_propertyId_fkey" FOREIGN KEY ("propertyId") REFERENCES "Property"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE "Payment" ADD CONSTRAINT "Payment_bookingId_fkey" FOREIGN KEY ("bookingId") REFERENCES "Booking"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "Enquiry" ADD CONSTRAINT "Enquiry_propertyId_fkey" FOREIGN KEY ("propertyId") REFERENCES "Property"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "Testimonial" ADD CONSTRAINT "Testimonial_propertyId_fkey" FOREIGN KEY ("propertyId") REFERENCES "Property"("id") ON DELETE SET NULL ON UPDATE CASCADE;

227
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,227 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum BookingStatus {
PENDING_PAYMENT
PAYMENT_RECEIVED
CONFIRMED
CANCELLED
FAILED
}
enum PaymentStatus {
REQUIRES_PAYMENT
COMPLETED
FAILED
REFUNDED
}
enum AvailabilityBlockReason {
MAINTENANCE
OWNER_BLOCKED
BASE_RULE
OTHER
}
enum ContentPageStatus {
DRAFT
PUBLISHED
ARCHIVED
}
model SiteSettings {
id String @id @default(cuid())
businessName String
tagline String?
contactEmail String?
contactPhone String?
defaultSeoTitle String?
defaultSeoDescription String?
bookingHoldMinutes Int @default(30)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Property {
id String @id @default(cuid())
slug String @unique
title String
summary String
longDescription String
locationText String
sleeps Int
bedrooms Int
bathrooms Int
petsAllowed Boolean @default(false)
published Boolean @default(false)
featured Boolean @default(false)
minStayNights Int?
checkInTime String?
checkOutTime String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
images PropertyImage[]
amenities PropertyAmenity[]
pricingRules PricingRule[]
availability AvailabilityBlock[]
bookings Booking[]
enquiries Enquiry[]
testimonials Testimonial[]
}
model PropertyImage {
id String @id @default(cuid())
propertyId String
url String
altText String
displayOrder Int @default(0)
primaryImage Boolean @default(false)
createdAt DateTime @default(now())
property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
@@index([propertyId, displayOrder])
}
model Amenity {
id String @id @default(cuid())
slug String @unique
name String
createdAt DateTime @default(now())
properties PropertyAmenity[]
}
model PropertyAmenity {
propertyId String
amenityId String
property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
amenity Amenity @relation(fields: [amenityId], references: [id], onDelete: Cascade)
@@id([propertyId, amenityId])
@@index([amenityId])
}
model PricingRule {
id String @id @default(cuid())
propertyId String
label String?
basePriceCents Int
weekendPriceCents Int?
guestDeltaCents Int?
validFrom DateTime?
validTo DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
@@index([propertyId, validFrom, validTo])
}
model AvailabilityBlock {
id String @id @default(cuid())
propertyId String
startDate DateTime
endDate DateTime
reason AvailabilityBlockReason @default(OTHER)
notes String?
createdAt DateTime @default(now())
property Property @relation(fields: [propertyId], references: [id], onDelete: Cascade)
@@index([propertyId, startDate, endDate])
}
model Booking {
id String @id @default(cuid())
propertyId String
firstName String
lastName String
email String
phone String?
arrivalDate DateTime
departureDate DateTime
adults Int
children Int @default(0)
pets Int @default(0)
specialRequests String?
termsAccepted Boolean @default(false)
holdExpiresAt DateTime?
totalCents Int
currency String @default("GBP")
status BookingStatus @default(PENDING_PAYMENT)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
property Property @relation(fields: [propertyId], references: [id], onDelete: Restrict)
payment Payment?
@@index([propertyId, arrivalDate, departureDate])
@@index([status])
}
model Payment {
id String @id @default(cuid())
bookingId String @unique
stripeCheckoutSessionId String? @unique
stripePaymentIntentId String? @unique
stripeEventId String? @unique
amountCents Int
currency String @default("GBP")
status PaymentStatus @default(REQUIRES_PAYMENT)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
booking Booking @relation(fields: [bookingId], references: [id], onDelete: Cascade)
}
model Enquiry {
id String @id @default(cuid())
propertyId String?
name String
email String
phone String?
message String
createdAt DateTime @default(now())
property Property? @relation(fields: [propertyId], references: [id], onDelete: SetNull)
@@index([propertyId, createdAt])
}
model Testimonial {
id String @id @default(cuid())
propertyId String?
authorName String
content String
rating Int?
published Boolean @default(false)
displayOrder Int @default(0)
createdAt DateTime @default(now())
property Property? @relation(fields: [propertyId], references: [id], onDelete: SetNull)
@@index([published, displayOrder])
}
model ContentPage {
id String @id @default(cuid())
slug String @unique
title String
body String
status ContentPageStatus @default(DRAFT)
seoTitle String?
seoDescription String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

31
prisma/seed.ts Normal file
View File

@@ -0,0 +1,31 @@
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
const existing = await prisma.siteSettings.findFirst();
if (existing) {
return;
}
await prisma.siteSettings.create({
data: {
businessName: 'Holiday Property Booking',
tagline: 'Curated stays, clear availability, and a direct booking flow.',
contactEmail: 'hello@example.com',
defaultSeoTitle: 'Holiday Property Booking',
defaultSeoDescription: 'Book holiday properties with live availability, clear pricing, and secure checkout.',
},
});
}
main()
.catch((error) => {
console.error(error);
process.exitCode = 1;
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -0,0 +1,8 @@
export async function GET() {
return Response.json({
status: 'ok',
service: 'holiday-property-booking',
timestamp: new Date().toISOString(),
});
}

294
src/app/globals.scss Normal file
View File

@@ -0,0 +1,294 @@
$body-bg: #f4efe7;
$body-color: #1a1714;
$primary: #7a543d;
$secondary: #2e6661;
$success: #2f6b43;
$warning: #b07d24;
$danger: #a63d3d;
$border-radius: 1rem;
$font-family-sans-serif: "Avenir Next", "Segoe UI", sans-serif;
$headings-font-family: "Iowan Old Style", "Palatino Linotype", Georgia, serif;
@import "bootstrap/scss/bootstrap";
:root {
--shell-bg: #f4efe7;
--panel-bg: rgba(255, 255, 255, 0.72);
--panel-border: rgba(26, 23, 20, 0.08);
--accent: #7a543d;
--accent-2: #2e6661;
--text-muted: #63594f;
--shadow: 0 24px 80px rgba(23, 19, 14, 0.12);
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
min-height: 100vh;
margin: 0;
background:
radial-gradient(circle at top left, rgba(122, 84, 61, 0.18), transparent 30%),
radial-gradient(circle at 80% 10%, rgba(46, 102, 97, 0.18), transparent 24%),
var(--shell-bg);
}
a {
color: inherit;
text-decoration: none;
}
main {
position: relative;
}
.app-shell {
min-height: 100vh;
padding: 1.25rem;
}
.surface {
max-width: 1180px;
margin: 0 auto;
border: 1px solid var(--panel-border);
border-radius: 1.75rem;
background: var(--panel-bg);
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.site-header,
.site-footer,
.hero,
.section-shell {
padding: 1.5rem;
}
.site-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
border-bottom: 1px solid var(--panel-border);
}
.brand-lockup {
display: flex;
align-items: center;
gap: 1rem;
}
.brand-mark {
display: inline-grid;
place-items: center;
width: 3rem;
height: 3rem;
border-radius: 999px;
background: linear-gradient(135deg, var(--accent), var(--accent-2));
color: #fff;
font-weight: 700;
letter-spacing: 0.08em;
}
.brand-kicker,
.footer-label,
.section-eyebrow {
margin: 0 0 0.25rem;
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--text-muted);
}
.brand-lockup h1 {
margin: 0;
font-size: 1.05rem;
}
.site-nav {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.site-nav a {
padding: 0.55rem 0.85rem;
border: 1px solid rgba(26, 23, 20, 0.12);
border-radius: 999px;
font-size: 0.92rem;
color: var(--text-muted);
transition:
transform 160ms ease,
border-color 160ms ease,
background-color 160ms ease;
}
.site-nav a:hover,
.site-nav a:focus-visible {
transform: translateY(-1px);
border-color: rgba(122, 84, 61, 0.35);
background: rgba(255, 255, 255, 0.8);
color: var(--body-color);
}
.hero {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 1.5rem;
align-items: stretch;
}
.hero-copy,
.hero-panel,
.info-card,
.phase-card,
.data-card {
border: 1px solid var(--panel-border);
border-radius: 1.5rem;
background: rgba(255, 255, 255, 0.78);
}
.hero-copy {
padding: 2rem;
}
.hero-copy h2 {
margin: 0 0 1rem;
font-size: clamp(2.2rem, 4vw, 4.2rem);
line-height: 0.95;
letter-spacing: -0.04em;
}
.hero-copy p {
max-width: 60ch;
color: var(--text-muted);
font-size: 1.06rem;
}
.hero-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 1.5rem;
}
.hero-actions .btn {
border-radius: 999px;
padding-inline: 1.1rem;
}
.hero-panel {
display: grid;
gap: 1rem;
padding: 1.5rem;
}
.info-card,
.phase-card,
.data-card {
padding: 1rem;
}
.metric-grid,
.phase-grid,
.data-grid {
display: grid;
gap: 1rem;
}
.metric-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.metric {
padding: 1rem;
border-radius: 1rem;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(246, 240, 231, 0.9));
}
.metric strong {
display: block;
margin-bottom: 0.3rem;
font-size: 1.6rem;
}
.section-heading h2 {
margin: 0;
font-size: clamp(1.5rem, 2vw, 2.1rem);
}
.section-description {
max-width: 65ch;
margin: 0.5rem 0 0;
color: var(--text-muted);
}
.phase-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.phase-card h3,
.data-card h3 {
margin-top: 0;
font-size: 1.05rem;
}
.phase-card ul,
.data-card ul {
margin: 0.75rem 0 0;
padding-left: 1.1rem;
color: var(--text-muted);
}
.data-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.site-footer {
display: flex;
justify-content: space-between;
gap: 1rem;
border-top: 1px solid var(--panel-border);
color: var(--text-muted);
}
.site-footer p {
margin: 0;
}
@media (max-width: 900px) {
.hero,
.phase-grid,
.data-grid {
grid-template-columns: 1fr;
}
.site-header,
.site-footer {
flex-direction: column;
align-items: flex-start;
}
}
@media (max-width: 640px) {
.app-shell {
padding: 0.75rem;
}
.site-header,
.hero,
.section-shell,
.site-footer {
padding: 1rem;
}
.metric-grid {
grid-template-columns: 1fr;
}
}

31
src/app/layout.tsx Normal file
View File

@@ -0,0 +1,31 @@
import type { Metadata } from 'next';
import './globals.scss';
import { SiteFooter } from '@/components/SiteFooter';
import { SiteHeader } from '@/components/SiteHeader';
import { site } from '@/lib/site';
export const metadata: Metadata = {
title: site.name,
description: site.description,
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body>
<div className="app-shell">
<div className="surface">
<SiteHeader />
<main>{children}</main>
<SiteFooter />
</div>
</div>
</body>
</html>
);
}

153
src/app/page.tsx Normal file
View File

@@ -0,0 +1,153 @@
import { Section } from '@/components/Section';
import { site } 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 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'],
},
];
export default function HomePage() {
return (
<>
<section className="hero" id="top">
<div className="hero-copy">
<p className="brand-kicker">Phase 1 foundation</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>
</div>
</div>
<aside className="hero-panel" aria-label="Build snapshot">
<div className="info-card">
<p className="footer-label">Current state</p>
<strong>Scaffold created</strong>
<p className="mb-0 text-body-secondary">
The project now has a repo boundary, app shell, and database foundation to build on.
</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>
</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."
>
<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>
))}
</ul>
</article>
))}
</div>
</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."
>
<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>
</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."
>
<div className="data-card">
<h3>Seeded core entities</h3>
<ul>
{site.highlights.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</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."
>
<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>
</Section>
</>
);
}

View File

@@ -0,0 +1,23 @@
import type { ReactNode } from 'react';
type SectionProps = {
eyebrow?: string;
title: string;
description?: string;
children: ReactNode;
id?: string;
};
export function Section({ eyebrow, title, description, children, id }: SectionProps) {
return (
<section id={id} className="section-shell">
<div className="section-heading">
{eyebrow ? <p className="section-eyebrow">{eyebrow}</p> : null}
<h2>{title}</h2>
{description ? <p className="section-description">{description}</p> : null}
</div>
{children}
</section>
);
}

View File

@@ -0,0 +1,20 @@
export function SiteFooter() {
const year = new Date().getFullYear();
return (
<footer className="site-footer">
<div>
<p className="footer-label">Status</p>
<p>Phase 1 scaffold underway.</p>
</div>
<div>
<p className="footer-label">Build</p>
<p>
{year} {process.env.NEXT_PUBLIC_BUILD_COMMIT || 'local'} iteration{' '}
{process.env.NEXT_PUBLIC_BUILD_ITERATION || '0'}
</p>
</div>
</footer>
);
}

View File

@@ -0,0 +1,29 @@
const navItems = [
{ href: '#foundation', label: 'Foundation' },
{ href: '#stack', label: 'Stack' },
{ href: '#data', label: 'Data model' },
{ href: '#launch', label: 'Launch' },
];
export function SiteHeader() {
return (
<header className="site-header">
<div className="brand-lockup">
<span className="brand-mark">HPB</span>
<div>
<p className="brand-kicker">Project scaffold</p>
<h1>Holiday Property Booking</h1>
</div>
</div>
<nav aria-label="Primary" className="site-nav">
{navItems.map((item) => (
<a key={item.href} href={item.href}>
{item.label}
</a>
))}
</nav>
</header>
);
}

12
src/lib/prisma.ts Normal file
View File

@@ -0,0 +1,12 @@
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma?: PrismaClient;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}

20
src/lib/site.ts Normal file
View File

@@ -0,0 +1,20 @@
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.',
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',
],
};

29
tsconfig.json Normal file
View File

@@ -0,0 +1,29 @@
{
"compilerOptions": {
"target": "ES2017",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"noEmit": true,
"incremental": true,
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}