feat: Introduce core UI components, pages, and assets for the real estate application.

This commit is contained in:
Alaguraj0361 2025-11-22 20:57:15 +05:30
parent c5b07e0e67
commit ded22acd9b
35 changed files with 2866 additions and 218 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

34
src/app/about/page.tsx Normal file
View File

@ -0,0 +1,34 @@
import Header from "@/components/Header";
import InnerBanner from "@/components/InnerBanner";
import About from "@/components/About";
import WhyChooseUs from "@/components/WhyChooseUs";
import Testimonials from "@/components/Testimonials";
import Footer from "@/components/Footer";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "About Us | Sky and Soil Real Estate",
description: "Learn about Sky and Soil's mission to connect you with nature-inspired living spaces. We are authorized partners for Godrej Properties.",
};
export default function AboutPage() {
return (
<main className="min-h-screen bg-white">
<Header />
<InnerBanner
title="About Us"
subtitle="Discover our journey in creating exceptional living spaces"
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "About" }
]}
/>
<div>
<About />
<WhyChooseUs />
<Testimonials />
</div>
<Footer />
</main>
);
}

11
src/app/compare/page.tsx Normal file
View File

@ -0,0 +1,11 @@
import CompareClient from "@/components/CompareClient";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Compare Properties | Sky and Soil Real Estate",
description: "Compare up to 4 properties side-by-side to find the perfect home that meets your needs. Analyze price, amenities, and specifications.",
};
export default function ComparePage() {
return <CompareClient />;
}

30
src/app/contact/page.tsx Normal file
View File

@ -0,0 +1,30 @@
import Header from "@/components/Header";
import InnerBanner from "@/components/InnerBanner";
import ContactCTA from "@/components/ContactCTA";
import Footer from "@/components/Footer";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Contact Us | Sky and Soil Real Estate",
description: "Get in touch with Sky and Soil for inquiries about our premium properties in North Bengaluru. We are here to help you find your dream home.",
};
export default function ContactPage() {
return (
<main className="min-h-screen bg-white">
<Header />
<InnerBanner
title="Contact Us"
subtitle="Get in touch with our team for any inquiries"
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "Contact" }
]}
/>
<div>
<ContactCTA />
</div>
<Footer />
</main>
);
}

View File

@ -8,11 +8,18 @@ const inter = Inter({
});
export const metadata: Metadata = {
title: "Aurora Springs Realty | Redefining Modern Living",
description: "Premium villas, plots, and apartments in North Bengaluru. Experience luxury living with Aurora Springs Realty.",
title: {
default: "Sky and Soil | Premium Real Estate in North Bengaluru",
template: "%s | Sky and Soil"
},
description: "Discover luxury apartments, villas, and plots in North Bengaluru. Sky and Soil connects you with nature-inspired living spaces and Godrej Properties.",
keywords: ["Real Estate", "Bengaluru", "Luxury Homes", "Godrej Properties", "North Bengaluru", "Villas", "Apartments"],
};
import { ThemeProvider } from "@/components/ThemeProvider";
import { CompareProvider } from "@/context/CompareContext";
import CompareBar from "@/components/CompareBar";
import MouseAnimation from "@/components/MouseAnimation";
export default function RootLayout({
children,
@ -30,7 +37,11 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
<CompareProvider>
{children}
<CompareBar />
<MouseAnimation />
</CompareProvider>
</ThemeProvider>
</body>
</html>

View File

@ -0,0 +1,30 @@
import Header from "@/components/Header";
import InnerBanner from "@/components/InnerBanner";
import Lifestyle from "@/components/Lifestyle";
import Footer from "@/components/Footer";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Lifestyle | Sky and Soil Real Estate",
description: "Experience the Aurora Lifestyle with Sky and Soil. Discover world-class amenities, green spaces, and a community designed for luxury living.",
};
export default function LifestylePage() {
return (
<main className="min-h-screen bg-white">
<Header />
<InnerBanner
title="Lifestyle"
subtitle="Experience the perfect blend of comfort and luxury"
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "Lifestyle" }
]}
/>
<div>
<Lifestyle />
</div>
<Footer />
</main>
);
}

View File

@ -8,6 +8,14 @@ import Testimonials from "@/components/Testimonials";
import ContactCTA from "@/components/ContactCTA";
import Footer from "@/components/Footer";
import FAQ from "@/components/FAQ";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Sky and Soil | Luxury Homes & Premium Real Estate",
description: "Find your dream home with Sky and Soil. We offer exclusive listings of apartments, villas, and plots in Bangalore's prime locations.",
};
export default function Home() {
return (
<main className="min-h-screen bg-white">
@ -15,9 +23,10 @@ export default function Home() {
<Hero />
<About />
<WhyChooseUs />
<Properties />
<Properties layout="slider" />
<Lifestyle />
<Testimonials />
<FAQ />
<ContactCTA />
<Footer />
</main>

11
src/app/projects/page.tsx Normal file
View File

@ -0,0 +1,11 @@
import PropertiesClient from "@/components/PropertiesClient";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Our Properties | Sky and Soil Real Estate",
description: "Browse our exclusive collection of premium apartments, villas, and plots in North Bengaluru. Find your perfect home with Sky and Soil.",
};
export default function PropertiesPage() {
return <PropertiesClient />;
}

View File

@ -1,46 +1,294 @@
import { notFound } from "next/navigation";
import { use } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import Image from "next/image";
import PropertyGallery from "@/components/PropertyGallery";
import PropertyNav from "@/components/PropertyNav";
import InnerBanner from "@/components/InnerBanner";
import { properties } from "@/data/properties";
import PropertyHero from "@/components/property/PropertyHero";
import PropertyOverview from "@/components/property/PropertyOverview";
import PropertyAmenities from "@/components/property/PropertyAmenities";
import PropertyContact from "@/components/property/PropertyContact";
import { notFound } from "next/navigation";
// This is required for static site generation with dynamic routes
import { Metadata } from "next";
// Required for static site generation with dynamic routes
export function generateStaticParams() {
return properties.map((property) => ({
id: property.id.toString(),
}));
}
export default async function PropertyPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const property = properties.find((p) => p.id.toString() === id);
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const resolvedParams = await params;
const property = properties.find(p => p.id === parseInt(resolvedParams.id));
if (!property) {
return {
title: "Property Not Found | Sky and Soil",
description: "The requested property could not be found."
};
}
return {
title: `${property.title} | Sky and Soil Real Estate`,
description: `Explore ${property.title} in ${property.location}. ${property.overview.bhk} ${property.category} starting at ${property.price}.`,
};
}
const sections = [
{ id: "overview", label: "Overview" },
{ id: "about", label: "About" },
{ id: "amenities", label: "Amenities" },
{ id: "floor-plans", label: "Floor Plans" },
{ id: "location", label: "Location" },
{ id: "pricing", label: "Pricing" },
];
export default async function PropertyDetailPage({ params }: { params: Promise<{ id: string }> }) {
const resolvedParams = await params;
const property = properties.find(p => p.id === parseInt(resolvedParams.id));
if (!property) {
notFound();
}
return (
<main className="min-h-screen bg-background pb-24">
<PropertyHero property={property} />
<div className="min-h-screen bg-gray-50 dark:bg-black">
<Header />
<div className="max-w-7xl mx-auto px-6 mt-12">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
{/* Main Content */}
<div className="lg:col-span-2">
<PropertyOverview
overview={property.overview}
description={property.description}
<InnerBanner
title={property.title}
subtitle={property.location}
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "Properties", href: "/projects" },
{ label: property.title }
]}
backgroundImage={property.image}
/>
<PropertyAmenities amenities={property.amenities} />
<div>
{/* Sticky Navigation */}
<PropertyNav sections={sections} />
<div className="max-w-7xl mx-auto px-6 py-8">
{/* Property Header */}
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 mb-8 shadow-sm border border-gray-200 dark:border-gray-800">
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
<div>
<div className="flex items-center gap-3 mb-3">
<h1 className="text-3xl md:text-4xl font-bold text-foreground">{property.title}</h1>
<span className="px-4 py-1.5 bg-gradient-to-r from-green-500 to-emerald-500 text-white rounded-full text-sm font-semibold shadow-md">
{property.status}
</span>
</div>
<div className="flex items-center text-gray-600 dark:text-gray-400">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
</svg>
{property.location}
</div>
</div>
<div className="text-right">
<div className="text-sm text-gray-500 dark:text-gray-400 mb-1">Starting from</div>
<div className="text-4xl font-bold bg-gradient-to-r from-primary to-blue-600 bg-clip-text text-transparent">
{property.price}
</div>
</div>
</div>
{/* Quick Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-6 border-t border-gray-200 dark:border-gray-800">
<div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-2xl font-bold text-primary mb-1">{property.overview.bhk}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Configuration</div>
</div>
<div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-2xl font-bold text-primary mb-1">{property.overview.size}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Area</div>
</div>
<div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-2xl font-bold text-primary mb-1">{property.overview.possession}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Possession</div>
</div>
<div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-2xl font-bold text-primary mb-1">{property.overview.totalUnits}</div>
<div className="text-sm text-gray-600 dark:text-gray-400">Total Units</div>
</div>
</div>
</div>
{/* Image Gallery */}
<PropertyGallery images={property.images} title={property.title} />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Main Content */}
<div className="lg:col-span-2 space-y-8">
{/* Overview Section */}
<div id="overview" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
<div className="w-1 h-8 bg-primary rounded-full"></div>
Overview
</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-gray-500 dark:text-gray-400 text-sm mb-2">Property Type</div>
<div className="text-lg font-semibold text-foreground">{property.category}</div>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-gray-500 dark:text-gray-400 text-sm mb-2">RERA Status</div>
<div className="text-lg font-semibold text-green-600">Approved</div>
</div>
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<div className="text-gray-500 dark:text-gray-400 text-sm mb-2">Availability</div>
<div className="text-lg font-semibold text-foreground">Available</div>
</div>
</div>
</div>
{/* About Section */}
<div id="about" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
<div className="w-1 h-8 bg-primary rounded-full"></div>
About this Property
</h2>
<p className="text-gray-700 dark:text-gray-300 leading-relaxed text-lg">{property.description}</p>
</div>
{/* Amenities Section */}
<div id="amenities" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
<div className="w-1 h-8 bg-primary rounded-full"></div>
Amenities
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{property.amenities.map((amenity, idx) => (
<div key={idx} className="flex items-center gap-3 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-xl hover:shadow-md transition-shadow">
<div className="w-10 h-10 bg-primary rounded-full flex items-center justify-center flex-shrink-0">
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<span className="text-gray-700 dark:text-gray-300 font-medium">{amenity}</span>
</div>
))}
</div>
</div>
{/* Floor Plans Section */}
<div id="floor-plans" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
<div className="w-1 h-8 bg-primary rounded-full"></div>
Floor Plans
</h2>
<div className="bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900 rounded-2xl p-12 text-center">
<svg className="w-20 h-20 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<p className="text-gray-600 dark:text-gray-400 text-lg mb-4">Floor plans available on request</p>
<button className="px-6 py-3 bg-primary text-white rounded-lg hover:bg-blue-700 transition-colors font-medium">
Request Floor Plans
</button>
</div>
</div>
{/* Location Section */}
<div id="location" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
<div className="w-1 h-8 bg-primary rounded-full"></div>
Location
</h2>
<div className="bg-gray-100 dark:bg-gray-800 rounded-xl p-8 text-center">
<svg className="w-16 h-16 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
</svg>
<p className="text-gray-600 dark:text-gray-400">Interactive map coming soon</p>
</div>
</div>
{/* Pricing Section */}
<div id="pricing" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
<div className="w-1 h-8 bg-primary rounded-full"></div>
Pricing Details
</h2>
<div className="space-y-4">
<div className="flex justify-between items-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
<span className="text-gray-700 dark:text-gray-300">Base Price</span>
<span className="text-xl font-bold text-primary">{property.price}</span>
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">* Prices are subject to change. Please contact us for the latest pricing and offers.</p>
</div>
</div>
</div>
{/* Sidebar */}
<div className="lg:col-span-1">
<PropertyContact />
<div className="sticky top-32 space-y-6">
{/* Contact Form */}
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 shadow-lg">
<h3 className="text-xl font-bold text-foreground mb-4">Get in Touch</h3>
<form className="space-y-4">
<input
type="text"
placeholder="Your Name"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground transition-all"
/>
<input
type="email"
placeholder="Email Address"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground transition-all"
/>
<input
type="tel"
placeholder="Phone Number"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground transition-all"
/>
<textarea
rows={4}
placeholder="Message (Optional)"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground transition-all"
/>
<button
type="submit"
className="w-full bg-gradient-to-r from-primary to-blue-600 text-white py-3 rounded-lg font-semibold hover:shadow-lg transition-all transform hover:scale-105"
>
Request Callback
</button>
</form>
</div>
{/* Quick Actions */}
<div className="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 rounded-2xl p-6 border border-gray-200 dark:border-gray-700">
<h3 className="text-lg font-bold text-foreground mb-4">Quick Actions</h3>
<div className="space-y-3">
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg hover:shadow-md transition-all text-foreground">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
</svg>
Share Property
</button>
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg hover:shadow-md transition-all text-foreground">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
Save to Wishlist
</button>
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg hover:shadow-md transition-all text-foreground">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
</svg>
Print Details
</button>
</div>
</div>
</div>
</main>
</div>
</div>
</div>
</div>
<Footer />
</div>
);
}

View File

@ -1,12 +1,63 @@
"use client";
import { useState, useRef, MouseEvent } from "react";
import Image from "next/image";
import { FloatingHouse, RotatingKey, GrowingBuilding } from "./PropertyAnimations";
export default function About() {
const [rotation, setRotation] = useState({ x: -10, y: 0 });
const [isHovering, setIsHovering] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
if (!containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const width = rect.width;
const height = rect.height;
// Calculate rotation based on mouse position
// Y-axis rotation (horizontal mouse movement): -180 to 180 degrees
const rotateY = ((x / width) * 360) - 180;
// X-axis rotation (vertical mouse movement): -90 (top view) to 90 (bottom view)
// Inverting Y so top of screen corresponds to seeing the top face
const rotateX = -(((y / height) * 180) - 90);
setRotation({ x: rotateX, y: rotateY });
};
const handleMouseEnter = () => {
setIsHovering(true);
};
const handleMouseLeave = () => {
setIsHovering(false);
setRotation({ x: -10, y: 0 }); // Reset to default angle
};
return (
<section id="about" className="py-24 bg-white dark:bg-black">
<div className="max-w-7xl mx-auto px-6">
<section id="about" className="py-24 bg-white dark:bg-black relative overflow-hidden">
{/* Decorative Animation */}
<div className="absolute top-10 left-10 opacity-30 hidden lg:block">
<FloatingHouse />
</div>
<div className="absolute bottom-20 right-10 opacity-20 hidden lg:block">
<RotatingKey />
</div>
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-10 pointer-events-none hidden lg:block">
<GrowingBuilding className="scale-150" />
</div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
{/* Text Content */}
<div className="space-y-8">
<div className="space-y-8 z-10">
<h2 className="text-4xl font-bold tracking-tight text-foreground">
Where the Sky Meets <br />
<span className="text-accent dark:text-accent">The Soil.</span>
@ -29,16 +80,128 @@ export default function About() {
</button>
</div>
{/* Image Content */}
<div className="relative h-[500px] rounded-3xl overflow-hidden shadow-2xl bg-gray-100 dark:bg-gray-800">
{/* Placeholder for an actual image. Using a colored div for now if no image is available,
but ideally this would be a real image. I'll use a gradient placeholder. */}
<div className="absolute inset-0 bg-gradient-to-br from-gray-200 to-gray-300 dark:from-gray-800 dark:to-gray-900 flex items-center justify-center text-gray-400 dark:text-gray-600">
<span className="text-sm">Modern Architecture Image</span>
{/* 3D Cube Container */}
<div
className="h-[500px] w-full flex items-center justify-center perspective-container cursor-move"
ref={containerRef}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div
className={`cube relative w-64 h-64 md:w-80 md:h-80 transform-style-3d ${!isHovering ? 'animate-spin-slow' : ''}`}
style={isHovering ? { transform: `rotateX(${rotation.x}deg) rotateY(${rotation.y}deg)` } : {}}
>
{/* Front Face */}
<div className="absolute inset-0 transform translate-z-32 md:translate-z-40 bg-white shadow-xl border border-white/20">
<Image
src="/assets/images/front-side.jfif"
alt="Front View"
fill
className="object-cover"
/>
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Front View</div>
</div>
{/* Back Face */}
<div className="absolute inset-0 transform rotate-y-90 translate-z-32 md:translate-z-40 bg-white shadow-xl border border-white/20">
<Image
src="/assets/images/back-side.jfif"
alt="Back View"
fill
className="object-cover"
/>
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Back View</div>
</div>
{/* Right Face */}
<div className="absolute inset-0 transform rotate-y-90 translate-z-32 md:translate-z-40 bg-white shadow-xl border border-white/20">
<Image
src="/assets/images/right-side.jfif"
alt="Right View"
fill
className="object-cover"
/>
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Right View</div>
</div>
{/* Left Face */}
<div className="absolute inset-0 transform -rotate-y-90 translate-z-32 md:translate-z-40 bg-white shadow-xl border border-white/20">
<Image
src="/assets/images/left-side.jfif"
alt="Left View"
fill
className="object-cover"
/>
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Left View</div>
</div>
{/* Top Face */}
<div className="absolute inset-0 transform rotate-x-90 translate-z-32 md:translate-z-40 bg-gray-100 border border-white/20 flex items-center justify-center">
<Image
src="/assets/images/top-side.jfif"
alt="Top View"
fill
className="object-cover"
/>
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Top View</div>
</div>
{/* Bottom Face */}
<div className="absolute inset-0 transform -rotate-x-90 translate-z-32 md:translate-z-40 bg-gray-100 border border-white/20 flex items-center justify-center shadow-2xl">
<Image
src="/assets/images/bottom-side.jfif"
alt="Bottom View"
fill
className="object-cover"
/>
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Bottom View</div>
</div>
</div>
</div>
</div>
</div>
<style jsx global>{`
.perspective-container {
perspective: 1000px;
}
.transform-style-3d {
transform-style: preserve-3d;
transition: transform 0.1s ease-out; /* Smooth transition for mouse movement */
}
.translate-z-32 {
transform: rotateY(0deg) translateZ(8rem);
}
.translate-z-40 {
transform: rotateY(0deg) translateZ(10rem);
}
/* Custom transforms for faces */
.cube > div:nth-child(1) { transform: rotateY(0deg) translateZ(128px); }
.cube > div:nth-child(2) { transform: rotateY(180deg) translateZ(128px); }
.cube > div:nth-child(3) { transform: rotateY(90deg) translateZ(128px); }
.cube > div:nth-child(4) { transform: rotateY(-90deg) translateZ(128px); }
.cube > div:nth-child(5) { transform: rotateX(90deg) translateZ(128px); }
.cube > div:nth-child(6) { transform: rotateX(-90deg) translateZ(128px); }
@media (min-width: 768px) {
.cube > div:nth-child(1) { transform: rotateY(0deg) translateZ(160px); }
.cube > div:nth-child(2) { transform: rotateY(180deg) translateZ(160px); }
.cube > div:nth-child(3) { transform: rotateY(90deg) translateZ(160px); }
.cube > div:nth-child(4) { transform: rotateY(-90deg) translateZ(160px); }
.cube > div:nth-child(5) { transform: rotateX(90deg) translateZ(160px); }
.cube > div:nth-child(6) { transform: rotateX(-90deg) translateZ(160px); }
}
@keyframes spin-slow {
from { transform: rotateX(-10deg) rotateY(0deg); }
to { transform: rotateX(-10deg) rotateY(360deg); }
}
.animate-spin-slow {
animation: spin-slow 20s linear infinite;
}
`}</style>
</section>
);
}

View File

@ -0,0 +1,80 @@
"use client";
import { useCompare } from "@/context/CompareContext";
import Image from "next/image";
import Link from "next/link";
export default function CompareBar() {
const { compareList, removeFromCompare, clearCompare } = useCompare();
if (compareList.length === 0) return null;
return (
<div className="fixed bottom-0 left-0 right-0 z-40 bg-white dark:bg-gray-900 border-t-2 border-primary shadow-2xl">
<div className="max-w-7xl mx-auto px-6 py-4">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-4 flex-1 overflow-x-auto hide-scrollbar">
<div className="flex items-center gap-2 flex-shrink-0">
<svg className="w-6 h-6 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<span className="font-semibold text-foreground">Compare Properties ({compareList.length}/4)</span>
</div>
<div className="flex gap-3">
{compareList.map((property) => (
<div key={property.id} className="relative group flex-shrink-0">
<div className="w-20 h-20 rounded-lg overflow-hidden border-2 border-gray-200 dark:border-gray-700">
<Image
src={property.image}
alt={property.title}
width={80}
height={80}
className="object-cover w-full h-full"
/>
</div>
<button
onClick={() => removeFromCompare(property.id)}
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 text-white rounded-full flex items-center justify-center hover:bg-red-600 transition-colors shadow-md"
aria-label="Remove from comparison"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="absolute bottom-0 left-0 right-0 bg-black/70 text-white text-xs p-1 text-center truncate opacity-0 group-hover:opacity-100 transition-opacity">
{property.title}
</div>
</div>
))}
</div>
</div>
<div className="flex items-center gap-3 flex-shrink-0">
<button
onClick={clearCompare}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 transition-colors font-medium"
>
Clear All
</button>
<Link
href="/compare"
className={`px-6 py-3 rounded-lg font-semibold transition-all ${compareList.length >= 2
? "bg-primary text-white hover:bg-blue-700 shadow-lg hover:shadow-xl"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
}`}
onClick={(e) => {
if (compareList.length < 2) {
e.preventDefault();
alert("Please select at least 2 properties to compare");
}
}}
>
Compare Now
</Link>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,300 @@
"use client";
import { useCompare } from "@/context/CompareContext";
import { properties } from "@/data/properties";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import InnerBanner from "@/components/InnerBanner";
import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
export default function CompareClient() {
const { compareList, removeFromCompare, addToCompare } = useCompare();
const [showAddModal, setShowAddModal] = useState(false);
// Get properties not in compare list for adding
const availableProperties = properties.filter(
p => !compareList.find(c => c.id === p.id)
);
const handleAddProperty = (property: typeof properties[0]) => {
addToCompare(property);
setShowAddModal(false);
};
return (
<div className="min-h-screen bg-gray-50 dark:bg-black">
<Header />
<InnerBanner
title="Compare Properties"
subtitle="Compare up to 4 properties side by side to make an informed decision"
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "Properties", href: "/projects" },
{ label: "Compare" }
]}
/>
<div className="pb-12">
<div className="max-w-7xl mx-auto px-6 py-12">
{compareList.length < 2 ? (
<div className="text-center py-20">
<svg className="w-20 h-20 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
<h3 className="text-xl font-semibold text-gray-700 dark:text-gray-300 mb-2">
Select at least 2 properties to compare
</h3>
<p className="text-gray-500 dark:text-gray-400 mb-6">
Go to the properties page and click the compare icon on the properties you want to compare
</p>
<Link
href="/projects"
className="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-blue-700 transition-colors font-semibold"
>
Browse Properties
</Link>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-white dark:bg-gray-900 sticky top-20 z-10 shadow-md">
<th className="p-4 text-left font-semibold text-foreground border-b-2 border-gray-200 dark:border-gray-800 w-48">
Features
</th>
{compareList.map((property) => (
<th key={property.id} className="p-4 border-b-2 border-gray-200 dark:border-gray-800 min-w-[280px]">
<div className="relative">
<button
onClick={() => removeFromCompare(property.id)}
className="absolute -top-2 -right-2 w-8 h-8 bg-red-500 text-white rounded-full flex items-center justify-center hover:bg-red-600 transition-colors shadow-md z-10"
aria-label="Remove from comparison"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="relative h-40 rounded-xl overflow-hidden mb-3">
<Image
src={property.image}
alt={property.title}
fill
className="object-cover"
/>
</div>
<h3 className="font-bold text-foreground text-lg mb-1">{property.title}</h3>
<p className="text-sm text-gray-600 dark:text-gray-400">{property.location}</p>
</div>
</th>
))}
{/* Add Property Slot */}
{compareList.length < 4 && (
<th className="p-4 border-b-2 border-gray-200 dark:border-gray-800 min-w-[280px]">
<button
onClick={() => setShowAddModal(true)}
className="w-full h-40 border-2 border-dashed border-gray-300 dark:border-gray-700 rounded-xl flex items-center justify-center hover:border-primary hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all group"
>
<div className="text-center">
<svg className="w-12 h-12 mx-auto text-gray-400 group-hover:text-primary mb-2 transition-colors" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
<p className="text-sm text-gray-500 dark:text-gray-400 group-hover:text-primary font-medium transition-colors">Add Property</p>
</div>
</button>
</th>
)}
</tr>
</thead>
<tbody>
{/* Price */}
<tr className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Price</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center">
<span className="text-2xl font-bold text-primary">{property.price}</span>
</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Configuration */}
<tr className="bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Configuration</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.overview.bhk}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Area */}
<tr className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Area</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.overview.size}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Possession */}
<tr className="bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Possession</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.overview.possession}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Total Units */}
<tr className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Total Units</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.overview.totalUnits}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Category */}
<tr className="bg-gray-50 dark:bg-gray-900/50 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Property Type</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center text-foreground">{property.category}</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Status */}
<tr className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800">
<td className="p-4 font-semibold text-foreground">Status</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center">
<span className={`inline-block px-3 py-1 rounded-full text-sm font-semibold ${property.status === "Sold Out"
? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400"
: property.status === "New Launch"
? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"
}`}>
{property.status}
</span>
</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
{/* Amenities Header */}
<tr className="bg-gray-100 dark:bg-gray-800">
<td colSpan={compareList.length + (compareList.length < 4 ? 2 : 1)} className="p-4 font-bold text-foreground text-lg">
Amenities
</td>
</tr>
{/* Amenities - Get all unique amenities */}
{Array.from(new Set(compareList.flatMap(p => p.amenities))).map((amenity, idx) => (
<tr key={idx} className={idx % 2 === 0 ? "bg-white dark:bg-gray-900" : "bg-gray-50 dark:bg-gray-900/50"}>
<td className="p-4 text-foreground">{amenity}</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center">
{property.amenities.includes(amenity) ? (
<svg className="w-6 h-6 text-green-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : (
<svg className="w-6 h-6 text-red-500 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
)}
</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
))}
{/* View Details */}
<tr className="bg-white dark:bg-gray-900">
<td className="p-4 font-semibold text-foreground">Actions</td>
{compareList.map((property) => (
<td key={property.id} className="p-4 text-center">
<Link
href={`/properties/${property.id}`}
className="inline-block px-6 py-2 bg-primary text-white rounded-lg hover:bg-blue-700 transition-colors font-medium"
>
View Details
</Link>
</td>
))}
{compareList.length < 4 && <td className="p-4"></td>}
</tr>
</tbody>
</table>
</div>
)}
</div>
</div>
{/* Add Property Modal */}
{showAddModal && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/70 p-4">
<div className="bg-white dark:bg-gray-900 rounded-2xl max-w-4xl w-full max-h-[80vh] overflow-hidden shadow-2xl">
{/* Modal Header */}
<div className="flex items-center justify-between p-6 border-b border-gray-200 dark:border-gray-800">
<h2 className="text-2xl font-bold text-foreground">Add Property to Compare</h2>
<button
onClick={() => setShowAddModal(false)}
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-lg transition-colors"
>
<svg className="w-6 h-6 text-gray-600 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
{/* Modal Content */}
<div className="p-6 overflow-y-auto max-h-[calc(80vh-80px)]">
{availableProperties.length === 0 ? (
<div className="text-center py-12">
<p className="text-gray-600 dark:text-gray-400">All properties are already in comparison</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{availableProperties.map((property) => (
<button
key={property.id}
onClick={() => handleAddProperty(property)}
className="flex gap-4 p-4 border border-gray-200 dark:border-gray-800 rounded-xl hover:border-primary hover:bg-blue-50 dark:hover:bg-blue-900/20 transition-all text-left group"
>
<div className="relative w-24 h-24 flex-shrink-0 rounded-lg overflow-hidden">
<Image
src={property.image}
alt={property.title}
fill
className="object-cover"
/>
</div>
<div className="flex-1 min-w-0">
<h3 className="font-bold text-foreground mb-1 group-hover:text-primary transition-colors truncate">
{property.title}
</h3>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-2 truncate">
{property.location}
</p>
<div className="flex items-center justify-between">
<span className="text-lg font-bold text-primary">{property.price}</span>
<span className="text-sm text-gray-500 dark:text-gray-400">{property.overview.bhk}</span>
</div>
</div>
</button>
))}
</div>
)}
</div>
</div>
</div>
)}
<Footer />
</div>
);
}

197
src/components/FAQ.tsx Normal file
View File

@ -0,0 +1,197 @@
"use client";
import { useState } from "react";
import Image from "next/image";
const faqs = [
{
question: "What types of properties do you offer?",
answer: "We offer a wide range of properties including luxury apartments, premium villas, and sustainable eco-homes designed to blend with nature."
},
{
question: "How can I book a site visit?",
answer: "You can book a site visit by clicking the 'Book a Visit' button on our website or by contacting our sales team directly through the contact form."
},
{
question: "Do you provide assistance with home loans?",
answer: "Yes, we have tie-ups with leading banks and financial institutions to help you secure the best home loan rates and assist with the documentation process."
},
{
question: "Are your projects RERA registered?",
answer: "Absolutely. All our projects are fully compliant with RERA regulations and we ensure complete transparency in all our dealings."
},
{
question: "What amenities are included in your projects?",
answer: "Our projects feature world-class amenities such as swimming pools, clubhouses, landscaped gardens, 24/7 security, and dedicated fitness centers."
}
];
const carouselImages = [
{ src: "/1-f63fe3ad.png", label: "Building Exterior" },
{ src: "/hero-image.jpg", label: "Luxury Amenities" },
{ src: "/1-f63fe3ad.png", label: "Modern Architecture" }
];
export default function FAQ() {
const [openIndex, setOpenIndex] = useState<number | null>(null);
const [activeCard, setActiveCard] = useState(0);
const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
const [isHovering, setIsHovering] = useState(false);
const toggleFAQ = (index: number) => {
setOpenIndex(openIndex === index ? null : index);
};
const nextCard = () => {
setActiveCard((prev) => (prev + 1) % carouselImages.length);
};
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const centerX = rect.width / 2;
// Calculate offset: negative for left, positive for right
const offsetX = (x - centerX) / centerX; // Range: -1 to 1
setMousePosition({ x: offsetX * 30, y: 0 }); // Move up to 30px left or right
};
const handleMouseEnter = () => {
setIsHovering(true);
};
const handleMouseLeave = () => {
setIsHovering(false);
setMousePosition({ x: 0, y: 0 });
};
return (
<section className="py-24 bg-gray-50 dark:bg-gray-900 overflow-hidden">
<div className="max-w-7xl mx-auto px-6">
<div className="grid md:grid-cols-2 gap-12 items-start">
{/* Left Column: Stacked Card Carousel */}
<div
className="relative h-[600px] hidden md:flex items-center justify-center"
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<div className="relative w-full max-w-md h-[500px]">
{carouselImages.map((image, index) => {
const offset = index - activeCard;
const isActive = index === activeCard;
return (
<div
key={index}
onClick={nextCard}
className={`absolute inset-0 transition-all duration-500 ease-out cursor-pointer ${isActive ? 'z-30' : offset > 0 ? 'z-20' : 'z-10'
}`}
style={{
transform: `
translateY(${offset * 20}px)
translateX(${offset * 10 + (isActive && isHovering ? mousePosition.x : 0)}px)
scale(${isActive ? 1 : 0.9 - Math.abs(offset) * 0.05})
rotateZ(${offset * 2}deg)
`,
opacity: Math.abs(offset) > 1 ? 0 : 1 - Math.abs(offset) * 0.3,
pointerEvents: isActive ? 'auto' : 'none',
transition: isHovering ? 'transform 0.2s ease-out' : 'transform 0.5s ease-out'
}}
>
<div className="relative w-full h-full rounded-3xl overflow-hidden shadow-2xl border-4 border-white dark:border-gray-800">
<Image
src={image.src}
alt={image.label}
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/20 to-transparent" />
<div className="absolute bottom-6 left-6 right-6">
<h3 className="text-white text-2xl font-bold mb-2">{image.label}</h3>
<p className="text-white/80 text-sm">Click to explore more</p>
</div>
</div>
</div>
);
})}
</div>
{/* Navigation Dots */}
<div className="absolute bottom-8 left-1/2 -translate-x-1/2 flex gap-2 z-40">
{carouselImages.map((_, index) => (
<button
key={index}
onClick={() => setActiveCard(index)}
className={`w-2 h-2 rounded-full transition-all duration-300 ${index === activeCard
? 'bg-primary w-8'
: 'bg-gray-400 hover:bg-gray-600'
}`}
aria-label={`Go to slide ${index + 1}`}
/>
))}
</div>
</div>
{/* Right Column: FAQ Content */}
<div>
<div className="mb-12">
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
Frequently Asked Questions
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400">
Everything you need to know about our properties and services.
</p>
</div>
<div className="space-y-4">
{faqs.map((faq, index) => (
<div
key={index}
className="bg-white dark:bg-black border border-gray-200 dark:border-gray-800 rounded-2xl overflow-hidden transition-all duration-300 hover:shadow-md"
>
<button
onClick={() => toggleFAQ(index)}
className="w-full flex items-center justify-between p-6 text-left focus:outline-none"
>
<span className="text-lg font-semibold text-foreground">
{faq.question}
</span>
<span
className={`ml-6 flex-shrink-0 transition-transform duration-300 ${openIndex === index ? "rotate-180" : "rotate-0"
}`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={2}
stroke="currentColor"
className="w-5 h-5 text-primary"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
/>
</svg>
</span>
</button>
<div
className={`px-6 transition-all duration-300 ease-in-out overflow-hidden ${openIndex === index ? "max-h-48 pb-6 opacity-100" : "max-h-0 opacity-0"
}`}
>
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
{faq.answer}
</p>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</section>
);
}

View File

@ -2,13 +2,12 @@
import { useState, useEffect } from "react";
import Link from "next/link";
import Image from "next/image";
import { ThemeToggle } from "@/components/ThemeToggle";
import { properties } from "@/data/properties";
import Sidebar from "@/components/Sidebar";
export default function Header() {
const [isScrolled, setIsScrolled] = useState(false);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
useEffect(() => {
const handleScroll = () => {
@ -20,6 +19,7 @@ export default function Header() {
}, []);
return (
<>
<header
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isScrolled
? "bg-white/80 dark:bg-black/80 backdrop-blur-md shadow-sm py-4"
@ -36,80 +36,24 @@ export default function Header() {
className="object-contain p-1"
/>
</div>
<span className="text-2xl font-semibold tracking-tight text-foreground group-hover:text-primary transition-colors">
<span className={`text-2xl font-semibold tracking-tight group-hover:text-primary transition-colors ${isScrolled ? "text-foreground" : "text-white"
}`}>
Sky and Soil
</span>
</Link>
<nav className="hidden md:flex items-center space-x-8">
<Link href="#about" className="text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-foreground transition-colors">
About
</Link>
{/* Projects Mega Menu */}
<div className="group relative h-full flex items-center">
<Link href="#projects" className="text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-foreground transition-colors py-6">
Projects
</Link>
{/* Dropdown */}
<div className="absolute top-full left-1/2 -translate-x-1/2 w-[800px] bg-white/95 dark:bg-black/95 backdrop-blur-md border border-gray-100 dark:border-gray-800 rounded-2xl shadow-2xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300 transform translate-y-2 group-hover:translate-y-0 p-6 grid grid-cols-3 gap-6 z-50">
{properties.slice(0, 3).map((property) => (
<Link
key={property.id}
href={`/properties/${property.id}`}
className="group/card block"
>
<div className="relative h-32 w-full rounded-lg overflow-hidden mb-3">
<div className={`absolute inset-0 ${property.image.startsWith('/') ? '' : property.image}`} />
{property.image.startsWith('/') && (
<img src={property.image} alt={property.title} className="w-full h-full object-cover transition-transform duration-500 group-hover/card:scale-110" />
)}
<div className="absolute top-2 left-2 bg-white/90 dark:bg-black/80 backdrop-blur-sm px-2 py-0.5 rounded-full text-[10px] font-semibold text-foreground uppercase tracking-wider">
{property.status}
</div>
</div>
<h4 className="text-sm font-bold text-foreground mb-1 group-hover/card:text-primary transition-colors">
{property.title}
</h4>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
{property.location}
</p>
<p className="text-xs font-semibold text-accent">
{property.price}
</p>
</Link>
))}
<div className="col-span-3 pt-4 border-t border-gray-100 dark:border-gray-800 text-center">
<Link href="#projects" className="text-sm font-medium text-primary hover:underline">
View All Projects &rarr;
</Link>
</div>
</div>
</div>
<Link href="#lifestyle" className="text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-foreground transition-colors">
Lifestyle
</Link>
<Link href="#contact" className="text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-foreground transition-colors">
Contact
</Link>
<ThemeToggle />
</nav>
<div className="hidden md:flex items-center gap-4">
{/* Mobile menu button logic would go here if we were implementing full mobile menu */}
</div>
<div className="flex items-center gap-4">
<Link href="/contact">
<button className="hidden md:block px-5 py-2 text-sm font-medium text-white bg-primary rounded-full hover:bg-blue-600 transition-colors shadow-sm hover:shadow-md active:scale-95 transform duration-200">
Book a Visit
</button>
</Link>
<div className="md:hidden flex items-center gap-4">
<ThemeToggle />
<button className="text-foreground">
<button
className={`p-2 -mr-2 transition-colors ${isScrolled ? "text-foreground" : "text-white"
}`}
onClick={() => setIsSidebarOpen(true)}
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
@ -117,5 +61,8 @@ export default function Header() {
</div>
</div>
</header>
<Sidebar isOpen={isSidebarOpen} onClose={() => setIsSidebarOpen(false)} />
</>
);
}

View File

@ -1,23 +1,68 @@
import Link from "next/link";
import Image from "next/image";
export default function Hero() {
return (
<section className="relative h-screen flex flex-col items-center justify-center overflow-hidden">
{/* Background Image */}
{/* Video Background */}
<div className="absolute inset-0 z-0">
<Image
src="/hero-image.jpg"
alt="Sky and Soil Properties"
fill
className="object-cover"
priority
/>
<video
autoPlay
loop
muted
playsInline
className="absolute inset-0 w-full h-full object-cover"
poster="/assets/images/banglore-nit-view.mp4" // Fallback image
>
<source src="/assets/images/banglore-nit-view.mp4" type="video/mp4" />
{/* Note: This is a placeholder video. Replace with your Bangalore drone shot. */}
</video>
{/* Overlay */}
<div className="absolute inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-[2px]"></div>
<div className="absolute inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-[1px]"></div>
</div>
<div className="relative z-10 max-w-5xl mx-auto px-6 text-center">
{/* Location Pins (Decorative) */}
<div className="absolute inset-0 z-10 pointer-events-none hidden md:block">
{/* Pin 1: Hebbal */}
<div className="absolute top-[30%] left-[20%] animate-bounce" style={{ animationDuration: '3s' }}>
<div className="flex flex-col items-center">
<div className="bg-white/90 backdrop-blur-md px-3 py-1 rounded-lg shadow-lg mb-2">
<span className="text-xs font-bold text-black">Hebbal</span>
</div>
<div className="w-4 h-4 bg-primary rounded-full border-2 border-white shadow-lg relative">
<div className="absolute inset-0 bg-primary rounded-full animate-ping opacity-75"></div>
</div>
<div className="h-16 w-0.5 bg-gradient-to-b from-white/50 to-transparent"></div>
</div>
</div>
{/* Pin 2: Airport */}
<div className="absolute top-[20%] right-[25%] animate-bounce" style={{ animationDuration: '4s', animationDelay: '1s' }}>
<div className="flex flex-col items-center">
<div className="bg-white/90 backdrop-blur-md px-3 py-1 rounded-lg shadow-lg mb-2">
<span className="text-xs font-bold text-black">Airport</span>
</div>
<div className="w-4 h-4 bg-primary rounded-full border-2 border-white shadow-lg relative">
<div className="absolute inset-0 bg-primary rounded-full animate-ping opacity-75"></div>
</div>
<div className="h-24 w-0.5 bg-gradient-to-b from-white/50 to-transparent"></div>
</div>
</div>
{/* Pin 3: Whitefield */}
<div className="absolute bottom-[30%] right-[15%] animate-bounce" style={{ animationDuration: '3.5s', animationDelay: '0.5s' }}>
<div className="flex flex-col items-center">
<div className="bg-white/90 backdrop-blur-md px-3 py-1 rounded-lg shadow-lg mb-2">
<span className="text-xs font-bold text-black">Whitefield</span>
</div>
<div className="w-4 h-4 bg-primary rounded-full border-2 border-white shadow-lg relative">
<div className="absolute inset-0 bg-primary rounded-full animate-ping opacity-75"></div>
</div>
<div className="h-12 w-0.5 bg-gradient-to-b from-white/50 to-transparent"></div>
</div>
</div>
</div>
<div className="relative z-20 max-w-5xl mx-auto px-6 text-center">
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold tracking-tight text-white mb-6 animate-fade-in drop-shadow-lg">
Sky and Soil <br className="hidden md:block" />
<span className="text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-300">
@ -31,7 +76,7 @@ export default function Hero() {
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 animate-slide-up opacity-0" style={{ animationDelay: "0.4s" }}>
<Link
href="#projects"
href="/projects"
className="px-8 py-4 text-base font-medium text-primary bg-white rounded-full hover:bg-gray-100 transition-all shadow-lg hover:shadow-xl hover:-translate-y-0.5 active:scale-95"
>
Explore Projects
@ -39,13 +84,15 @@ export default function Hero() {
<button
className="px-8 py-4 text-base font-medium text-white border border-white/30 bg-white/10 backdrop-blur-sm rounded-full hover:bg-white/20 transition-all shadow-sm hover:shadow-md active:scale-95"
>
<Link href="/contact">
Book a Site Visit
</Link>
</button>
</div>
</div>
{/* Scroll Indicator */}
<div className="absolute bottom-10 left-1/2 transform -translate-x-1/2 animate-bounce z-10">
<div className="absolute bottom-10 left-1/2 transform -translate-x-1/2 animate-bounce z-20">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6 text-white/80">
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>

View File

@ -0,0 +1,121 @@
"use client";
import Image from "next/image";
interface InnerBannerProps {
title: string;
subtitle?: string;
breadcrumbs?: { label: string; href?: string }[];
backgroundImage?: string;
}
export default function InnerBanner({
title,
subtitle,
breadcrumbs,
backgroundImage = "/hero-image.jpg"
}: InnerBannerProps) {
return (
<div className="relative h-[450px] w-full overflow-hidden">
{/* Background Image */}
<div className="absolute inset-0">
<Image
src={backgroundImage}
alt={title}
fill
className="object-cover"
priority
/>
{/* Overlay */}
<div className="absolute inset-0 bg-gradient-to-r from-black/70 via-black/50 to-black/30" />
</div>
{/* Content */}
<div className="relative h-full max-w-7xl mx-auto px-6 flex flex-col justify-center">
{/* Breadcrumbs */}
{breadcrumbs && breadcrumbs.length > 0 && (
<nav className="mb-4">
<ol className="flex items-center gap-2 text-sm text-white/80">
{breadcrumbs.map((crumb, index) => (
<li key={index} className="flex items-center gap-2">
{crumb.href ? (
<a
href={crumb.href}
className="hover:text-white transition-colors"
>
{crumb.label}
</a>
) : (
<span className="text-white font-medium">{crumb.label}</span>
)}
{index < breadcrumbs.length - 1 && (
<svg className="w-4 h-4 text-white/60" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
)}
</li>
))}
</ol>
</nav>
)}
{/* Title */}
<h1 className="text-5xl md:text-6xl font-bold text-white mb-4 leading-tight">
{title}
</h1>
{/* Subtitle */}
{subtitle && (
<p className="text-xl text-white/90 max-w-2xl">
{subtitle}
</p>
)}
{/* Decorative Line */}
<div className="mt-6 w-24 h-1 bg-primary rounded-full" />
</div>
{/* Animated Particles */}
<div className="absolute inset-0 pointer-events-none">
{[
{ left: 20, top: 30, delay: 0, duration: 3.5 },
{ left: 60, top: 70, delay: 0.5, duration: 4 },
{ left: 80, top: 20, delay: 1, duration: 3.2 },
{ left: 40, top: 80, delay: 1.5, duration: 4.5 },
{ left: 10, top: 50, delay: 2, duration: 3.8 },
].map((particle, i) => (
<div
key={i}
className="absolute w-2 h-2 bg-white/20 rounded-full animate-float"
style={{
left: `${particle.left}%`,
top: `${particle.top}%`,
animationDelay: `${particle.delay}s`,
animationDuration: `${particle.duration}s`,
}}
/>
))}
</div>
<style jsx>{`
@keyframes float {
0%, 100% {
transform: translateY(0) translateX(0);
opacity: 0;
}
50% {
opacity: 1;
}
100% {
transform: translateY(-100px) translateX(50px);
opacity: 0;
}
}
.animate-float {
animation: float linear infinite;
}
`}</style>
</div>
);
}

View File

@ -1,15 +1,64 @@
"use client";
import { useState, useRef, MouseEvent } from "react";
import Image from "next/image";
import { FloatingHouse, RotatingKey } from "./PropertyAnimations";
export default function Lifestyle() {
const [zoomProps, setZoomProps] = useState({ x: 0, y: 0, show: false });
const imageRef = useRef<HTMLDivElement>(null);
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
if (!imageRef.current) return;
const { left, top, width, height } = imageRef.current.getBoundingClientRect();
const x = ((e.clientX - left) / width) * 100;
const y = ((e.clientY - top) / height) * 100;
setZoomProps({ x, y, show: true });
};
const handleMouseLeave = () => {
setZoomProps((prev) => ({ ...prev, show: false }));
};
return (
<section id="lifestyle" className="py-24 bg-secondary dark:bg-gray-900">
<div className="max-w-7xl mx-auto px-6">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
{/* Image Side */}
<div className="order-2 lg:order-1 relative h-[600px] rounded-3xl overflow-hidden shadow-2xl group">
{/* Placeholder for lifestyle image */}
<div className="absolute inset-0 bg-gradient-to-tr from-gray-800 to-gray-600 flex items-center justify-center text-white">
<span className="text-xl font-light tracking-widest uppercase">Clubhouse & Amenities</span>
<section id="lifestyle" className="py-24 bg-secondary dark:bg-gray-900 relative overflow-hidden">
{/* Decorative Animations */}
<div className="absolute top-20 right-10 opacity-30 hidden lg:block">
<FloatingHouse />
</div>
<div className="absolute bottom-20 left-10 opacity-20 hidden lg:block">
<RotatingKey />
</div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
{/* Image Side with Zoom Effect */}
<div
className="order-2 lg:order-1 relative h-[600px] rounded-3xl overflow-hidden shadow-2xl group cursor-crosshair"
ref={imageRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
>
<Image
src="/hero-image.jpg"
alt="Lifestyle"
fill
className={`object-cover transition-transform duration-200 ease-out ${zoomProps.show ? 'scale-[2.5]' : 'scale-100'}`}
style={{
transformOrigin: `${zoomProps.x}% ${zoomProps.y}%`
}}
/>
{/* Overlay Text - Hidden on Hover to see details */}
<div className={`absolute inset-0 bg-gradient-to-tr from-gray-800/60 to-transparent flex items-center justify-center pointer-events-none transition-opacity duration-300 ${zoomProps.show ? 'opacity-0' : 'opacity-100'}`}>
<span className="text-xl font-light tracking-widest uppercase text-white border border-white/30 px-6 py-2 rounded-full backdrop-blur-sm">
Clubhouse & Amenities
</span>
</div>
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/10 transition-colors duration-700" />
</div>
{/* Content Side */}

View File

@ -0,0 +1,176 @@
"use client";
import { useEffect, useState } from "react";
interface Particle {
id: number;
x: number;
y: number;
size: number;
color: string;
}
export default function MouseAnimation() {
const [particles, setParticles] = useState<Particle[]>([]);
const [mousePos, setMousePos] = useState({ x: 0, y: 0 });
const colors = [
"rgba(59, 130, 246, 0.8)", // Blue
"rgba(139, 92, 246, 0.8)", // Purple
"rgba(236, 72, 153, 0.8)", // Pink
"rgba(34, 197, 94, 0.8)", // Green
];
useEffect(() => {
let particleId = 0;
const handleMouseMove = (e: MouseEvent) => {
setMousePos({ x: e.clientX, y: e.clientY });
// Create particles more frequently
if (particleId % 2 === 0) {
const angle = Math.random() * Math.PI * 2;
const distance = Math.random() * 15;
const newParticle: Particle = {
id: particleId++,
x: e.clientX + Math.cos(angle) * distance,
y: e.clientY + Math.sin(angle) * distance,
size: Math.random() * 6 + 2,
color: colors[Math.floor(Math.random() * colors.length)],
};
setParticles((prev) => [...prev, newParticle].slice(-25));
}
particleId++;
};
window.addEventListener("mousemove", handleMouseMove);
return () => window.removeEventListener("mousemove", handleMouseMove);
}, []);
// Remove old particles
useEffect(() => {
const interval = setInterval(() => {
setParticles((prev) => prev.slice(1));
}, 100);
return () => clearInterval(interval);
}, []);
return (
<div className="pointer-events-none fixed inset-0 z-50 hidden md:block overflow-hidden">
{/* SVG for connecting lines */}
<svg className="absolute inset-0 w-full h-full">
<defs>
<linearGradient id="lineGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="rgba(59, 130, 246, 0.3)" />
<stop offset="100%" stopColor="rgba(139, 92, 246, 0.3)" />
</linearGradient>
</defs>
{/* Draw lines connecting recent particles */}
{particles.slice(-10).map((particle, index) => {
if (index === 0) return null;
const prevParticle = particles[particles.length - 10 + index - 1];
if (!prevParticle) return null;
return (
<line
key={`line-${particle.id}`}
x1={prevParticle.x}
y1={prevParticle.y}
x2={particle.x}
y2={particle.y}
stroke="url(#lineGradient)"
strokeWidth="1"
opacity={0.3 - (index * 0.03)}
/>
);
})}
</svg>
{/* Glowing cursor circle */}
<div
className="absolute w-8 h-8 rounded-full pointer-events-none transition-all duration-100"
style={{
left: mousePos.x,
top: mousePos.y,
transform: 'translate(-50%, -50%)',
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.4) 0%, transparent 70%)',
boxShadow: '0 0 20px rgba(59, 130, 246, 0.6)',
}}
/>
{/* Particles */}
{particles.map((particle, index) => (
<div
key={particle.id}
className="absolute rounded-full animate-particle-fade"
style={{
left: particle.x,
top: particle.y,
width: particle.size,
height: particle.size,
backgroundColor: particle.color,
boxShadow: `0 0 ${particle.size * 2}px ${particle.color}`,
animationDelay: `${index * 20}ms`,
transform: 'translate(-50%, -50%)',
}}
/>
))}
{/* Sparkle effects */}
{particles.slice(-5).map((particle, index) => (
<div
key={`sparkle-${particle.id}`}
className="absolute animate-sparkle"
style={{
left: particle.x,
top: particle.y,
animationDelay: `${index * 100}ms`,
}}
>
<svg width="12" height="12" viewBox="0 0 12 12" className="text-yellow-300">
<path
d="M6 0 L7 5 L12 6 L7 7 L6 12 L5 7 L0 6 L5 5 Z"
fill="currentColor"
opacity="0.6"
/>
</svg>
</div>
))}
<style jsx>{`
@keyframes particle-fade {
0% {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
100% {
opacity: 0;
transform: translate(-50%, -50%) scale(0.3);
}
}
@keyframes sparkle {
0%, 100% {
opacity: 0;
transform: translate(-50%, -50%) scale(0) rotate(0deg);
}
50% {
opacity: 1;
transform: translate(-50%, -50%) scale(1) rotate(180deg);
}
}
.animate-particle-fade {
animation: particle-fade 1s ease-out forwards;
}
.animate-sparkle {
animation: sparkle 0.8s ease-in-out forwards;
}
`}</style>
</div>
);
}

View File

@ -1,20 +1,33 @@
"use client";
import { useState } from "react";
import { useState, useRef } from "react";
import Link from "next/link";
import { properties, Property } from "@/data/properties";
import { properties } from "@/data/properties";
type Category = "Apartments" | "Premium Homes" | "Luxury";
export default function Properties() {
interface PropertiesProps {
layout?: "slider" | "grid";
}
export default function Properties({ layout = "slider" }: PropertiesProps) {
const [activeTab, setActiveTab] = useState<Category | "All">("All");
const scrollContainerRef = useRef<HTMLDivElement>(null);
const filteredProperties = activeTab === "All"
? properties
: properties.filter((property) => property.category === activeTab);
const scroll = (direction: "left" | "right") => {
if (scrollContainerRef.current) {
const container = scrollContainerRef.current;
const scrollAmount = direction === "left" ? -400 : 400;
container.scrollBy({ left: scrollAmount, behavior: "smooth" });
}
};
return (
<section id="projects" className="py-24 bg-white dark:bg-black">
<section id="projects" className="py-24 bg-white dark:bg-black overflow-hidden">
<div className="max-w-7xl mx-auto px-6">
<div className="text-center mb-12">
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
@ -43,7 +56,101 @@ export default function Properties() {
</div>
</div>
{/* Grid */}
{layout === "slider" ? (
/* Slider Layout - 3 cards for All, 1 card for specific tabs */
<div className="relative">
{/* Navigation Buttons */}
<button
onClick={() => scroll("left")}
className="absolute left-0 md:left-4 top-1/2 -translate-y-1/2 z-10 bg-white dark:bg-gray-800 p-3 rounded-full shadow-lg hover:scale-110 transition-all duration-300"
aria-label="Previous project"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5 text-foreground">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
</button>
<button
onClick={() => scroll("right")}
className="absolute right-0 md:right-4 top-1/2 -translate-y-1/2 z-10 bg-white dark:bg-gray-800 p-3 rounded-full shadow-lg hover:scale-110 transition-all duration-300"
aria-label="Next project"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5 text-foreground">
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
{/* Scrollable Area */}
<div
ref={scrollContainerRef}
className={`flex gap-6 overflow-x-auto pb-8 snap-x snap-mandatory hide-scrollbar ${activeTab === "All" ? "px-4" : "px-4 md:px-16"
}`}
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{filteredProperties.map((property, index) => {
// Show only first card for specific tabs
if (activeTab !== "All" && index > 0) return null;
return (
<div
key={property.id}
className={activeTab === "All"
? "min-w-[300px] md:min-w-[380px] snap-center"
: "flex-shrink-0 w-full max-w-md mx-auto snap-center"
}
>
<Link
href={`/properties/${property.id}`}
className="group/card block bg-white dark:bg-gray-900 rounded-2xl overflow-hidden border border-gray-100 dark:border-gray-800 hover:border-gray-200 dark:hover:border-gray-700 shadow-lg hover:shadow-2xl transition-all duration-300 h-full flex flex-col"
>
{/* Image */}
<div className={activeTab === "All" ? "h-64 w-full relative overflow-hidden" : "h-80 w-full relative overflow-hidden"}>
<div className={`absolute inset-0 ${property.image.startsWith('/') ? '' : property.image}`} />
{property.image.startsWith('/') && (
<img src={property.image} alt={property.title} className="w-full h-full object-cover transition-transform duration-500 group-hover/card:scale-110" />
)}
<div className="absolute top-4 left-4 bg-white/90 dark:bg-black/80 backdrop-blur-sm px-3 py-1 rounded-full text-xs font-semibold text-foreground uppercase tracking-wider">
{property.status}
</div>
<div className="absolute inset-0 bg-black/0 group-hover/card:bg-black/5 transition-colors duration-300" />
</div>
{/* Content */}
<div className={activeTab === "All" ? "p-6 flex flex-col flex-grow" : "p-8 flex flex-col flex-grow"}>
<div className={activeTab === "All" ? "flex items-center justify-between mb-2" : "flex items-center justify-between mb-3"}>
<span className={activeTab === "All"
? "text-xs font-medium text-primary bg-blue-50 dark:bg-blue-900/30 px-2 py-1 rounded-md"
: "text-sm font-medium text-primary bg-blue-50 dark:bg-blue-900/30 px-3 py-1.5 rounded-md"
}>
{property.location}
</span>
</div>
<h3 className={activeTab === "All" ? "text-xl font-bold text-foreground mb-2" : "text-2xl font-bold text-foreground mb-3"}>
{property.title}
</h3>
<p className={activeTab === "All"
? "text-gray-600 dark:text-gray-400 text-sm mb-6 flex-grow line-clamp-2"
: "text-gray-600 dark:text-gray-400 text-base mb-6 flex-grow line-clamp-3"
}>
{property.description}
</p>
<div className={activeTab === "All"
? "w-full py-3 text-center text-sm font-medium text-foreground border border-gray-200 dark:border-gray-700 rounded-xl group-hover/card:bg-foreground group-hover/card:text-white dark:group-hover/card:bg-white dark:group-hover/card:text-black transition-colors"
: "w-full py-3 text-center text-base font-medium text-foreground border-2 border-gray-200 dark:border-gray-700 rounded-xl group-hover/card:bg-foreground group-hover/card:text-white dark:group-hover/card:bg-white dark:group-hover/card:text-black transition-colors"
}>
View Details
</div>
</div>
</Link>
</div>
);
})}
</div>
</div>
) : (
/* Grid Layout */
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
{filteredProperties.map((property) => (
<Link
@ -53,7 +160,6 @@ export default function Properties() {
>
{/* Image Placeholder */}
<div className={`h-64 w-full relative overflow-hidden`}>
{/* Using a colored div as placeholder if image fails or while loading, but ideally using Next/Image */}
<div className={`absolute inset-0 ${property.image.startsWith('/') ? '' : property.image}`} />
{property.image.startsWith('/') && (
<img src={property.image} alt={property.title} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
@ -85,6 +191,7 @@ export default function Properties() {
</Link>
))}
</div>
)}
</div>
</section>
);

View File

@ -0,0 +1,103 @@
"use client";
import { useState } from "react";
import Header from "@/components/Header";
import Footer from "@/components/Footer";
import InnerBanner from "@/components/InnerBanner";
import PropertyFilters, { FilterState } from "@/components/PropertyFilters";
import PropertyCard from "@/components/PropertyCard";
import { properties } from "@/data/properties";
export default function PropertiesClient() {
const [filteredProperties, setFilteredProperties] = useState(properties);
const handleFilterChange = (filters: FilterState) => {
let filtered = [...properties];
// Search filter
if (filters.search) {
filtered = filtered.filter(p =>
p.title.toLowerCase().includes(filters.search.toLowerCase()) ||
p.location.toLowerCase().includes(filters.search.toLowerCase())
);
}
// Type filter
if (filters.type !== "all") {
filtered = filtered.filter(p => p.category === filters.type);
}
// BHK filter
if (filters.bhk !== "all") {
filtered = filtered.filter(p => p.overview.bhk.includes(filters.bhk));
}
// Budget filter
if (filters.budgetMin || filters.budgetMax) {
filtered = filtered.filter(p => {
const priceStr = p.price.replace(/[^0-9.]/g, "");
const price = parseFloat(priceStr);
const min = filters.budgetMin ? parseFloat(filters.budgetMin) : 0;
const max = filters.budgetMax ? parseFloat(filters.budgetMax) : Infinity;
return price >= min && price <= max;
});
}
// Sort
if (filters.sortBy === "price-low") {
filtered.sort((a, b) => {
const priceA = parseFloat(a.price.replace(/[^0-9.]/g, ""));
const priceB = parseFloat(b.price.replace(/[^0-9.]/g, ""));
return priceA - priceB;
});
} else if (filters.sortBy === "price-high") {
filtered.sort((a, b) => {
const priceA = parseFloat(a.price.replace(/[^0-9.]/g, ""));
const priceB = parseFloat(b.price.replace(/[^0-9.]/g, ""));
return priceB - priceA;
});
}
setFilteredProperties(filtered);
};
return (
<div className="min-h-screen bg-gray-50 dark:bg-black">
<Header />
<InnerBanner
title="Our Properties"
subtitle="Discover your dream home from our exclusive collection of premium properties"
breadcrumbs={[
{ label: "Home", href: "/" },
{ label: "Properties" }
]}
/>
<div>
<PropertyFilters onFilterChange={handleFilterChange} />
<div className="max-w-7xl mx-auto px-6 py-12">
{/* Property Grid */}
{filteredProperties.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredProperties.map((property) => (
<PropertyCard key={property.id} property={property} />
))}
</div>
) : (
<div className="text-center py-20">
<svg className="w-20 h-20 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
<h3 className="text-xl font-semibold text-gray-700 dark:text-gray-300 mb-2">No properties found</h3>
<p className="text-gray-500 dark:text-gray-400">Try adjusting your filters to see more results</p>
</div>
)}
</div>
</div>
<Footer />
</div >
);
}

View File

@ -0,0 +1,50 @@
"use client";
export function FloatingHouse({ className = "" }: { className?: string }) {
return (
<div className={`animate-float ${className}`}>
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-primary/20">
<path strokeLinecap="round" strokeLinejoin="round" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" />
</svg>
</div>
);
}
export function RotatingKey({ className = "" }: { className?: string }) {
return (
<div className={`animate-pulse ${className}`}>
<svg width="50" height="50" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" className="text-primary/20 animate-[spin_4s_linear_infinite]">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
</div>
);
}
export function GrowingBuilding({ className = "" }: { className?: string }) {
return (
<div className={`flex items-end gap-1 ${className}`}>
<div className="w-3 bg-primary/20 rounded-t-sm animate-[height-grow_2s_ease-in-out_infinite]" style={{ height: '20px', animationDelay: '0s' }} />
<div className="w-3 bg-primary/20 rounded-t-sm animate-[height-grow_2s_ease-in-out_infinite]" style={{ height: '35px', animationDelay: '0.2s' }} />
<div className="w-3 bg-primary/20 rounded-t-sm animate-[height-grow_2s_ease-in-out_infinite]" style={{ height: '50px', animationDelay: '0.4s' }} />
<div className="w-3 bg-primary/20 rounded-t-sm animate-[height-grow_2s_ease-in-out_infinite]" style={{ height: '30px', animationDelay: '0.6s' }} />
<style jsx>{`
@keyframes height-grow {
0%, 100% { transform: scaleY(1); }
50% { transform: scaleY(1.5); }
}
`}</style>
</div>
);
}
export function BlueprintGrid({ className = "" }: { className?: string }) {
return (
<div className={`absolute inset-0 pointer-events-none opacity-[0.03] ${className}`}
style={{
backgroundImage: `linear-gradient(#000 1px, transparent 1px), linear-gradient(90deg, #000 1px, transparent 1px)`,
backgroundSize: '40px 40px'
}}
/>
);
}

View File

@ -0,0 +1,161 @@
"use client";
import Link from "next/link";
import Image from "next/image";
import { Property } from "@/data/properties";
import { useCompare } from "@/context/CompareContext";
import { useState, useEffect } from "react";
interface PropertyCardProps {
property: Property;
}
export default function PropertyCard({ property }: PropertyCardProps) {
const { addToCompare, removeFromCompare, isInCompare } = useCompare();
const inCompare = isInCompare(property.id);
const [isWishlisted, setIsWishlisted] = useState(false);
// Load wishlist from localStorage
useEffect(() => {
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
setIsWishlisted(wishlist.includes(property.id));
}, [property.id]);
const handleCompareClick = (e: React.MouseEvent) => {
e.preventDefault();
if (inCompare) {
removeFromCompare(property.id);
} else {
addToCompare(property);
}
};
const handleShareClick = (e: React.MouseEvent) => {
e.preventDefault();
const url = `${window.location.origin}/properties/${property.id}`;
if (navigator.share) {
navigator.share({
title: property.title,
text: `Check out ${property.title} in ${property.location}`,
url: url,
}).catch(() => {
// Fallback to clipboard
navigator.clipboard.writeText(url);
alert("Link copied to clipboard!");
});
} else {
// Fallback to clipboard
navigator.clipboard.writeText(url);
alert("Link copied to clipboard!");
}
};
const handleWishlistClick = (e: React.MouseEvent) => {
e.preventDefault();
const wishlist = JSON.parse(localStorage.getItem("wishlist") || "[]");
if (isWishlisted) {
// Remove from wishlist
const updated = wishlist.filter((id: number) => id !== property.id);
localStorage.setItem("wishlist", JSON.stringify(updated));
setIsWishlisted(false);
} else {
// Add to wishlist
wishlist.push(property.id);
localStorage.setItem("wishlist", JSON.stringify(wishlist));
setIsWishlisted(true);
}
};
return (
<Link href={`/properties/${property.id}`}>
<div className="group bg-white dark:bg-gray-900 rounded-2xl overflow-hidden shadow-md hover:shadow-2xl transition-all duration-300 border border-gray-200 dark:border-gray-800">
{/* Image Section */}
<div className="relative h-64 overflow-hidden">
<Image
src={property.image}
alt={property.title}
fill
className="object-cover group-hover:scale-110 transition-transform duration-500"
/>
{/* Status Badge */}
{property.status && (
<div className={`absolute top-4 left-4 px-3 py-1 rounded-full text-xs font-semibold ${property.status === "Sold Out"
? "bg-red-500 text-white"
: property.status === "New Launch"
? "bg-green-500 text-white"
: "bg-white/90 text-gray-900"
}`}>
{property.status}
</div>
)}
{/* Action Buttons */}
<div className="absolute top-4 right-4 flex gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<button
onClick={handleCompareClick}
className={`p-2 rounded-full shadow-lg transition-all ${inCompare
? "bg-primary text-white scale-110"
: "bg-white hover:bg-gray-100 text-gray-700"
}`}
aria-label={inCompare ? "Remove from compare" : "Add to compare"}
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
</button>
<button
onClick={handleShareClick}
className="p-2 bg-white rounded-full shadow-lg hover:bg-gray-100 transition-colors"
aria-label="Share"
>
<svg className="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
</svg>
</button>
<button
onClick={handleWishlistClick}
className={`p-2 rounded-full shadow-lg transition-all ${isWishlisted
? "bg-red-500 text-white scale-110"
: "bg-white hover:bg-gray-100 text-gray-700"
}`}
aria-label={isWishlisted ? "Remove from wishlist" : "Add to wishlist"}
>
<svg className="w-5 h-5" fill={isWishlisted ? "currentColor" : "none"} stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
</button>
</div>
</div>
{/* Content Section */}
<div className="p-6">
<h3 className="text-xl font-bold text-foreground mb-2 group-hover:text-primary transition-colors">
{property.title}
</h3>
<div className="flex items-center text-gray-600 dark:text-gray-400 text-sm mb-4">
<svg className="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
{property.location}
</div>
<div className="flex items-center justify-between pt-4 border-t border-gray-200 dark:border-gray-800">
<div>
<div className="text-2xl font-bold text-primary">{property.price}</div>
<div className="text-sm text-gray-500 dark:text-gray-400">{property.overview.size}</div>
</div>
<div className="text-right">
<div className="text-sm font-semibold text-foreground">{property.overview.bhk}</div>
<div className="text-xs text-gray-500 dark:text-gray-400">Possession: {property.overview.possession}</div>
</div>
</div>
</div>
</div>
</Link>
);
}

View File

@ -0,0 +1,186 @@
"use client";
import { useState } from "react";
interface PropertyFiltersProps {
onFilterChange: (filters: FilterState) => void;
}
export interface FilterState {
search: string;
type: string;
budgetMin: string;
budgetMax: string;
bhk: string;
sortBy: string;
}
export default function PropertyFilters({ onFilterChange }: PropertyFiltersProps) {
const [filters, setFilters] = useState<FilterState>({
search: "",
type: "all",
budgetMin: "",
budgetMax: "",
bhk: "all",
sortBy: "popularity"
});
const [showFilters, setShowFilters] = useState(false);
const handleFilterUpdate = (key: keyof FilterState, value: string) => {
const newFilters = { ...filters, [key]: value };
setFilters(newFilters);
onFilterChange(newFilters);
};
const clearFilters = () => {
const defaultFilters: FilterState = {
search: "",
type: "all",
budgetMin: "",
budgetMax: "",
bhk: "all",
sortBy: "popularity"
};
setFilters(defaultFilters);
onFilterChange(defaultFilters);
};
const hasActiveFilters = filters.search || filters.type !== "all" || filters.budgetMin || filters.budgetMax || filters.bhk !== "all" || filters.sortBy !== "popularity";
return (
<div className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-800 sticky top-0 z-40">
<div className="max-w-7xl mx-auto px-6 py-4">
{/* Main Filter Bar */}
<div className="flex flex-col md:flex-row gap-4 items-center">
{/* Search */}
<div className="flex-1 w-full">
<div className="relative">
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<input
type="text"
placeholder="Try Godrej or Whitefield"
value={filters.search}
onChange={(e) => handleFilterUpdate("search", e.target.value)}
className="w-full pl-10 pr-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground"
/>
</div>
</div>
{/* Type Dropdown */}
<select
value={filters.type}
onChange={(e) => handleFilterUpdate("type", e.target.value)}
className="px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary bg-white dark:bg-gray-800 text-foreground cursor-pointer"
>
<option value="all">All Types</option>
<option value="Apartments">Apartments</option>
<option value="Premium Homes">Premium Homes</option>
<option value="Luxury">Luxury</option>
<option value="Villas">Villas</option>
<option value="Plots">Plots</option>
</select>
{/* Budget Dropdown */}
<button
onClick={() => setShowFilters(!showFilters)}
className="px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center gap-2 bg-white dark:bg-gray-900 text-foreground"
>
<span>Budget</span>
{filters.budgetMin || filters.budgetMax ? (
<span className="bg-primary text-white text-xs px-2 py-0.5 rounded-full">1</span>
) : null}
</button>
{/* More Filters */}
<button
onClick={() => setShowFilters(!showFilters)}
className="px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800 flex items-center gap-2 bg-white dark:bg-gray-900 text-foreground"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
</svg>
<span>Filters</span>
</button>
{/* Sort By */}
<select
value={filters.sortBy}
onChange={(e) => handleFilterUpdate("sortBy", e.target.value)}
className="px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary bg-white dark:bg-gray-800 text-foreground cursor-pointer"
>
<option value="popularity">Sort By: Popularity</option>
<option value="price-low">Price: Low to High</option>
<option value="price-high">Price: High to Low</option>
<option value="newest">Newest First</option>
</select>
{/* Clear Filters Button */}
{hasActiveFilters && (
<button
onClick={clearFilters}
className="px-4 py-3 border border-red-300 dark:border-red-700 text-red-600 dark:text-red-400 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors whitespace-nowrap font-medium flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
Clear
</button>
)}
{/* Help Me Decide CTA */}
<button className="px-6 py-3 bg-primary text-white rounded-lg hover:bg-blue-700 transition-colors whitespace-nowrap font-medium">
Help Me Decide
</button>
</div>
{/* Expanded Filters */}
{showFilters && (
<div className="mt-4 p-4 border border-gray-200 dark:border-gray-800 rounded-lg bg-gray-50 dark:bg-gray-800">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Min Budget (Cr)</label>
<input
type="number"
step="0.1"
placeholder="1.0"
value={filters.budgetMin}
onChange={(e) => handleFilterUpdate("budgetMin", e.target.value)}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-foreground"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Max Budget (Cr)</label>
<input
type="number"
step="0.1"
placeholder="5.0"
value={filters.budgetMax}
onChange={(e) => handleFilterUpdate("budgetMax", e.target.value)}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-foreground"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">BHK</label>
<select
value={filters.bhk}
onChange={(e) => handleFilterUpdate("bhk", e.target.value)}
className="w-full px-4 py-2 border border-gray-300 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-900 text-foreground"
>
<option value="all">All BHK</option>
<option value="1">1 BHK</option>
<option value="2">2 BHK</option>
<option value="3">3 BHK</option>
<option value="4">4 BHK</option>
<option value="4+">4+ BHK</option>
</select>
</div>
</div>
</div>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,151 @@
"use client";
import { useState } from "react";
import Image from "next/image";
interface PropertyGalleryProps {
images: string[];
title: string;
}
export default function PropertyGallery({ images, title }: PropertyGalleryProps) {
const [activeImage, setActiveImage] = useState(0);
const [showLightbox, setShowLightbox] = useState(false);
const [lightboxIndex, setLightboxIndex] = useState(0);
const openLightbox = (index: number) => {
setLightboxIndex(index);
setShowLightbox(true);
document.body.style.overflow = 'hidden';
};
const closeLightbox = () => {
setShowLightbox(false);
document.body.style.overflow = 'unset';
};
const nextImage = () => {
setLightboxIndex((prev) => (prev + 1) % images.length);
};
const prevImage = () => {
setLightboxIndex((prev) => (prev - 1 + images.length) % images.length);
};
return (
<>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div className="md:col-span-2 relative h-[500px] rounded-2xl overflow-hidden group cursor-pointer" onClick={() => openLightbox(activeImage)}>
<Image
src={images[activeImage]}
alt={title}
fill
className="object-cover transition-transform duration-300 group-hover:scale-105"
/>
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-300" />
<div className="absolute bottom-4 right-4 bg-black/70 text-white px-3 py-1.5 rounded-lg text-sm opacity-0 group-hover:opacity-100 transition-opacity">
Click to enlarge
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-1 gap-4">
{images.slice(0, 2).map((img, idx) => (
<div
key={idx}
onClick={() => setActiveImage(idx)}
className={`relative h-[160px] rounded-xl overflow-hidden cursor-pointer transition-all duration-300 ${activeImage === idx ? 'ring-4 ring-primary scale-105' : 'hover:scale-105'
}`}
>
<Image src={img} alt={`View ${idx + 1}`} fill className="object-cover" />
</div>
))}
{/* View All Photos Button */}
<div
onClick={() => openLightbox(0)}
className="relative h-[160px] rounded-xl overflow-hidden cursor-pointer group"
>
<Image
src={images[2] || images[0]}
alt="View all photos"
fill
className="object-cover"
/>
<div className="absolute inset-0 bg-black/60 group-hover:bg-black/70 transition-colors flex flex-col items-center justify-center text-white">
<svg className="w-10 h-10 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<span className="font-semibold text-lg">View All Photos</span>
<span className="text-sm opacity-90">({images.length} images)</span>
</div>
</div>
</div>
</div>
{/* Lightbox Modal */}
{showLightbox && (
<div className="fixed inset-0 z-50 bg-black/95 flex items-center justify-center">
{/* Close Button */}
<button
onClick={closeLightbox}
className="absolute top-4 right-4 text-white hover:text-gray-300 transition-colors z-10"
aria-label="Close gallery"
>
<svg className="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/* Image Counter */}
<div className="absolute top-4 left-1/2 -translate-x-1/2 text-white text-lg font-medium bg-black/50 px-4 py-2 rounded-full">
{lightboxIndex + 1} / {images.length}
</div>
{/* Previous Button */}
<button
onClick={prevImage}
className="absolute left-4 text-white hover:text-gray-300 transition-colors p-2 bg-black/50 rounded-full hover:bg-black/70"
aria-label="Previous image"
>
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
{/* Main Image */}
<div className="relative w-full h-full max-w-6xl max-h-[90vh] mx-4">
<Image
src={images[lightboxIndex]}
alt={`${title} - Image ${lightboxIndex + 1}`}
fill
className="object-contain"
/>
</div>
{/* Next Button */}
<button
onClick={nextImage}
className="absolute right-4 text-white hover:text-gray-300 transition-colors p-2 bg-black/50 rounded-full hover:bg-black/70"
aria-label="Next image"
>
<svg className="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
{/* Thumbnail Strip */}
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex gap-2 overflow-x-auto max-w-[90vw] px-4 py-2 bg-black/50 rounded-lg hide-scrollbar">
{images.map((img, idx) => (
<div
key={idx}
onClick={() => setLightboxIndex(idx)}
className={`relative w-20 h-20 flex-shrink-0 rounded-lg overflow-hidden cursor-pointer transition-all ${lightboxIndex === idx ? 'ring-4 ring-white scale-110' : 'opacity-60 hover:opacity-100'
}`}
>
<Image src={img} alt={`Thumbnail ${idx + 1}`} fill className="object-cover" />
</div>
))}
</div>
</div>
)}
</>
);
}

View File

@ -0,0 +1,66 @@
"use client";
import { useState, useEffect } from "react";
interface PropertyNavProps {
sections: { id: string; label: string }[];
}
export default function PropertyNav({ sections }: PropertyNavProps) {
const [activeSection, setActiveSection] = useState(sections[0]?.id || "");
useEffect(() => {
const handleScroll = () => {
const scrollPosition = window.scrollY + 200;
for (const section of sections) {
const element = document.getElementById(section.id);
if (element) {
const { offsetTop, offsetHeight } = element;
if (scrollPosition >= offsetTop && scrollPosition < offsetTop + offsetHeight) {
setActiveSection(section.id);
break;
}
}
}
};
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [sections]);
const scrollToSection = (id: string) => {
const element = document.getElementById(id);
if (element) {
const offset = 150; // Account for sticky header
const elementPosition = element.getBoundingClientRect().top;
const offsetPosition = elementPosition + window.pageYOffset - offset;
window.scrollTo({
top: offsetPosition,
behavior: "smooth"
});
}
};
return (
<div className="sticky top-20 z-30 bg-white/95 dark:bg-gray-900/95 backdrop-blur-md border-b border-gray-200 dark:border-gray-800 shadow-sm">
<div className="max-w-7xl mx-auto px-6">
<nav className="flex gap-1 overflow-x-auto hide-scrollbar py-3">
{sections.map((section) => (
<button
key={section.id}
onClick={() => scrollToSection(section.id)}
className={`px-6 py-2 rounded-lg font-medium whitespace-nowrap transition-all duration-200 ${activeSection === section.id
? "bg-primary text-white shadow-md"
: "text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800"
}`}
>
{section.label}
</button>
))}
</nav>
</div>
</div>
);
}

111
src/components/Sidebar.tsx Normal file
View File

@ -0,0 +1,111 @@
"use client";
import Link from "next/link";
import { useEffect } from "react";
import { ThemeToggle } from "@/components/ThemeToggle";
interface SidebarProps {
isOpen: boolean;
onClose: () => void;
}
export default function Sidebar({ isOpen, onClose }: SidebarProps) {
// Prevent scrolling when sidebar is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "unset";
}
return () => {
document.body.style.overflow = "unset";
};
}, [isOpen]);
return (
<>
{/* Backdrop */}
<div
className={`fixed inset-0 z-[60] bg-black/50 backdrop-blur-sm transition-opacity duration-300 ${isOpen ? "opacity-100 visible" : "opacity-0 invisible"
}`}
onClick={onClose}
/>
{/* Sidebar */}
<div
className={`fixed top-0 right-0 bottom-0 z-[70] w-[300px] bg-white dark:bg-black border-l border-gray-200 dark:border-gray-800 shadow-2xl transform transition-transform duration-300 ease-in-out ${isOpen ? "translate-x-0" : "translate-x-full"
}`}
>
<div className="flex flex-col h-full p-6">
<div className="flex items-center justify-between mb-8">
<h2 className="text-xl font-bold text-foreground">Menu</h2>
<button
onClick={onClose}
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-6 h-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
<nav className="flex flex-col space-y-6">
<Link
href="/about"
className="text-lg font-medium text-gray-600 dark:text-gray-300 hover:text-primary transition-colors"
onClick={onClose}
>
About
</Link>
<Link
href="/projects"
className="text-lg font-medium text-gray-600 dark:text-gray-300 hover:text-primary transition-colors"
onClick={onClose}
>
Projects
</Link>
<Link
href="/lifestyle"
className="text-lg font-medium text-gray-600 dark:text-gray-300 hover:text-primary transition-colors"
onClick={onClose}
>
Lifestyle
</Link>
<Link
href="/contact"
className="text-lg font-medium text-gray-600 dark:text-gray-300 hover:text-primary transition-colors"
onClick={onClose}
>
Contact
</Link>
</nav>
<div className="mt-auto space-y-6">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
Theme
</span>
<ThemeToggle />
</div>
<Link href="/contact" onClick={onClose}>
<button className="w-full py-3 text-base font-medium text-white bg-primary rounded-xl hover:bg-blue-600 transition-colors shadow-lg hover:shadow-xl active:scale-95 transform duration-200">
Book a Visit
</button>
</Link>
</div>
</div>
</div>
</>
);
}

View File

@ -1,51 +1,171 @@
"use client";
import { useRef } from "react";
import { GrowingBuilding } from "./PropertyAnimations";
export default function Testimonials() {
const scrollContainerRef = useRef<HTMLDivElement>(null);
const testimonials = [
{
quote: "Aurora Springs transformed our idea of a dream home into reality. The attention to detail is unmatched.",
author: "Rajesh Kumar",
location: "Owner at Aurora Heights",
rating: 5,
date: "2 months ago"
},
{
quote: "The transparency and professionalism shown by the team made the entire buying process seamless.",
author: "Priya Sharma",
location: "Owner at Serene Meadows",
rating: 5,
date: "3 months ago"
},
{
quote: "Living here feels like a permanent vacation. The amenities and the community are world-class.",
author: "David Miller",
location: "Resident at The Grandeur",
rating: 5,
date: "1 month ago"
},
{
quote: "Exceptional service from start to finish. The team went above and beyond to ensure we found our perfect home.",
author: "Anita Desai",
location: "Owner at Green Valley",
rating: 5,
date: "4 months ago"
},
{
quote: "The quality of construction and the beautiful surroundings make this the best investment we've ever made.",
author: "Michael Chen",
location: "Resident at Skyline Towers",
rating: 5,
date: "5 months ago"
}
];
const scroll = (direction: "left" | "right") => {
if (scrollContainerRef.current) {
const scrollAmount = direction === "left" ? -400 : 400;
scrollContainerRef.current.scrollBy({ left: scrollAmount, behavior: "smooth" });
}
};
const renderStars = (rating: number) => {
return (
<section className="py-24 bg-white dark:bg-black">
<div className="max-w-7xl mx-auto px-6">
<div className="flex gap-1">
{[...Array(5)].map((_, i) => (
<svg
key={i}
className={`w-5 h-5 ${i < rating ? 'text-yellow-400' : 'text-gray-300'}`}
fill="currentColor"
viewBox="0 0 20 20"
>
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
</svg>
))}
</div>
);
};
const getInitials = (name: string) => {
return name.split(' ').map(n => n[0]).join('').toUpperCase();
};
return (
<section className="py-24 bg-white dark:bg-black relative overflow-hidden">
{/* Decorative Animations */}
<div className="absolute top-1/2 left-10 -translate-y-1/2 opacity-20 hidden lg:block">
<GrowingBuilding />
</div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
Stories of Satisfaction
</h2>
<div className="flex items-center justify-center gap-2 mt-4">
<div className="flex">
{renderStars(5)}
</div>
<span className="text-lg font-semibold text-foreground">5.0</span>
<span className="text-gray-500"> Based on {testimonials.length} reviews</span>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
{/* Slider Container */}
<div className="relative group">
{/* Navigation Buttons */}
<button
onClick={() => scroll("left")}
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-4 z-10 bg-white dark:bg-gray-800 p-3 rounded-full shadow-lg opacity-0 group-hover:opacity-100 transition-all duration-300 hover:scale-110 hidden md:block border border-gray-200 dark:border-gray-700"
aria-label="Previous reviews"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5 text-foreground">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
</svg>
</button>
<button
onClick={() => scroll("right")}
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-4 z-10 bg-white dark:bg-gray-800 p-3 rounded-full shadow-lg opacity-0 group-hover:opacity-100 transition-all duration-300 hover:scale-110 hidden md:block border border-gray-200 dark:border-gray-700"
aria-label="Next reviews"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5 text-foreground">
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
{/* Scrollable Reviews */}
<div
ref={scrollContainerRef}
className="flex gap-6 overflow-x-auto pb-8 snap-x snap-mandatory hide-scrollbar"
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{testimonials.map((item, index) => (
<div
key={index}
className="bg-secondary dark:bg-gray-900 p-8 rounded-2xl relative hover:-translate-y-1 transition-transform duration-300"
className="min-w-[350px] md:min-w-[400px] snap-center"
>
<div className="absolute top-6 left-6 text-4xl text-primary opacity-20 font-serif">
&ldquo;
<div className="bg-white dark:bg-gray-900 p-6 rounded-2xl border border-gray-200 dark:border-gray-800 hover:shadow-xl transition-all duration-300 h-full flex flex-col">
{/* Header with Avatar and Info */}
<div className="flex items-start gap-4 mb-4">
<div className="w-12 h-12 rounded-full bg-primary text-white flex items-center justify-center font-bold text-lg flex-shrink-0">
{getInitials(item.author)}
</div>
<p className="text-lg text-gray-700 dark:text-gray-300 italic mb-6 relative z-10 pt-4">
{item.quote}
</p>
<div>
<div className="flex-1">
<h4 className="font-semibold text-foreground">{item.author}</h4>
<p className="text-sm text-gray-500 dark:text-gray-400">{item.location}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{item.date}</p>
</div>
</div>
{/* Star Rating */}
<div className="mb-4">
{renderStars(item.rating)}
</div>
{/* Review Text */}
<p className="text-gray-700 dark:text-gray-300 leading-relaxed mb-4 flex-grow">
"{item.quote}"
</p>
{/* Location Badge */}
<div className="pt-4 border-t border-gray-100 dark:border-gray-800">
<p className="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-2">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
{item.location}
</p>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</section>
);
}

View File

@ -1,4 +1,12 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { FloatingHouse, RotatingKey, GrowingBuilding } from "./PropertyAnimations";
export default function WhyChooseUs() {
const [visibleCards, setVisibleCards] = useState<boolean[]>([false, false, false, false]);
const sectionRef = useRef<HTMLDivElement>(null);
const features = [
{
title: "Prime Locations",
@ -39,14 +47,58 @@ export default function WhyChooseUs() {
},
];
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// Trigger cards to appear one by one with delay
features.forEach((_, index) => {
setTimeout(() => {
setVisibleCards((prev) => {
const newVisible = [...prev];
newVisible[index] = true;
return newVisible;
});
}, index * 150); // 150ms delay between each card
});
}
});
},
{ threshold: 0.2 }
);
if (sectionRef.current) {
observer.observe(sectionRef.current);
}
return () => {
if (sectionRef.current) {
observer.unobserve(sectionRef.current);
}
};
}, []);
return (
<section className="py-24 bg-secondary dark:bg-gray-900/50">
<div className="max-w-7xl mx-auto px-6">
<section ref={sectionRef} className="py-24 bg-secondary dark:bg-gray-900/50 relative overflow-hidden">
{/* Decorative Animations */}
<div className="absolute top-20 left-10 opacity-50 hidden lg:block">
<FloatingHouse />
</div>
<div className="absolute top-1/2 right-10 -translate-y-1/2 opacity-40 hidden lg:block">
<RotatingKey />
</div>
<div className="absolute bottom-10 left-1/4 opacity-30 hidden lg:block">
<GrowingBuilding />
</div>
<div className="max-w-7xl mx-auto px-6 relative z-10">
<div className="text-center mb-16">
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4 animate-fade-in">
Why Sky and Soil?
</h2>
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto animate-slide-up">
We bridge the gap between your dreams and reality with premium properties and unmatched service.
</p>
</div>
@ -55,9 +107,15 @@ export default function WhyChooseUs() {
{features.map((feature, index) => (
<div
key={index}
className="bg-white dark:bg-gray-800 p-8 rounded-2xl shadow-sm hover:shadow-md transition-shadow duration-300 flex flex-col items-center text-center group"
className={`bg-white dark:bg-gray-800 p-8 rounded-2xl shadow-sm hover:shadow-xl transition-all duration-500 flex flex-col items-center text-center group transform ${visibleCards[index]
? 'translate-y-0 opacity-100'
: 'translate-y-10 opacity-0'
}`}
style={{
transitionDelay: `${index * 100}ms`
}}
>
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900/30 text-primary rounded-full group-hover:scale-110 transition-transform duration-300">
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900/30 text-primary rounded-full group-hover:scale-110 group-hover:rotate-6 transition-all duration-300">
{feature.icon}
</div>
<h3 className="text-xl font-semibold text-foreground mb-3">

View File

@ -0,0 +1,71 @@
"use client";
import React, { createContext, useContext, useState, useEffect } from "react";
import { Property } from "@/data/properties";
interface CompareContextType {
compareList: Property[];
addToCompare: (property: Property) => void;
removeFromCompare: (propertyId: number) => void;
clearCompare: () => void;
isInCompare: (propertyId: number) => boolean;
}
const CompareContext = createContext<CompareContextType | undefined>(undefined);
export function CompareProvider({ children }: { children: React.ReactNode }) {
const [compareList, setCompareList] = useState<Property[]>([]);
// Load from localStorage on mount
useEffect(() => {
const saved = localStorage.getItem("compareProperties");
if (saved) {
try {
setCompareList(JSON.parse(saved));
} catch (e) {
console.error("Failed to load compare list:", e);
}
}
}, []);
// Save to localStorage whenever compareList changes
useEffect(() => {
localStorage.setItem("compareProperties", JSON.stringify(compareList));
}, [compareList]);
const addToCompare = (property: Property) => {
if (compareList.length >= 4) {
alert("You can compare up to 4 properties at a time");
return;
}
if (!compareList.find(p => p.id === property.id)) {
setCompareList([...compareList, property]);
}
};
const removeFromCompare = (propertyId: number) => {
setCompareList(compareList.filter(p => p.id !== propertyId));
};
const clearCompare = () => {
setCompareList([]);
};
const isInCompare = (propertyId: number) => {
return compareList.some(p => p.id === propertyId);
};
return (
<CompareContext.Provider value={{ compareList, addToCompare, removeFromCompare, clearCompare, isInCompare }}>
{children}
</CompareContext.Provider>
);
}
export function useCompare() {
const context = useContext(CompareContext);
if (context === undefined) {
throw new Error("useCompare must be used within a CompareProvider");
}
return context;
}