commit 59ebafd9934760f8e044e3d0332723e5a65a8135 Author: v0agent Date: Wed Jun 24 14:47:12 2026 +0000 Initial commit from v0 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f650315 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts \ No newline at end of file diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..92d1b03 --- /dev/null +++ b/app/globals.css @@ -0,0 +1,269 @@ +@import 'tailwindcss'; +@import 'tw-animate-css'; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: #FFFFFF; + --foreground: #0A0A0A; + --card: #FFFFFF; + --card-foreground: #0A0A0A; + --popover: #FFFFFF; + --popover-foreground: #0A0A0A; + --primary: #0A0A0A; + --primary-foreground: #FFFFFF; + --secondary: #F5F5F5; + --secondary-foreground: #0A0A0A; + --muted: #F5F5F5; + --muted-foreground: #737373; + --accent: #0A0A0A; + --accent-foreground: #FFFFFF; + --destructive: #DC2626; + --destructive-foreground: #FFFFFF; + --border: #E5E5E5; + --input: #F5F5F5; + --ring: #0A0A0A; + --chart-1: #0A0A0A; + --chart-2: #525252; + --chart-3: #737373; + --chart-4: #A3A3A3; + --chart-5: #D4D4D4; + --radius: 0rem; + --sidebar: #0A0A0A; + --sidebar-foreground: #FFFFFF; + --sidebar-primary: #FFFFFF; + --sidebar-primary-foreground: #0A0A0A; + --sidebar-accent: #262626; + --sidebar-accent-foreground: #FFFFFF; + --sidebar-border: #262626; + --sidebar-ring: #FFFFFF; +} + +.dark { + --background: #0A0A0A; + --foreground: #FAFAFA; + --card: #0A0A0A; + --card-foreground: #FAFAFA; + --popover: #0A0A0A; + --popover-foreground: #FAFAFA; + --primary: #FAFAFA; + --primary-foreground: #0A0A0A; + --secondary: #171717; + --secondary-foreground: #FAFAFA; + --muted: #262626; + --muted-foreground: #A3A3A3; + --accent: #FAFAFA; + --accent-foreground: #0A0A0A; + --destructive: #DC2626; + --destructive-foreground: #FFFFFF; + --border: #262626; + --input: #171717; + --ring: #FAFAFA; + --chart-1: #FAFAFA; + --chart-2: #A3A3A3; + --chart-3: #737373; + --chart-4: #525252; + --chart-5: #262626; + --sidebar: #171717; + --sidebar-foreground: #FAFAFA; + --sidebar-primary: #FAFAFA; + --sidebar-primary-foreground: #0A0A0A; + --sidebar-accent: #262626; + --sidebar-accent-foreground: #FAFAFA; + --sidebar-border: #262626; + --sidebar-ring: #FAFAFA; +} + +@theme inline { + --font-sans: 'Inter', 'Inter Fallback', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', monospace; + --font-display: 'PP Editorial New', 'Times New Roman', serif; + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } + + html { + scroll-behavior: smooth; + } +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(100%); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes float { + 0%, 100% { transform: translateY(0px); } + 50% { transform: translateY(-20px); } +} + +@keyframes reveal-up { + from { + opacity: 0; + transform: translateY(60px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes reveal-left { + from { + opacity: 0; + transform: translateX(-60px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes reveal-right { + from { + opacity: 0; + transform: translateX(60px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes scale-in { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +@keyframes grain { + 0%, 100% { transform: translate(0, 0); } + 10% { transform: translate(-5%, -10%); } + 20% { transform: translate(-15%, 5%); } + 30% { transform: translate(7%, -25%); } + 40% { transform: translate(-5%, 25%); } + 50% { transform: translate(-15%, 10%); } + 60% { transform: translate(15%, 0%); } + 70% { transform: translate(0%, 15%); } + 80% { transform: translate(3%, 35%); } + 90% { transform: translate(-10%, 10%); } +} + +.animate-float { + animation: float 6s ease-in-out infinite; +} + +.animate-reveal-up { + animation: reveal-up 0.8s ease-out forwards; +} + +.animate-reveal-left { + animation: reveal-left 0.8s ease-out forwards; +} + +.animate-reveal-right { + animation: reveal-right 0.8s ease-out forwards; +} + +.animate-scale-in { + animation: scale-in 0.6s ease-out forwards; +} + +.animation-delay-100 { animation-delay: 100ms; } +.animation-delay-200 { animation-delay: 200ms; } +.animation-delay-300 { animation-delay: 300ms; } +.animation-delay-400 { animation-delay: 400ms; } +.animation-delay-500 { animation-delay: 500ms; } +.animation-delay-600 { animation-delay: 600ms; } + +.grain-overlay::before { + content: ''; + position: absolute; + inset: -100%; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E"); + opacity: 0.03; + pointer-events: none; + animation: grain 8s steps(10) infinite; +} + +.text-stroke { + -webkit-text-stroke: 1px currentColor; + -webkit-text-fill-color: transparent; +} + +.clip-diagonal { + clip-path: polygon(0 0, 100% 0, 100% 85%, 0 100%); +} + +.clip-diagonal-reverse { + clip-path: polygon(0 15%, 100% 0, 100% 100%, 0 100%); +} + +/* Fade in animation for images */ +@keyframes fadeIn { + from { + opacity: 0; + transform: scale(1.02); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.animate-fade-in { + animation: fadeIn 0.7s ease-out forwards; +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..559eced --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,45 @@ +import React from "react" +import type { Metadata } from 'next' +import { Inter } from 'next/font/google' +import { Analytics } from '@vercel/analytics/next' +import './globals.css' + +const inter = Inter({ subsets: ["latin"], variable: '--font-inter' }); + +export const metadata: Metadata = { + title: 'EVASION', + description: 'High-performance outdoor gear engineered for the modern explorer. Lightweight, durable, adventure-ready.', + generator: 'v0.app', + icons: { + icon: [ + { + url: '/icon-light-32x32.png', + media: '(prefers-color-scheme: light)', + }, + { + url: '/icon-dark-32x32.png', + media: '(prefers-color-scheme: dark)', + }, + { + url: '/icon.svg', + type: 'image/svg+xml', + }, + ], + apple: '/apple-icon.png', + }, +} + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode +}>) { + return ( + + + {children} + + + + ) +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..1b5cdfb --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,27 @@ +import { Header } from "@/components/header"; +import { HeroSection } from "@/components/sections/hero-section"; +import { PhilosophySection } from "@/components/sections/philosophy-section"; +import { FeaturedProductsSection } from "@/components/sections/featured-products-section"; +import { TechnologySection } from "@/components/sections/technology-section"; +import { GallerySection } from "@/components/sections/gallery-section"; +import { CollectionSection } from "@/components/sections/collection-section"; +import { EditorialSection } from "@/components/sections/editorial-section"; +import { TestimonialsSection } from "@/components/sections/testimonials-section"; +import { FooterSection } from "@/components/sections/footer-section"; + +export default function Home() { + return ( +
+
+ + + + + + + + + +
+ ); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..4ee62ee --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} diff --git a/components/fade-image.tsx b/components/fade-image.tsx new file mode 100644 index 0000000..3a6b42d --- /dev/null +++ b/components/fade-image.tsx @@ -0,0 +1,49 @@ +"use client"; + +import Image, { type ImageProps } from "next/image"; +import { useEffect, useRef, useState } from "react"; + +interface FadeImageProps extends Omit { + fadeDelay?: number; +} + +export function FadeImage({ className, fadeDelay = 0, ...props }: FadeImageProps) { + const [isVisible, setIsVisible] = useState(false); + const [isLoaded, setIsLoaded] = useState(false); + const ref = useRef(null); + + useEffect(() => { + const observer = new IntersectionObserver( + ([entry]) => { + if (entry.isIntersecting) { + setTimeout(() => { + setIsVisible(true); + }, fadeDelay); + observer.disconnect(); + } + }, + { + threshold: 0.1, + rootMargin: "50px", + } + ); + + if (ref.current) { + observer.observe(ref.current); + } + + return () => observer.disconnect(); + }, [fadeDelay]); + + return ( +
+ setIsLoaded(true)} + /> +
+ ); +} diff --git a/components/header.tsx b/components/header.tsx new file mode 100644 index 0000000..8eac87a --- /dev/null +++ b/components/header.tsx @@ -0,0 +1,126 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; +import { Menu, X } from "lucide-react"; + +export function Header() { + const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 50); + }; + + window.addEventListener("scroll", handleScroll, { passive: true }); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + return ( +
+
+ {/* Logo */} + + MONO + + + {/* Desktop Navigation */} + + + {/* CTA */} +
+ + Contact + +
+ + {/* Mobile Menu Button */} + +
+ + {/* Mobile Menu */} + {isMenuOpen && ( +
+ +
+ )} +
+ ); +} diff --git a/components/sections/collection-section.tsx b/components/sections/collection-section.tsx new file mode 100644 index 0000000..df3edf4 --- /dev/null +++ b/components/sections/collection-section.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { FadeImage } from "@/components/fade-image"; + +const accessories = [ + { + id: 1, + name: "Compact Model", + description: "120m² living space with optimal energy efficiency", + price: "$285,000", + image: "/images/hero-side-1.png", + }, + { + id: 2, + name: "Standard Model", + description: "180m² perfect balance of space and sustainability", + price: "$395,000", + image: "/images/hero-side-2.png", + }, + { + id: 3, + name: "Premium Model", + description: "250m² expansive design with maximum comfort", + price: "$525,000", + image: "/images/hero-side-4.png", + }, +]; + +export function CollectionSection() { + return ( +
+ {/* Section Title */} +
+

+ Surface Options +

+
+ + {/* Accessories Grid/Carousel */} +
+ {/* Mobile: Horizontal Carousel */} +
+ {accessories.map((accessory) => ( +
+ {/* Image */} +
+ +
+ + {/* Content */} +
+
+
+

+ {accessory.name} +

+

+ {accessory.description} +

+
+ + {accessory.price} + +
+
+
+ ))} +
+ + {/* Desktop: Grid */} +
+ {accessories.map((accessory) => ( +
+ {/* Image */} +
+ +
+ + {/* Content */} +
+
+
+

+ {accessory.name} +

+

+ {accessory.description} +

+
+ + {accessory.price} + +
+
+
+ ))} +
+
+
+ ); +} diff --git a/components/sections/editorial-section.tsx b/components/sections/editorial-section.tsx new file mode 100644 index 0000000..8ea95db --- /dev/null +++ b/components/sections/editorial-section.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { useRef, useState, useEffect, useCallback } from "react"; + +const specs = [ + { label: "Surface Area", value: "180m²" }, + { label: "Energy Use", value: "15 kWh/m²" }, + { label: "Solar Panels", value: "40 m²" }, + { label: "Carbon Balance", value: "-20%" }, +]; + +export function EditorialSection() { + const videoRef = useRef(null); + const [scrollProgress, setScrollProgress] = useState(0); + const rafRef = useRef(null); + + const updateParallax = useCallback(() => { + if (!videoRef.current) return; + + const rect = videoRef.current.getBoundingClientRect(); + const windowHeight = window.innerHeight; + + // Calculate when video enters and exits viewport + const videoTop = rect.top; + const videoBottom = rect.bottom; + + // Progress from 0 (entering viewport) to 1 (exiting viewport) + if (videoBottom > 0 && videoTop < windowHeight) { + const progress = 1 - (videoTop + rect.height / 2) / (windowHeight + rect.height); + setScrollProgress(Math.max(0, Math.min(1, progress))); + } + }, []); + + useEffect(() => { + const handleScroll = () => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + rafRef.current = requestAnimationFrame(updateParallax); + }; + + window.addEventListener("scroll", handleScroll, { passive: true }); + updateParallax(); + + return () => { + window.removeEventListener("scroll", handleScroll); + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + }; + }, [updateParallax]); + + // Parallax effect: video moves up as you scroll down + const parallaxY = (scrollProgress - 0.5) * 30; // -15px to +15px range + + return ( +
+ {/* Newsletter Banner */} + + + {/* Decorative Icons */} +
+ + +
+ + {/* Full-width Video with Parallax */} +
+
+ + {/* Specs Grid */} +
+ {specs.map((spec) => ( +
+

+ {spec.label} +

+

+ {spec.value} +

+
+ ))} +
+
+ ); +} diff --git a/components/sections/featured-products-section.tsx b/components/sections/featured-products-section.tsx new file mode 100644 index 0000000..14e3f4b --- /dev/null +++ b/components/sections/featured-products-section.tsx @@ -0,0 +1,71 @@ +"use client"; + +import { FadeImage } from "@/components/fade-image"; + +const features = [ + { + image: "/images/4312e1bb-e030-4528-b6df-8a6ea69fe384.png", + span: "col-span-2 row-span-2", // Large + }, + { + image: "/images/b2401fa5-4eac-465f-b1f9-014aadc182ee.png", + span: "col-span-1 row-span-1", // Small + }, + { + image: "/images/dd1b32a8-3722-4ea2-8808-10d53532809d.png", + span: "col-span-1 row-span-1", // Small + }, + { + image: "/images/61af06cc-84d0-4031-a0ed-76fc43b1c1e1.png", + span: "col-span-1 row-span-2", // Tall + }, + { + image: "/images/249083d2-c49c-4c06-a125-376284d90c42.png", + span: "col-span-1 row-span-1", // Small + }, + { + image: "/images/7638f650-8586-4403-8c13-141921a04f9d.png", + span: "col-span-2 row-span-1", // Wide + }, + { + image: "/images/5b3bdb95-fac7-4d22-aa97-98b5d547b2db.png", + span: "col-span-1 row-span-1", // Small + }, + { + image: "/images/634f7bae-77a5-49d0-a0ab-5271a6194e66.png", + span: "col-span-1 row-span-2", // Tall + }, + { + image: "/images/09ffa8fd-cdd1-453f-9aa2-d6c702a1f4b5.png", + span: "col-span-2 row-span-1", // Wide + }, + { + image: "/images/040e36b1-d16f-474b-a712-a9979e6ab479.png", + span: "col-span-1 row-span-1", // Small + }, +]; + +export function FeaturedProductsSection() { + return ( +
+
+ {/* Bento Grid */} +
+ {features.map((feature, index) => ( +
+ +
+ ))} +
+
+
+ ); +} diff --git a/components/sections/footer-section.tsx b/components/sections/footer-section.tsx new file mode 100644 index 0000000..073686d --- /dev/null +++ b/components/sections/footer-section.tsx @@ -0,0 +1,129 @@ +"use client"; + +import Link from "next/link"; + +const footerLinks = { + explore: [ + { label: "Products", href: "#products" }, + { label: "Technology", href: "#technology" }, + { label: "Gallery", href: "#gallery" }, + { label: "Accessories", href: "#accessories" }, + ], + about: [ + { label: "Our Story", href: "#" }, + { label: "Team", href: "#" }, + { label: "Careers", href: "#" }, + { label: "Contact", href: "#" }, + ], + service: [ + { label: "FAQ", href: "#" }, + { label: "Shipping", href: "#" }, + { label: "Returns", href: "#" }, + { label: "Warranty", href: "#" }, + ], +}; + +export function FooterSection() { + return ( +
+ {/* Main Footer Content */} +
+
+ {/* Brand */} +
+ + MONO + +

+ Sustainable design homes combining contemporary aesthetics with energy efficiency and eco-friendly materials. +

+
+ + {/* Explore */} +
+

Explore

+
    + {footerLinks.explore.map((link) => ( +
  • + + {link.label} + +
  • + ))} +
+
+ + {/* About */} +
+

About

+
    + {footerLinks.about.map((link) => ( +
  • + + {link.label} + +
  • + ))} +
+
+ + {/* Service */} +
+

Service

+
    + {footerLinks.service.map((link) => ( +
  • + + {link.label} + +
  • + ))} +
+
+
+
+ + {/* Bottom Bar */} +
+
+

+ 2026 MONO. All rights reserved. +

+ + + + {/* Social Links */} +
+ + Instagram + + + Twitter + + + YouTube + +
+
+
+
+ ); +} diff --git a/components/sections/gallery-section.tsx b/components/sections/gallery-section.tsx new file mode 100644 index 0000000..538de80 --- /dev/null +++ b/components/sections/gallery-section.tsx @@ -0,0 +1,134 @@ +"use client"; + +import Image from "next/image"; +import { useEffect, useRef, useState, useCallback } from "react"; + +export function GallerySection() { + const galleryRef = useRef(null); + const [scrollProgress, setScrollProgress] = useState(0); + const rafRef = useRef(null); + + const images = [ + { src: "/images/mono-1.png", alt: "Modern architecture at sunrise" }, + { src: "/images/mono-2.png", alt: "Modern architecture in daylight" }, + { src: "/images/mono-3.png", alt: "Modern architecture at dusk" }, + { src: "/images/mono-4.png", alt: "Modern architecture at night" }, + ]; + + const updateTransform = useCallback(() => { + if (!galleryRef.current) return; + + const rect = galleryRef.current.getBoundingClientRect(); + const windowHeight = window.innerHeight; + const sectionHeight = galleryRef.current.offsetHeight; + + // Calculate scroll progress through the section + const scrollableRange = sectionHeight - windowHeight; + const scrolled = -rect.top; + const progress = Math.max(0, Math.min(1, scrolled / scrollableRange)); + + setScrollProgress(progress); + }, []); + + useEffect(() => { + const handleScroll = () => { + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + rafRef.current = requestAnimationFrame(updateTransform); + }; + + window.addEventListener("scroll", handleScroll, { passive: true }); + updateTransform(); + + return () => { + window.removeEventListener("scroll", handleScroll); + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + }; + }, [updateTransform]); + + const isLastImage = images.length - 1; + + // Calculate fullscreen progress for the last image - more gradual + // Start expanding when last image is 80% stacked, finish at end of section + const fullscreenStartProgress = 0.6; // Start earlier for smoother transition + const fullscreenProgress = Math.max(0, Math.min(1, (scrollProgress - fullscreenStartProgress) / (1 - fullscreenStartProgress))); + + // Ease out cubic for smoother animation + const easedFullscreenProgress = 1 - Math.pow(1 - fullscreenProgress, 3); + + return ( + + ); +} diff --git a/components/sections/hero-section.tsx b/components/sections/hero-section.tsx new file mode 100644 index 0000000..9f8b530 --- /dev/null +++ b/components/sections/hero-section.tsx @@ -0,0 +1,208 @@ +"use client"; + +import Image from "next/image"; +import { useEffect, useRef, useState } from "react"; + +const word = "MONO"; + +const sideImages = [ + { + src: "/images/hero-side-1.png", + alt: "Modern architecture with corten steel", + position: "left", + span: 1, + }, + { + src: "/images/hero-side-2.png", + alt: "Aerial view of modern home", + position: "left", + span: 1, + }, + { + src: "/images/hero-side-3.png", + alt: "Interior view with landscape", + position: "right", + span: 1, + }, + { + src: "/images/hero-side-4.png", + alt: "Modern architecture at night", + position: "right", + span: 1, + }, +]; + +export function HeroSection() { + const sectionRef = useRef(null); + const [scrollProgress, setScrollProgress] = useState(0); + + useEffect(() => { + const handleScroll = () => { + if (!sectionRef.current) return; + + const rect = sectionRef.current.getBoundingClientRect(); + const scrollableHeight = window.innerHeight * 2; + const scrolled = -rect.top; + const progress = Math.max(0, Math.min(1, scrolled / scrollableHeight)); + + setScrollProgress(progress); + }; + + window.addEventListener("scroll", handleScroll, { passive: true }); + handleScroll(); + + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); + + // Text fades out first (0 to 0.2) + const textOpacity = Math.max(0, 1 - (scrollProgress / 0.2)); + + // Image transforms start after text fades (0.2 to 1) + const imageProgress = Math.max(0, Math.min(1, (scrollProgress - 0.2) / 0.8)); + + // Smooth interpolations - More balanced distribution + const centerWidth = 100 - (imageProgress * 80); // 100% to 20% (same as each side image) + const centerHeight = 100; // Always 100% height + const sideWidth = imageProgress * 40; // 0% to 40% (20% per image, 2 images = 40%) + const sideOpacity = imageProgress; + const sideTranslateLeft = -100 + (imageProgress * 100); // -100% to 0% + const sideTranslateRight = 100 - (imageProgress * 100); // 100% to 0% + const borderRadius = 0; // No border radius + const gap = imageProgress * 8; // 0px to 8px + + // Vertical offset for side columns to move them up on mobile + const sideTranslateY = -(imageProgress * 15); // Move up by 15% when fully expanded + + return ( +
+ {/* Sticky container for scroll animation */} +
+
+ {/* Bento Grid Container */} +
+ + {/* Left Column */} +
+ {sideImages.filter(img => img.position === "left").map((img, idx) => ( +
+ {img.alt} +
+ ))} +
+ + {/* Main Hero Image - Center */} +
+ {/* Text Behind - Fades out first */} +
+

+ {word.split("").map((letter, index) => ( + + {letter} + + ))} +

+
+ + Modern architectural structure with reflection +
+ + {/* Right Column */} +
+ {sideImages.filter(img => img.position === "right").map((img, idx) => ( +
+ {img.alt} +
+ ))} +
+ +
+
+
+ + {/* Tagline Section - Fixed at bottom */} +
+

+ Lightweight, durable +
+ and adventure-ready. +

+
+ + {/* Scroll space to enable animation */} +
+
+ ); +} diff --git a/components/sections/philosophy-section.tsx b/components/sections/philosophy-section.tsx new file mode 100644 index 0000000..9cb9c0e --- /dev/null +++ b/components/sections/philosophy-section.tsx @@ -0,0 +1,171 @@ +"use client"; + +import Image from "next/image"; +import { useEffect, useRef, useState, useCallback } from "react"; + +const titles = [ + "Sustainable Architecture.", + "Built for Tomorrow.", + "Eco-Responsible.", +]; + +export function PhilosophySection() { + const sectionRef = useRef(null); + const descriptionRef = useRef(null); + const [titleOpacity, setTitleOpacity] = useState(0); + const [descriptionProgress, setDescriptionProgress] = useState(0); + const rafRef = useRef(null); + + const updateTransforms = useCallback(() => { + if (!sectionRef.current) return; + + const rect = sectionRef.current.getBoundingClientRect(); + const windowHeight = window.innerHeight; + const sectionHeight = sectionRef.current.offsetHeight; + + // Calculate progress based on scroll position + const scrollableRange = sectionHeight - windowHeight; + const scrolled = -rect.top; + const progress = Math.max(0, Math.min(1, scrolled / scrollableRange)); + + // Title rotates through 3 texts based on scroll progress + setTitleOpacity(progress); + + // Description word animation + if (descriptionRef.current) { + const descRect = descriptionRef.current.getBoundingClientRect(); + const descTop = descRect.top; + const descHeight = descRect.height; + + // Start animation when element enters viewport + const startTrigger = windowHeight * 0.8; + const endTrigger = windowHeight * 0.2; + + if (descTop < startTrigger && descTop > endTrigger - descHeight) { + const descProgress = Math.max(0, Math.min(1, (startTrigger - descTop) / (startTrigger - endTrigger))); + setDescriptionProgress(descProgress); + } + } + }, []); + + useEffect(() => { + const handleScroll = () => { + // Cancel any pending animation frame + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + + // Use requestAnimationFrame for smooth updates + rafRef.current = requestAnimationFrame(updateTransforms); + }; + + window.addEventListener("scroll", handleScroll, { passive: true }); + updateTransforms(); + + return () => { + window.removeEventListener("scroll", handleScroll); + if (rafRef.current) { + cancelAnimationFrame(rafRef.current); + } + }; + }, [updateTransforms]); + + return ( +
+ {/* Scroll-Animated Product Grid */} +
+
+
+ {/* Title - centered with 3D rotation */} +
+
+ {titles.map((title, index) => { + // Last text "Built to last" stays visible at the end + const isLastText = index === titles.length - 1; + + // Calculate which text should be visible based on scroll progress + const segmentSize = 1 / titles.length; + const startProgress = index * segmentSize; + const endProgress = (index + 1) * segmentSize; + + let rotateX = 0; + let opacity = 0; + + if (titleOpacity >= startProgress && titleOpacity < endProgress) { + // Active text - rotating in + const localProgress = (titleOpacity - startProgress) / segmentSize; + rotateX = (1 - localProgress) * 90; + opacity = localProgress; + } else if (titleOpacity >= endProgress) { + // Text that has passed - last text stays visible + if (isLastText) { + rotateX = 0; + opacity = 1; + } else { + rotateX = -90; + opacity = 0; + } + } else { + // Text that hasn't appeared yet + rotateX = 90; + opacity = 0; + } + + return ( +

+ {title} +

+ ); + })} +
+
+
+
+
+ + {/* Description */} +
+
+ +

+ {("A design home that combines contemporary aesthetics and energy performance. Built with eco-friendly materials, it minimizes carbon footprint while offering optimal comfort.").split(" ").map((word, index, array) => { + const wordProgress = Math.max(0, Math.min(1, (descriptionProgress * array.length) - index)); + const opacity = wordProgress; + const blur = (1 - wordProgress) * 40; + + return ( + + {word}{index < array.length - 1 ? " " : ""} + + ); + })} +

+
+
+
+ ); +} diff --git a/components/sections/technology-section.tsx b/components/sections/technology-section.tsx new file mode 100644 index 0000000..1c396f7 --- /dev/null +++ b/components/sections/technology-section.tsx @@ -0,0 +1,348 @@ +"use client"; + +import Image from "next/image"; +import { useEffect, useRef, useState } from "react"; + +function ScrollRevealText({ text }: { text: string }) { + const containerRef = useRef(null); + const [progress, setProgress] = useState(0); + + useEffect(() => { + const handleScroll = () => { + if (!containerRef.current) return; + + const rect = containerRef.current.getBoundingClientRect(); + const windowHeight = window.innerHeight; + + // Slower animation - more viewport range + const startOffset = windowHeight * 0.9; + const endOffset = windowHeight * 0.1; + + const totalDistance = startOffset - endOffset; + const currentPosition = startOffset - rect.top; + + const newProgress = Math.max(0, Math.min(1, currentPosition / totalDistance)); + setProgress(newProgress); + }; + + window.addEventListener("scroll", handleScroll, { passive: true }); + handleScroll(); // Initial check + + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + const words = text.split(" "); + + return ( +

+ {words.map((word, index) => { + // Calculate blur and opacity based on scroll progress + const appearProgress = progress * (words.length + 1); + const wordAppearProgress = Math.max(0, Math.min(1, appearProgress - index)); + const wordOpacity = wordAppearProgress; + const wordBlur = (1 - wordAppearProgress) * 40; + + return ( + + {word} + + ); + })} +

+ ); +} + +const sideImages = [ + { + src: "/images/interior-view.png", + alt: "Interior view with landscape", + position: "left", + }, + { + src: "/images/rusted-metal.png", + alt: "Rusted metal texture", + position: "right", + }, +]; + +const textCycles = [ + "Design & Sustainability.", + "Passive Energy.", + "Bio-sourced Construction.", +]; + +export function TechnologySection() { + const sectionRef = useRef(null); + const textSectionRef = useRef(null); + const [scrollProgress, setScrollProgress] = useState(0); + const [textProgress, setTextProgress] = useState(0); + + const descriptionText = "Passive architecture reimagining modern living. Triple glazing, reinforced insulation and natural ventilation combine with solar panels to create an energy-autonomous home. Bio-sourced materials like solid wood and hemp wool ensure healthy indoor air and minimal ecological footprint."; + + useEffect(() => { + const handleScroll = () => { + if (!sectionRef.current) return; + + const rect = sectionRef.current.getBoundingClientRect(); + const scrollableHeight = window.innerHeight * 4; // Increased for 3 text cycles + const scrolled = -rect.top; + const progress = Math.max(0, Math.min(1, scrolled / scrollableHeight)); + + setScrollProgress(progress); + + // Text scroll progress + if (textSectionRef.current) { + const textRect = textSectionRef.current.getBoundingClientRect(); + const windowHeight = window.innerHeight; + + const startOffset = windowHeight * 0.9; + const endOffset = windowHeight * 0.1; + + const totalDistance = startOffset - endOffset; + const currentPosition = startOffset - textRect.top; + + const newTextProgress = Math.max(0, Math.min(1, currentPosition / totalDistance)); + setTextProgress(newTextProgress); + } + }; + + window.addEventListener("scroll", handleScroll, { passive: true }); + handleScroll(); + + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); + + // Title fades out first (0 to 0.2) + const titleOpacity = Math.max(0, 1 - (scrollProgress / 0.2)); + + // Image transforms start after title fades (0.2 to 1) + const imageProgress = Math.max(0, Math.min(1, (scrollProgress - 0.2) / 0.8)); + + // Smooth interpolations + const centerWidth = 100 - (imageProgress * 58); // 100% to 42% + const centerHeight = 100 - (imageProgress * 30); // 100% to 70% + const sideWidth = imageProgress * 22; // 0% to 22% + const sideOpacity = imageProgress; + const sideTranslateLeft = -100 + (imageProgress * 100); // -100% to 0% + const sideTranslateRight = 100 - (imageProgress * 100); // 100% to 0% + const gap = imageProgress * 16; // 0px to 16px + + // Calculate grayscale for text section based on textProgress + const grayscaleAmount = Math.round((1 - textProgress) * 100); + + return ( +
+ {/* Sticky container for scroll animation */} +
+
+ {/* Bento Grid Container */} +
+ + {/* Left Column */} +
+ {sideImages.filter(img => img.position === "left").map((img, idx) => ( + {img.alt} + ))} +
+ + {/* Main Center Image */} +
+ {/* Layered Images - Progressive Fade In */} + {/* Image 1 - Base layer - Sunrise/Sunset with sun rays */} + Modern architecture at sunrise + + {/* Image 2 - Daytime scene - Fades in during first text cycle */} + Modern architecture in daylight + + {/* Image 3 - Dusk/Evening - Fades in during second text cycle */} + Modern architecture at dusk + + {/* Image 4 - Night with stars - Fades in during third text cycle */} + Modern architecture at night + +
+ + {/* Title Text - Cycles through 3 texts with blur effect */} +
+ {textCycles.map((text, cycleIndex) => { + // Each text cycle takes 1/3 of the scroll progress + const cycleStart = cycleIndex / textCycles.length; + const cycleEnd = (cycleIndex + 1) / textCycles.length; + const cycleMid = (cycleStart + cycleEnd) / 2; + + const words = text.split(" "); + + return ( +

+ {words.map((word, wordIndex) => { + let wordOpacity = 0; + let wordBlur = 40; + + if (scrollProgress >= cycleStart && scrollProgress < cycleEnd) { + const localProgress = (scrollProgress - cycleStart) / (cycleEnd - cycleStart); + + // First half: appear (blur 40→0, opacity 0→1) + if (localProgress < 0.5) { + const appearProgress = (localProgress / 0.5) * (words.length + 1); + const wordAppearProgress = Math.max(0, Math.min(1, appearProgress - wordIndex)); + wordOpacity = wordAppearProgress; + wordBlur = (1 - wordAppearProgress) * 40; + } + // Second half: disappear (blur 0→40, opacity 1→0) + else { + const disappearProgress = ((localProgress - 0.5) / 0.5) * (words.length + 1); + const wordDisappearProgress = Math.max(0, Math.min(1, disappearProgress - wordIndex)); + wordOpacity = 1 - wordDisappearProgress; + wordBlur = wordDisappearProgress * 40; + } + } + + return ( + + {word} + + ); + })} +

+ ); + })} +
+
+ + {/* Right Column */} +
+ {sideImages.filter(img => img.position === "right").map((img, idx) => ( + {img.alt} + ))} +
+ +
+
+
+ + {/* Scroll space to enable animation - increased for 3 text cycles */} +
+ + {/* Description Section with Background Image and Scroll Reveal */} +
+ {/* Gradient Overlay - Top to transparent */} +
+ + {/* Text Content */} +
+ +
+
+
+ ); +} diff --git a/components/sections/testimonials-section.tsx b/components/sections/testimonials-section.tsx new file mode 100644 index 0000000..597f3a2 --- /dev/null +++ b/components/sections/testimonials-section.tsx @@ -0,0 +1,29 @@ +"use client"; + +import Image from "next/image"; + +export function TestimonialsSection() { + return ( +
+ {/* About Image with Text Overlay */} +
+ Modern corten steel architecture in natural landscape + {/* Fade gradient overlay - dark at bottom fading to transparent at top */} +
+ + {/* Text Overlay */} +
+

+ A passive house that combines contemporary design with environmental respect — + built for those who refuse to choose between modern comfort and ecological responsibility. +

+
+
+
+ ); +} diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx new file mode 100644 index 0000000..55c2f6e --- /dev/null +++ b/components/theme-provider.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as React from 'react' +import { + ThemeProvider as NextThemesProvider, + type ThemeProviderProps, +} from 'next-themes' + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children} +} diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..e538a33 --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,66 @@ +'use client' + +import * as React from 'react' +import * as AccordionPrimitive from '@radix-ui/react-accordion' +import { ChevronDownIcon } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Accordion({ + ...props +}: React.ComponentProps) { + return +} + +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180', + className, + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + +
{children}
+
+ ) +} + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..9704452 --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,157 @@ +'use client' + +import * as React from 'react' +import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog' + +import { cn } from '@/lib/utils' +import { buttonVariants } from '@/components/ui/button' + +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} + +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogOverlay({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + + + + ) +} + +function AlertDialogHeader({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..e6751ab --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,66 @@ +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const alertVariants = cva( + 'relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current', + { + variants: { + variant: { + default: 'bg-card text-card-foreground', + destructive: + 'text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<'div'> & VariantProps) { + return ( +
+ ) +} + +function AlertTitle({ className, ...props }: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +function AlertDescription({ + className, + ...props +}: React.ComponentProps<'div'>) { + return ( +
+ ) +} + +export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..40bb120 --- /dev/null +++ b/components/ui/aspect-ratio.tsx @@ -0,0 +1,11 @@ +'use client' + +import * as AspectRatioPrimitive from '@radix-ui/react-aspect-ratio' + +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} + +export { AspectRatio } diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..aa98465 --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,53 @@ +'use client' + +import * as React from 'react' +import * as AvatarPrimitive from '@radix-ui/react-avatar' + +import { cn } from '@/lib/utils' + +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..fc4126b --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,46 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' + +import { cn } from '@/lib/utils' + +const badgeVariants = cva( + 'inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden', + { + variants: { + variant: { + default: + 'border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90', + secondary: + 'border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90', + destructive: + 'border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', + outline: + 'text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + }, +) + +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<'span'> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : 'span' + + return ( + + ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..1750ff2 --- /dev/null +++ b/components/ui/breadcrumb.tsx @@ -0,0 +1,109 @@ +import * as React from 'react' +import { Slot } from '@radix-ui/react-slot' +import { ChevronRight, MoreHorizontal } from 'lucide-react' + +import { cn } from '@/lib/utils' + +function Breadcrumb({ ...props }: React.ComponentProps<'nav'>) { + return