Initial commit
This commit is contained in:
parent
84ca2c6fba
commit
48e6b100ea
22
components.json
Normal file
22
components.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide",
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {}
|
||||||
|
}
|
||||||
100
package-lock.json
generated
100
package-lock.json
generated
@ -8,9 +8,15 @@
|
|||||||
"name": "summit-painting",
|
"name": "summit-painting",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"framer-motion": "^12.23.12",
|
||||||
|
"lucide-react": "^0.542.0",
|
||||||
"next": "15.5.2",
|
"next": "15.5.2",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0"
|
"react-dom": "19.1.0",
|
||||||
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
@ -2291,12 +2297,33 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/class-variance-authority": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://polar.sh/cva"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/client-only": {
|
"node_modules/client-only": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz",
|
||||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/clsx": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/color": {
|
"node_modules/color": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
@ -3309,6 +3336,33 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/framer-motion": {
|
||||||
|
"version": "12.23.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.12.tgz",
|
||||||
|
"integrity": "sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-dom": "^12.23.12",
|
||||||
|
"motion-utils": "^12.23.6",
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@emotion/is-prop-valid": "*",
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@emotion/is-prop-valid": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/function-bind": {
|
"node_modules/function-bind": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||||
@ -4475,6 +4529,15 @@
|
|||||||
"loose-envify": "cli.js"
|
"loose-envify": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.542.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.542.0.tgz",
|
||||||
|
"integrity": "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.18",
|
"version": "0.30.18",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
|
||||||
@ -4581,6 +4644,21 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/motion-dom": {
|
||||||
|
"version": "12.23.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.12.tgz",
|
||||||
|
"integrity": "sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"motion-utils": "^12.23.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/motion-utils": {
|
||||||
|
"version": "12.23.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz",
|
||||||
|
"integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@ -5689,13 +5767,31 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwind-merge": {
|
||||||
|
"version": "3.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
|
||||||
|
"integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/dcastil"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.13",
|
"version": "4.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||||
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
|
"integrity": "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/tailwindcss-animate": {
|
||||||
|
"version": "1.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz",
|
||||||
|
"integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"tailwindcss": ">=3.0.0 || insiders"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tapable": {
|
"node_modules/tapable": {
|
||||||
"version": "2.2.3",
|
"version": "2.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
|
||||||
|
|||||||
16
package.json
16
package.json
@ -9,19 +9,25 @@
|
|||||||
"lint": "eslint"
|
"lint": "eslint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"framer-motion": "^12.23.12",
|
||||||
|
"lucide-react": "^0.542.0",
|
||||||
|
"next": "15.5.2",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"next": "15.5.2"
|
"tailwind-merge": "^3.3.1",
|
||||||
|
"tailwindcss-animate": "^1.0.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"typescript": "^5",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"@tailwindcss/postcss": "^4",
|
|
||||||
"tailwindcss": "^4",
|
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.5.2",
|
"eslint-config-next": "15.5.2",
|
||||||
"@eslint/eslintrc": "^3"
|
"tailwindcss": "^4",
|
||||||
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
const config = {
|
export default {
|
||||||
plugins: ["@tailwindcss/postcss"],
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
|
||||||
|
|||||||
68
src/app/about/page.tsx
Normal file
68
src/app/about/page.tsx
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import AboutUsSection from "@/components/ui/about-us-section";
|
||||||
|
|
||||||
|
export default function AboutPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col">
|
||||||
|
|
||||||
|
{/* Revamped About section (21st.dev base, adapted to our palette/content) */}
|
||||||
|
<AboutUsSection />
|
||||||
|
|
||||||
|
{/* Keep key parts of our original content to complement the new section */}
|
||||||
|
<section className="py-20 bg-white">
|
||||||
|
<div className="max-w-5xl mx-auto px-4 grid grid-cols-1 md:grid-cols-2 gap-12">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl md:text-3xl font-semibold text-taupe-900 mb-4">Why Choose Us</h2>
|
||||||
|
<p className="text-taupe-700 mb-4">
|
||||||
|
We combine careful surface prep, premium materials, and a tidy jobsite to deliver long‑lasting results.
|
||||||
|
You'll get reliable scheduling, clear updates, and a thorough walkthrough at completion.
|
||||||
|
</p>
|
||||||
|
<ul className="space-y-2 text-taupe-800">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-olive-700" />
|
||||||
|
Premium coatings and primers for exterior and interior applications
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-olive-700" />
|
||||||
|
Clean lines, texture matching, and seamless drywall repairs
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-olive-700" />
|
||||||
|
Wood repair and refinishing to protect architectural details
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-olive-700" />
|
||||||
|
Respect for your home with daily cleanup and tidy edges
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl md:text-3xl font-semibold text-taupe-900 mb-4">Our Values</h2>
|
||||||
|
<ul className="space-y-2 text-taupe-800">
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-olive-700" />
|
||||||
|
Quality craftsmanship
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-olive-700" />
|
||||||
|
Honest communication
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-olive-700" />
|
||||||
|
Customer satisfaction
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-olive-700" />
|
||||||
|
Professional integrity
|
||||||
|
</li>
|
||||||
|
<li className="flex items-start gap-2">
|
||||||
|
<span className="mt-1 h-2 w-2 rounded-full bg-olive-700" />
|
||||||
|
Continuous improvement
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/app/areas/page.tsx
Normal file
46
src/app/areas/page.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
import ServiceAreasShowcase from "@/components/ServiceAreasShowcase";
|
||||||
|
|
||||||
|
export default function AreasPage() {
|
||||||
|
const areas = [
|
||||||
|
{ name: "Colorado Springs", description: "Serving the entire Colorado Springs metro area" },
|
||||||
|
{ name: "Monument", description: "Including Monument and surrounding communities" },
|
||||||
|
{ name: "Black Forest", description: "Serving Black Forest and nearby neighborhoods" },
|
||||||
|
{ name: "Woodland Park", description: "Covering Woodland Park and surrounding areas" },
|
||||||
|
{ name: "Guffey", description: "Including Guffey and Lake George region" },
|
||||||
|
{ name: "Lake George", description: "Serving Lake George and surrounding communities" }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="bg-gradient-to-br from-taupe-50 to-taupe-100 py-16 md:py-24">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-taupe-900 mb-6">
|
||||||
|
Service Areas
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-taupe-700 max-w-2xl mx-auto mb-10">
|
||||||
|
We proudly serve these communities in Colorado Springs and surrounding areas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Areas Showcase (replaces static list) */}
|
||||||
|
<ServiceAreasShowcase areas={areas} />
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section className="py-16 bg-taupe-50">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<h2 className="text-3xl font-bold text-taupe-900 mb-6">Need our services in your area?</h2>
|
||||||
|
<p className="text-xl text-taupe-700 max-w-2xl mx-auto mb-10">
|
||||||
|
Contact us today for a free consultation and quote.
|
||||||
|
</p>
|
||||||
|
<Button variant="primary">
|
||||||
|
<Link href="/quote">Get a Free Quote</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
63
src/app/cookies/page.tsx
Normal file
63
src/app/cookies/page.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
|
||||||
|
export default function CookiesPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="bg-gradient-to-br from-taupe-50 to-taupe-100 py-16 md:py-24">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-taupe-900 mb-6">
|
||||||
|
Cookie Policy
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<section className="py-16 bg-white">
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
|
<div className="prose prose-taupe max-w-none">
|
||||||
|
<h2>What Are Cookies?</h2>
|
||||||
|
<p>Cookies are small text files that are placed on your device when you visit a website. They help websites remember information about your visit and improve your browsing experience.</p>
|
||||||
|
|
||||||
|
<h2>How We Use Cookies</h2>
|
||||||
|
<p>We use cookies for the following purposes:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Essential cookies: Required for the website to function properly</li>
|
||||||
|
<li>Performance cookies: Help us understand how visitors interact with our website</li>
|
||||||
|
<li>Functional cookies: Enable enhanced functionality and personalization</li>
|
||||||
|
<li>Targeting cookies: Used to deliver relevant advertising</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h2>Types of Cookies We Use</h2>
|
||||||
|
<h3>Essential Cookies</h3>
|
||||||
|
<p>These cookies are necessary for the website to function properly and cannot be switched off in our systems. They are usually only set in response to actions made by you which amount to a request for services, such as setting your privacy preferences, logging in, or filling in forms.</p>
|
||||||
|
|
||||||
|
<h3>Performance Cookies</h3>
|
||||||
|
<p>These cookies allow us to count visits and traffic sources so we can measure and improve the performance of our site. They help us understand which pages are the most and least popular and see how visitors move around the site.</p>
|
||||||
|
|
||||||
|
<h3>Functional Cookies</h3>
|
||||||
|
<p>These cookies enable the website to provide enhanced functionality and personalization. They may be set by us or by third-party providers whose services we have added to our pages.</p>
|
||||||
|
|
||||||
|
<h3>Targeting Cookies</h3>
|
||||||
|
<p>These cookies may be set through our site by our advertising partners. They may be used by those companies to build a profile of your interests and show you relevant advertisements on other sites.</p>
|
||||||
|
|
||||||
|
<h2>Managing Cookies</h2>
|
||||||
|
<p>You can control and/or delete cookies as you wish. You can delete all cookies that are already on your device and you can set most browsers to prevent them from being placed. However, if you do this, you may have to manually adjust some preferences every time you visit the site.</p>
|
||||||
|
|
||||||
|
<h2>Changes to This Policy</h2>
|
||||||
|
<p>We reserve the right to modify this cookie policy at any time. Changes will be posted on this page with an updated effective date.</p>
|
||||||
|
|
||||||
|
<h2>Contact Us</h2>
|
||||||
|
<p>If you have any questions about this cookie policy, please contact us at:</p>
|
||||||
|
<p>
|
||||||
|
Summit Painting & Handyman<br />
|
||||||
|
Email: nicholai@biohazardvfx.com<br />
|
||||||
|
Phone: (719) 660-4281
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
src/app/gallery/page.tsx
Normal file
131
src/app/gallery/page.tsx
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
|
||||||
|
export default function GalleryPage() {
|
||||||
|
// Mock data for gallery items
|
||||||
|
const galleryItems = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Front Porch Renovation",
|
||||||
|
description: "Complete exterior paint refresh with new trim and siding.",
|
||||||
|
location: "Colorado Springs",
|
||||||
|
category: "painting",
|
||||||
|
beforeImage: "/placeholder-before.jpg",
|
||||||
|
afterImage: "/placeholder-after.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Kitchen Remodel",
|
||||||
|
description: "Full kitchen renovation with new cabinets and paint.",
|
||||||
|
location: "Monument",
|
||||||
|
category: "painting",
|
||||||
|
beforeImage: "/placeholder-before.jpg",
|
||||||
|
afterImage: "/placeholder-after.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Deck Restoration",
|
||||||
|
description: "Complete deck restoration and staining project.",
|
||||||
|
location: "Black Forest",
|
||||||
|
category: "wood",
|
||||||
|
beforeImage: "/placeholder-before.jpg",
|
||||||
|
afterImage: "/placeholder-after.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "Drywall Repair",
|
||||||
|
description: "Ceiling repair and texture matching.",
|
||||||
|
location: "Woodland Park",
|
||||||
|
category: "drywall",
|
||||||
|
beforeImage: "/placeholder-before.jpg",
|
||||||
|
afterImage: "/placeholder-after.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: "Exterior Painting",
|
||||||
|
description: "Complete home exterior paint refresh.",
|
||||||
|
location: "Guffey",
|
||||||
|
category: "painting",
|
||||||
|
beforeImage: "/placeholder-before.jpg",
|
||||||
|
afterImage: "/placeholder-after.jpg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
title: "Trim Installation",
|
||||||
|
description: "New trim installation in living room.",
|
||||||
|
location: "Lake George",
|
||||||
|
category: "installations",
|
||||||
|
beforeImage: "/placeholder-before.jpg",
|
||||||
|
afterImage: "/placeholder-after.jpg"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const categories = ["all", "painting", "drywall", "wood", "installations"];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="bg-gradient-to-br from-taupe-50 to-taupe-100 py-16 md:py-24">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-taupe-900 mb-6">
|
||||||
|
Before & After Gallery
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-taupe-700 max-w-2xl mx-auto mb-10">
|
||||||
|
See our transformation work in action. Real projects, real results.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Category Filter */}
|
||||||
|
<section className="py-8 bg-white border-b border-taupe-200">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="flex flex-wrap justify-center gap-2">
|
||||||
|
{categories.map((category) => (
|
||||||
|
<button
|
||||||
|
key={category}
|
||||||
|
className="px-4 py-2 rounded-full text-taupe-700 hover:bg-taupe-100 transition-colors capitalize"
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Gallery Grid */}
|
||||||
|
<section className="py-16 bg-white">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{galleryItems.map((item) => (
|
||||||
|
<div key={item.id} className="border border-taupe-200 rounded-lg overflow-hidden hover:shadow-md transition-shadow">
|
||||||
|
<div className="relative h-64">
|
||||||
|
<div className="bg-gray-200 border-2 border-dashed w-full h-full flex items-center justify-center text-gray-500">
|
||||||
|
Before/After Image
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className="text-xl font-bold text-taupe-900 mb-2">{item.title}</h3>
|
||||||
|
<p className="text-taupe-700 mb-2">{item.description}</p>
|
||||||
|
<p className="text-sm text-taupe-600"><strong>Location:</strong> {item.location}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section className="py-16 bg-taupe-50">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<h2 className="text-3xl font-bold text-taupe-900 mb-6">Want to see more?</h2>
|
||||||
|
<p className="text-xl text-taupe-700 max-w-2xl mx-auto mb-10">
|
||||||
|
Contact us today for a free consultation and quote.
|
||||||
|
</p>
|
||||||
|
<Button variant="primary">
|
||||||
|
<Link href="/quote">Get a Free Quote</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,26 +1,164 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
:root {
|
@plugin "tailwindcss-animate";
|
||||||
--background: #ffffff;
|
|
||||||
--foreground: #171717;
|
@custom-variant dark (&:is(.dark *));
|
||||||
}
|
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-geist-sans);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
/* Custom palette */
|
||||||
:root {
|
--color-taupe-50: #f8f6f4;
|
||||||
--background: #0a0a0a;
|
--color-taupe-100: #f0ede9;
|
||||||
--foreground: #ededed;
|
--color-taupe-200: #e1ddd7;
|
||||||
}
|
--color-taupe-300: #d2cdc5;
|
||||||
|
--color-taupe-400: #b8b0a5;
|
||||||
|
--color-taupe-500: #9e968a;
|
||||||
|
--color-taupe-600: #7a6c5d;
|
||||||
|
--color-taupe-700: #6b5e51;
|
||||||
|
--color-taupe-800: #5b4f43;
|
||||||
|
--color-taupe-900: #4a3b2f;
|
||||||
|
--color-taupe-950: #2d2218;
|
||||||
|
|
||||||
|
--color-olive-50: #f4f7f1;
|
||||||
|
--color-olive-100: #e8f0e2;
|
||||||
|
--color-olive-200: #d1e1c5;
|
||||||
|
--color-olive-300: #b9d2a8;
|
||||||
|
--color-olive-400: #9fb98a;
|
||||||
|
--color-olive-500: #748e58;
|
||||||
|
--color-olive-600: #5f7446;
|
||||||
|
--color-olive-700: #4b5b38;
|
||||||
|
--color-olive-800: #37432a;
|
||||||
|
--color-olive-900: #242b1c;
|
||||||
|
--color-olive-950: #12160d;
|
||||||
|
|
||||||
|
--color-brown-50: #faf8f6;
|
||||||
|
--color-brown-100: #f4f0ec;
|
||||||
|
--color-brown-200: #e8e0d7;
|
||||||
|
--color-brown-300: #d9cfc4;
|
||||||
|
--color-brown-400: #bda08d;
|
||||||
|
--color-brown-500: #9e7a64;
|
||||||
|
--color-brown-600: #7a5e4d;
|
||||||
|
--color-brown-700: #5b4636;
|
||||||
|
--color-brown-800: #4a372a;
|
||||||
|
--color-brown-900: #382a1f;
|
||||||
|
--color-brown-950: #261d15;
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
font-family: var(--font-geist-sans), Arial, Helvetica, sans-serif;
|
||||||
color: var(--foreground);
|
}
|
||||||
font-family: Arial, Helvetica, sans-serif;
|
|
||||||
|
:root {
|
||||||
|
--radius: 0.625rem;
|
||||||
|
--background: oklch(1 0 0);
|
||||||
|
--foreground: oklch(0.145 0 0);
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.145 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.145 0 0);
|
||||||
|
--primary: oklch(0.205 0 0);
|
||||||
|
--primary-foreground: oklch(0.985 0 0);
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.205 0 0);
|
||||||
|
--muted: oklch(0.97 0 0);
|
||||||
|
--muted-foreground: oklch(0.556 0 0);
|
||||||
|
--accent: oklch(0.97 0 0);
|
||||||
|
--accent-foreground: oklch(0.205 0 0);
|
||||||
|
--destructive: oklch(0.577 0.245 27.325);
|
||||||
|
--border: oklch(0.922 0 0);
|
||||||
|
--input: oklch(0.922 0 0);
|
||||||
|
--ring: oklch(0.708 0 0);
|
||||||
|
--chart-1: oklch(0.646 0.222 41.116);
|
||||||
|
--chart-2: oklch(0.6 0.118 184.704);
|
||||||
|
--chart-3: oklch(0.398 0.07 227.392);
|
||||||
|
--chart-4: oklch(0.828 0.189 84.429);
|
||||||
|
--chart-5: oklch(0.769 0.188 70.08);
|
||||||
|
--sidebar: oklch(0.985 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.145 0 0);
|
||||||
|
--sidebar-primary: oklch(0.205 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.97 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.205 0 0);
|
||||||
|
--sidebar-border: oklch(0.922 0 0);
|
||||||
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0.145 0 0);
|
||||||
|
--foreground: oklch(0.985 0 0);
|
||||||
|
--card: oklch(0.205 0 0);
|
||||||
|
--card-foreground: oklch(0.985 0 0);
|
||||||
|
--popover: oklch(0.205 0 0);
|
||||||
|
--popover-foreground: oklch(0.985 0 0);
|
||||||
|
--primary: oklch(0.922 0 0);
|
||||||
|
--primary-foreground: oklch(0.205 0 0);
|
||||||
|
--secondary: oklch(0.269 0 0);
|
||||||
|
--secondary-foreground: oklch(0.985 0 0);
|
||||||
|
--muted: oklch(0.269 0 0);
|
||||||
|
--muted-foreground: oklch(0.708 0 0);
|
||||||
|
--accent: oklch(0.269 0 0);
|
||||||
|
--accent-foreground: oklch(0.985 0 0);
|
||||||
|
--destructive: oklch(0.704 0.191 22.216);
|
||||||
|
--border: oklch(1 0 0 / 10%);
|
||||||
|
--input: oklch(1 0 0 / 15%);
|
||||||
|
--ring: oklch(0.556 0 0);
|
||||||
|
--chart-1: oklch(0.488 0.243 264.376);
|
||||||
|
--chart-2: oklch(0.696 0.17 162.48);
|
||||||
|
--chart-3: oklch(0.769 0.188 70.08);
|
||||||
|
--chart-4: oklch(0.627 0.265 303.9);
|
||||||
|
--chart-5: oklch(0.645 0.246 16.439);
|
||||||
|
--sidebar: oklch(0.205 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-primary: oklch(0.488 0.243 264.376);
|
||||||
|
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-accent: oklch(0.269 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 10%);
|
||||||
|
--sidebar-ring: oklch(0.556 0 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import UnifiedNav from "@/components/UnifiedNav";
|
||||||
|
import { Footer } from "@/components/Footer";
|
||||||
|
import { MobileCTA } from "@/components/MobileCTA";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
@ -13,8 +16,33 @@ const geistMono = Geist_Mono({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Summit Painting & Handyman Services",
|
||||||
description: "Generated by create next app",
|
description: "Professional residential painting and handyman services in Colorado Springs and surrounding areas",
|
||||||
|
};
|
||||||
|
|
||||||
|
const localBusinessJsonLd = {
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "HomeAndConstructionBusiness",
|
||||||
|
"name": "Summit Painting & Handyman Services",
|
||||||
|
"url": "https://localhost:3000/",
|
||||||
|
"telephone": "+1-719-660-4281",
|
||||||
|
"email": "nicholai@biohazardvfx.com",
|
||||||
|
"areaServed": [
|
||||||
|
"Colorado Springs",
|
||||||
|
"Monument",
|
||||||
|
"Black Forest",
|
||||||
|
"Woodland Park",
|
||||||
|
"Guffey",
|
||||||
|
"Lake George"
|
||||||
|
],
|
||||||
|
"servesCuisine": undefined,
|
||||||
|
"image": undefined,
|
||||||
|
"sameAs": [],
|
||||||
|
"priceRange": "$$",
|
||||||
|
"serviceArea": {
|
||||||
|
"@type": "AdministrativeArea",
|
||||||
|
"name": "Colorado Springs and surrounding areas"
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
@ -23,11 +51,27 @@ export default function RootLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" className="relative">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased min-h-screen flex flex-col`}
|
||||||
>
|
>
|
||||||
{children}
|
<a
|
||||||
|
href="#main-content"
|
||||||
|
className="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-[100] focus:px-3 focus:py-2 focus:rounded-md focus:bg-white focus:text-taupe-900 focus:shadow"
|
||||||
|
>
|
||||||
|
Skip to content
|
||||||
|
</a>
|
||||||
|
<script
|
||||||
|
type="application/ld+json"
|
||||||
|
suppressHydrationWarning
|
||||||
|
dangerouslySetInnerHTML={{ __html: JSON.stringify(localBusinessJsonLd) }}
|
||||||
|
/>
|
||||||
|
<UnifiedNav />
|
||||||
|
<main id="main-content" className="flex-grow">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
<Footer />
|
||||||
|
<MobileCTA />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
285
src/app/page.tsx
285
src/app/page.tsx
@ -1,103 +1,200 @@
|
|||||||
import Image from "next/image";
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
import GentleScrollingServices from "@/components/GentleScrollingServices";
|
||||||
|
import EnhancedHero from "@/components/EnhancedHero";
|
||||||
|
import EnhancedProcess from "@/components/EnhancedProcess";
|
||||||
|
import { VisualBridge } from "@/components/SeamlessTransition";
|
||||||
|
import TrustBar from "@/components/TrustBar";
|
||||||
|
import TestimonialsSection from "@/components/Testimonials";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
function PaintIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...props}>
|
||||||
|
<path d="M3 6.75h13.5A2.25 2.25 0 0 0 18.75 4.5V4A1.5 1.5 0 0 0 17.25 2.5H5.25A2.25 2.25 0 0 0 3 4.75v2z" />
|
||||||
|
<path d="M10.5 6.75V12a2.25 2.25 0 0 1-2.25 2.25H7.5A1.5 1.5 0 0 0 6 15.75v2.25a3 3 0 0 0 3 3h.75" />
|
||||||
|
<path d="M18.75 6.75H3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DrywallIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...props}>
|
||||||
|
<rect x="3.75" y="3.75" width="16.5" height="16.5" rx="2" />
|
||||||
|
<path d="M3.75 10.5h16.5M10.5 20.25V3.75" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function WoodIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" {...props}>
|
||||||
|
<path d="M4 4.75h16v14.5H4z" />
|
||||||
|
<path d="M8 6.75c1.5 1 1.5 2 0 3s-1.5 2 0 3 1.5 2 0 3M16 6.75c1.5 1 1.5 2 0 3s-1.5 2 0 3 1.5 2 0 3" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
const [servicesCompleted, setServicesCompleted] = useState(false);
|
||||||
<div className="font-sans grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20">
|
|
||||||
<main className="flex flex-col gap-[32px] row-start-2 items-center sm:items-start">
|
|
||||||
<Image
|
|
||||||
className="dark:invert"
|
|
||||||
src="/next.svg"
|
|
||||||
alt="Next.js logo"
|
|
||||||
width={180}
|
|
||||||
height={38}
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
<ol className="font-mono list-inside list-decimal text-sm/6 text-center sm:text-left">
|
|
||||||
<li className="mb-2 tracking-[-.01em]">
|
|
||||||
Get started by editing{" "}
|
|
||||||
<code className="bg-black/[.05] dark:bg-white/[.06] font-mono font-semibold px-1 py-0.5 rounded">
|
|
||||||
src/app/page.tsx
|
|
||||||
</code>
|
|
||||||
.
|
|
||||||
</li>
|
|
||||||
<li className="tracking-[-.01em]">
|
|
||||||
Save and see your changes instantly.
|
|
||||||
</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<div className="flex gap-4 items-center flex-col sm:flex-row">
|
const handleServicesComplete = (completed: boolean) => {
|
||||||
<a
|
setServicesCompleted(completed);
|
||||||
className="rounded-full border border-solid border-transparent transition-colors flex items-center justify-center bg-foreground text-background gap-2 hover:bg-[#383838] dark:hover:bg-[#ccc] font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 sm:w-auto"
|
};
|
||||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
return (
|
||||||
rel="noopener noreferrer"
|
<div className="min-h-screen flex flex-col">
|
||||||
|
{/* Enhanced Hero Section */}
|
||||||
|
<EnhancedHero />
|
||||||
|
{/* Trust signals */}
|
||||||
|
<TrustBar />
|
||||||
|
|
||||||
|
{/* Gentle Scrolling Services with Callback */}
|
||||||
|
<section className="py-20 bg-white">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 text-center">
|
||||||
|
<motion.h2
|
||||||
|
className="text-3xl md:text-4xl font-semibold text-center text-taupe-900 mb-8 [text-wrap:balance]"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
>
|
>
|
||||||
<Image
|
Our Services
|
||||||
className="dark:invert"
|
</motion.h2>
|
||||||
src="/vercel.svg"
|
|
||||||
alt="Vercel logomark"
|
|
||||||
width={20}
|
|
||||||
height={20}
|
|
||||||
/>
|
|
||||||
Deploy now
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className="rounded-full border border-solid border-black/[.08] dark:border-white/[.145] transition-colors flex items-center justify-center hover:bg-[#f2f2f2] dark:hover:bg-[#1a1a1a] hover:border-transparent font-medium text-sm sm:text-base h-10 sm:h-12 px-4 sm:px-5 w-full sm:w-auto md:w-[158px]"
|
|
||||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
Read our docs
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</main>
|
<GentleScrollingServices onComplete={handleServicesComplete} showLeadIn={false} />
|
||||||
<footer className="row-start-3 flex gap-[24px] flex-wrap items-center justify-center">
|
<motion.div
|
||||||
<a
|
className="max-w-7xl mx-auto px-4 mt-10 text-center"
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
initial={{ opacity: 0 }}
|
||||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
animate={{ opacity: servicesCompleted ? 1 : 0 }}
|
||||||
target="_blank"
|
transition={{ delay: 0.5 }}
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
<Image
|
<Button variant="outline" className="px-6 py-3">
|
||||||
aria-hidden
|
<Link href="/services">View All Services</Link>
|
||||||
src="/file.svg"
|
</Button>
|
||||||
alt="File icon"
|
</motion.div>
|
||||||
width={16}
|
</section>
|
||||||
height={16}
|
|
||||||
/>
|
{/* Visual Bridge - Smooth transition between sections */}
|
||||||
Learn
|
<VisualBridge />
|
||||||
</a>
|
|
||||||
<a
|
{/* Enhanced Process with Seamless Entrance */}
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
<motion.div
|
||||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
initial={{ opacity: 0, y: 50 }}
|
||||||
target="_blank"
|
animate={{ opacity: servicesCompleted ? 1 : 0, y: servicesCompleted ? 0 : 50 }}
|
||||||
rel="noopener noreferrer"
|
transition={{
|
||||||
>
|
duration: 0.8,
|
||||||
<Image
|
ease: "easeOut",
|
||||||
aria-hidden
|
delay: servicesCompleted ? 0.3 : 0
|
||||||
src="/window.svg"
|
}}
|
||||||
alt="Window icon"
|
>
|
||||||
width={16}
|
<AnimatePresence>
|
||||||
height={16}
|
{servicesCompleted && (
|
||||||
/>
|
<motion.div
|
||||||
Examples
|
initial={{ opacity: 0 }}
|
||||||
</a>
|
animate={{ opacity: 1 }}
|
||||||
<a
|
exit={{ opacity: 0 }}
|
||||||
className="flex items-center gap-2 hover:underline hover:underline-offset-4"
|
transition={{ duration: 0.5 }}
|
||||||
href="https://nextjs.org?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
>
|
||||||
target="_blank"
|
<EnhancedProcess />
|
||||||
rel="noopener noreferrer"
|
</motion.div>
|
||||||
>
|
)}
|
||||||
<Image
|
</AnimatePresence>
|
||||||
aria-hidden
|
</motion.div>
|
||||||
src="/globe.svg"
|
|
||||||
alt="Globe icon"
|
{/* Testimonials */}
|
||||||
width={16}
|
<motion.div
|
||||||
height={16}
|
initial={{ opacity: 0, y: 30 }}
|
||||||
/>
|
animate={{ opacity: servicesCompleted ? 1 : 0, y: servicesCompleted ? 0 : 30 }}
|
||||||
Go to nextjs.org →
|
transition={{
|
||||||
</a>
|
duration: 0.6,
|
||||||
</footer>
|
ease: "easeOut",
|
||||||
|
delay: servicesCompleted ? 0.6 : 0
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnimatePresence>
|
||||||
|
{servicesCompleted && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<TestimonialsSection />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Service Areas with Delayed Entrance */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={{ opacity: servicesCompleted ? 1 : 0, y: servicesCompleted ? 0 : 30 }}
|
||||||
|
transition={{
|
||||||
|
duration: 0.6,
|
||||||
|
ease: "easeOut",
|
||||||
|
delay: servicesCompleted ? 0.8 : 0
|
||||||
|
}}
|
||||||
|
className="bg-taupe-50"
|
||||||
|
>
|
||||||
|
<AnimatePresence>
|
||||||
|
{servicesCompleted && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<section className="py-20">
|
||||||
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
|
<motion.h2
|
||||||
|
className="text-3xl md:text-4xl font-semibold text-center text-taupe-900 mb-12 [text-wrap:balance]"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
>
|
||||||
|
Service Areas
|
||||||
|
</motion.h2>
|
||||||
|
<div className="max-w-3xl mx-auto text-center">
|
||||||
|
<motion.p
|
||||||
|
className="text-taupe-700 mb-8"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
transition={{ delay: 0.2 }}
|
||||||
|
>
|
||||||
|
Serving the following communities in Colorado Springs and surrounding areas:
|
||||||
|
</motion.p>
|
||||||
|
<div className="flex flex-wrap justify-center gap-2">
|
||||||
|
{[
|
||||||
|
"Colorado Springs",
|
||||||
|
"Monument",
|
||||||
|
"Black Forest",
|
||||||
|
"Woodland Park",
|
||||||
|
"Guffey",
|
||||||
|
"Lake George",
|
||||||
|
].map((city, index) => (
|
||||||
|
<motion.span
|
||||||
|
key={city}
|
||||||
|
className="inline-flex items-center rounded-full border border-taupe-200 bg-white/80 px-4 py-2 text-taupe-800 shadow-sm"
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
whileInView={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ delay: 0.3 + index * 0.1 }}
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
>
|
||||||
|
{city}
|
||||||
|
</motion.span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
91
src/app/privacy/page.tsx
Normal file
91
src/app/privacy/page.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Privacy Policy | Summit Painting & Handyman",
|
||||||
|
description: "How we collect, use, and protect your personal information.",
|
||||||
|
robots: { index: true, follow: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function PrivacyPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="bg-gradient-to-br from-taupe-50 to-taupe-100 py-16 md:py-24">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-taupe-900 mb-6">
|
||||||
|
Privacy Policy
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<section className="py-16 bg-white">
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
|
<div className="prose prose-taupe max-w-none">
|
||||||
|
<p className="text-sm text-taupe-600">Last updated: September 7, 2025</p>
|
||||||
|
<nav className="not-prose mt-4 mb-8">
|
||||||
|
<ul className="flex flex-wrap gap-x-6 gap-y-2 text-sm text-taupe-700">
|
||||||
|
<li><a href="#intro" className="hover:text-olive-700">Introduction</a></li>
|
||||||
|
<li><a href="#info" className="hover:text-olive-700">Information We Collect</a></li>
|
||||||
|
<li><a href="#use" className="hover:text-olive-700">How We Use Information</a></li>
|
||||||
|
<li><a href="#sharing" className="hover:text-olive-700">Information Sharing</a></li>
|
||||||
|
<li><a href="#security" className="hover:text-olive-700">Data Security</a></li>
|
||||||
|
<li><a href="#rights" className="hover:text-olive-700">Your Rights</a></li>
|
||||||
|
<li><a href="#changes" className="hover:text-olive-700">Changes</a></li>
|
||||||
|
<li><a href="#contact" className="hover:text-olive-700">Contact</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<a id="intro" />
|
||||||
|
<h2>Introduction</h2>
|
||||||
|
<p>This privacy policy explains how Summit Painting & Handyman Services collects, uses, and protects your personal information when you visit our website or use our services.</p>
|
||||||
|
|
||||||
|
<a id="info" />
|
||||||
|
<h2>Information We Collect</h2>
|
||||||
|
<p>We may collect the following types of information:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Personal identification information (name, address, phone number, email address)</li>
|
||||||
|
<li>Service-related information (project details, preferences)</li>
|
||||||
|
<li>Technical information (browser type, IP address, device information)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a id="use" />
|
||||||
|
<h2>How We Use Your Information</h2>
|
||||||
|
<p>We use the collected information for:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Providing and improving our services</li>
|
||||||
|
<li>Communicating with you about your projects</li>
|
||||||
|
<li>Processing quotes and orders</li>
|
||||||
|
<li>Marketing and promotional purposes (with your consent)</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<a id="sharing" />
|
||||||
|
<h2>Information Sharing</h2>
|
||||||
|
<p>We do not sell, trade, or rent your personal identification information to others. We may share information with trusted third parties who assist us in operating our website, conducting our business, or serving our customers.</p>
|
||||||
|
|
||||||
|
<a id="security" />
|
||||||
|
<h2>Data Security</h2>
|
||||||
|
<p>We implement a variety of security measures to maintain the safety of your personal information when you enter, submit, or access your personal information.</p>
|
||||||
|
|
||||||
|
<a id="rights" />
|
||||||
|
<h2>Your Rights</h2>
|
||||||
|
<p>You have the right to access, update, or delete your personal information. You may also withdraw your consent for marketing communications at any time.</p>
|
||||||
|
|
||||||
|
<a id="changes" />
|
||||||
|
<h2>Changes to This Policy</h2>
|
||||||
|
<p>We reserve the right to modify this privacy policy at any time. Changes will be posted on this page with an updated effective date.</p>
|
||||||
|
|
||||||
|
<a id="contact" />
|
||||||
|
<h2>Contact Us</h2>
|
||||||
|
<p>If you have any questions about this privacy policy, please contact us at:</p>
|
||||||
|
<p>
|
||||||
|
Summit Painting & Handyman<br />
|
||||||
|
Email: nicholai@biohazardvfx.com<br />
|
||||||
|
Phone: (719) 660-4281
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
157
src/app/quote/page.tsx
Normal file
157
src/app/quote/page.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
|
||||||
|
export default function QuotePage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="bg-gradient-to-br from-taupe-50 to-taupe-100 py-16 md:py-24">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-taupe-900 mb-6">
|
||||||
|
Get a Free Quote
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-taupe-700 max-w-2xl mx-auto mb-10">
|
||||||
|
Fill out our discovery form to get a personalized estimate for your project.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Form Steps */}
|
||||||
|
<section className="py-16 bg-white">
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
|
<div className="bg-white border border-taupe-200 rounded-lg shadow-md p-6 md:p-8">
|
||||||
|
{/* Progress Bar */}
|
||||||
|
<div className="mb-10">
|
||||||
|
<div className="flex justify-between relative">
|
||||||
|
<div className="absolute top-1/2 left-0 right-0 h-1 bg-taupe-200 -z-10 transform -translate-y-1/2"></div>
|
||||||
|
<div className="absolute top-1/2 left-0 right-0 h-1 bg-taupe-700 -z-10 transform -translate-y-1/2" style={{ width: '25%' }}></div>
|
||||||
|
{[1, 2, 3, 4, 5].map((step) => (
|
||||||
|
<div key={step} className="flex flex-col items-center">
|
||||||
|
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${
|
||||||
|
step <= 1 ? 'bg-taupe-700 text-white' : 'bg-taupe-100 text-taupe-700'
|
||||||
|
}`}>
|
||||||
|
{step}
|
||||||
|
</div>
|
||||||
|
<span className="mt-2 text-sm font-medium text-taupe-700 hidden md:block">
|
||||||
|
{step === 1 ? 'Contact' :
|
||||||
|
step === 2 ? 'Services' :
|
||||||
|
step === 3 ? 'Details' :
|
||||||
|
step === 4 ? 'Timing' :
|
||||||
|
'Review'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Step Content - Contact Info */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<h2 className="text-2xl font-bold text-taupe-900 mb-6">Contact Information</h2>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="name" className="block text-sm font-medium text-taupe-700 mb-1">Full Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
className="w-full px-4 py-2 border border-taupe-300 rounded-md focus:ring-taupe-700 focus:border-taupe-700"
|
||||||
|
placeholder="John Smith"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="phone" className="block text-sm font-medium text-taupe-700 mb-1">Phone Number *</label>
|
||||||
|
<input
|
||||||
|
type="tel"
|
||||||
|
id="phone"
|
||||||
|
className="w-full px-4 py-2 border border-taupe-300 rounded-md focus:ring-taupe-700 focus:border-taupe-700"
|
||||||
|
placeholder="(719) 555-0123"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-taupe-700 mb-1">Email Address</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
className="w-full px-4 py-2 border border-taupe-300 rounded-md focus:ring-taupe-700 focus:border-taupe-700"
|
||||||
|
placeholder="john@example.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="address" className="block text-sm font-medium text-taupe-700 mb-1">Service Address *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="address"
|
||||||
|
className="w-full px-4 py-2 border border-taupe-300 rounded-md focus:ring-taupe-700 focus:border-taupe-700"
|
||||||
|
placeholder="123 Main St"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="city" className="block text-sm font-medium text-taupe-700 mb-1">City *</label>
|
||||||
|
<select
|
||||||
|
id="city"
|
||||||
|
className="w-full px-4 py-2 border border-taupe-300 rounded-md focus:ring-taupe-700 focus:border-taupe-700"
|
||||||
|
>
|
||||||
|
<option value="">Select a city</option>
|
||||||
|
<option value="colorado-springs">Colorado Springs</option>
|
||||||
|
<option value="monument">Monument</option>
|
||||||
|
<option value="black-forest">Black Forest</option>
|
||||||
|
<option value="woodland-park">Woodland Park</option>
|
||||||
|
<option value="guffey">Guffey</option>
|
||||||
|
<option value="lake-george">Lake George</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-taupe-700 mb-1">Preferred Contact Method</label>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="contact-call"
|
||||||
|
name="contact-method"
|
||||||
|
className="h-4 w-4 text-taupe-700 focus:ring-taupe-700"
|
||||||
|
/>
|
||||||
|
<label htmlFor="contact-call" className="ml-2 text-taupe-700">Call</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="contact-text"
|
||||||
|
name="contact-method"
|
||||||
|
className="h-4 w-4 text-taupe-700 focus:ring-taupe-700"
|
||||||
|
/>
|
||||||
|
<label htmlFor="contact-text" className="ml-2 text-taupe-700">Text</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
id="contact-email"
|
||||||
|
name="contact-method"
|
||||||
|
className="h-4 w-4 text-taupe-700 focus:ring-taupe-700"
|
||||||
|
/>
|
||||||
|
<label htmlFor="contact-email" className="ml-2 text-taupe-700">Email</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Navigation Buttons */}
|
||||||
|
<div className="flex justify-end space-x-4">
|
||||||
|
<Button variant="outline">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary">
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
90
src/app/services/page.tsx
Normal file
90
src/app/services/page.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
|
||||||
|
export default function ServicesPage() {
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
id: "painting",
|
||||||
|
title: "Painting",
|
||||||
|
description: "Professional residential painting services for both interior and exterior projects with premium materials.",
|
||||||
|
icon: "🎨"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "drywall",
|
||||||
|
title: "Drywall & Texture",
|
||||||
|
description: "Drywall repair, popcorn ceiling removal, and texture matching for a seamless finish.",
|
||||||
|
icon: "🧱"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "wood",
|
||||||
|
title: "Wood Repair & Replacement",
|
||||||
|
description: "Residential wood repair, replacement, and refinishing for decks, trim, and structural elements.",
|
||||||
|
icon: "🪵"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "installations",
|
||||||
|
title: "Installations",
|
||||||
|
description: "Trim installation, faucet replacement, electrical fixture replacement, and TV mounting.",
|
||||||
|
icon: "🔧"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "exterior",
|
||||||
|
title: "Exterior Care",
|
||||||
|
description: "Power washing of home exteriors and concrete, gutter cleaning, door installation, and deck repairs.",
|
||||||
|
icon: "🏠"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "more",
|
||||||
|
title: "More Services",
|
||||||
|
description: "Including landscaping, tree trimming, junk haul away, and basic auto maintenance.",
|
||||||
|
icon: "🛠️"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="bg-gradient-to-br from-taupe-50 to-taupe-100 py-16 md:py-24">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-taupe-900 mb-6">
|
||||||
|
Our Services
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-taupe-700 max-w-2xl mx-auto mb-10">
|
||||||
|
Comprehensive residential painting and handyman services tailored to your needs.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Services List */}
|
||||||
|
<section className="py-16 bg-white">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||||
|
{services.map((service) => (
|
||||||
|
<div key={service.id} className="border border-taupe-200 rounded-lg p-6 hover:shadow-md transition-shadow">
|
||||||
|
<div className="text-4xl mb-4">{service.icon}</div>
|
||||||
|
<h3 className="text-xl font-bold text-taupe-900 mb-3">{service.title}</h3>
|
||||||
|
<p className="text-taupe-700 mb-4">{service.description}</p>
|
||||||
|
<Link href={`/services/${service.id}`} className="text-taupe-700 hover:text-taupe-900 font-medium">
|
||||||
|
Learn more →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<section className="py-16 bg-taupe-50">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<h2 className="text-3xl font-bold text-taupe-900 mb-6">Ready to get started?</h2>
|
||||||
|
<p className="text-xl text-taupe-700 max-w-2xl mx-auto mb-10">
|
||||||
|
Contact us today for a free consultation and quote.
|
||||||
|
</p>
|
||||||
|
<Button variant="primary">
|
||||||
|
<Link href="/quote">Get a Free Quote</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
src/app/terms/page.tsx
Normal file
96
src/app/terms/page.tsx
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Terms of Service | Summit Painting & Handyman",
|
||||||
|
description: "Terms and conditions for using our website and services.",
|
||||||
|
robots: { index: true, follow: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function TermsPage() {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex flex-col">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="bg-gradient-to-br from-taupe-50 to-taupe-100 py-16 md:py-24">
|
||||||
|
<div className="container mx-auto px-4 text-center">
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold text-taupe-900 mb-6">
|
||||||
|
Terms of Service
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<section className="py-16 bg-white">
|
||||||
|
<div className="container mx-auto px-4 max-w-4xl">
|
||||||
|
<div className="prose prose-taupe max-w-none">
|
||||||
|
<p className="text-sm text-taupe-600">Last updated: September 7, 2025</p>
|
||||||
|
|
||||||
|
<nav className="not-prose mt-4 mb-8">
|
||||||
|
<ul className="flex flex-wrap gap-x-6 gap-y-2 text-sm text-taupe-700">
|
||||||
|
<li><a className="hover:text-olive-700" href="#intro">Introduction</a></li>
|
||||||
|
<li><a className="hover:text-olive-700" href="#ip">Intellectual Property</a></li>
|
||||||
|
<li><a className="hover:text-olive-700" href="#user">User Content</a></li>
|
||||||
|
<li><a className="hover:text-olive-700" href="#liability">Limitation of Liability</a></li>
|
||||||
|
<li><a className="hover:text-olive-700" href="#changes">Changes</a></li>
|
||||||
|
<li><a className="hover:text-olive-700" href="#contact">Contact</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<a id="intro" />
|
||||||
|
<h2>Introduction</h2>
|
||||||
|
<p>
|
||||||
|
Welcome to Summit Painting & Handyman Services. These terms and conditions outline the rules
|
||||||
|
and regulations for the use of our website and services.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a id="ip" />
|
||||||
|
<h2>Intellectual Property Rights</h2>
|
||||||
|
<p>
|
||||||
|
Unless otherwise stated, Summit Painting & Handyman Services and/or its licensors own the
|
||||||
|
intellectual property rights for all material on our website. All intellectual property rights
|
||||||
|
are reserved.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a id="user" />
|
||||||
|
<h2>User Content</h2>
|
||||||
|
<p>
|
||||||
|
We do not claim ownership of any user-generated content submitted through our services.
|
||||||
|
By submitting content, you grant us a non-exclusive, royalty-free license to use, reproduce,
|
||||||
|
and display such content in connection with providing our services.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a id="liability" />
|
||||||
|
<h2>Limitation of Liability</h2>
|
||||||
|
<p>
|
||||||
|
In no event shall Summit Painting & Handyman Services be liable for any indirect,
|
||||||
|
incidental, special, consequential or punitive damages, including without limitation, loss of
|
||||||
|
profits, data, use, goodwill, or other intangible losses.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a id="changes" />
|
||||||
|
<h2>Changes to These Terms</h2>
|
||||||
|
<p>
|
||||||
|
We reserve the right to modify these terms at any time. Changes will be posted on this page
|
||||||
|
with an updated effective date. Continued use of the website following any changes constitutes
|
||||||
|
acceptance of the updated terms.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<a id="contact" />
|
||||||
|
<h2>Contact Us</h2>
|
||||||
|
<p>If you have any questions about these terms, please contact us at:</p>
|
||||||
|
<p>
|
||||||
|
Summit Painting & Handyman<br />
|
||||||
|
Email: <a href="mailto:nicholai@biohazardvfx.com">nicholai@biohazardvfx.com</a><br />
|
||||||
|
Phone: <a href="tel:+17196604281">(719) 660-4281</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="text-sm text-taupe-600">
|
||||||
|
For information on how we handle personal data, please see our{" "}
|
||||||
|
<Link className="text-olive-700 hover:underline" href="/privacy">Privacy Policy</Link>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
237
src/components/EnhancedHero.tsx
Normal file
237
src/components/EnhancedHero.tsx
Normal file
@ -0,0 +1,237 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { motion, useMotionValue, useTransform, useSpring, Variants } from "framer-motion";
|
||||||
|
import { ArrowRight, MousePointer } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
// Small utility to sequence children animations
|
||||||
|
const container: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: { staggerChildren: 0.08, delayChildren: 0.15 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const item: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 16, filter: "blur(6px)" },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
filter: "blur(0px)",
|
||||||
|
transition: { type: "spring", bounce: 0.25, duration: 0.9 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EnhancedHero() {
|
||||||
|
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
|
||||||
|
const prefersReducedMotion =
|
||||||
|
typeof window !== "undefined"
|
||||||
|
? window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
||||||
|
: false;
|
||||||
|
|
||||||
|
// Mouse tracking for parallax effects
|
||||||
|
const mouseX = useMotionValue(0);
|
||||||
|
const mouseY = useMotionValue(0);
|
||||||
|
|
||||||
|
// Smooth spring animations for parallax
|
||||||
|
const parallaxX = useSpring(useTransform(mouseX, [0, 1], [-30, 30]), {
|
||||||
|
stiffness: 50,
|
||||||
|
damping: 20
|
||||||
|
});
|
||||||
|
const parallaxY = useSpring(useTransform(mouseY, [0, 1], [-20, 20]), {
|
||||||
|
stiffness: 50,
|
||||||
|
damping: 20
|
||||||
|
});
|
||||||
|
|
||||||
|
// Typing animation state
|
||||||
|
const [displayText, setDisplayText] = useState("");
|
||||||
|
const fullText = "Transform Your Home with Expert Painting & Handyman Services";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (prefersReducedMotion) {
|
||||||
|
setDisplayText(fullText);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let index = 0;
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (index <= fullText.length) {
|
||||||
|
setDisplayText(fullText.slice(0, index));
|
||||||
|
index++;
|
||||||
|
} else {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [prefersReducedMotion]);
|
||||||
|
|
||||||
|
// Mouse move handler
|
||||||
|
const handleMouseMove = (e: React.MouseEvent) => {
|
||||||
|
if (prefersReducedMotion) return;
|
||||||
|
const { clientX, clientY } = e;
|
||||||
|
const { innerWidth, innerHeight } = window;
|
||||||
|
|
||||||
|
mouseX.set(clientX / innerWidth);
|
||||||
|
mouseY.set(clientY / innerHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Particle animation variants
|
||||||
|
const particleVariants = {
|
||||||
|
animate: (i: number) => ({
|
||||||
|
y: [0, -20, 0],
|
||||||
|
opacity: [0.3, 0.8, 0.3],
|
||||||
|
transition: {
|
||||||
|
duration: 3 + i * 0.5,
|
||||||
|
repeat: Infinity,
|
||||||
|
ease: "easeInOut" as const,
|
||||||
|
delay: i * 0.2
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Magnetic button effect
|
||||||
|
const magneticButton = {
|
||||||
|
rest: { scale: 1 },
|
||||||
|
hover: {
|
||||||
|
scale: 1.05,
|
||||||
|
transition: { type: "spring" as const, stiffness: 400, damping: 30 }
|
||||||
|
},
|
||||||
|
tap: { scale: 0.95 }
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
className="relative isolate overflow-hidden bg-taupe-900 min-h-screen flex items-center"
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
>
|
||||||
|
{/* Enhanced background with parallax elements */}
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute inset-0 -z-10"
|
||||||
|
>
|
||||||
|
{/* Parallax background elements */}
|
||||||
|
<motion.div
|
||||||
|
style={{ x: parallaxX, y: parallaxY }}
|
||||||
|
className="absolute left-[-20%] top-[-30%] h-[80rem] w-[35rem] -rotate-45 rounded-full bg-[radial-gradient(68.54%_68.72%_at_55.02%_31.46%,rgba(255,255,255,0.08)_0,rgba(255,255,255,0.02)_50%,rgba(255,255,255,0)_80%)]"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
style={{ x: useTransform(parallaxX, v => v * -0.5), y: useTransform(parallaxY, v => v * -0.3) }}
|
||||||
|
className="absolute left-[10%] top-[-20%] h-[70rem] w-56 -rotate-45 rounded-full bg-[radial-gradient(50%_50%_at_50%_50%,rgba(255,255,255,0.06)_0,rgba(255,255,255,0.02)_80%,transparent_100%)]"
|
||||||
|
/>
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(125%_125%_at_50%_100%,transparent_0%,rgba(0,0,0,0.25)_65%)]" />
|
||||||
|
|
||||||
|
{/* Floating particles */}
|
||||||
|
{(!prefersReducedMotion ? [...Array(6)] : []).map((_, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className="absolute w-2 h-2 bg-white/20 rounded-full"
|
||||||
|
style={{
|
||||||
|
left: `${20 + i * 15}%`,
|
||||||
|
top: `${30 + i * 10}%`,
|
||||||
|
}}
|
||||||
|
variants={particleVariants}
|
||||||
|
animate="animate"
|
||||||
|
custom={i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto max-w-7xl px-4 pt-24 pb-20 md:pt-36 md:pb-28 text-center">
|
||||||
|
<motion.div
|
||||||
|
variants={container}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
className="flex flex-col items-center"
|
||||||
|
>
|
||||||
|
<motion.div variants={item}>
|
||||||
|
<Link
|
||||||
|
href="/services"
|
||||||
|
className="group mx-auto inline-flex w-auto items-center gap-3 rounded-full border border-white/15 bg-white/5 px-4 py-1.5 text-sm text-white/90 shadow-sm ring-1 ring-white/10 backdrop-blur transition hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<span className="hidden sm:inline">See how we can help</span>
|
||||||
|
<span className="sm:hidden">Our Services</span>
|
||||||
|
<span className="block h-4 w-px bg-white/20" />
|
||||||
|
<motion.span
|
||||||
|
className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-white/90 text-taupe-900 transition group-hover:translate-x-0.5"
|
||||||
|
whileHover={{ scale: 1.1 }}
|
||||||
|
>
|
||||||
|
<ArrowRight className="h-3 w-3" />
|
||||||
|
</motion.span>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.h1
|
||||||
|
variants={item}
|
||||||
|
className="mt-8 max-w-5xl text-balance text-4xl font-semibold tracking-tight text-white md:text-6xl lg:text-7xl"
|
||||||
|
>
|
||||||
|
{displayText}
|
||||||
|
{!prefersReducedMotion && (
|
||||||
|
<motion.span
|
||||||
|
animate={{ opacity: [1, 0] }}
|
||||||
|
transition={{ duration: 0.8, repeat: Infinity, repeatType: "reverse" }}
|
||||||
|
className="inline-block w-1 h-16 bg-white ml-1"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
variants={item}
|
||||||
|
className="mx-auto mt-6 max-w-2xl text-lg text-taupe-100/90"
|
||||||
|
>
|
||||||
|
Professional residential painting, drywall repair, wood restoration,
|
||||||
|
and small installs—clean lines, durable finishes, and reliable
|
||||||
|
scheduling throughout Colorado Springs.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={item}
|
||||||
|
className="mt-10 flex flex-col items-center justify-center gap-3 sm:flex-row"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
variants={magneticButton}
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="tap"
|
||||||
|
>
|
||||||
|
<Button variant="primary" className="px-6 py-3">
|
||||||
|
<Link href="/quote">Get a Free Quote</Link>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={magneticButton}
|
||||||
|
whileHover="hover"
|
||||||
|
whileTap="tap"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="px-6 py-3 border-white/30 text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<Link href="tel:7196604281">Call Now</Link>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Scroll indicator with enhanced animation */}
|
||||||
|
<motion.div
|
||||||
|
variants={item}
|
||||||
|
className="mt-16"
|
||||||
|
animate={{ y: [0, 10, 0] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col items-center gap-2 text-white/60"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
>
|
||||||
|
<MousePointer className="h-5 w-5" />
|
||||||
|
<span className="text-sm">Scroll to explore</span>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EnhancedHero;
|
||||||
230
src/components/EnhancedProcess.tsx
Normal file
230
src/components/EnhancedProcess.tsx
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { useInView } from "framer-motion";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { CheckCircle, Calendar, Truck, Sparkles } from "lucide-react";
|
||||||
|
|
||||||
|
interface ProcessStep {
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
color: string;
|
||||||
|
bgColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processSteps: ProcessStep[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Get a Quote",
|
||||||
|
description: "Fill out our discovery form to get a personalized estimate tailored to your specific project needs.",
|
||||||
|
icon: <Sparkles className="h-8 w-8" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgColor: "bg-olive-100"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Schedule",
|
||||||
|
description: "We'll coordinate a convenient time for your project that works with your schedule and timeline.",
|
||||||
|
icon: <Calendar className="h-8 w-8" />,
|
||||||
|
color: "text-taupe-700",
|
||||||
|
bgColor: "bg-taupe-100"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Deliver",
|
||||||
|
description: "Professional service with quality craftsmanship and attention to every detail of your project.",
|
||||||
|
icon: <CheckCircle className="h-8 w-8" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgColor: "bg-olive-100"
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function EnhancedProcess() {
|
||||||
|
const ref = useRef(null);
|
||||||
|
const isInView = useInView(ref, { once: true, amount: 0.3 });
|
||||||
|
|
||||||
|
// Container animation variants
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.3,
|
||||||
|
delayChildren: 0.2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Step card animation variants
|
||||||
|
const stepVariants = {
|
||||||
|
hidden: { opacity: 0, y: 50, rotateX: -15 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
rotateX: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.6,
|
||||||
|
type: "spring" as const,
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Connecting line animation variants
|
||||||
|
const lineVariants = {
|
||||||
|
hidden: { scaleX: 0 },
|
||||||
|
visible: {
|
||||||
|
scaleX: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.6,
|
||||||
|
ease: "easeOut" as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Icon animation variants
|
||||||
|
const iconVariants = {
|
||||||
|
hidden: { scale: 0, rotate: -180 },
|
||||||
|
visible: {
|
||||||
|
scale: 1,
|
||||||
|
rotate: 0,
|
||||||
|
transition: {
|
||||||
|
type: "spring" as const,
|
||||||
|
stiffness: 200,
|
||||||
|
damping: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Number animation variants
|
||||||
|
const numberVariants = {
|
||||||
|
hidden: { scale: 0 },
|
||||||
|
visible: {
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
type: "spring" as const,
|
||||||
|
stiffness: 300,
|
||||||
|
damping: 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={ref} className="py-20 bg-white">
|
||||||
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-center mb-12"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-semibold text-taupe-900 mb-4 [text-wrap:balance]">
|
||||||
|
Our Process
|
||||||
|
</h2>
|
||||||
|
<p className="text-taupe-700 text-lg max-w-2xl mx-auto">
|
||||||
|
A simple, transparent process from quote to completion
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
{/* Connecting lines */}
|
||||||
|
<div className="absolute top-24 left-0 right-0 hidden md:block">
|
||||||
|
{processSteps.slice(0, -1).map((_, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
variants={lineVariants}
|
||||||
|
className="absolute h-1 bg-gradient-to-r from-olive-300 to-taupe-300"
|
||||||
|
style={{
|
||||||
|
left: `${(i * 33.33) + 16.66}%`,
|
||||||
|
width: `${33.33}%`,
|
||||||
|
top: '2rem'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative">
|
||||||
|
{processSteps.map((step, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={step.id}
|
||||||
|
variants={stepVariants}
|
||||||
|
whileHover={{
|
||||||
|
scale: 1.02,
|
||||||
|
transition: { duration: 0.2 }
|
||||||
|
}}
|
||||||
|
className="group"
|
||||||
|
custom={i}
|
||||||
|
>
|
||||||
|
<div className="text-center rounded-2xl border border-taupe-200 bg-white p-8 shadow-lg hover:shadow-xl transition-all duration-300">
|
||||||
|
{/* Step number with animation */}
|
||||||
|
<motion.div
|
||||||
|
variants={numberVariants}
|
||||||
|
className="w-16 h-16 rounded-full bg-gradient-to-br from-olive-500 to-olive-600 text-white flex items-center justify-center mx-auto mb-6 font-bold text-xl shadow-lg"
|
||||||
|
>
|
||||||
|
{step.id}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Icon with animation */}
|
||||||
|
<motion.div
|
||||||
|
variants={iconVariants}
|
||||||
|
className={`w-16 h-16 rounded-full ${step.bgColor} ${step.color} flex items-center justify-center mx-auto mb-4`}
|
||||||
|
>
|
||||||
|
{step.icon}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Title with hover effect */}
|
||||||
|
<motion.h3
|
||||||
|
className="text-xl font-bold text-taupe-900 mb-3 group-hover:text-olive-700 transition-colors"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
>
|
||||||
|
{step.title}
|
||||||
|
</motion.h3>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<motion.p
|
||||||
|
className="text-taupe-700 leading-relaxed"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ delay: i * 0.2 + 0.4 }}
|
||||||
|
>
|
||||||
|
{step.description}
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
{/* Hover indicator */}
|
||||||
|
<motion.div
|
||||||
|
className="mt-4 h-1 bg-gradient-to-r from-olive-400 to-taupe-400 rounded-full"
|
||||||
|
initial={{ scaleX: 0 }}
|
||||||
|
whileInView={{ scaleX: 1 }}
|
||||||
|
transition={{ delay: i * 0.2 + 0.6, duration: 0.4 }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom decoration */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 20 }}
|
||||||
|
transition={{ delay: 1.2, duration: 0.6 }}
|
||||||
|
className="text-center mt-12"
|
||||||
|
>
|
||||||
|
<div className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-olive-100 to-taupe-100 rounded-full border border-olive-200">
|
||||||
|
<div className="w-2 h-2 bg-olive-500 rounded-full animate-pulse" />
|
||||||
|
<span className="text-olive-700 font-medium">Ready to start your project?</span>
|
||||||
|
<div className="w-2 h-2 bg-olive-500 rounded-full animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
375
src/components/EnhancedScrollingServices.tsx
Normal file
375
src/components/EnhancedScrollingServices.tsx
Normal file
@ -0,0 +1,375 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { motion, useScroll, useTransform, useMotionTemplate, AnimatePresence } from "framer-motion";
|
||||||
|
import { Card } from "@/components/ui/Card";
|
||||||
|
import { Badge } from "@/components/ui/Badge";
|
||||||
|
import {
|
||||||
|
Paintbrush,
|
||||||
|
Hammer,
|
||||||
|
Ruler,
|
||||||
|
PanelsTopLeft,
|
||||||
|
ArrowDown,
|
||||||
|
Sparkles,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EnhancedScrollingServices
|
||||||
|
* - Shows a heading, then a center-locked card that changes contents as you scroll.
|
||||||
|
* - After the sequence completes, the card is released and page continues normally.
|
||||||
|
* - Enhanced with smooth morphing transitions, contextual backgrounds, and better UX.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function EnhancedScrollingServices() {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const [chipIndex, setChipIndex] = useState(0);
|
||||||
|
const CHIP_LABELS = ["EXPERT", "CRAFTSMANSHIP", "QUALITY", "TRUSTED", "LOCAL"] as const;
|
||||||
|
|
||||||
|
// Set the scroll tracking across the whole container section
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: containerRef,
|
||||||
|
offset: ["start start", "end end"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced card entrance/exit transforms with smooth transitions
|
||||||
|
const opacity = useTransform(scrollYProgress, [0, 0.05, 0.95, 1], [0, 1, 1, 0]);
|
||||||
|
const scale = useTransform(scrollYProgress, [0, 0.08, 0.92, 1], [0.95, 1, 1, 0.95]);
|
||||||
|
const y = useTransform(scrollYProgress, [0, 0.12, 0.88, 1], [200, 0, 0, -200]);
|
||||||
|
const overlayOpacity = useTransform(scrollYProgress, [0, 0.06, 0.94, 1], [0, 0.9, 0.9, 0]);
|
||||||
|
|
||||||
|
// Enhanced rotating border with gradient
|
||||||
|
const borderAngle = useTransform(scrollYProgress, [0, 1], [0, 360]);
|
||||||
|
const borderGradient = useMotionTemplate`conic-gradient(from ${borderAngle}deg, rgba(95,116,70,0.7) 0deg, rgba(184,176,165,0.6) 150deg, rgba(95,116,70,0.7) 360deg)`;
|
||||||
|
|
||||||
|
// Contextual background colors for each service
|
||||||
|
const backgroundGradients = {
|
||||||
|
0: "from-olive-900 via-taupe-800 to-olive-950", // Painting
|
||||||
|
1: "from-taupe-900 via-olive-800 to-taupe-950", // Drywall
|
||||||
|
2: "from-olive-950 via-taupe-900 to-olive-900", // Wood
|
||||||
|
3: "from-taupe-950 via-olive-950 to-taupe-900", // Installations
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine which service section should be shown based on scroll progress
|
||||||
|
const SECTIONS = SERVICES.length;
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = scrollYProgress.on("change", (latest) => {
|
||||||
|
const next = Math.min(Math.floor(latest * SECTIONS), SECTIONS - 1);
|
||||||
|
setIndex(next);
|
||||||
|
});
|
||||||
|
return () => unsub();
|
||||||
|
}, [scrollYProgress, SECTIONS]);
|
||||||
|
|
||||||
|
// Chip highlight progression across the whole section
|
||||||
|
const CHIPS = CHIP_LABELS.length;
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = scrollYProgress.on("change", (latest) => {
|
||||||
|
const next = Math.min(Math.floor(latest * CHIPS), CHIPS - 1);
|
||||||
|
setChipIndex(next);
|
||||||
|
});
|
||||||
|
return () => unsub();
|
||||||
|
}, [scrollYProgress, CHIPS]);
|
||||||
|
|
||||||
|
const current = SERVICES[index];
|
||||||
|
|
||||||
|
// Smooth morphing transition variants
|
||||||
|
const contentVariants = {
|
||||||
|
enter: {
|
||||||
|
opacity: 0,
|
||||||
|
y: 30,
|
||||||
|
scale: 0.9,
|
||||||
|
transition: { duration: 0.4, ease: "easeOut" as const }
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
transition: { duration: 0.4, ease: "easeOut" as const }
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
y: -30,
|
||||||
|
scale: 0.9,
|
||||||
|
transition: { duration: 0.4, ease: "easeIn" as const }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decorative element variants
|
||||||
|
const decorativeVariants = {
|
||||||
|
hidden: { scale: 0, opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
type: "spring" as const,
|
||||||
|
stiffness: 200,
|
||||||
|
damping: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
ref={containerRef}
|
||||||
|
className="relative w-full overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Enhanced lead-in content */}
|
||||||
|
<div className="max-w-5xl mx-auto px-4 pt-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-semibold text-taupe-900 mb-4">
|
||||||
|
Discover Our Craftsmanship
|
||||||
|
</h2>
|
||||||
|
<p className="text-taupe-700 text-lg">
|
||||||
|
Scroll to explore our core services. Each card reveals our dedication to quality.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spacer above to create distance before the card locks in */}
|
||||||
|
<div className="h-[60vh]" />
|
||||||
|
|
||||||
|
{/* Enhanced backdrop overlay */}
|
||||||
|
<motion.div style={{ opacity: overlayOpacity }} className="fixed inset-0 z-20 pointer-events-none">
|
||||||
|
{/* Base dark gradient in site palette */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-olive-950 via-olive-950/95 to-taupe-950" />
|
||||||
|
{/* Subtle radial vignette for focus */}
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(120%_80%_at_50%_40%,rgba(255,255,255,0.05),transparent)]" />
|
||||||
|
{/* Soft diagonal wash in olive tint */}
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(120deg,rgba(159,185,138,0.06)_0%,transparent_40%,transparent_60%,rgba(159,185,138,0.06)_100%)]" />
|
||||||
|
{/* Animated particles */}
|
||||||
|
{[...Array(8)].map((_, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className="absolute w-1 h-1 bg-white/30 rounded-full"
|
||||||
|
style={{
|
||||||
|
left: `${Math.random() * 100}%`,
|
||||||
|
top: `${Math.random() * 100}%`,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
y: [0, -30, 0],
|
||||||
|
opacity: [0, 0.6, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 3 + Math.random() * 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
delay: Math.random() * 2,
|
||||||
|
ease: "easeInOut"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Fixed centered card with enhanced styling */}
|
||||||
|
<div className="fixed inset-0 z-30 flex items-center justify-center">
|
||||||
|
<motion.div style={{ opacity, scale, y }} className="pointer-events-auto">
|
||||||
|
<motion.div
|
||||||
|
key={`frame-${current.id}`}
|
||||||
|
className="rounded-[28px] p-[5px] shadow-[0_0_24px_rgba(95,116,70,0.3)]"
|
||||||
|
style={{ background: borderGradient }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className={`rounded-[24px] overflow-hidden w-[98vw] max-w-[96rem] h-[45rem] md:h-[52.5rem] border-transparent shadow-2xl bg-gradient-to-br ${backgroundGradients[index as keyof typeof backgroundGradients]} backdrop-blur`}
|
||||||
|
>
|
||||||
|
<div className="relative h-full grid grid-cols-12 gap-8 p-8 md:p-10">
|
||||||
|
{/* Enhanced right-side visual with animated elements */}
|
||||||
|
<div className="pointer-events-none absolute inset-0">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 1.02 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||||
|
className="absolute inset-y-0 right-0 w-full md:w-[68%] bg-gradient-to-br from-olive-900/60 via-olive-950/80 to-taupe-950/60 overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Enhanced decorative elements */}
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<motion.span
|
||||||
|
key={i}
|
||||||
|
className="absolute h-4 w-4 rounded-full"
|
||||||
|
style={{
|
||||||
|
left: `${20 + i * 15}%`,
|
||||||
|
top: `${20 + i * 12}%`,
|
||||||
|
backgroundColor: i % 2 === 0 ? 'rgba(248,246,244,0.9)' : i % 3 === 0 ? 'rgba(159,185,138,0.8)' : 'rgba(184,176,165,0.7)'
|
||||||
|
}}
|
||||||
|
variants={decorativeVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
custom={i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{/* Subtle vignette */}
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(60%_60%_at_55%_55%,rgba(159,185,138,0.08),transparent)]" />
|
||||||
|
</motion.div>
|
||||||
|
{/* Fade the right visual smoothly into the left solid background */}
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(18,22,13,1)_0%,rgba(18,22,13,0.85)_12%,rgba(18,22,13,0.6)_24%,rgba(18,22,13,0)_48%)]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Left column: chips, title, description with morphing animations */}
|
||||||
|
<div className="relative z-10 col-span-12 md:col-span-5 flex flex-col">
|
||||||
|
{/* Enhanced chip row with animations */}
|
||||||
|
<motion.div
|
||||||
|
key={`chips-${current.id}`}
|
||||||
|
initial={{ opacity: 0, y: -8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{CHIP_LABELS.map((label, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={label}
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
scale: i === chipIndex ? 1.1 : 1,
|
||||||
|
opacity: 1
|
||||||
|
}}
|
||||||
|
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
className={`px-3 py-1 rounded-full border text-xs tracking-wide transition-all duration-300 ${
|
||||||
|
i === chipIndex
|
||||||
|
? "bg-olive-600 text-white border-olive-600 shadow-lg shadow-olive-600/30"
|
||||||
|
: "bg-olive-600/10 text-olive-300 border-olive-400/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Badge>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Enhanced title with morphing animation */}
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.h3
|
||||||
|
key={`title-${current.id}`}
|
||||||
|
variants={contentVariants}
|
||||||
|
initial="enter"
|
||||||
|
animate="center"
|
||||||
|
exit="exit"
|
||||||
|
className="mt-12 text-left text-3xl md:text-4xl lg:text-5xl font-semibold text-taupe-50/95"
|
||||||
|
>
|
||||||
|
{current.title}
|
||||||
|
</motion.h3>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Enhanced description with morphing animation */}
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.p
|
||||||
|
key={`desc-${current.id}`}
|
||||||
|
variants={contentVariants}
|
||||||
|
initial="enter"
|
||||||
|
animate="center"
|
||||||
|
exit="exit"
|
||||||
|
className="mt-6 text-left text-taupe-200/80 max-w-xl leading-relaxed"
|
||||||
|
>
|
||||||
|
{current.description}
|
||||||
|
</motion.p>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Enhanced service icon with animation */}
|
||||||
|
<motion.div
|
||||||
|
key={`icon-${current.id}`}
|
||||||
|
initial={{ scale: 0, rotate: -180 }}
|
||||||
|
animate={{ scale: 1, rotate: 0 }}
|
||||||
|
transition={{ type: "spring", stiffness: 200, damping: 15 }}
|
||||||
|
className="mt-8"
|
||||||
|
>
|
||||||
|
<div className={`p-3 rounded-full bg-white/10 backdrop-blur border border-white/20 inline-flex`}>
|
||||||
|
{current.icon}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spacer column keeps left content width; right side visual is a background layer */}
|
||||||
|
<div className="hidden md:block md:col-span-7" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enhanced progress dots with animations */}
|
||||||
|
<div className="fixed bottom-7 left-1/2 -translate-x-1/2 z-20">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{SERVICES.map((_, i) => (
|
||||||
|
<motion.span
|
||||||
|
key={i}
|
||||||
|
className={`h-2 w-2 rounded-full transition-all duration-300 ${
|
||||||
|
i === index ? "bg-olive-400 scale-125" : "bg-taupe-400"
|
||||||
|
}`}
|
||||||
|
animate={{ scale: i === index ? 1.3 : 1 }}
|
||||||
|
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enhanced scroll hint with particle effects */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 1 }}
|
||||||
|
animate={{ opacity: index > 0 ? 0 : 1 }}
|
||||||
|
className="fixed bottom-16 left-1/2 -translate-x-1/2 text-taupe-200/80 z-20"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
animate={{ y: [0, 5, 0] }}
|
||||||
|
transition={{ duration: 1.6, repeat: Infinity }}
|
||||||
|
>
|
||||||
|
<Sparkles className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Scroll to explore</span>
|
||||||
|
<motion.div animate={{ y: [0, 3, 0] }} transition={{ duration: 1.6, repeat: Infinity }}>
|
||||||
|
<ArrowDown className="h-4 w-4" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Spacer below to allow sequence to complete and then release */}
|
||||||
|
<div className="h-[120vh]" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced Services content
|
||||||
|
* Each item controls its icon, gradient, and accent color with improved visual hierarchy.
|
||||||
|
*/
|
||||||
|
const SERVICES = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Interior & Exterior Painting",
|
||||||
|
description:
|
||||||
|
"Crisp lines and durable coatings for walls, trim, and siding. Proper prep and premium materials for lasting results that transform your space with professional craftsmanship.",
|
||||||
|
icon: <Paintbrush className="h-10 w-10 text-olive-400" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-olive-100 to-taupe-50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Drywall Repair & Texture",
|
||||||
|
description:
|
||||||
|
"Expert repairs, patching, and texture matching—including popcorn removal—blended seamlessly into surrounding surfaces for flawless finishes.",
|
||||||
|
icon: <PanelsTopLeft className="h-10 w-10 text-taupe-400" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-taupe-100 to-white",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Wood Repair & Refinishing",
|
||||||
|
description:
|
||||||
|
"Professional rot replacement, sanding, and refinishing to restore and protect fascia, trim, and architectural details with lasting beauty.",
|
||||||
|
icon: <Hammer className="h-10 w-10 text-olive-400" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-olive-50 to-taupe-50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "Installations & Handyman",
|
||||||
|
description:
|
||||||
|
"Precise trim work, fixture installations, and small projects with tight alignment, clean caulk lines, and meticulous finishing touches.",
|
||||||
|
icon: <Ruler className="h-10 w-10 text-taupe-400" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-white to-olive-50",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
385
src/components/EnhancedScrollingServicesWithCallback.tsx
Normal file
385
src/components/EnhancedScrollingServicesWithCallback.tsx
Normal file
@ -0,0 +1,385 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { motion, useScroll, useTransform, useMotionTemplate, AnimatePresence } from "framer-motion";
|
||||||
|
import { Card } from "@/components/ui/Card";
|
||||||
|
import { Badge } from "@/components/ui/Badge";
|
||||||
|
import {
|
||||||
|
Paintbrush,
|
||||||
|
Hammer,
|
||||||
|
Ruler,
|
||||||
|
PanelsTopLeft,
|
||||||
|
ArrowDown,
|
||||||
|
Sparkles,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EnhancedScrollingServicesWithCallback
|
||||||
|
* - Enhanced version with completion callback for seamless transitions
|
||||||
|
* - Shows a heading, then a center-locked card that changes contents as you scroll
|
||||||
|
* - After the sequence completes, triggers callback for smooth section transitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function EnhancedScrollingServicesWithCallback({ onComplete }: { onComplete?: (completed: boolean) => void }) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const [chipIndex, setChipIndex] = useState(0);
|
||||||
|
const [isCompleted, setIsCompleted] = useState(false);
|
||||||
|
const CHIP_LABELS = ["EXPERT", "CRAFTSMANSHIP", "QUALITY", "TRUSTED", "LOCAL"] as const;
|
||||||
|
|
||||||
|
// Set the scroll tracking across the whole container section
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: containerRef,
|
||||||
|
offset: ["start start", "end end"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enhanced card entrance/exit transforms with smooth transitions
|
||||||
|
const opacity = useTransform(scrollYProgress, [0, 0.05, 0.95, 1], [0, 1, 1, 0]);
|
||||||
|
const scale = useTransform(scrollYProgress, [0, 0.08, 0.92, 1], [0.95, 1, 1, 0.95]);
|
||||||
|
const y = useTransform(scrollYProgress, [0, 0.12, 0.88, 1], [200, 0, 0, -200]);
|
||||||
|
const overlayOpacity = useTransform(scrollYProgress, [0, 0.06, 0.94, 1], [0, 0.9, 0.9, 0]);
|
||||||
|
|
||||||
|
// Enhanced rotating border with gradient
|
||||||
|
const borderAngle = useTransform(scrollYProgress, [0, 1], [0, 360]);
|
||||||
|
const borderGradient = useMotionTemplate`conic-gradient(from ${borderAngle}deg, rgba(95,116,70,0.7) 0deg, rgba(184,176,165,0.6) 150deg, rgba(95,116,70,0.7) 360deg)`;
|
||||||
|
|
||||||
|
// Contextual background colors for each service
|
||||||
|
const backgroundGradients = {
|
||||||
|
0: "from-olive-900 via-taupe-800 to-olive-950", // Painting
|
||||||
|
1: "from-taupe-900 via-olive-800 to-taupe-950", // Drywall
|
||||||
|
2: "from-olive-950 via-taupe-900 to-olive-900", // Wood
|
||||||
|
3: "from-taupe-950 via-olive-950 to-taupe-900", // Installations
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine which service section should be shown based on scroll progress
|
||||||
|
const SECTIONS = SERVICES.length;
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = scrollYProgress.on("change", (latest) => {
|
||||||
|
const next = Math.min(Math.floor(latest * SECTIONS), SECTIONS - 1);
|
||||||
|
setIndex(next);
|
||||||
|
|
||||||
|
// Trigger completion callback when nearly complete
|
||||||
|
if (latest >= 0.9 && !isCompleted) {
|
||||||
|
setIsCompleted(true);
|
||||||
|
onComplete?.(true);
|
||||||
|
} else if (latest <= 0.1 && isCompleted) {
|
||||||
|
setIsCompleted(false);
|
||||||
|
onComplete?.(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => unsub();
|
||||||
|
}, [scrollYProgress, SECTIONS, isCompleted, onComplete]);
|
||||||
|
|
||||||
|
// Chip highlight progression across the whole section
|
||||||
|
const CHIPS = CHIP_LABELS.length;
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = scrollYProgress.on("change", (latest) => {
|
||||||
|
const next = Math.min(Math.floor(latest * CHIPS), CHIPS - 1);
|
||||||
|
setChipIndex(next);
|
||||||
|
});
|
||||||
|
return () => unsub();
|
||||||
|
}, [scrollYProgress, CHIPS]);
|
||||||
|
|
||||||
|
const current = SERVICES[index];
|
||||||
|
|
||||||
|
// Smooth morphing transition variants
|
||||||
|
const contentVariants = {
|
||||||
|
enter: {
|
||||||
|
opacity: 0,
|
||||||
|
y: 30,
|
||||||
|
scale: 0.9,
|
||||||
|
transition: { duration: 0.4, ease: "easeOut" as const }
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
transition: { duration: 0.4, ease: "easeOut" as const }
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
y: -30,
|
||||||
|
scale: 0.9,
|
||||||
|
transition: { duration: 0.4, ease: "easeIn" as const }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Decorative element variants
|
||||||
|
const decorativeVariants = {
|
||||||
|
hidden: { scale: 0, opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
scale: 1,
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
type: "spring" as const,
|
||||||
|
stiffness: 200,
|
||||||
|
damping: 15
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
ref={containerRef}
|
||||||
|
className="relative w-full overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Enhanced lead-in content with smooth entrance */}
|
||||||
|
<div className="max-w-5xl mx-auto px-4 pt-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6 }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-semibold text-taupe-900 mb-4">
|
||||||
|
Discover Our Craftsmanship
|
||||||
|
</h2>
|
||||||
|
<p className="text-taupe-700 text-lg">
|
||||||
|
Scroll to explore our core services. Each card reveals our dedication to quality.
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spacer above to create distance before the card locks in */}
|
||||||
|
<div className="h-[60vh]" />
|
||||||
|
|
||||||
|
{/* Enhanced backdrop overlay */}
|
||||||
|
<motion.div style={{ opacity: overlayOpacity }} className="fixed inset-0 z-20 pointer-events-none">
|
||||||
|
{/* Base dark gradient in site palette */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-olive-950 via-olive-950/95 to-taupe-950" />
|
||||||
|
{/* Subtle radial vignette for focus */}
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(120%_80%_at_50%_40%,rgba(255,255,255,0.05),transparent)]" />
|
||||||
|
{/* Soft diagonal wash in olive tint */}
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(120deg,rgba(159,185,138,0.06)_0%,transparent_40%,transparent_60%,rgba(159,185,138,0.06)_100%)]" />
|
||||||
|
{/* Animated particles */}
|
||||||
|
{[...Array(8)].map((_, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className="absolute w-1 h-1 bg-white/30 rounded-full"
|
||||||
|
style={{
|
||||||
|
left: `${Math.random() * 100}%`,
|
||||||
|
top: `${Math.random() * 100}%`,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
y: [0, -30, 0],
|
||||||
|
opacity: [0, 0.6, 0],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 3 + Math.random() * 2,
|
||||||
|
repeat: Infinity,
|
||||||
|
delay: Math.random() * 2,
|
||||||
|
ease: "easeInOut"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Fixed centered card with enhanced styling */}
|
||||||
|
<div className="fixed inset-0 z-30 flex items-center justify-center">
|
||||||
|
<motion.div style={{ opacity, scale, y }} className="pointer-events-auto">
|
||||||
|
<motion.div
|
||||||
|
key={`frame-${current.id}`}
|
||||||
|
className="rounded-[28px] p-[5px] shadow-[0_0_24px_rgba(95,116,70,0.3)]"
|
||||||
|
style={{ background: borderGradient }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className={`rounded-[24px] overflow-hidden w-[98vw] max-w-[96rem] h-[45rem] md:h-[52.5rem] border-transparent shadow-2xl bg-gradient-to-br ${backgroundGradients[index as keyof typeof backgroundGradients]} backdrop-blur`}
|
||||||
|
>
|
||||||
|
<div className="relative h-full grid grid-cols-12 gap-8 p-8 md:p-10">
|
||||||
|
{/* Enhanced right-side visual with animated elements */}
|
||||||
|
<div className="pointer-events-none absolute inset-0">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 1.02 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||||
|
className="absolute inset-y-0 right-0 w-full md:w-[68%] bg-gradient-to-br from-olive-900/60 via-olive-950/80 to-taupe-950/60 overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Enhanced decorative elements */}
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<motion.span
|
||||||
|
key={i}
|
||||||
|
className="absolute h-4 w-4 rounded-full"
|
||||||
|
style={{
|
||||||
|
left: `${20 + i * 15}%`,
|
||||||
|
top: `${20 + i * 12}%`,
|
||||||
|
backgroundColor: i % 2 === 0 ? 'rgba(248,246,244,0.9)' : i % 3 === 0 ? 'rgba(159,185,138,0.8)' : 'rgba(184,176,165,0.7)'
|
||||||
|
}}
|
||||||
|
variants={decorativeVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
custom={i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{/* Subtle vignette */}
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(60%_60%_at_55%_55%,rgba(159,185,138,0.08),transparent)]" />
|
||||||
|
</motion.div>
|
||||||
|
{/* Fade the right visual smoothly into the left solid background */}
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(18,22,13,1)_0%,rgba(18,22,13,0.85)_12%,rgba(18,22,13,0.6)_24%,rgba(18,22,13,0)_48%)]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Left column: chips, title, description with morphing animations */}
|
||||||
|
<div className="relative z-10 col-span-12 md:col-span-5 flex flex-col">
|
||||||
|
{/* Enhanced chip row with animations */}
|
||||||
|
<motion.div
|
||||||
|
key={`chips-${current.id}`}
|
||||||
|
initial={{ opacity: 0, y: -8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{CHIP_LABELS.map((label, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={label}
|
||||||
|
initial={{ scale: 0.8, opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
scale: i === chipIndex ? 1.1 : 1,
|
||||||
|
opacity: 1
|
||||||
|
}}
|
||||||
|
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
||||||
|
>
|
||||||
|
<Badge
|
||||||
|
className={`px-3 py-1 rounded-full border text-xs tracking-wide transition-all duration-300 ${
|
||||||
|
i === chipIndex
|
||||||
|
? "bg-olive-600 text-white border-olive-600 shadow-lg shadow-olive-600/30"
|
||||||
|
: "bg-olive-600/10 text-olive-300 border-olive-400/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Badge>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Enhanced title with morphing animation */}
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.h3
|
||||||
|
key={`title-${current.id}`}
|
||||||
|
variants={contentVariants}
|
||||||
|
initial="enter"
|
||||||
|
animate="center"
|
||||||
|
exit="exit"
|
||||||
|
className="mt-12 text-left text-3xl md:text-4xl lg:text-5xl font-semibold text-taupe-50/95"
|
||||||
|
>
|
||||||
|
{current.title}
|
||||||
|
</motion.h3>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Enhanced description with morphing animation */}
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.p
|
||||||
|
key={`desc-${current.id}`}
|
||||||
|
variants={contentVariants}
|
||||||
|
initial="enter"
|
||||||
|
animate="center"
|
||||||
|
exit="exit"
|
||||||
|
className="mt-6 text-left text-taupe-200/80 max-w-xl leading-relaxed"
|
||||||
|
>
|
||||||
|
{current.description}
|
||||||
|
</motion.p>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Enhanced service icon with animation */}
|
||||||
|
<motion.div
|
||||||
|
key={`icon-${current.id}`}
|
||||||
|
initial={{ scale: 0, rotate: -180 }}
|
||||||
|
animate={{ scale: 1, rotate: 0 }}
|
||||||
|
transition={{ type: "spring", stiffness: 200, damping: 15 }}
|
||||||
|
className="mt-8"
|
||||||
|
>
|
||||||
|
<div className={`p-3 rounded-full bg-white/10 backdrop-blur border border-white/20 inline-flex`}>
|
||||||
|
{current.icon}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spacer column keeps left content width; right side visual is a background layer */}
|
||||||
|
<div className="hidden md:block md:col-span-7" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enhanced progress dots with animations */}
|
||||||
|
<div className="fixed bottom-7 left-1/2 -translate-x-1/2 z-20">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{SERVICES.map((_, i) => (
|
||||||
|
<motion.span
|
||||||
|
key={i}
|
||||||
|
className={`h-2 w-2 rounded-full transition-all duration-300 ${
|
||||||
|
i === index ? "bg-olive-400 scale-125" : "bg-taupe-400"
|
||||||
|
}`}
|
||||||
|
animate={{ scale: i === index ? 1.3 : 1 }}
|
||||||
|
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enhanced scroll hint with particle effects */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 1 }}
|
||||||
|
animate={{ opacity: index > 0 ? 0 : 1 }}
|
||||||
|
className="fixed bottom-16 left-1/2 -translate-x-1/2 text-taupe-200/80 z-20"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
animate={{ y: [0, 5, 0] }}
|
||||||
|
transition={{ duration: 1.6, repeat: Infinity }}
|
||||||
|
>
|
||||||
|
<Sparkles className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Scroll to explore</span>
|
||||||
|
<motion.div animate={{ y: [0, 3, 0] }} transition={{ duration: 1.6, repeat: Infinity }}>
|
||||||
|
<ArrowDown className="h-4 w-4" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Spacer below to allow sequence to complete and then release */}
|
||||||
|
<div className="h-[120vh]" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enhanced Services content
|
||||||
|
* Each item controls its icon, gradient, and accent color with improved visual hierarchy.
|
||||||
|
*/
|
||||||
|
const SERVICES = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Interior & Exterior Painting",
|
||||||
|
description:
|
||||||
|
"Crisp lines and durable coatings for walls, trim, and siding. Proper prep and premium materials for lasting results that transform your space with professional craftsmanship.",
|
||||||
|
icon: <Paintbrush className="h-10 w-10 text-olive-400" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-olive-100 to-taupe-50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Drywall Repair & Texture",
|
||||||
|
description:
|
||||||
|
"Expert repairs, patching, and texture matching—including popcorn removal—blended seamlessly into surrounding surfaces for flawless finishes.",
|
||||||
|
icon: <PanelsTopLeft className="h-10 w-10 text-taupe-400" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-taupe-100 to-white",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Wood Repair & Refinishing",
|
||||||
|
description:
|
||||||
|
"Professional rot replacement, sanding, and refinishing to restore and protect fascia, trim, and architectural details with lasting beauty.",
|
||||||
|
icon: <Hammer className="h-10 w-10 text-olive-400" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-olive-50 to-taupe-50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "Installations & Handyman",
|
||||||
|
description:
|
||||||
|
"Precise trim work, fixture installations, and small projects with tight alignment, clean caulk lines, and meticulous finishing touches.",
|
||||||
|
icon: <Ruler className="h-10 w-10 text-taupe-400" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-white to-olive-50",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
87
src/components/Footer.tsx
Normal file
87
src/components/Footer.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="bg-taupe-900 border-t border-taupe-800 py-12 text-taupe-100">
|
||||||
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-4">Summit Painting & Handyman</h3>
|
||||||
|
<p className="text-taupe-200 mb-4">
|
||||||
|
Professional residential painting and handyman services in Colorado Springs and surrounding areas.
|
||||||
|
</p>
|
||||||
|
<div className="flex space-x-4">
|
||||||
|
<Link href="#" className="text-taupe-300 hover:text-white">
|
||||||
|
<span className="sr-only">Facebook</span>
|
||||||
|
<svg className="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path fillRule="evenodd" d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
<Link href="#" className="text-taupe-300 hover:text-white">
|
||||||
|
<span className="sr-only">Instagram</span>
|
||||||
|
<svg className="h-6 w-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
||||||
|
<path fillRule="evenodd" d="M12.316 3.664c-3.313 0-6.003 2.69-6.003 6.003s2.69 6.003 6.003 6.003c3.313 0 6.003-2.69 6.003-6.003s-2.69-6.003-6.003-6.003zM16.5 12c0 2.485-2.015 4.5-4.5 4.5s-4.5-2.015-4.5-4.5 2.015-4.5 4.5-4.5 4.5 2.015 4.5 4.5zm-1.5 0c0 1.657-1.343 3-3 3s-3-1.343-3-3 1.343-3 3-3 3 1.343 3 3z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-4">Services</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li><Link href="/services#painting" className="text-taupe-200 hover:text-white">Painting</Link></li>
|
||||||
|
<li><Link href="/services#drywall" className="text-taupe-200 hover:text-white">Drywall Repair</Link></li>
|
||||||
|
<li><Link href="/services#wood" className="text-taupe-200 hover:text-white">Wood Repair</Link></li>
|
||||||
|
<li><Link href="/services#installations" className="text-taupe-200 hover:text-white">Installations</Link></li>
|
||||||
|
<li><Link href="/services#exterior" className="text-taupe-200 hover:text-white">Exterior Care</Link></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-4">Areas</h3>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
<li><Link href="/areas" className="text-taupe-200 hover:text-white">Colorado Springs</Link></li>
|
||||||
|
<li><Link href="/areas" className="text-taupe-200 hover:text-white">Monument</Link></li>
|
||||||
|
<li><Link href="/areas" className="text-taupe-200 hover:text-white">Black Forest</Link></li>
|
||||||
|
<li><Link href="/areas" className="text-taupe-200 hover:text-white">Woodland Park</Link></li>
|
||||||
|
<li><Link href="/areas" className="text-taupe-200 hover:text-white">Guffey</Link></li>
|
||||||
|
<li><Link href="/areas" className="text-taupe-200 hover:text-white">Lake George</Link></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-semibold text-white mb-4">Contact</h3>
|
||||||
|
<address className="not-italic text-taupe-200">
|
||||||
|
<p className="mb-2">
|
||||||
|
<a href="tel:7196604281" className="hover:text-white underline underline-offset-4">
|
||||||
|
(719) 660-4281
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="mb-2">
|
||||||
|
<a href="mailto:nicholai@biohazardvfx.com" className="hover:text-white underline underline-offset-4">
|
||||||
|
nicholai@biohazardvfx.com
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p className="mb-2">Serving Colorado Springs, Monument, Black Forest, Woodland Park, Guffey, Lake George</p>
|
||||||
|
</address>
|
||||||
|
<div className="mt-4">
|
||||||
|
<Button variant="primary" className="w-full sm:w-auto">
|
||||||
|
<Link href="/quote">Get a Free Quote</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex space-x-4">
|
||||||
|
<Link href="/terms" className="text-taupe-300 hover:text-white text-sm">Terms</Link>
|
||||||
|
<Link href="/privacy" className="text-taupe-300 hover:text-white text-sm">Privacy</Link>
|
||||||
|
<Link href="/cookies" className="text-taupe-300 hover:text-white text-sm">Cookies</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-t border-taupe-800 mt-8 pt-6 text-center text-sm text-taupe-300">
|
||||||
|
<p>© {new Date().getFullYear()} Summit Painting & Handyman Services. All rights reserved.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
}
|
||||||
354
src/components/GentleScrollingServices.tsx
Normal file
354
src/components/GentleScrollingServices.tsx
Normal file
@ -0,0 +1,354 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { motion, useScroll, useTransform, useSpring, AnimatePresence, useMotionTemplate } from "framer-motion";
|
||||||
|
import { Card } from "@/components/ui/Card";
|
||||||
|
import { Badge } from "@/components/ui/Badge";
|
||||||
|
import {
|
||||||
|
Paintbrush,
|
||||||
|
Hammer,
|
||||||
|
Ruler,
|
||||||
|
PanelsTopLeft,
|
||||||
|
Sparkles,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GentleScrollingServices
|
||||||
|
* - Accessibility-friendly version with reduced motion
|
||||||
|
* - Gentle transitions and no rapid animations
|
||||||
|
* - Respects prefers-reduced-motion
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function GentleScrollingServices({ onComplete, showLeadIn = true }: { onComplete?: (completed: boolean) => void; showLeadIn?: boolean }) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const [isCompleted, setIsCompleted] = useState(false);
|
||||||
|
|
||||||
|
// Check for reduced motion preference
|
||||||
|
const prefersReducedMotion = typeof window !== 'undefined'
|
||||||
|
? window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||||
|
: false;
|
||||||
|
|
||||||
|
// Set the scroll tracking across the whole container section
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: containerRef,
|
||||||
|
offset: ["start start", "end end"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gentle card transforms - much slower and smoother
|
||||||
|
const opacity = useTransform(scrollYProgress, [0, 0.1, 0.9, 1], [0, 1, 1, 0]);
|
||||||
|
const scale = useTransform(scrollYProgress, [0, 0.15, 0.85, 1], [0.98, 1, 1, 0.98]);
|
||||||
|
const y = useTransform(scrollYProgress, [0, 0.2, 0.8, 1], [100, 0, 0, -100]);
|
||||||
|
const overlayOpacity = useTransform(scrollYProgress, [0, 0.1, 0.9, 1], [0, 0.7, 0.7, 0]);
|
||||||
|
|
||||||
|
// Gentle rotating border - much slower
|
||||||
|
const borderAngle = useTransform(scrollYProgress, [0, 1], [0, 180]);
|
||||||
|
const borderGradient = useSpring(
|
||||||
|
useMotionTemplate`conic-gradient(from ${borderAngle}deg, rgba(95,116,70,0.4) 0deg, rgba(184,176,165,0.3) 150deg, rgba(95,116,70,0.4) 360deg)`,
|
||||||
|
{ stiffness: 20, damping: 15 }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Contextual background colors for each service
|
||||||
|
const backgroundGradients = {
|
||||||
|
0: "from-olive-900 via-taupe-800 to-olive-950", // Painting
|
||||||
|
1: "from-taupe-900 via-olive-800 to-taupe-950", // Drywall
|
||||||
|
2: "from-olive-950 via-taupe-900 to-olive-900", // Wood
|
||||||
|
3: "from-taupe-950 via-olive-950 to-taupe-900", // Installations
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine which service section should be shown based on scroll progress
|
||||||
|
const SECTIONS = SERVICES.length;
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = scrollYProgress.on("change", (latest) => {
|
||||||
|
const next = Math.min(Math.floor(latest * SECTIONS), SECTIONS - 1);
|
||||||
|
setIndex(next);
|
||||||
|
|
||||||
|
// Trigger completion callback when nearly complete
|
||||||
|
if (latest >= 0.85 && !isCompleted) {
|
||||||
|
setIsCompleted(true);
|
||||||
|
onComplete?.(true);
|
||||||
|
} else if (latest <= 0.15 && isCompleted) {
|
||||||
|
setIsCompleted(false);
|
||||||
|
onComplete?.(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => unsub();
|
||||||
|
}, [scrollYProgress, SECTIONS, isCompleted, onComplete]);
|
||||||
|
|
||||||
|
const current = SERVICES[index];
|
||||||
|
|
||||||
|
// Clickable pagination: scroll to section index
|
||||||
|
const handleDotClick = (i: number) => {
|
||||||
|
const el = containerRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const start = window.scrollY + rect.top;
|
||||||
|
const end = start + el.offsetHeight - window.innerHeight;
|
||||||
|
const denom = Math.max(1, SECTIONS - 1);
|
||||||
|
const targetY = start + (i / denom) * (end - start);
|
||||||
|
window.scrollTo({ top: targetY, behavior: "smooth" });
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gentle content transition variants
|
||||||
|
const contentVariants = {
|
||||||
|
enter: {
|
||||||
|
opacity: 0,
|
||||||
|
y: 20,
|
||||||
|
transition: {
|
||||||
|
duration: prefersReducedMotion ? 0.1 : 0.6,
|
||||||
|
ease: "easeOut" as const
|
||||||
|
}
|
||||||
|
},
|
||||||
|
center: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: prefersReducedMotion ? 0.1 : 0.6,
|
||||||
|
ease: "easeOut" as const
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exit: {
|
||||||
|
opacity: 0,
|
||||||
|
y: -20,
|
||||||
|
transition: {
|
||||||
|
duration: prefersReducedMotion ? 0.1 : 0.6,
|
||||||
|
ease: "easeIn" as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Gentle decorative element variants
|
||||||
|
const decorativeVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
duration: prefersReducedMotion ? 0.1 : 0.8,
|
||||||
|
ease: "easeOut" as const
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
ref={containerRef}
|
||||||
|
className="relative w-full overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Gentle lead-in content */}
|
||||||
|
{showLeadIn && (
|
||||||
|
<div className="max-w-5xl mx-auto px-4 pt-16">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: prefersReducedMotion ? 0.1 : 0.6 }}
|
||||||
|
className="text-center"
|
||||||
|
>
|
||||||
|
<h2 className="text-3xl md:text-4xl font-semibold text-taupe-900 mb-4">
|
||||||
|
Our Craftsmanship
|
||||||
|
</h2>
|
||||||
|
<p className="text-taupe-700 text-lg">
|
||||||
|
Discover our core services with care and attention to detail
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Gentle spacer - reduced from 60vh to 40vh for better pacing */}
|
||||||
|
<div className="h-[40vh]" />
|
||||||
|
|
||||||
|
{/* Gentle backdrop overlay - much lower opacity */}
|
||||||
|
<motion.div aria-hidden style={{ opacity: overlayOpacity }} className="fixed inset-0 z-20 pointer-events-none">
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-olive-950 via-olive-950/90 to-taupe-950" />
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(120%_80%_at_50%_40%,rgba(255,255,255,0.03),transparent)]" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Fixed centered card with gentle styling */}
|
||||||
|
<div className="fixed inset-0 z-30 flex items-center justify-center">
|
||||||
|
<motion.div style={{ opacity, scale, y }} className="pointer-events-auto">
|
||||||
|
<motion.div
|
||||||
|
key={`frame-${current.id}`}
|
||||||
|
className="rounded-[24px] p-[3px] shadow-[0_0_16px_rgba(95,116,70,0.2)]"
|
||||||
|
style={{ background: borderGradient }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className={`rounded-[22px] overflow-hidden w-[95vw] max-w-[90rem] h-[40rem] md:h-[45rem] border-transparent shadow-xl bg-gradient-to-br ${backgroundGradients[index as keyof typeof backgroundGradients]} backdrop-blur`}
|
||||||
|
>
|
||||||
|
<div className="relative h-full grid grid-cols-12 gap-6 p-6 md:p-8">
|
||||||
|
{/* Gentle right-side visual */}
|
||||||
|
<div className="pointer-events-none absolute inset-0">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: prefersReducedMotion ? 0.1 : 0.8 }}
|
||||||
|
className="absolute inset-y-0 right-0 w-full md:w-[65%] bg-gradient-to-br from-olive-900/40 via-olive-950/60 to-taupe-950/40 overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* Gentle decorative elements - fewer and slower */}
|
||||||
|
{[...Array(3)].map((_, i) => (
|
||||||
|
<motion.span
|
||||||
|
key={i}
|
||||||
|
className="absolute h-3 w-3 rounded-full"
|
||||||
|
style={{
|
||||||
|
left: `${25 + i * 20}%`,
|
||||||
|
top: `${25 + i * 15}%`,
|
||||||
|
backgroundColor: i % 2 === 0 ? 'rgba(248,246,244,0.7)' : 'rgba(159,185,138,0.6)'
|
||||||
|
}}
|
||||||
|
variants={decorativeVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
custom={i}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
{/* Gentle fade */}
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(18,22,13,0.95)_0%,rgba(18,22,13,0.7)_20%,rgba(18,22,13,0)_50%)]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Left column: gentle content transitions */}
|
||||||
|
<div className="relative z-10 col-span-12 md:col-span-6 flex flex-col">
|
||||||
|
{/* Service title with gentle animation */}
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.h3
|
||||||
|
aria-live="polite"
|
||||||
|
key={`title-${current.id}`}
|
||||||
|
variants={contentVariants}
|
||||||
|
initial="enter"
|
||||||
|
animate="center"
|
||||||
|
exit="exit"
|
||||||
|
className="mt-8 text-left text-2xl md:text-3xl lg:text-4xl font-semibold text-taupe-50/95"
|
||||||
|
>
|
||||||
|
{current.title}
|
||||||
|
</motion.h3>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Service description with gentle animation */}
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
<motion.p
|
||||||
|
key={`desc-${current.id}`}
|
||||||
|
variants={contentVariants}
|
||||||
|
initial="enter"
|
||||||
|
animate="center"
|
||||||
|
exit="exit"
|
||||||
|
className="mt-4 text-left text-taupe-200/90 max-w-lg leading-relaxed"
|
||||||
|
>
|
||||||
|
{current.description}
|
||||||
|
</motion.p>
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{/* Service icon with gentle animation */}
|
||||||
|
<motion.div
|
||||||
|
key={`icon-${current.id}`}
|
||||||
|
initial={{ opacity: 0, scale: 0.8 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: prefersReducedMotion ? 0.1 : 0.5 }}
|
||||||
|
className="mt-6"
|
||||||
|
>
|
||||||
|
<div className={`p-2 rounded-full bg-white/10 backdrop-blur border border-white/15 inline-flex`}>
|
||||||
|
{current.icon}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spacer column */}
|
||||||
|
<div className="hidden md:block md:col-span-6" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gentle progress dots (clickable, accessible) */}
|
||||||
|
<div className="fixed bottom-8 left-1/2 -translate-x-1/2 z-20">
|
||||||
|
<div
|
||||||
|
role="tablist"
|
||||||
|
aria-label="Services"
|
||||||
|
className="flex gap-3"
|
||||||
|
>
|
||||||
|
{SERVICES.map((s, i) => {
|
||||||
|
const active = i === index;
|
||||||
|
return (
|
||||||
|
<motion.button
|
||||||
|
key={s.id}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={active}
|
||||||
|
aria-label={s.title}
|
||||||
|
title={s.title}
|
||||||
|
onClick={() => handleDotClick(i)}
|
||||||
|
className={`h-3 w-3 rounded-full ring-1 transition-all duration-300 focus:outline-none focus-visible:ring-2 focus-visible:ring-olive-500 ${
|
||||||
|
active ? "bg-olive-500 ring-olive-400" : "bg-taupe-400/80 ring-taupe-400/60"
|
||||||
|
}`}
|
||||||
|
animate={{
|
||||||
|
scale: active ? 1.25 : 1,
|
||||||
|
opacity: active ? 1 : 0.8,
|
||||||
|
}}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Gentle completion indicator */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 1 }}
|
||||||
|
animate={{ opacity: index > 0 ? 0 : 1 }}
|
||||||
|
className="fixed bottom-12 left-1/2 -translate-x-1/2 text-taupe-200/70 z-20"
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
animate={{ y: [0, 3, 0] }}
|
||||||
|
transition={{ duration: 2, repeat: Infinity }}
|
||||||
|
>
|
||||||
|
<Sparkles className="h-4 w-4" />
|
||||||
|
<span className="text-sm">Continue scrolling</span>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Gentle spacer below */}
|
||||||
|
<div className="h-[80vh]" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gentle Services content
|
||||||
|
* Simplified and calming content
|
||||||
|
*/
|
||||||
|
const SERVICES = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Interior & Exterior Painting",
|
||||||
|
description:
|
||||||
|
"Professional painting services with attention to detail and quality materials for lasting beauty.",
|
||||||
|
icon: <Paintbrush className="h-8 w-8 text-olive-400" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-olive-100 to-taupe-50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Drywall Repair & Texture",
|
||||||
|
description:
|
||||||
|
"Expert repairs and texture matching to restore your walls to perfect condition.",
|
||||||
|
icon: <PanelsTopLeft className="h-8 w-8 text-taupe-400" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-taupe-100 to-white",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Wood Repair & Refinishing",
|
||||||
|
description:
|
||||||
|
"Restore and protect your wood surfaces with professional refinishing services.",
|
||||||
|
icon: <Hammer className="h-8 w-8 text-olive-400" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-olive-50 to-taupe-50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "Installations & Handyman",
|
||||||
|
description:
|
||||||
|
"Professional installation and handyman services for all your home improvement needs.",
|
||||||
|
icon: <Ruler className="h-8 w-8 text-taupe-400" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-white to-olive-50",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
41
src/components/Header.tsx
Normal file
41
src/components/Header.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
return (
|
||||||
|
<header className="bg-white/90 backdrop-blur border-b border-taupe-200 sticky top-0 z-50 shadow-sm">
|
||||||
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
|
{/* Top contact bar */}
|
||||||
|
<div className="hidden md:flex justify-between items-center py-2 text-xs text-taupe-700">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<span>Call Now: (719) 660-4281</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>Serving Colorado Springs, Monument, Black Forest, Woodland Park, Guffey, Lake George</span>
|
||||||
|
</div>
|
||||||
|
<Link href="/services" className="text-taupe-700 hover:text-taupe-900 transition-colors">
|
||||||
|
Our Services
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main header */}
|
||||||
|
<div className="flex justify-between items-center py-3 md:py-4">
|
||||||
|
<Link href="/" className="text-2xl md:text-3xl font-semibold tracking-tight text-taupe-900">
|
||||||
|
Summit Painting & Handyman
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<nav className="hidden md:flex space-x-8">
|
||||||
|
<Link href="/services" className="text-taupe-700 hover:text-taupe-900 transition-colors">Services</Link>
|
||||||
|
<Link href="/gallery" className="text-taupe-700 hover:text-taupe-900 transition-colors">Gallery</Link>
|
||||||
|
<Link href="/areas" className="text-taupe-700 hover:text-taupe-900 transition-colors">Service Areas</Link>
|
||||||
|
<Link href="/about" className="text-taupe-700 hover:text-taupe-900 transition-colors">About</Link>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<Button variant="primary" className="hidden md:block px-4 py-2">
|
||||||
|
<Link href="/quote">Get a Quote</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
97
src/components/Hero.tsx
Normal file
97
src/components/Hero.tsx
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { motion, Variants } from "framer-motion";
|
||||||
|
import { ArrowRight } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
|
||||||
|
// Small utility to sequence children animations
|
||||||
|
const container: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: { staggerChildren: 0.08, delayChildren: 0.15 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const item: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 16, filter: "blur(6px)" },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
filter: "blur(0px)",
|
||||||
|
transition: { type: "spring", bounce: 0.25, duration: 0.9 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Hero() {
|
||||||
|
return (
|
||||||
|
<section className="relative isolate overflow-hidden bg-taupe-900">
|
||||||
|
{/* Subtle vignette + radial accents */}
|
||||||
|
<div
|
||||||
|
aria-hidden
|
||||||
|
className="pointer-events-none absolute inset-0 -z-10 opacity-60"
|
||||||
|
>
|
||||||
|
<div className="absolute left-[-20%] top-[-30%] h-[80rem] w-[35rem] -rotate-45 rounded-full bg-[radial-gradient(68.54%_68.72%_at_55.02%_31.46%,rgba(255,255,255,0.08)_0,rgba(255,255,255,0.02)_50%,rgba(255,255,255,0)_80%)]" />
|
||||||
|
<div className="absolute left-[10%] top-[-20%] h-[70rem] w-56 -rotate-45 rounded-full bg-[radial-gradient(50%_50%_at_50%_50%,rgba(255,255,255,0.06)_0,rgba(255,255,255,0.02)_80%,transparent_100%)]" />
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(125%_125%_at_50%_100%,transparent_0%,rgba(0,0,0,0.25)_65%)]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-auto max-w-7xl px-4 pt-24 pb-20 md:pt-36 md:pb-28 text-center">
|
||||||
|
<motion.div
|
||||||
|
variants={container}
|
||||||
|
initial="hidden"
|
||||||
|
animate="visible"
|
||||||
|
className="flex flex-col items-center"
|
||||||
|
>
|
||||||
|
<motion.div variants={item}>
|
||||||
|
<Link
|
||||||
|
href="/services"
|
||||||
|
className="group mx-auto inline-flex w-auto items-center gap-3 rounded-full border border-white/15 bg-white/5 px-4 py-1.5 text-sm text-white/90 shadow-sm ring-1 ring-white/10 backdrop-blur transition hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<span className="hidden sm:inline">See how we can help</span>
|
||||||
|
<span className="sm:hidden">Our Services</span>
|
||||||
|
<span className="block h-4 w-px bg-white/20" />
|
||||||
|
<span className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-white/90 text-taupe-900 transition group-hover:translate-x-0.5">
|
||||||
|
<ArrowRight className="h-3 w-3" />
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.h1
|
||||||
|
variants={item}
|
||||||
|
className="mt-8 max-w-5xl text-balance text-4xl font-semibold tracking-tight text-white md:text-6xl lg:text-7xl"
|
||||||
|
>
|
||||||
|
Transform Your Home with Expert Painting & Handyman Services
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
variants={item}
|
||||||
|
className="mx-auto mt-6 max-w-2xl text-lg text-taupe-100/90"
|
||||||
|
>
|
||||||
|
Professional residential painting, drywall repair, wood restoration,
|
||||||
|
and small installs—clean lines, durable finishes, and reliable
|
||||||
|
scheduling throughout Colorado Springs.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
variants={item}
|
||||||
|
className="mt-10 flex flex-col items-center justify-center gap-3 sm:flex-row"
|
||||||
|
>
|
||||||
|
<Button variant="primary" className="px-6 py-3">
|
||||||
|
<Link href="/quote">Get a Free Quote</Link>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="px-6 py-3 border-white/30 text-white hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<Link href="tel:7196604281">Call Now</Link>
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Hero;
|
||||||
19
src/components/MobileCTA.tsx
Normal file
19
src/components/MobileCTA.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
|
||||||
|
export function MobileCTA() {
|
||||||
|
return (
|
||||||
|
<div className="md:hidden fixed bottom-0 left-0 right-0 bg-white border-t border-taupe-200 z-50">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 py-3">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Button variant="outline" className="flex-1 px-5 py-3">
|
||||||
|
<Link href="tel:7196604281">Call Now</Link>
|
||||||
|
</Button>
|
||||||
|
<Button variant="primary" className="flex-1 px-5 py-3">
|
||||||
|
<Link href="/quote">Get a Quote</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
251
src/components/ScrollingServices.tsx
Normal file
251
src/components/ScrollingServices.tsx
Normal file
@ -0,0 +1,251 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import { motion, useScroll, useTransform, useMotionTemplate } from "framer-motion";
|
||||||
|
import { Card } from "@/components/ui/Card";
|
||||||
|
import { Badge } from "@/components/ui/Badge";
|
||||||
|
import {
|
||||||
|
Paintbrush,
|
||||||
|
Hammer,
|
||||||
|
Ruler,
|
||||||
|
PanelsTopLeft,
|
||||||
|
ArrowDown,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ScrollingServices
|
||||||
|
* - Shows a heading, then a center-locked card that changes contents as you scroll.
|
||||||
|
* - After the sequence completes, the card is released and page continues normally.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default function ScrollingServices() {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const [chipIndex, setChipIndex] = useState(0);
|
||||||
|
const CHIP_LABELS = ["A","LAUNCH","B","C","D"] as const;
|
||||||
|
|
||||||
|
// Set the scroll tracking across the whole container section
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: containerRef,
|
||||||
|
offset: ["start start", "end end"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Card entrance/exit transforms
|
||||||
|
// Make the card visibly scroll into frame from the bottom, pin, then scroll out the top.
|
||||||
|
const opacity = useTransform(scrollYProgress, [0, 0.07, 0.93, 1], [0, 1, 1, 0]);
|
||||||
|
const scale = useTransform(scrollYProgress, [0, 0.12, 0.88, 1], [0.98, 1, 1, 0.98]);
|
||||||
|
const y = useTransform(scrollYProgress, [0, 0.15, 0.85, 1], [320, 0, 0, -320]);
|
||||||
|
const overlayOpacity = useTransform(scrollYProgress, [0, 0.08, 0.92, 1], [0, 0.85, 0.85, 0]);
|
||||||
|
// Rotating border angle tied to scroll progress
|
||||||
|
const borderAngle = useTransform(scrollYProgress, [0, 1], [0, 360]);
|
||||||
|
const borderGradient = useMotionTemplate`conic-gradient(from ${borderAngle}deg, rgba(95,116,70,0.6) 0deg, rgba(184,176,165,0.5) 150deg, rgba(95,116,70,0.6) 360deg)`;
|
||||||
|
|
||||||
|
// Determine which service section should be shown based on scroll progress
|
||||||
|
const SECTIONS = SERVICES.length;
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = scrollYProgress.on("change", (latest) => {
|
||||||
|
const next = Math.min(Math.floor(latest * SECTIONS), SECTIONS - 1);
|
||||||
|
setIndex(next);
|
||||||
|
});
|
||||||
|
return () => unsub();
|
||||||
|
}, [scrollYProgress, SECTIONS]);
|
||||||
|
|
||||||
|
// Chip highlight progression across the whole section
|
||||||
|
const CHIPS = CHIP_LABELS.length;
|
||||||
|
useEffect(() => {
|
||||||
|
const unsub = scrollYProgress.on("change", (latest) => {
|
||||||
|
const next = Math.min(Math.floor(latest * CHIPS), CHIPS - 1);
|
||||||
|
setChipIndex(next);
|
||||||
|
});
|
||||||
|
return () => unsub();
|
||||||
|
}, [scrollYProgress, CHIPS]);
|
||||||
|
|
||||||
|
const current = SERVICES[index];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
ref={containerRef}
|
||||||
|
className="relative w-full"
|
||||||
|
>
|
||||||
|
{/* Lead-in content to satisfy: begins with a header then card takes center */}
|
||||||
|
<div className="max-w-5xl mx-auto px-4">
|
||||||
|
<p className="text-center text-taupe-700">
|
||||||
|
Scroll to explore our core services. The card will guide you through each offering.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spacer above to create distance before the card locks in */}
|
||||||
|
<div className="h-[120vh]" />
|
||||||
|
|
||||||
|
{/* Backdrop overlay that darkens while card is centered */}
|
||||||
|
<motion.div style={{ opacity: overlayOpacity }} className="fixed inset-0 z-20 pointer-events-none">
|
||||||
|
{/* Base dark gradient in site palette */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-br from-olive-950 via-olive-950/95 to-taupe-950" />
|
||||||
|
{/* Subtle radial vignette for focus */}
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(120%_80%_at_50%_40%,rgba(255,255,255,0.05),transparent)]" />
|
||||||
|
{/* Soft diagonal wash in olive tint */}
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(120deg,rgba(159,185,138,0.06)_0%,transparent_40%,transparent_60%,rgba(159,185,138,0.06)_100%)]" />
|
||||||
|
</motion.div>
|
||||||
|
{/* Fixed centered card. pointer-events allowed on children */}
|
||||||
|
<div className="fixed inset-0 z-30 flex items-center justify-center">
|
||||||
|
<motion.div style={{ opacity, scale, y }} className="pointer-events-auto">
|
||||||
|
<motion.div
|
||||||
|
key={`frame-${current.id}`}
|
||||||
|
className="rounded-[28px] p-[5px] shadow-[0_0_24px_rgba(95,116,70,0.2)]"
|
||||||
|
style={{ background: borderGradient }}
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
className={`rounded-[24px] overflow-hidden w-[98vw] max-w-[96rem] h-[45rem] md:h-[52.5rem] border-transparent shadow-2xl bg-gradient-to-br from-olive-950 to-taupe-950 backdrop-blur`}
|
||||||
|
>
|
||||||
|
<div className="relative h-full grid grid-cols-12 gap-8 p-8 md:p-10">
|
||||||
|
{/* Right-side visual fills the card and fades into left background */}
|
||||||
|
<div className="pointer-events-none absolute inset-0">
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, scale: 1.02 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.5, ease: 'easeOut' }}
|
||||||
|
className="absolute inset-y-0 right-0 w-full md:w-[68%] bg-gradient-to-br from-olive-900/60 via-olive-950/80 to-taupe-950/60 overflow-hidden"
|
||||||
|
>
|
||||||
|
{/* decorative dots */}
|
||||||
|
<span className="absolute left-[22%] top-[34%] h-4 w-4 rounded-full bg-taupe-50/90" />
|
||||||
|
<span className="absolute left-[35%] top-[22%] h-4 w-4 rounded-full bg-olive-600" />
|
||||||
|
<span className="absolute left-[44%] top-[58%] h-4 w-4 rounded-full bg-taupe-400" />
|
||||||
|
<span className="absolute right-[28%] top-[36%] h-4 w-4 rounded-full bg-olive-600" />
|
||||||
|
<span className="absolute right-[20%] top-[26%] h-4 w-4 rounded-full bg-taupe-50/90" />
|
||||||
|
{/* subtle vignette */}
|
||||||
|
<div className="absolute inset-0 bg-[radial-gradient(60%_60%_at_55%_55%,rgba(159,185,138,0.08),transparent)]" />
|
||||||
|
</motion.div>
|
||||||
|
{/* fade the right visual smoothly into the left solid background */}
|
||||||
|
<div className="absolute inset-0 bg-[linear-gradient(to_right,rgba(18,22,13,1)_0%,rgba(18,22,13,0.85)_12%,rgba(18,22,13,0.6)_24%,rgba(18,22,13,0)_48%)]" />
|
||||||
|
</div>
|
||||||
|
{/* Left column: chips, title, description (like the reference) */}
|
||||||
|
<div className="relative z-10 col-span-12 md:col-span-5 flex flex-col">
|
||||||
|
{/* Chip row */}
|
||||||
|
<motion.div
|
||||||
|
key={`chips-${current.id}`}
|
||||||
|
initial={{ opacity: 0, y: -8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.3 }}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
{CHIP_LABELS.map((label, i) => (
|
||||||
|
<Badge
|
||||||
|
key={label}
|
||||||
|
className={`px-3 py-1 rounded-full border text-xs tracking-wide ${
|
||||||
|
i === chipIndex
|
||||||
|
? "bg-olive-600 text-white border-olive-600"
|
||||||
|
: "bg-olive-600/10 text-olive-300 border-olive-400/30"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<motion.h3
|
||||||
|
key={`title-${current.id}`}
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.35 }}
|
||||||
|
className="mt-12 text-left text-3xl md:text-4xl lg:text-5xl font-semibold text-taupe-50/95"
|
||||||
|
>
|
||||||
|
{current.title}
|
||||||
|
</motion.h3>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<motion.p
|
||||||
|
key={`desc-${current.id}`}
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.35, delay: 0.05 }}
|
||||||
|
className="mt-6 text-left text-taupe-200/80 max-w-xl leading-relaxed"
|
||||||
|
>
|
||||||
|
{current.description}
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Spacer column keeps left content width; right side visual is a background layer */}
|
||||||
|
<div className="hidden md:block md:col-span-7" />
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress dots */}
|
||||||
|
<div className="fixed bottom-7 left-1/2 -translate-x-1/2 z-20">
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{SERVICES.map((_, i) => (
|
||||||
|
<motion.span
|
||||||
|
key={i}
|
||||||
|
className={`h-2 w-2 rounded-full ${i === index ? "bg-olive-700" : "bg-taupe-300"}`}
|
||||||
|
animate={{ scale: i === index ? 1.2 : 1 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scroll hint at the beginning */}
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 1 }}
|
||||||
|
animate={{ opacity: index > 0 ? 0 : 1 }}
|
||||||
|
className="fixed bottom-16 left-1/2 -translate-x-1/2 text-taupe-700/80 z-20"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-sm">Scroll to explore</span>
|
||||||
|
<motion.div animate={{ y: [0, 6, 0] }} transition={{ duration: 1.6, repeat: Infinity }}>
|
||||||
|
<ArrowDown className="h-4 w-4" />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* Spacer below to allow sequence to complete and then release */}
|
||||||
|
<div className="h-[140vh]" />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Services content
|
||||||
|
* Each item controls its icon, gradient, and accent color.
|
||||||
|
*/
|
||||||
|
const SERVICES = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Interior & Exterior Painting",
|
||||||
|
description:
|
||||||
|
"Crisp lines and durable coatings for walls, trim, and siding. Proper prep and premium materials for lasting results.",
|
||||||
|
icon: <Paintbrush className="h-10 w-10" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-olive-100 to-taupe-50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Drywall Repair & Texture",
|
||||||
|
description:
|
||||||
|
"Repairs, patching, and texture matching—including popcorn removal—blended seamlessly into surrounding surfaces.",
|
||||||
|
icon: <PanelsTopLeft className="h-10 w-10" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-taupe-100 to-white",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Wood Repair & Refinishing",
|
||||||
|
description:
|
||||||
|
"Rot replacement, sanding, and refinishing to restore and protect fascia, trim, and architectural details.",
|
||||||
|
icon: <Hammer className="h-10 w-10" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-olive-50 to-taupe-50",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "Installations & Handyman",
|
||||||
|
description:
|
||||||
|
"Trim, fixtures, and small installs with tight alignment, clean caulk lines, and tidy finishing.",
|
||||||
|
icon: <Ruler className="h-10 w-10" />,
|
||||||
|
color: "text-olive-700",
|
||||||
|
bgGradient: "from-white to-olive-50",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
122
src/components/SeamlessTransition.tsx
Normal file
122
src/components/SeamlessTransition.tsx
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion, useScroll, useTransform, useSpring } from "framer-motion";
|
||||||
|
import { useRef, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface SeamlessTransitionProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SeamlessTransition({ children, className = "" }: SeamlessTransitionProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [scrollProgress, setScrollProgress] = useState(0);
|
||||||
|
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: containerRef,
|
||||||
|
offset: ["start end", "end start"],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Smooth spring animation for the transition
|
||||||
|
const smoothProgress = useSpring(scrollYProgress, {
|
||||||
|
stiffness: 100,
|
||||||
|
damping: 30,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create a smooth fade and scale transition
|
||||||
|
const opacity = useTransform(smoothProgress, [0, 0.2, 0.8, 1], [0, 1, 1, 0]);
|
||||||
|
const scale = useTransform(smoothProgress, [0, 0.2, 0.8, 1], [0.95, 1, 1, 0.95]);
|
||||||
|
const y = useTransform(smoothProgress, [0, 0.2, 0.8, 1], [50, 0, 0, -50]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
ref={containerRef}
|
||||||
|
className={className}
|
||||||
|
style={{ opacity, scale, y }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook to detect when scrolling services section is complete
|
||||||
|
export function useScrollServicesComplete() {
|
||||||
|
const [isComplete, setIsComplete] = useState(false);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: containerRef,
|
||||||
|
offset: ["start start", "end end"],
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const unsubscribe = scrollYProgress.on("change", (latest) => {
|
||||||
|
if (latest >= 0.95) {
|
||||||
|
setIsComplete(true);
|
||||||
|
} else if (latest <= 0.05) {
|
||||||
|
setIsComplete(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => unsubscribe();
|
||||||
|
}, [scrollYProgress]);
|
||||||
|
|
||||||
|
return { isComplete, containerRef };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Component to create a visual bridge between sections
|
||||||
|
export function VisualBridge() {
|
||||||
|
return (
|
||||||
|
<div className="relative h-32 overflow-hidden bg-gradient-to-b from-white to-taupe-50">
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 1 }}
|
||||||
|
>
|
||||||
|
{/* Subtle wave pattern */}
|
||||||
|
<svg
|
||||||
|
className="absolute inset-0 w-full h-full"
|
||||||
|
viewBox="0 0 1200 120"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<motion.path
|
||||||
|
d="M0,60 C200,20 400,100 600,60 C800,20 1000,100 1200,60 L1200,120 L0,120 Z"
|
||||||
|
fill="url(#waveGradient)"
|
||||||
|
initial={{ pathLength: 0 }}
|
||||||
|
animate={{ pathLength: 1 }}
|
||||||
|
transition={{ duration: 2, ease: "easeOut" }}
|
||||||
|
/>
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="waveGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" stopColor="rgba(248,246,244,0.8)" />
|
||||||
|
<stop offset="100%" stopColor="rgba(232,229,224,0.8)" />
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{/* Floating elements */}
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<motion.div
|
||||||
|
key={i}
|
||||||
|
className="absolute w-2 h-2 bg-olive-400 rounded-full"
|
||||||
|
style={{
|
||||||
|
left: `${20 + i * 15}%`,
|
||||||
|
top: `${30 + i * 10}%`,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
y: [0, -20, 0],
|
||||||
|
opacity: [0.3, 0.8, 0.3],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 3 + i * 0.5,
|
||||||
|
repeat: Infinity,
|
||||||
|
delay: i * 0.3,
|
||||||
|
ease: "easeInOut"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
144
src/components/ServiceAreasShowcase.tsx
Normal file
144
src/components/ServiceAreasShowcase.tsx
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import type { Variants } from "framer-motion";
|
||||||
|
import { MapPin } from "lucide-react";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type Area = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ServiceAreasShowcaseProps = {
|
||||||
|
areas: Area[];
|
||||||
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const container: Variants = {
|
||||||
|
hidden: {},
|
||||||
|
show: {
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.08,
|
||||||
|
delayChildren: 0.05,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const item: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20, scale: 0.98 },
|
||||||
|
show: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
type: "spring" as const,
|
||||||
|
stiffness: 220,
|
||||||
|
damping: 22,
|
||||||
|
mass: 0.6,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const glow: Variants = {
|
||||||
|
rest: { opacity: 0, scale: 0.95 },
|
||||||
|
hover: {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1.02,
|
||||||
|
transition: { type: "spring" as const, stiffness: 200, damping: 18 },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function ServiceAreasShowcase({
|
||||||
|
areas,
|
||||||
|
title = "Communities We Serve",
|
||||||
|
subtitle = "Proudly serving Colorado Springs and surrounding areas",
|
||||||
|
}: ServiceAreasShowcaseProps) {
|
||||||
|
return (
|
||||||
|
<section className="relative py-16 md:py-20 bg-white">
|
||||||
|
{/* Decorative background to compliment site's soft neutral theme */}
|
||||||
|
<div
|
||||||
|
className="pointer-events-none absolute inset-0 -z-10"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-taupe-50/80 via-white to-white" />
|
||||||
|
<div className="absolute left-1/2 top-0 h-64 w-[80vw] -translate-x-1/2 rounded-[50%] bg-gradient-to-tr from-emerald-200/30 via-emerald-100/20 to-transparent blur-3xl" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container mx-auto px-4 relative">
|
||||||
|
<div className="mx-auto mb-10 max-w-2xl text-center">
|
||||||
|
<motion.h2
|
||||||
|
initial={{ opacity: 0, y: 14 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ type: "spring", stiffness: 200, damping: 20 }}
|
||||||
|
viewport={{ once: true, amount: 0.4 }}
|
||||||
|
className="text-3xl md:text-4xl font-bold text-taupe-900"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</motion.h2>
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 10 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ delay: 0.05 }}
|
||||||
|
viewport={{ once: true, amount: 0.5 }}
|
||||||
|
className="mt-3 text-lg text-taupe-700"
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid with staggered reveal and subtle hover lift */}
|
||||||
|
<motion.ul
|
||||||
|
variants={container}
|
||||||
|
initial="hidden"
|
||||||
|
whileInView="show"
|
||||||
|
viewport={{ once: true, amount: 0.2 }}
|
||||||
|
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 md:gap-8"
|
||||||
|
>
|
||||||
|
{areas.map((area, idx) => (
|
||||||
|
<motion.li
|
||||||
|
key={area.name + idx}
|
||||||
|
variants={item}
|
||||||
|
className="group relative"
|
||||||
|
whileHover="hover"
|
||||||
|
initial="rest"
|
||||||
|
animate="rest"
|
||||||
|
>
|
||||||
|
{/* glow/outline */}
|
||||||
|
<motion.div
|
||||||
|
variants={glow}
|
||||||
|
className="absolute -inset-0.5 rounded-xl bg-gradient-to-br from-emerald-300/20 via-emerald-200/10 to-transparent blur-md"
|
||||||
|
/>
|
||||||
|
<div className="relative h-full rounded-xl border border-taupe-200/70 bg-white/90 p-6 shadow-sm transition-shadow duration-300 group-hover:shadow-md">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<div className="rounded-lg bg-emerald-50 p-2 text-emerald-700 ring-1 ring-emerald-200/60">
|
||||||
|
<MapPin size={18} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-xl font-semibold text-taupe-900">
|
||||||
|
{area.name}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1 text-taupe-700">
|
||||||
|
{area.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* subtle bottom accent that animates on hover */}
|
||||||
|
<div className="mt-5 h-px w-full overflow-hidden rounded-full bg-gradient-to-r from-transparent via-taupe-200 to-transparent">
|
||||||
|
<motion.div
|
||||||
|
initial={{ x: "-30%" }}
|
||||||
|
whileHover={{ x: "30%" }}
|
||||||
|
transition={{ type: "tween", ease: "easeInOut", duration: 0.8, repeat: Infinity, repeatType: "reverse" }}
|
||||||
|
className="h-[2px] w-1/3 bg-emerald-400/60"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.li>
|
||||||
|
))}
|
||||||
|
</motion.ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
158
src/components/SiteNav.tsx
Normal file
158
src/components/SiteNav.tsx
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Menu, X } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ href: "/services", label: "Services" },
|
||||||
|
{ href: "/gallery", label: "Gallery" },
|
||||||
|
{ href: "/areas", label: "Service Areas" },
|
||||||
|
{ href: "/about", label: "About" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function SiteNav() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
// atTop === true when near the top of the page
|
||||||
|
const [atTop, setAtTop] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const onScroll = () => setAtTop(window.scrollY <= 12);
|
||||||
|
onScroll();
|
||||||
|
window.addEventListener("scroll", onScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener("scroll", onScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Higher-contrast nav link colors for readability
|
||||||
|
const linkClass = atTop
|
||||||
|
? "text-taupe-900 hover:text-taupe-950 transition-colors"
|
||||||
|
: "text-white hover:text-white/90 transition-colors";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header className="fixed inset-x-0 top-0 z-50">
|
||||||
|
{/* top contact bar */}
|
||||||
|
<div className="hidden md:block bg-white/70 backdrop-blur border-b border-taupe-200">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 py-2 text-xs text-taupe-700 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span>Call Now: (719) 660-4281</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>
|
||||||
|
Serving Colorado Springs, Monument, Black Forest, Woodland Park,
|
||||||
|
Guffey, Lake George
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href="/services"
|
||||||
|
className="hover:text-taupe-900 transition-colors"
|
||||||
|
>
|
||||||
|
Our Services
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* main nav */}
|
||||||
|
<nav className="border-b border-transparent">
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mx-auto transition-all",
|
||||||
|
atTop
|
||||||
|
? "max-w-7xl px-4 py-3 md:py-4"
|
||||||
|
: "max-w-6xl px-6 py-3 md:py-4 rounded-2xl ring-1 ring-white/15 border border-white/10 bg-white/10 backdrop-blur mt-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className={cn(
|
||||||
|
"text-2xl md:text-3xl font-semibold tracking-tight",
|
||||||
|
atTop ? "text-taupe-900" : "text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Summit Painting & Handyman
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* desktop */}
|
||||||
|
<div className="hidden md:flex items-center gap-8">
|
||||||
|
<ul className="flex items-center gap-8">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<li key={item.href}>
|
||||||
|
<Link href={item.href} className={linkClass}>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
className={cn(
|
||||||
|
"px-4 py-2",
|
||||||
|
!atTop && "bg-olive-600 hover:bg-olive-700 text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link href="/quote">Get a Quote</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* mobile */}
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"md:hidden inline-flex items-center justify-center rounded-md p-2",
|
||||||
|
atTop
|
||||||
|
? "border border-taupe-300 text-taupe-800"
|
||||||
|
: "border border-white/40 text-white"
|
||||||
|
)}
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
>
|
||||||
|
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* mobile drawer */}
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"md:hidden overflow-hidden transition-[max-height] duration-300",
|
||||||
|
open ? "max-h-96" : "max-h-0"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"px-4 pb-4 pt-1 border-t",
|
||||||
|
atTop
|
||||||
|
? "border-taupe-200 bg-white/90 backdrop-blur"
|
||||||
|
: "border-white/20 bg-white/10 backdrop-blur"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ul className="flex flex-col gap-3">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<li key={item.href}>
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
className={cn("block py-2", linkClass)}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="mt-3">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
className={cn(
|
||||||
|
"w-full",
|
||||||
|
!atTop && "bg-olive-600 hover:bg-olive-700 text-white"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Link href="/quote">Get a Quote</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
82
src/components/Testimonials.tsx
Normal file
82
src/components/Testimonials.tsx
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Star } from "lucide-react";
|
||||||
|
|
||||||
|
type Testimonial = {
|
||||||
|
name: string;
|
||||||
|
location: string;
|
||||||
|
quote: string;
|
||||||
|
rating?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const testimonials: Testimonial[] = [
|
||||||
|
{
|
||||||
|
name: "Megan R.",
|
||||||
|
location: "Colorado Springs",
|
||||||
|
quote:
|
||||||
|
"Flawless lines and cleanup. They finished ahead of schedule and helped us pick colors that look great in every light.",
|
||||||
|
rating: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Daniel P.",
|
||||||
|
location: "Monument",
|
||||||
|
quote:
|
||||||
|
"Patched drywall and repainted our living room after a water leak. You'd never know there was damage. Highly recommend.",
|
||||||
|
rating: 5,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Lauren T.",
|
||||||
|
location: "Woodland Park",
|
||||||
|
quote:
|
||||||
|
"Friendly, punctual, and meticulous. The exterior paint has totally refreshed our home. We'll be calling again.",
|
||||||
|
rating: 5,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function Stars({ count = 5 }: { count?: number }) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-0.5 text-olive-600" aria-label={`${count} out of 5 stars`}>
|
||||||
|
{Array.from({ length: count }).map((_, i) => (
|
||||||
|
<Star key={i} className="h-4 w-4 fill-current" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TestimonialsSection() {
|
||||||
|
return (
|
||||||
|
<section aria-label="Customer testimonials" className="bg-taupe-50">
|
||||||
|
<div className="max-w-7xl mx-auto px-4 py-14 md:py-16">
|
||||||
|
<div className="text-center mb-8 md:mb-10">
|
||||||
|
<h2 className="text-3xl md:text-4xl font-semibold text-taupe-900 [text-wrap:balance]">
|
||||||
|
Homeowners Love Our Work
|
||||||
|
</h2>
|
||||||
|
<p className="mt-3 text-taupe-700 max-w-2xl mx-auto">
|
||||||
|
Real feedback from local projects across Colorado Springs and surrounding areas.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
{testimonials.map((t) => (
|
||||||
|
<li
|
||||||
|
key={t.name}
|
||||||
|
className="rounded-xl border border-taupe-200 bg-white p-5 shadow-sm"
|
||||||
|
>
|
||||||
|
<blockquote className="space-y-3">
|
||||||
|
<Stars />
|
||||||
|
<p className="text-taupe-800 leading-relaxed">
|
||||||
|
“{t.quote}”
|
||||||
|
</p>
|
||||||
|
<footer className="text-sm text-taupe-600">
|
||||||
|
<span className="font-medium text-taupe-900">{t.name}</span>{" "}
|
||||||
|
<span aria-hidden>•</span>{" "}
|
||||||
|
<span>{t.location}</span>
|
||||||
|
</footer>
|
||||||
|
</blockquote>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
59
src/components/TrustBar.tsx
Normal file
59
src/components/TrustBar.tsx
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ShieldCheck, Star, BadgeCheck, MapPin } from "lucide-react";
|
||||||
|
|
||||||
|
export default function TrustBar() {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
icon: ShieldCheck,
|
||||||
|
title: "Licensed & Insured",
|
||||||
|
desc: "Professional, insured workmanship",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Star,
|
||||||
|
title: "5‑Star Reviews",
|
||||||
|
desc: "Homeowners trust our quality",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: BadgeCheck,
|
||||||
|
title: "Warranty",
|
||||||
|
desc: "Workmanship covered for peace of mind",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: MapPin,
|
||||||
|
title: "Local Experts",
|
||||||
|
desc: "Colorado Springs & surrounding areas",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
aria-label="Trust and credentials"
|
||||||
|
className="bg-white"
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto px-4 py-8 md:py-10">
|
||||||
|
<ul className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 md:gap-6">
|
||||||
|
{items.map(({ icon: Icon, title, desc }) => (
|
||||||
|
<li
|
||||||
|
key={title}
|
||||||
|
className="group rounded-xl border border-taupe-200/70 bg-white/80 backdrop-blur-sm p-4 md:p-5 shadow-sm hover:shadow transition-shadow"
|
||||||
|
>
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<span
|
||||||
|
aria-hidden
|
||||||
|
className="inline-flex h-9 w-9 items-center justify-center rounded-lg bg-olive-50 text-olive-700 ring-1 ring-olive-200/60"
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-semibold text-taupe-900">{title}</h3>
|
||||||
|
<p className="text-sm text-taupe-700/90">{desc}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
155
src/components/UnifiedNav.tsx
Normal file
155
src/components/UnifiedNav.tsx
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import { Menu, X, Phone } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/Button";
|
||||||
|
import { usePathname } from "next/navigation";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ href: "/services", label: "Services" },
|
||||||
|
{ href: "/gallery", label: "Gallery" },
|
||||||
|
{ href: "/areas", label: "Service Areas" },
|
||||||
|
{ href: "/about", label: "About" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function UnifiedNav() {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [scrolled, setScrolled] = useState(false);
|
||||||
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => setScrolled(window.scrollY > 12);
|
||||||
|
handleScroll();
|
||||||
|
window.addEventListener("scroll", handleScroll, { passive: true });
|
||||||
|
return () => window.removeEventListener("scroll", handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Minimal, light background that slightly solidifies on scroll
|
||||||
|
const navVariants = {
|
||||||
|
top: {
|
||||||
|
backgroundColor: "rgba(255,255,255,0.7)",
|
||||||
|
backdropFilter: "blur(8px)",
|
||||||
|
boxShadow: "0 1px 0 rgba(0,0,0,0.06)",
|
||||||
|
},
|
||||||
|
scrolled: {
|
||||||
|
backgroundColor: "rgba(255,255,255,0.96)",
|
||||||
|
backdropFilter: "blur(10px)",
|
||||||
|
boxShadow: "0 6px 20px rgba(0,0,0,0.08)",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.header
|
||||||
|
className="fixed inset-x-0 top-0 z-50"
|
||||||
|
initial="top"
|
||||||
|
animate={scrolled ? "scrolled" : "top"}
|
||||||
|
variants={navVariants}
|
||||||
|
transition={{ duration: 0.25, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
<nav className="max-w-7xl mx-auto px-4 py-3" aria-label="Main navigation">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
{/* Logo / Brand */}
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="text-xl md:text-2xl font-semibold tracking-tight text-taupe-900"
|
||||||
|
>
|
||||||
|
Summit Painting & Handyman
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
{/* Desktop Nav */}
|
||||||
|
<div className="hidden md:flex items-center gap-6">
|
||||||
|
<ul className="flex items-center gap-6">
|
||||||
|
{navItems.map((item) => {
|
||||||
|
const active = pathname.startsWith(item.href);
|
||||||
|
return (
|
||||||
|
<li key={item.href}>
|
||||||
|
<Link
|
||||||
|
href={item.href}
|
||||||
|
aria-current={active ? "page" : undefined}
|
||||||
|
className={cn(
|
||||||
|
"rounded text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-olive-600 focus-visible:ring-offset-2",
|
||||||
|
active ? "text-olive-800 font-medium" : "text-taupe-700 hover:text-olive-700"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<a
|
||||||
|
href="tel:7196604281"
|
||||||
|
className="inline-flex items-center gap-2 rounded text-sm text-taupe-800 hover:text-olive-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-olive-600 focus-visible:ring-offset-2"
|
||||||
|
aria-label="Call Summit Painting & Handyman at 719-660-4281"
|
||||||
|
>
|
||||||
|
<Phone className="h-4 w-4" />
|
||||||
|
<span>(719) 660-4281</span>
|
||||||
|
</a>
|
||||||
|
<Button variant="primary" className="text-sm px-3 py-1.5">
|
||||||
|
<Link href="/quote" aria-label="Get a Free Quote">Get a Free Quote</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile trigger */}
|
||||||
|
<button
|
||||||
|
className={cn(
|
||||||
|
"md:hidden p-2 rounded-md text-taupe-800 border border-taupe-300/60"
|
||||||
|
)}
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
aria-expanded={open}
|
||||||
|
>
|
||||||
|
{open ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Menu */}
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<motion.div
|
||||||
|
className="md:hidden mt-3 rounded-lg border border-taupe-200 bg-white/95 backdrop-blur p-3 shadow-sm"
|
||||||
|
initial={{ opacity: 0, y: -8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: -8 }}
|
||||||
|
transition={{ duration: 0.18 }}
|
||||||
|
>
|
||||||
|
<nav className="flex flex-col gap-1.5">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className="block rounded-md px-2 py-2 text-taupe-800 hover:bg-taupe-100/70 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-olive-600"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
<a
|
||||||
|
href="tel:7196604281"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
className="mt-1 inline-flex items-center gap-2 rounded-md px-2 py-2 text-taupe-800 hover:bg-taupe-100/70 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-olive-600"
|
||||||
|
aria-label="Call Summit Painting & Handyman at 719-660-4281"
|
||||||
|
>
|
||||||
|
<Phone className="h-4 w-4" />
|
||||||
|
<span>(719) 660-4281</span>
|
||||||
|
</a>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
className="mt-2 w-full"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
<Link href="/quote" className="w-full text-center" aria-label="Get a Free Quote">
|
||||||
|
Get a Free Quote
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</nav>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</nav>
|
||||||
|
</motion.header>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/components/ui/Badge.tsx
Normal file
23
src/components/ui/Badge.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export type BadgeVariant = "default" | "secondary" | "outline";
|
||||||
|
|
||||||
|
export interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
|
||||||
|
variant?: BadgeVariant;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Badge({ className, variant = "default", ...props }: BadgeProps) {
|
||||||
|
const base =
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors";
|
||||||
|
const variants: Record<BadgeVariant, string> = {
|
||||||
|
default:
|
||||||
|
"bg-olive-700 text-white border-olive-700",
|
||||||
|
secondary:
|
||||||
|
"bg-taupe-100 text-taupe-900 border-taupe-200",
|
||||||
|
outline:
|
||||||
|
"border-taupe-300 text-taupe-800",
|
||||||
|
};
|
||||||
|
|
||||||
|
return <span className={cn(base, variants[variant], className)} {...props} />;
|
||||||
|
}
|
||||||
29
src/components/ui/Button.tsx
Normal file
29
src/components/ui/Button.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
|
variant?: "primary" | "secondary" | "outline";
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({
|
||||||
|
variant = "primary",
|
||||||
|
children,
|
||||||
|
className = "",
|
||||||
|
...props
|
||||||
|
}: ButtonProps) {
|
||||||
|
const baseClasses = "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none px-4 py-2 ring-offset-white";
|
||||||
|
|
||||||
|
const variantClasses = {
|
||||||
|
primary: "bg-olive-600 text-white hover:bg-olive-700 focus-visible:ring-olive-600",
|
||||||
|
secondary: "bg-taupe-100 text-taupe-900 hover:bg-taupe-200 focus-visible:ring-taupe-200",
|
||||||
|
outline: "border border-taupe-300 bg-transparent text-taupe-800 hover:bg-taupe-50 focus-visible:ring-taupe-300"
|
||||||
|
};
|
||||||
|
|
||||||
|
const classes = `${baseClasses} ${variantClasses[variant]} ${className}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button className={classes} {...props}>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/components/ui/Card.tsx
Normal file
38
src/components/ui/Card.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import * as React from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
export function Card({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"rounded-2xl border border-taupe-200 bg-white shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardHeader({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn("p-6", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn("p-6 pt-0", className)} {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CardFooter({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return <div className={cn("p-6 pt-0", className)} {...props} />;
|
||||||
|
}
|
||||||
473
src/components/ui/about-us-section.tsx
Normal file
473
src/components/ui/about-us-section.tsx
Normal file
@ -0,0 +1,473 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import type React from "react"
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react"
|
||||||
|
import {
|
||||||
|
Pen,
|
||||||
|
PaintBucket,
|
||||||
|
Home,
|
||||||
|
Ruler,
|
||||||
|
PenTool,
|
||||||
|
Building2,
|
||||||
|
Award,
|
||||||
|
Users,
|
||||||
|
Calendar,
|
||||||
|
CheckCircle,
|
||||||
|
Sparkles,
|
||||||
|
Star,
|
||||||
|
ArrowRight,
|
||||||
|
Zap,
|
||||||
|
TrendingUp,
|
||||||
|
} from "lucide-react"
|
||||||
|
import { motion, useScroll, useTransform, useInView, useSpring, type Variants } from "framer-motion"
|
||||||
|
|
||||||
|
export default function AboutUsSection() {
|
||||||
|
const [isVisible, setIsVisible] = useState(false)
|
||||||
|
const sectionRef = useRef<HTMLDivElement>(null)
|
||||||
|
const statsRef = useRef<HTMLDivElement>(null)
|
||||||
|
const isInView = useInView(sectionRef, { once: false, amount: 0.1 })
|
||||||
|
const isStatsInView = useInView(statsRef, { once: false, amount: 0.3 })
|
||||||
|
|
||||||
|
// Parallax effect for decorative elements
|
||||||
|
const { scrollYProgress } = useScroll({
|
||||||
|
target: sectionRef,
|
||||||
|
offset: ["start end", "end start"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const y1 = useTransform(scrollYProgress, [0, 1], [0, -50])
|
||||||
|
const y2 = useTransform(scrollYProgress, [0, 1], [0, 50])
|
||||||
|
const rotate1 = useTransform(scrollYProgress, [0, 1], [0, 20])
|
||||||
|
const rotate2 = useTransform(scrollYProgress, [0, 1], [0, -20])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsVisible(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const containerVariants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.2,
|
||||||
|
delayChildren: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemVariants: Variants = {
|
||||||
|
hidden: { y: 20, opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
y: 0,
|
||||||
|
opacity: 1,
|
||||||
|
transition: { duration: 0.6, ease: [0.16, 1, 0.3, 1] },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const services = [
|
||||||
|
{
|
||||||
|
icon: <Pen className="w-6 h-6" />,
|
||||||
|
secondaryIcon: <Sparkles className="w-4 h-4 absolute -top-1 -right-1 text-olive-300" />,
|
||||||
|
title: "Interior",
|
||||||
|
description:
|
||||||
|
"Transform your living spaces with precise prep, premium coatings, and clean lines for a durable, beautiful finish.",
|
||||||
|
position: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Home className="w-6 h-6" />,
|
||||||
|
secondaryIcon: <CheckCircle className="w-4 h-4 absolute -top-1 -right-1 text-olive-300" />,
|
||||||
|
title: "Exterior",
|
||||||
|
description:
|
||||||
|
"Curb-appeal upgrades with long-lasting exterior systems, proper prep, and weather-ready protection.",
|
||||||
|
position: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PenTool className="w-6 h-6" />,
|
||||||
|
secondaryIcon: <Star className="w-4 h-4 absolute -top-1 -right-1 text-olive-300" />,
|
||||||
|
title: "Drywall & Texture",
|
||||||
|
description:
|
||||||
|
"Repairs, texture matching, and popcorn removal blended seamlessly into the surrounding surfaces.",
|
||||||
|
position: "left",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <PaintBucket className="w-6 h-6" />,
|
||||||
|
secondaryIcon: <Sparkles className="w-4 h-4 absolute -top-1 -right-1 text-olive-300" />,
|
||||||
|
title: "Wood Repair",
|
||||||
|
description:
|
||||||
|
"Rot replacement, sanding, and refinishing to restore and protect trim, siding, and architectural details.",
|
||||||
|
position: "right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Ruler className="w-6 h-6" />,
|
||||||
|
secondaryIcon: <CheckCircle className="w-4 h-4 absolute -top-1 -right-1 text-olive-300" />,
|
||||||
|
title: "Installations",
|
||||||
|
description:
|
||||||
|
"Trim, fixtures, and small handyman installs with an eye for alignment, caulk lines, and finish quality.",
|
||||||
|
position: "right",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <Building2 className="w-6 h-6" />,
|
||||||
|
secondaryIcon: <Star className="w-4 h-4 absolute -top-1 -right-1 text-olive-300" />,
|
||||||
|
title: "Exterior Care",
|
||||||
|
description:
|
||||||
|
"Power washing and maintenance to keep your property protected and looking its best.",
|
||||||
|
position: "right",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const stats = [
|
||||||
|
{ icon: <Award />, value: 150, label: "Projects Completed", suffix: "+" },
|
||||||
|
{ icon: <Users />, value: 1200, label: "Happy Clients", suffix: "+" },
|
||||||
|
{ icon: <Calendar />, value: 12, label: "Years Experience", suffix: "" },
|
||||||
|
{ icon: <TrendingUp />, value: 98, label: "Satisfaction Rate", suffix: "%" },
|
||||||
|
]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
id="about-section"
|
||||||
|
ref={sectionRef}
|
||||||
|
className="w-full py-24 px-4 bg-gradient-to-b from-taupe-50 to-taupe-100 text-taupe-900 overflow-hidden relative"
|
||||||
|
>
|
||||||
|
{/* Decorative background elements */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-20 left-10 w-64 h-64 rounded-full bg-olive-700/10 blur-3xl"
|
||||||
|
style={{ y: y1, rotate: rotate1 }}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute bottom-20 right-10 w-80 h-80 rounded-full bg-olive-300/20 blur-3xl"
|
||||||
|
style={{ y: y2, rotate: rotate2 }}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-1/2 left-1/4 w-4 h-4 rounded-full bg-olive-700/30"
|
||||||
|
animate={{
|
||||||
|
y: [0, -15, 0],
|
||||||
|
opacity: [0.5, 1, 0.5],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 3,
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute bottom-1/3 right-1/4 w-6 h-6 rounded-full bg-olive-300/40"
|
||||||
|
animate={{
|
||||||
|
y: [0, 20, 0],
|
||||||
|
opacity: [0.5, 1, 0.5],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 4,
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
ease: "easeInOut",
|
||||||
|
delay: 1,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="container mx-auto max-w-6xl relative z-10"
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
variants={containerVariants}
|
||||||
|
>
|
||||||
|
<motion.div className="flex flex-col items-center mb-6" variants={itemVariants}>
|
||||||
|
<motion.span
|
||||||
|
className="text-olive-700 font-medium mb-2 flex items-center gap-2"
|
||||||
|
initial={{ opacity: 0, y: -10 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<Zap className="w-4 h-4" />
|
||||||
|
DISCOVER OUR STORY
|
||||||
|
</motion.span>
|
||||||
|
<h2 className="text-4xl md:text-5xl font-light mb-4 text-center">About Us</h2>
|
||||||
|
<motion.div
|
||||||
|
className="w-24 h-1 bg-olive-700"
|
||||||
|
initial={{ width: 0 }}
|
||||||
|
animate={{ width: 96 }}
|
||||||
|
transition={{ duration: 1, delay: 0.5 }}
|
||||||
|
></motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.p className="text-center max-w-2xl mx-auto mb-16 text-taupe-800" variants={itemVariants}>
|
||||||
|
We're a small, detail-focused team serving Colorado Springs with clean lines, durable finishes, and
|
||||||
|
reliable scheduling. From prep to final walkthrough, our process is built for quality and communication.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 relative">
|
||||||
|
{/* Left Column */}
|
||||||
|
<div className="space-y-16">
|
||||||
|
{services
|
||||||
|
.filter((service) => service.position === "left")
|
||||||
|
.map((service, index) => (
|
||||||
|
<ServiceItem
|
||||||
|
key={`left-${index}`}
|
||||||
|
icon={service.icon}
|
||||||
|
secondaryIcon={service.secondaryIcon}
|
||||||
|
title={service.title}
|
||||||
|
description={service.description}
|
||||||
|
variants={itemVariants}
|
||||||
|
delay={index * 0.2}
|
||||||
|
direction="left"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Center Image */}
|
||||||
|
<div className="flex justify-center items-center order-first md:order-none mb-8 md:mb-0">
|
||||||
|
<motion.div className="relative w-full max-w-xs" variants={itemVariants}>
|
||||||
|
<motion.div
|
||||||
|
className="rounded-md overflow-hidden shadow-xl"
|
||||||
|
initial={{ scale: 0.9, opacity: 0 }}
|
||||||
|
animate={{ scale: 1, opacity: 1 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.3 }}
|
||||||
|
whileHover={{ scale: 1.03, transition: { duration: 0.3 } }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://images.unsplash.com/photo-1747582411588-f9b4acabe995?q=80&w=3027&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
|
||||||
|
alt="Modern House"
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 bg-gradient-to-t from-taupe-900/50 to-transparent flex items-end justify-center p-4"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.9 }}
|
||||||
|
>
|
||||||
|
<motion.button
|
||||||
|
className="bg-white text-taupe-900 px-4 py-2 rounded-full flex items-center gap-2 text-sm font-medium"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
Our Portfolio <ArrowRight className="w-4 h-4" />
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 border-4 border-olive-300 rounded-md -m-3 z-[-1]"
|
||||||
|
initial={{ opacity: 0, scale: 1.1 }}
|
||||||
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.6 }}
|
||||||
|
></motion.div>
|
||||||
|
|
||||||
|
{/* Floating accent elements */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute -top-4 -right-8 w-16 h-16 rounded-full bg-olive-700/10"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 1, delay: 0.9 }}
|
||||||
|
style={{ y: y1 }}
|
||||||
|
></motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="absolute -bottom-6 -left-10 w-20 h-20 rounded-full bg-olive-300/20"
|
||||||
|
initial={{ opacity: 0, y: -20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 1, delay: 1.1 }}
|
||||||
|
style={{ y: y2 }}
|
||||||
|
></motion.div>
|
||||||
|
|
||||||
|
{/* Additional decorative elements */}
|
||||||
|
<motion.div
|
||||||
|
className="absolute -top-10 left-1/2 -translate-x-1/2 w-3 h-3 rounded-full bg-olive-700"
|
||||||
|
animate={{
|
||||||
|
y: [0, -10, 0],
|
||||||
|
opacity: [0.5, 1, 0.5],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
ease: "easeInOut",
|
||||||
|
}}
|
||||||
|
></motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="absolute -bottom-12 left-1/2 -translate-x-1/2 w-2 h-2 rounded-full bg-olive-300"
|
||||||
|
animate={{
|
||||||
|
y: [0, 10, 0],
|
||||||
|
opacity: [0.5, 1, 0.5],
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: 2,
|
||||||
|
repeat: Number.POSITIVE_INFINITY,
|
||||||
|
ease: "easeInOut",
|
||||||
|
delay: 0.5,
|
||||||
|
}}
|
||||||
|
></motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right Column */}
|
||||||
|
<div className="space-y-16">
|
||||||
|
{services
|
||||||
|
.filter((service) => service.position === "right")
|
||||||
|
.map((service, index) => (
|
||||||
|
<ServiceItem
|
||||||
|
key={`right-${index}`}
|
||||||
|
icon={service.icon}
|
||||||
|
secondaryIcon={service.secondaryIcon}
|
||||||
|
title={service.title}
|
||||||
|
description={service.description}
|
||||||
|
variants={itemVariants}
|
||||||
|
delay={index * 0.2}
|
||||||
|
direction="right"
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats Section */}
|
||||||
|
<motion.div
|
||||||
|
ref={statsRef}
|
||||||
|
className="mt-24 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8"
|
||||||
|
initial="hidden"
|
||||||
|
animate={isStatsInView ? "visible" : "hidden"}
|
||||||
|
variants={containerVariants}
|
||||||
|
>
|
||||||
|
{stats.map((stat, index) => (
|
||||||
|
<StatCounter
|
||||||
|
key={index}
|
||||||
|
icon={stat.icon}
|
||||||
|
value={stat.value}
|
||||||
|
label={stat.label}
|
||||||
|
suffix={stat.suffix}
|
||||||
|
delay={index * 0.1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
{/* CTA Section */}
|
||||||
|
<motion.div
|
||||||
|
className="mt-20 bg-taupe-900 text-white p-8 rounded-xl flex flex-col md:flex-row items-center justify-between gap-6"
|
||||||
|
initial={{ opacity: 0, y: 30 }}
|
||||||
|
animate={isStatsInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 30 }}
|
||||||
|
transition={{ duration: 0.8, delay: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h3 className="text-2xl font-medium mb-2">Ready to transform your space?</h3>
|
||||||
|
<p className="text-white/80">Let's create something beautiful together.</p>
|
||||||
|
</div>
|
||||||
|
<motion.button
|
||||||
|
className="bg-olive-700 hover:bg-olive-700/90 text-white px-6 py-3 rounded-lg flex items-center gap-2 font-medium transition-colors"
|
||||||
|
whileHover={{ scale: 1.05 }}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
Get Started <ArrowRight className="w-4 h-4" />
|
||||||
|
</motion.button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ServiceItemProps {
|
||||||
|
icon: React.ReactNode
|
||||||
|
secondaryIcon?: React.ReactNode
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
variants: Variants
|
||||||
|
delay: number
|
||||||
|
direction: "left" | "right"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ServiceItem({ icon, secondaryIcon, title, description, variants, delay, direction }: ServiceItemProps) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="flex flex-col group"
|
||||||
|
variants={variants}
|
||||||
|
transition={{ delay }}
|
||||||
|
whileHover={{ y: -5, transition: { duration: 0.2 } }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center gap-3 mb-3"
|
||||||
|
initial={{ x: direction === "left" ? -20 : 20, opacity: 0 }}
|
||||||
|
animate={{ x: 0, opacity: 1 }}
|
||||||
|
transition={{ duration: 0.6, delay: delay + 0.2 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="text-olive-700 bg-olive-700/10 p-3 rounded-lg transition-colors duration-300 group-hover:bg-olive-700/20 relative"
|
||||||
|
whileHover={{ rotate: [0, -10, 10, -5, 0], transition: { duration: 0.5 } }}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
{secondaryIcon}
|
||||||
|
</motion.div>
|
||||||
|
<h3 className="text-xl font-medium text-taupe-900 group-hover:text-olive-700 transition-colors duration-300">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
</motion.div>
|
||||||
|
<motion.p
|
||||||
|
className="text-sm text-taupe-800 leading-relaxed pl-12"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.6, delay: delay + 0.4 }}
|
||||||
|
>
|
||||||
|
{description}
|
||||||
|
</motion.p>
|
||||||
|
<motion.div
|
||||||
|
className="mt-3 pl-12 flex items-center text-olive-700 text-xs font-medium opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 0 }}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
Learn more <ArrowRight className="w-3 h-3" />
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatCounterProps {
|
||||||
|
icon: React.ReactNode
|
||||||
|
value: number
|
||||||
|
label: string
|
||||||
|
suffix: string
|
||||||
|
delay: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCounter({ icon, value, label, suffix, delay }: StatCounterProps) {
|
||||||
|
const countRef = useRef(null)
|
||||||
|
const isInView = useInView(countRef, { once: false })
|
||||||
|
const [hasAnimated, setHasAnimated] = useState(false)
|
||||||
|
|
||||||
|
const springValue = useSpring(0, {
|
||||||
|
stiffness: 50,
|
||||||
|
damping: 10,
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInView && !hasAnimated) {
|
||||||
|
springValue.set(value)
|
||||||
|
setHasAnimated(true)
|
||||||
|
} else if (!isInView && hasAnimated) {
|
||||||
|
springValue.set(0)
|
||||||
|
setHasAnimated(false)
|
||||||
|
}
|
||||||
|
}, [isInView, value, springValue, hasAnimated])
|
||||||
|
|
||||||
|
const displayValue = useTransform(springValue, (latest) => Math.floor(latest))
|
||||||
|
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="bg-white/50 backdrop-blur-sm p-6 rounded-xl flex flex-col items-center text-center group hover:bg-white transition-colors duration-300"
|
||||||
|
variants={{
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: { duration: 0.6, delay },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
whileHover={{ y: -5, transition: { duration: 0.2 } }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="w-14 h-14 rounded-full bg-taupe-900/5 flex items-center justify-center mb-4 text-olive-700 group-hover:bg-olive-700/10 transition-colors duration-300"
|
||||||
|
whileHover={{ rotate: 360, transition: { duration: 0.8 } }}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</motion.div>
|
||||||
|
<motion.div ref={countRef} className="text-3xl font-bold text-taupe-900 flex items-center">
|
||||||
|
<motion.span>{displayValue}</motion.span>
|
||||||
|
<span>{suffix}</span>
|
||||||
|
</motion.div>
|
||||||
|
<p className="text-taupe-700 text-sm mt-1">{label}</p>
|
||||||
|
<motion.div className="w-10 h-0.5 bg-olive-700 mt-3 group-hover:w-16 transition-all duration-300" />
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
60
tailwind.config.ts
Normal file
60
tailwind.config.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
content: [
|
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
taupe: {
|
||||||
|
50: '#f8f6f4',
|
||||||
|
100: '#f0ede9',
|
||||||
|
200: '#e1ddd7',
|
||||||
|
300: '#d2cdc5',
|
||||||
|
400: '#b8b0a5',
|
||||||
|
500: '#9e968a',
|
||||||
|
600: '#7a6c5d',
|
||||||
|
700: '#6b5e51',
|
||||||
|
800: '#5b4f43',
|
||||||
|
900: '#4a3b2f',
|
||||||
|
950: '#2d2218',
|
||||||
|
},
|
||||||
|
olive: {
|
||||||
|
50: '#f4f7f1',
|
||||||
|
100: '#e8f0e2',
|
||||||
|
200: '#d1e1c5',
|
||||||
|
300: '#b9d2a8',
|
||||||
|
400: '#9fb98a',
|
||||||
|
500: '#748e58',
|
||||||
|
600: '#5f7446',
|
||||||
|
700: '#4b5b38',
|
||||||
|
800: '#37432a',
|
||||||
|
900: '#242b1c',
|
||||||
|
950: '#12160d',
|
||||||
|
},
|
||||||
|
brown: {
|
||||||
|
50: '#faf8f6',
|
||||||
|
100: '#f4f0ec',
|
||||||
|
200: '#e8e0d7',
|
||||||
|
300: '#d9cfc4',
|
||||||
|
400: '#bda08d',
|
||||||
|
500: '#9e7a64',
|
||||||
|
600: '#7a5e4d',
|
||||||
|
700: '#5b4636',
|
||||||
|
800: '#4a372a',
|
||||||
|
900: '#382a1f',
|
||||||
|
950: '#261d15',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ['var(--font-geist-sans)', 'sans-serif'],
|
||||||
|
mono: ['var(--font-geist-mono)', 'monospace']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
export default config;
|
||||||
Loading…
x
Reference in New Issue
Block a user