diff --git a/README.md b/README.md index f0f7063..1144d70 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,80 @@ -# Vail Renovations Static Site +# Vail Renovations Platform Foundation -Static Netlify-ready website for Vail Renovations built around the Vail Home Command intake paths. +This repo is being converted from a static Netlify HTML site into a Next.js App Router foundation for the Vail Renovations public website and future mobile-friendly CRM. + +## Current scope + +Implemented in the platform foundation: + +- Next.js App Router +- TypeScript +- Tailwind configuration +- Public layout, header, footer and mobile sticky CTAs +- Homepage foundation +- Public route placeholders +- Legacy static-route redirects +- Netlify Next.js build configuration +- Platform foundation report + +Not implemented in this ticket: + +- CRM +- Database +- Auth +- Lead creation +- Project Starter functionality +- Scope Builder functionality +- Callback submission +- Photo uploads +- Private storage +- Notifications +- Analytics +- Estimate workflow ## Required routes - `/` -- `/start-a-project` -- `/fix-list-builder` -- `/maintenance-plans` -- `/inspection-report-repairs` -- `/thank-you` +- `/services` +- `/services/bathroom-renovations` +- `/services/basement-renovations` +- `/services/kitchen-updates` +- `/services/repairs-refreshes` +- `/services/pre-sale-improvements` +- `/services/rental-turnovers` +- `/how-it-works` +- `/project-request` +- `/scope-builder` +- `/book-call` +- `/callback` +- `/contact` +- `/privacy` +- `/terms` -## Netlify Forms +## Legacy redirects -Included forms: +Legacy static URLs redirect to the new foundation routes: -- `vail-project-intake` -- `vail-fix-list` -- `vail-maintenance-plan` -- `vail-inspection-report` +- `/start-a-project` -> `/project-request` +- `/fix-list-builder` -> `/scope-builder` +- `/maintenance-plans` -> `/services/repairs-refreshes` +- `/inspection-report-repairs` -> `/services/pre-sale-improvements` +- `/renovations` -> `/services` +- `/thank-you` -> `/contact` -## Deploy +## Scripts -Netlify settings: +```bash +npm run dev +npm run typecheck +npm run lint +npm run build +npm run check +``` + +## Deploy -- Base directory: blank -- Build command: blank -- Publish directory: `.` +Netlify settings for this foundation: -Run `npm run check` before deploying. +- Build command: `npm run build` +- Publish directory: `.next` +- Plugin: `@netlify/plugin-nextjs` diff --git a/app/book-call/page.tsx b/app/book-call/page.tsx new file mode 100644 index 0000000..4447d2f --- /dev/null +++ b/app/book-call/page.tsx @@ -0,0 +1,20 @@ +import { PlaceholderPage } from "@/components/placeholder-page"; + +export const metadata = { + title: "Book a Quick Call", + description: "Vail quick call foundation route." +}; + +export default function BookCallPage() { + return ( + + ); +} diff --git a/app/callback/page.tsx b/app/callback/page.tsx new file mode 100644 index 0000000..01eaadd --- /dev/null +++ b/app/callback/page.tsx @@ -0,0 +1,20 @@ +import { PlaceholderPage } from "@/components/placeholder-page"; + +export const metadata = { + title: "Request a Callback", + description: "Vail callback request foundation route." +}; + +export default function CallbackPage() { + return ( + + ); +} diff --git a/app/contact/page.tsx b/app/contact/page.tsx new file mode 100644 index 0000000..fb8df6b --- /dev/null +++ b/app/contact/page.tsx @@ -0,0 +1,20 @@ +import { PlaceholderPage } from "@/components/placeholder-page"; + +export const metadata = { + title: "Contact", + description: "Contact Vail Renovations." +}; + +export default function ContactPage() { + return ( + + ); +} diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..664f41f --- /dev/null +++ b/app/globals.css @@ -0,0 +1,41 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --background: #f6f1e9; + --foreground: #16201a; +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + min-width: 320px; + margin: 0; + background: var(--background); + color: var(--foreground); + font-family: Arial, Helvetica, sans-serif; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +input, +select, +textarea { + font: inherit; +} + +::selection { + background: #b98b4b; + color: #16201a; +} diff --git a/app/how-it-works/page.tsx b/app/how-it-works/page.tsx new file mode 100644 index 0000000..a1b746c --- /dev/null +++ b/app/how-it-works/page.tsx @@ -0,0 +1,20 @@ +import { PlaceholderPage } from "@/components/placeholder-page"; + +export const metadata = { + title: "How It Works", + description: "The Vail Project Path foundation route." +}; + +export default function HowItWorksPage() { + return ( + + ); +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..f2c1940 --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { MobileCtaBar } from "@/components/mobile-cta-bar"; +import { SiteFooter } from "@/components/site-footer"; +import { SiteHeader } from "@/components/site-header"; + +export const metadata: Metadata = { + title: { + default: "Vail Renovations | Renovation Help Without the Runaround", + template: "%s | Vail Renovations" + }, + description: + "Vail Renovations helps Ottawa homeowners start repairs, updates and renovation projects with one clear point of contact and practical scope guidance.", + metadataBase: new URL("https://vail-renovations.netlify.app"), + openGraph: { + title: "Vail Renovations", + description: "Renovation help without the runaround.", + type: "website" + } +}; + +export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { + return ( + + + + {children} + + + + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..721101d --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,120 @@ +import Link from "next/link"; +import { serviceRoutes } from "@/lib/routes"; + +const projectSteps = [ + "Tell Vail what you want fixed, changed or improved.", + "Share photos or a budget only if you already have them.", + "Vail reviews the request and gives you the next clear step." +]; + +export default function Home() { + return ( +
+
+
+ +
+
+
+

Low friction by design

+

You do not need the whole project figured out.

+

+ The site is now a Next.js platform foundation ready for the future Project Starter, Scope Builder and CRM work. This ticket intentionally keeps forms, CRM, auth, database and uploads out of scope. +

+
+
+
+

Simple starting point

+

Homeowners should be able to start with plain-language notes, not contractor terminology.

+
+
+

Optional detail

+

Photos, budget and exact measurements belong as optional helpers, not barriers.

+
+
+

Clear next step

+

Every route should point back to a project request, callback or quick call.

+
+
+
+
+ +
+
+
+
+

Services

+

Repairs, updates and renovation work organized properly.

+
+ + View Services + +
+
+ {serviceRoutes.map((service) => ( + +

{service.title}

+

{service.description}

+ + ))} +
+
+
+ +
+
+

Shareable by design

+

Know someone trying to figure out a renovation or repair?

+

+ Send them the Project Starter when the route is connected in the next implementation ticket. For now, this foundation preserves the Vail positioning and app structure without faking lead capture. +

+
+ + Start a Project Request + + + Build My Scope + +
+
+
+
+ ); +} diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx new file mode 100644 index 0000000..a39ba1b --- /dev/null +++ b/app/privacy/page.tsx @@ -0,0 +1,20 @@ +import { PlaceholderPage } from "@/components/placeholder-page"; + +export const metadata = { + title: "Privacy", + description: "Vail Renovations privacy placeholder." +}; + +export default function PrivacyPage() { + return ( + + ); +} diff --git a/app/project-request/page.tsx b/app/project-request/page.tsx new file mode 100644 index 0000000..b73d83c --- /dev/null +++ b/app/project-request/page.tsx @@ -0,0 +1,20 @@ +import { PlaceholderPage } from "@/components/placeholder-page"; + +export const metadata = { + title: "Project Request", + description: "Vail Project Starter foundation route." +}; + +export default function ProjectRequestPage() { + return ( + + ); +} diff --git a/app/scope-builder/page.tsx b/app/scope-builder/page.tsx new file mode 100644 index 0000000..cee0973 --- /dev/null +++ b/app/scope-builder/page.tsx @@ -0,0 +1,20 @@ +import { PlaceholderPage } from "@/components/placeholder-page"; + +export const metadata = { + title: "Scope Builder", + description: "Vail Scope Builder foundation route." +}; + +export default function ScopeBuilderPage() { + return ( + + ); +} diff --git a/app/services/[slug]/page.tsx b/app/services/[slug]/page.tsx new file mode 100644 index 0000000..872b78f --- /dev/null +++ b/app/services/[slug]/page.tsx @@ -0,0 +1,70 @@ +import Link from "next/link"; +import { notFound } from "next/navigation"; +import { serviceRoutes } from "@/lib/routes"; + +interface ServicePageProps { + params: Promise<{ slug: string }>; +} + +function getServiceBySlug(slug: string) { + return serviceRoutes.find((route) => route.href.endsWith(`/${slug}`)); +} + +export function generateStaticParams() { + return serviceRoutes.map((route) => { + const slug = route.href.split("/").pop(); + + if (!slug) { + throw new Error(`Missing service slug for route: ${route.href}`); + } + + return { slug }; + }); +} + +export async function generateMetadata({ params }: ServicePageProps) { + const { slug } = await params; + const service = getServiceBySlug(slug); + + if (!service) { + return {}; + } + + return { + title: service.title, + description: service.description + }; +} + +export default async function ServicePage({ params }: ServicePageProps) { + const { slug } = await params; + const service = getServiceBySlug(slug); + + if (!service) { + notFound(); + } + + return ( +
+
+

Vail service route

+

{service.title}

+

{service.description}

+
+

Foundation placeholder

+

+ This route exists so the public site architecture is ready before the full content, Project Starter, Scope Builder and CRM tickets are implemented. No lead is created from this page in the foundation ticket. +

+
+
+ + Start a Project Request + + + Back to Services + +
+
+
+ ); +} diff --git a/app/services/page.tsx b/app/services/page.tsx new file mode 100644 index 0000000..cf683e5 --- /dev/null +++ b/app/services/page.tsx @@ -0,0 +1,29 @@ +import Link from "next/link"; +import { serviceRoutes } from "@/lib/routes"; + +export const metadata = { + title: "Services", + description: "Vail Renovations service route placeholders for bathroom, basement, kitchen, repair, pre-sale and rental turnover work." +}; + +export default function ServicesPage() { + return ( +
+
+

Services

+

Renovation work organized around the next clear step.

+

+ These routes establish the public-site foundation for Vail Renovations. They are placeholders for the next content and intake tickets, not live estimating or lead creation flows. +

+
+ {serviceRoutes.map((service) => ( + +

{service.title}

+

{service.description}

+ + ))} +
+
+
+ ); +} diff --git a/app/terms/page.tsx b/app/terms/page.tsx new file mode 100644 index 0000000..e778c8c --- /dev/null +++ b/app/terms/page.tsx @@ -0,0 +1,20 @@ +import { PlaceholderPage } from "@/components/placeholder-page"; + +export const metadata = { + title: "Terms", + description: "Vail Renovations terms and disclaimer placeholder." +}; + +export default function TermsPage() { + return ( + + ); +} diff --git a/components/mobile-cta-bar.tsx b/components/mobile-cta-bar.tsx new file mode 100644 index 0000000..f9bd7b8 --- /dev/null +++ b/components/mobile-cta-bar.tsx @@ -0,0 +1,16 @@ +import Link from "next/link"; + +export function MobileCtaBar() { + return ( +
+
+ + Start Project + + + Book Call + +
+
+ ); +} diff --git a/components/placeholder-page.tsx b/components/placeholder-page.tsx new file mode 100644 index 0000000..38d2a2b --- /dev/null +++ b/components/placeholder-page.tsx @@ -0,0 +1,41 @@ +import Link from "next/link"; + +interface PlaceholderPageProps { + eyebrow: string; + title: string; + body: string; + primaryHref?: string; + primaryLabel?: string; + secondaryHref?: string; + secondaryLabel?: string; +} + +export function PlaceholderPage({ + eyebrow, + title, + body, + primaryHref = "/project-request", + primaryLabel = "Start a Project Request", + secondaryHref = "/book-call", + secondaryLabel = "Book a Quick Call" +}: PlaceholderPageProps) { + return ( +
+
+
+

{eyebrow}

+

{title}

+

{body}

+
+ + {primaryLabel} + + + {secondaryLabel} + +
+
+
+
+ ); +} diff --git a/components/site-footer.tsx b/components/site-footer.tsx new file mode 100644 index 0000000..8cdf31a --- /dev/null +++ b/components/site-footer.tsx @@ -0,0 +1,40 @@ +import Link from "next/link"; +import { publicRoutes, serviceRoutes } from "@/lib/routes"; + +export function SiteFooter() { + return ( +
+
+
+

Vail Renovations

+

+ Renovation help without the runaround. Photos, budget and a complete scope are not required to start. +

+

Ottawa and surrounding areas.

+
+
+

Public routes

+
+ {publicRoutes.map((route) => ( + + {route.label} + + ))} + Privacy + Terms +
+
+
+

Service pages

+
+ {serviceRoutes.slice(0, 6).map((route) => ( + + {route.title} + + ))} +
+
+
+
+ ); +} diff --git a/components/site-header.tsx b/components/site-header.tsx new file mode 100644 index 0000000..81a5786 --- /dev/null +++ b/components/site-header.tsx @@ -0,0 +1,55 @@ +import Link from "next/link"; +import { publicRoutes } from "@/lib/routes"; + +export function SiteHeader() { + return ( +
+ +
+ ); +} diff --git a/docs/control/VAIL_PLATFORM_FOUNDATION_REPORT.md b/docs/control/VAIL_PLATFORM_FOUNDATION_REPORT.md new file mode 100644 index 0000000..7ef953a --- /dev/null +++ b/docs/control/VAIL_PLATFORM_FOUNDATION_REPORT.md @@ -0,0 +1,152 @@ +# Vail Platform Foundation Report + +## GO/HOLD + +HOLD pending live dependency installation and command execution in CI or a local environment with npm registry access. + +The repository has been converted at the file level from a static Netlify HTML site into a Next.js App Router foundation. Verification commands were defined but not executed by this implementation environment. + +## Repo conversion summary + +Before this ticket, the repo was a static Netlify site with root-level HTML files, static CSS/JS assets, Netlify Forms and a static checker. + +This ticket adds a Next.js App Router foundation with TypeScript, Tailwind, public route placeholders, shared public layout components and Netlify Next.js deployment settings. + +## Changed file categories + +### Platform configuration + +- `package.json` — replaces static-only package with Next.js, React, TypeScript, Tailwind, ESLint and Netlify Next plugin scripts/dependencies. +- `next.config.ts` — adds Next.js configuration and redirects from legacy static routes to the new route architecture. +- `tsconfig.json` — adds strict TypeScript configuration. +- `next-env.d.ts` — adds Next.js type references. +- `tailwind.config.ts` — adds Tailwind configuration and Vail design tokens. +- `postcss.config.js` — adds Tailwind/PostCSS configuration. +- `eslint.config.mjs` — adds Next.js ESLint flat config. +- `netlify.toml` — switches Netlify from static root publish to Next.js build with `.next` and `@netlify/plugin-nextjs`. + +### App routes + +- `app/layout.tsx` — creates the root public layout. +- `app/page.tsx` — creates the homepage foundation. +- `app/globals.css` — adds global CSS and Tailwind directives. +- `app/services/page.tsx` — adds services overview route. +- `app/services/[slug]/page.tsx` — adds service-detail placeholders. +- `app/how-it-works/page.tsx` — adds How It Works placeholder. +- `app/project-request/page.tsx` — adds Project Starter placeholder. +- `app/scope-builder/page.tsx` — adds Scope Builder placeholder. +- `app/book-call/page.tsx` — adds booking handoff placeholder. +- `app/callback/page.tsx` — adds callback placeholder. +- `app/contact/page.tsx` — adds contact placeholder. +- `app/privacy/page.tsx` — adds privacy placeholder. +- `app/terms/page.tsx` — adds terms placeholder. + +### Shared code + +- `components/site-header.tsx` — adds desktop/mobile public header. +- `components/site-footer.tsx` — adds public footer. +- `components/mobile-cta-bar.tsx` — adds mobile sticky CTAs. +- `components/placeholder-page.tsx` — adds reusable placeholder-page component. +- `lib/routes.ts` — centralizes public and service route metadata. + +### Verification and docs + +- `scripts/check-site.js` — replaces static HTML/Netlify Forms checker with platform-foundation checks. +- `README.md` — updates project documentation for the Next.js foundation. +- `sitemap.xml` — updates sitemap to new public routes. +- `docs/control/VAIL_PLATFORM_FOUNDATION_REPORT.md` — records this report. + +## Routes added + +- `/` +- `/services` +- `/services/bathroom-renovations` +- `/services/basement-renovations` +- `/services/kitchen-updates` +- `/services/repairs-refreshes` +- `/services/pre-sale-improvements` +- `/services/rental-turnovers` +- `/how-it-works` +- `/project-request` +- `/scope-builder` +- `/book-call` +- `/callback` +- `/contact` +- `/privacy` +- `/terms` + +## Legacy redirects + +- `/start-a-project` -> `/project-request` +- `/fix-list-builder` -> `/scope-builder` +- `/maintenance-plans` -> `/services/repairs-refreshes` +- `/inspection-report-repairs` -> `/services/pre-sale-improvements` +- `/renovations` -> `/services` +- `/thank-you` -> `/contact` + +## Scripts added + +- `npm run dev` +- `npm run build` +- `npm run start` +- `npm run typecheck` +- `npm run lint` +- `npm run check` + +## Deployment assumptions + +- Netlify remains the deployment target for this foundation. +- Netlify must install dependencies and run `npm run build`. +- The publish directory is `.next`. +- `@netlify/plugin-nextjs` is required for runtime support. + +## Explicit V1 deferrals + +This ticket does not implement: + +- CRM +- Database +- Auth +- Lead creation +- Project Starter functionality +- Scope Builder functionality +- Callback submission +- Photo uploads +- Private storage +- Notifications +- Analytics +- Estimate workflow +- Native booking engine + +## Remaining blockers before full V1 CRM + +The full V1 CRM remains blocked until a real backend path is implemented or selected: + +1. Database persistence. +2. Authenticated CRM users. +3. Role-protected CRM routes. +4. Private photo/file storage. +5. Durable migrations or equivalent schema mechanism. +6. Server-side validation and permission enforcement. +7. First-admin bootstrap path. +8. Environment variable inventory for backend/auth/storage. + +## Verification commands required + +Run in an environment with npm registry access: + +```bash +npm install +npm run check +npm run typecheck +npm run lint +npm run build +``` + +## Known limitations + +- No package lock file was generated in this environment. +- Build/typecheck/lint were not executed in this environment. +- Existing root-level static HTML files remain in the repo for now and are superseded by Next.js routes and redirects. +- The current public routes are placeholders where the implementation ticket required placeholders. +- No backend, auth, storage or CRM behavior exists yet. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..0749562 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,14 @@ +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 eslintConfig = [...compat.extends("next/core-web-vitals", "next/typescript")]; + +export default eslintConfig; diff --git a/lib/routes.ts b/lib/routes.ts new file mode 100644 index 0000000..5edb36a --- /dev/null +++ b/lib/routes.ts @@ -0,0 +1,50 @@ +export const publicRoutes = [ + { href: "/services", label: "Services" }, + { href: "/how-it-works", label: "How It Works" }, + { href: "/project-request", label: "Project Request" }, + { href: "/scope-builder", label: "Scope Builder" }, + { href: "/contact", label: "Contact" } +] as const; + +export const serviceRoutes = [ + { + href: "/services/bathroom-renovations", + title: "Bathroom Renovations", + description: "Bathroom updates, repairs and practical refreshes organized around the work that actually needs to happen." + }, + { + href: "/services/basement-renovations", + title: "Basement Renovations", + description: "Basement finishing, repair planning and multi-step interior projects with clear next steps." + }, + { + href: "/services/kitchen-updates", + title: "Kitchen Updates", + description: "Kitchen improvements, finish updates and functional fixes without forcing a full luxury remodel path." + }, + { + href: "/services/repairs-refreshes", + title: "Repairs and Refreshes", + description: "Drywall, trim, paint, flooring and home repair lists grouped into a manageable request." + }, + { + href: "/services/pre-sale-improvements", + title: "Pre-Sale Improvements", + description: "Focused improvements that help homeowners prepare a property for listing or turnover." + }, + { + href: "/services/rental-turnovers", + title: "Rental Turnovers", + description: "Repair and refresh planning for rental units, turnover lists and practical make-ready work." + } +] as const; + +export const placeholderRoutes = [ + "/project-request", + "/scope-builder", + "/book-call", + "/callback", + "/contact", + "/privacy", + "/terms" +] as const; diff --git a/netlify.toml b/netlify.toml index d12e8d2..771d822 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,28 +1,6 @@ [build] - publish = "." - command = "" + command = "npm run build" + publish = ".next" -[[redirects]] - from = "/start-a-project" - to = "/start-a-project.html" - status = 200 - -[[redirects]] - from = "/fix-list-builder" - to = "/fix-list-builder.html" - status = 200 - -[[redirects]] - from = "/maintenance-plans" - to = "/maintenance-plans.html" - status = 200 - -[[redirects]] - from = "/inspection-report-repairs" - to = "/inspection-report-repairs.html" - status = 200 - -[[redirects]] - from = "/thank-you" - to = "/thank-you.html" - status = 200 +[[plugins]] + package = "@netlify/plugin-nextjs" diff --git a/next-env.d.ts b/next-env.d.ts new file mode 100644 index 0000000..efc031d --- /dev/null +++ b/next-env.d.ts @@ -0,0 +1,4 @@ +/// +/// + +// This file is automatically maintained by Next.js. Do not edit manually. diff --git a/next.config.ts b/next.config.ts new file mode 100644 index 0000000..afaa36c --- /dev/null +++ b/next.config.ts @@ -0,0 +1,41 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + reactStrictMode: true, + async redirects() { + return [ + { + source: "/start-a-project", + destination: "/project-request", + permanent: true + }, + { + source: "/fix-list-builder", + destination: "/scope-builder", + permanent: true + }, + { + source: "/maintenance-plans", + destination: "/services/repairs-refreshes", + permanent: false + }, + { + source: "/inspection-report-repairs", + destination: "/services/pre-sale-improvements", + permanent: false + }, + { + source: "/renovations", + destination: "/services", + permanent: true + }, + { + source: "/thank-you", + destination: "/contact", + permanent: false + } + ]; + } +}; + +export default nextConfig; diff --git a/package.json b/package.json index db7d3ac..dda48f8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,31 @@ { - "name": "vail-renovations-static-site", + "name": "vail-renovations-platform", + "version": "0.1.0", "private": true, "scripts": { - "check": "node scripts/check-site.js" + "dev": "next dev", + "build": "next build", + "start": "next start", + "typecheck": "tsc --noEmit", + "lint": "eslint . --max-warnings=0", + "check": "node scripts/check-site.js && npm run typecheck && npm run build" + }, + "dependencies": { + "next": "15.3.4", + "react": "19.0.0", + "react-dom": "19.0.0" + }, + "devDependencies": { + "@eslint/eslintrc": "3.3.1", + "@netlify/plugin-nextjs": "5.11.3", + "@types/node": "22.15.30", + "@types/react": "19.0.12", + "@types/react-dom": "19.0.4", + "autoprefixer": "10.4.21", + "eslint": "9.28.0", + "eslint-config-next": "15.3.4", + "postcss": "8.5.6", + "tailwindcss": "3.4.17", + "typescript": "5.8.3" } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..5cbc2c7 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/scripts/check-site.js b/scripts/check-site.js index 4726247..bb1fd82 100644 --- a/scripts/check-site.js +++ b/scripts/check-site.js @@ -3,103 +3,117 @@ const path = require("node:path"); const root = process.cwd(); const requiredFiles = [ - "index.html", - "start-a-project.html", - "fix-list-builder.html", - "maintenance-plans.html", - "inspection-report-repairs.html", - "thank-you.html", - "assets/styles.css", - "assets/app.js", + "package.json", + "next.config.ts", + "tsconfig.json", + "tailwind.config.ts", + "postcss.config.js", + "eslint.config.mjs", + "app/layout.tsx", + "app/page.tsx", + "app/globals.css", + "app/services/page.tsx", + "app/services/[slug]/page.tsx", + "app/how-it-works/page.tsx", + "app/project-request/page.tsx", + "app/scope-builder/page.tsx", + "app/book-call/page.tsx", + "app/callback/page.tsx", + "app/contact/page.tsx", + "app/privacy/page.tsx", + "app/terms/page.tsx", + "components/site-header.tsx", + "components/site-footer.tsx", + "components/mobile-cta-bar.tsx", + "components/placeholder-page.tsx", + "lib/routes.ts", "netlify.toml", - "_redirects", - "robots.txt", - "sitemap.xml", + "docs/control/VAIL_PLATFORM_FOUNDATION_REPORT.md" ]; -const formRequirements = { - "start-a-project.html": "vail-project-intake", - "fix-list-builder.html": "vail-fix-list", - "maintenance-plans.html": "vail-maintenance-plan", - "inspection-report-repairs.html": "vail-inspection-report", -}; - -const bannedPatterns = [/SummitLine/i, /Roofing/i, /roof repair/i, /roofing-lead/i]; +const bannedLegacyTerms = ["SummitLine", "Roofing", "roof repair", "roofing-lead"]; +const forbiddenImplementationTerms = [ + "createLead", + "lead_photos", + "supabase", + "data-netlify", + "localStorage" +]; const failures = []; -function read(file) { - return fs.readFileSync(path.join(root, file), "utf8"); -} - function exists(file) { return fs.existsSync(path.join(root, file)); } +function read(file) { + return fs.readFileSync(path.join(root, file), "utf8"); +} + for (const file of requiredFiles) { if (!exists(file)) { failures.push(`Missing required file: ${file}`); } } -for (const [file, formName] of Object.entries(formRequirements)) { - if (!exists(file)) { - continue; +if (exists("package.json")) { + const packageJson = JSON.parse(read("package.json")); + const scripts = packageJson.scripts || {}; + for (const script of ["dev", "build", "typecheck", "lint", "check"]) { + if (!scripts[script]) { + failures.push(`package.json is missing script: ${script}`); + } } - - const html = read(file); - const formPattern = new RegExp(`]*name=["']${formName}["'][\\s\\S]*?`, "i"); - const formMatch = html.match(formPattern); - - if (!formMatch) { - failures.push(`${file} is missing form ${formName}`); - continue; + for (const dependency of ["next", "react", "react-dom"]) { + if (!packageJson.dependencies || !packageJson.dependencies[dependency]) { + failures.push(`package.json is missing dependency: ${dependency}`); + } } +} - const form = formMatch[0]; - const checks = [ - { label: "method=\"POST\"", pattern: /method=["']POST["']/i }, - { label: "data-netlify=\"true\"", pattern: /data-netlify=["']true["']/i }, - { label: "action=\"/thank-you\"", pattern: /action=["']\/thank-you["']/i }, - { label: "netlify-honeypot=\"bot-field\"", pattern: /netlify-honeypot=["']bot-field["']/i }, - { label: "hidden form-name input", pattern: new RegExp(`]*type=["']hidden["'][^>]*name=["']form-name["'][^>]*value=["']${formName}["']`, "i") }, - { label: "honeypot input", pattern: /]*name=["']bot-field["']/i }, - ]; - - for (const check of checks) { - if (!check.pattern.test(form)) { - failures.push(`${file} form ${formName} is missing ${check.label}`); - } +if (exists("netlify.toml")) { + const netlifyConfig = read("netlify.toml"); + if (!/command\s*=\s*["']npm run build["']/.test(netlifyConfig)) { + failures.push('netlify.toml must run "npm run build"'); + } + if (!/publish\s*=\s*["']\.next["']/.test(netlifyConfig)) { + failures.push('netlify.toml must publish ".next"'); + } + if (!/@netlify\/plugin-nextjs/.test(netlifyConfig)) { + failures.push("netlify.toml must include @netlify/plugin-nextjs"); } } -for (const file of fs.readdirSync(root).filter((name) => name.endsWith(".html"))) { - const html = read(file); - for (const pattern of bannedPatterns) { - if (pattern.test(html)) { - failures.push(`${file} contains banned legacy term: ${pattern}`); +for (const file of fs.readdirSync(root, { recursive: true }).filter((name) => typeof name === "string" && /\.(ts|tsx|js|html|md|toml)$/.test(name))) { + if (file.includes("node_modules") || file.includes(".next") || file === "scripts/check-site.js") { + continue; + } + const body = read(file); + for (const term of bannedLegacyTerms) { + if (body.includes(term)) { + failures.push(`${file} contains banned legacy term: ${term}`); } } } -if (exists("netlify.toml")) { - const netlifyConfig = read("netlify.toml"); - if (!/publish\s*=\s*["']\.["']/.test(netlifyConfig)) { - failures.push('netlify.toml must contain publish = "."'); - } - if (/publish\s*=\s*["']dist["']/.test(netlifyConfig)) { - failures.push('netlify.toml must not contain publish = "dist"'); +const scopedFiles = ["app/project-request/page.tsx", "app/scope-builder/page.tsx", "app/callback/page.tsx"]; +for (const file of scopedFiles) { + if (!exists(file)) { + continue; } - if (!/command\s*=\s*["']["']/.test(netlifyConfig)) { - failures.push('netlify.toml must contain command = ""'); + const body = read(file); + for (const term of forbiddenImplementationTerms) { + if (body.includes(term)) { + failures.push(`${file} appears to introduce out-of-scope foundation behavior: ${term}`); + } } } if (failures.length > 0) { - console.error("Static site check failed:"); + console.error("Platform foundation check failed:"); for (const failure of failures) { console.error(`- ${failure}`); } process.exit(1); } -console.log("Static site check passed."); +console.log("Platform foundation check passed."); diff --git a/sitemap.xml b/sitemap.xml index f292fe6..5901c25 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -1,9 +1,19 @@ https://vail-renovations.netlify.app/ - https://vail-renovations.netlify.app/start-a-project - https://vail-renovations.netlify.app/fix-list-builder - https://vail-renovations.netlify.app/maintenance-plans - https://vail-renovations.netlify.app/inspection-report-repairs - https://vail-renovations.netlify.app/thank-you + https://vail-renovations.netlify.app/services + https://vail-renovations.netlify.app/services/bathroom-renovations + https://vail-renovations.netlify.app/services/basement-renovations + https://vail-renovations.netlify.app/services/kitchen-updates + https://vail-renovations.netlify.app/services/repairs-refreshes + https://vail-renovations.netlify.app/services/pre-sale-improvements + https://vail-renovations.netlify.app/services/rental-turnovers + https://vail-renovations.netlify.app/how-it-works + https://vail-renovations.netlify.app/project-request + https://vail-renovations.netlify.app/scope-builder + https://vail-renovations.netlify.app/book-call + https://vail-renovations.netlify.app/callback + https://vail-renovations.netlify.app/contact + https://vail-renovations.netlify.app/privacy + https://vail-renovations.netlify.app/terms diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..56ec307 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,23 @@ +import type { Config } from "tailwindcss"; + +const config: Config = { + content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}", "./lib/**/*.{ts,tsx}"], + theme: { + extend: { + colors: { + ink: "#16201a", + forest: "#254734", + moss: "#6f8364", + stone: "#e9e1d5", + oatmeal: "#f6f1e9", + brass: "#b98b4b" + }, + boxShadow: { + soft: "0 24px 80px rgba(22, 32, 26, 0.12)" + } + } + }, + plugins: [] +}; + +export default config; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6b9a958 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +}