feat: Introduce core UI components, pages, and assets for the real estate application.
This commit is contained in:
parent
c5b07e0e67
commit
ded22acd9b
BIN
public/assets/images/back-side.jfif
Normal file
BIN
public/assets/images/back-side.jfif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 278 KiB |
BIN
public/assets/images/banglore-nit-view.mp4
Normal file
BIN
public/assets/images/banglore-nit-view.mp4
Normal file
Binary file not shown.
BIN
public/assets/images/bottom-side.jfif
Normal file
BIN
public/assets/images/bottom-side.jfif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 240 KiB |
BIN
public/assets/images/front-side.jfif
Normal file
BIN
public/assets/images/front-side.jfif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 316 KiB |
BIN
public/assets/images/left-side.jfif
Normal file
BIN
public/assets/images/left-side.jfif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 305 KiB |
BIN
public/assets/images/right-side.jfif
Normal file
BIN
public/assets/images/right-side.jfif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 292 KiB |
BIN
public/assets/images/top-side.jfif
Normal file
BIN
public/assets/images/top-side.jfif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 375 KiB |
34
src/app/about/page.tsx
Normal file
34
src/app/about/page.tsx
Normal 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
11
src/app/compare/page.tsx
Normal 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
30
src/app/contact/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,11 +8,18 @@ const inter = Inter({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Aurora Springs Realty | Redefining Modern Living",
|
title: {
|
||||||
description: "Premium villas, plots, and apartments in North Bengaluru. Experience luxury living with Aurora Springs Realty.",
|
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 { ThemeProvider } from "@/components/ThemeProvider";
|
||||||
|
import { CompareProvider } from "@/context/CompareContext";
|
||||||
|
import CompareBar from "@/components/CompareBar";
|
||||||
|
import MouseAnimation from "@/components/MouseAnimation";
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
@ -30,7 +37,11 @@ export default function RootLayout({
|
|||||||
enableSystem
|
enableSystem
|
||||||
disableTransitionOnChange
|
disableTransitionOnChange
|
||||||
>
|
>
|
||||||
{children}
|
<CompareProvider>
|
||||||
|
{children}
|
||||||
|
<CompareBar />
|
||||||
|
<MouseAnimation />
|
||||||
|
</CompareProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
30
src/app/lifestyle/page.tsx
Normal file
30
src/app/lifestyle/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -8,6 +8,14 @@ import Testimonials from "@/components/Testimonials";
|
|||||||
import ContactCTA from "@/components/ContactCTA";
|
import ContactCTA from "@/components/ContactCTA";
|
||||||
import Footer from "@/components/Footer";
|
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() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-white">
|
<main className="min-h-screen bg-white">
|
||||||
@ -15,9 +23,10 @@ export default function Home() {
|
|||||||
<Hero />
|
<Hero />
|
||||||
<About />
|
<About />
|
||||||
<WhyChooseUs />
|
<WhyChooseUs />
|
||||||
<Properties />
|
<Properties layout="slider" />
|
||||||
<Lifestyle />
|
<Lifestyle />
|
||||||
<Testimonials />
|
<Testimonials />
|
||||||
|
<FAQ />
|
||||||
<ContactCTA />
|
<ContactCTA />
|
||||||
<Footer />
|
<Footer />
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
11
src/app/projects/page.tsx
Normal file
11
src/app/projects/page.tsx
Normal 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 />;
|
||||||
|
}
|
||||||
@ -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 { properties } from "@/data/properties";
|
||||||
import PropertyHero from "@/components/property/PropertyHero";
|
import { notFound } from "next/navigation";
|
||||||
import PropertyOverview from "@/components/property/PropertyOverview";
|
|
||||||
import PropertyAmenities from "@/components/property/PropertyAmenities";
|
|
||||||
import PropertyContact from "@/components/property/PropertyContact";
|
|
||||||
|
|
||||||
// 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() {
|
export function generateStaticParams() {
|
||||||
return properties.map((property) => ({
|
return properties.map((property) => ({
|
||||||
id: property.id.toString(),
|
id: property.id.toString(),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function PropertyPage({ params }: { params: Promise<{ id: string }> }) {
|
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
||||||
const { id } = await params;
|
const resolvedParams = await params;
|
||||||
const property = properties.find((p) => p.id.toString() === id);
|
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) {
|
if (!property) {
|
||||||
notFound();
|
notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen bg-background pb-24">
|
<div className="min-h-screen bg-gray-50 dark:bg-black">
|
||||||
<PropertyHero property={property} />
|
<Header />
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto px-6 mt-12">
|
<InnerBanner
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
title={property.title}
|
||||||
{/* Main Content */}
|
subtitle={property.location}
|
||||||
<div className="lg:col-span-2">
|
breadcrumbs={[
|
||||||
<PropertyOverview
|
{ label: "Home", href: "/" },
|
||||||
overview={property.overview}
|
{ label: "Properties", href: "/projects" },
|
||||||
description={property.description}
|
{ label: property.title }
|
||||||
/>
|
]}
|
||||||
<PropertyAmenities amenities={property.amenities} />
|
backgroundImage={property.image}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Sidebar */}
|
{/* Image Gallery */}
|
||||||
<div className="lg:col-span-1">
|
<PropertyGallery images={property.images} title={property.title} />
|
||||||
<PropertyContact />
|
|
||||||
|
<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">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
|
||||||
|
<Footer />
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,63 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useRef, MouseEvent } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
|
import { FloatingHouse, RotatingKey, GrowingBuilding } from "./PropertyAnimations";
|
||||||
|
|
||||||
export default function About() {
|
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 (
|
return (
|
||||||
<section id="about" className="py-24 bg-white dark:bg-black">
|
<section id="about" className="py-24 bg-white dark:bg-black relative overflow-hidden">
|
||||||
<div className="max-w-7xl mx-auto px-6">
|
|
||||||
|
{/* 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">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
|
||||||
{/* Text Content */}
|
{/* Text Content */}
|
||||||
<div className="space-y-8">
|
<div className="space-y-8 z-10">
|
||||||
<h2 className="text-4xl font-bold tracking-tight text-foreground">
|
<h2 className="text-4xl font-bold tracking-tight text-foreground">
|
||||||
Where the Sky Meets <br />
|
Where the Sky Meets <br />
|
||||||
<span className="text-accent dark:text-accent">The Soil.</span>
|
<span className="text-accent dark:text-accent">The Soil.</span>
|
||||||
@ -29,16 +80,128 @@ export default function About() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image Content */}
|
{/* 3D Cube Container */}
|
||||||
<div className="relative h-[500px] rounded-3xl overflow-hidden shadow-2xl bg-gray-100 dark:bg-gray-800">
|
<div
|
||||||
{/* Placeholder for an actual image. Using a colored div for now if no image is available,
|
className="h-[500px] w-full flex items-center justify-center perspective-container cursor-move"
|
||||||
but ideally this would be a real image. I'll use a gradient placeholder. */}
|
ref={containerRef}
|
||||||
<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">
|
onMouseMove={handleMouseMove}
|
||||||
<span className="text-sm">Modern Architecture Image</span>
|
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>
|
||||||
</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>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
80
src/components/CompareBar.tsx
Normal file
80
src/components/CompareBar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
300
src/components/CompareClient.tsx
Normal file
300
src/components/CompareClient.tsx
Normal 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
197
src/components/FAQ.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -2,13 +2,12 @@
|
|||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
import Sidebar from "@/components/Sidebar";
|
||||||
import { properties } from "@/data/properties";
|
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleScroll = () => {
|
const handleScroll = () => {
|
||||||
@ -20,102 +19,50 @@ export default function Header() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<>
|
||||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isScrolled
|
<header
|
||||||
? "bg-white/80 dark:bg-black/80 backdrop-blur-md shadow-sm py-4"
|
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isScrolled
|
||||||
: "bg-transparent py-6"
|
? "bg-white/80 dark:bg-black/80 backdrop-blur-md shadow-sm py-4"
|
||||||
}`}
|
: "bg-transparent py-6"
|
||||||
>
|
}`}
|
||||||
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between">
|
>
|
||||||
<Link href="/" className="flex items-center gap-3 group">
|
<div className="max-w-7xl mx-auto px-6 flex items-center justify-between">
|
||||||
<div className="relative w-10 h-10 overflow-hidden rounded-full bg-white/10 backdrop-blur-sm border border-white/20 shadow-sm group-hover:scale-105 transition-transform">
|
<Link href="/" className="flex items-center gap-3 group">
|
||||||
<Image
|
<div className="relative w-10 h-10 overflow-hidden rounded-full bg-white/10 backdrop-blur-sm border border-white/20 shadow-sm group-hover:scale-105 transition-transform">
|
||||||
src="/logo.png"
|
<Image
|
||||||
alt="Sky and Soil Logo"
|
src="/logo.png"
|
||||||
fill
|
alt="Sky and Soil Logo"
|
||||||
className="object-contain p-1"
|
fill
|
||||||
/>
|
className="object-contain p-1"
|
||||||
</div>
|
/>
|
||||||
<span className="text-2xl font-semibold tracking-tight text-foreground group-hover:text-primary transition-colors">
|
</div>
|
||||||
Sky and Soil
|
<span className={`text-2xl font-semibold tracking-tight group-hover:text-primary transition-colors ${isScrolled ? "text-foreground" : "text-white"
|
||||||
</span>
|
}`}>
|
||||||
</Link>
|
Sky and Soil
|
||||||
|
</span>
|
||||||
<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>
|
</Link>
|
||||||
|
|
||||||
{/* Projects Mega Menu */}
|
<div className="flex items-center gap-4">
|
||||||
<div className="group relative h-full flex items-center">
|
<Link href="/contact">
|
||||||
<Link href="#projects" className="text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-foreground transition-colors py-6">
|
<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">
|
||||||
Projects
|
Book a Visit
|
||||||
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{/* Dropdown */}
|
<button
|
||||||
<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">
|
className={`p-2 -mr-2 transition-colors ${isScrolled ? "text-foreground" : "text-white"
|
||||||
{properties.slice(0, 3).map((property) => (
|
}`}
|
||||||
<Link
|
onClick={() => setIsSidebarOpen(true)}
|
||||||
key={property.id}
|
>
|
||||||
href={`/properties/${property.id}`}
|
<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">
|
||||||
className="group/card block"
|
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||||
>
|
</svg>
|
||||||
<div className="relative h-32 w-full rounded-lg overflow-hidden mb-3">
|
</button>
|
||||||
<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 →
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</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>
|
||||||
|
</header>
|
||||||
|
|
||||||
<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">
|
<Sidebar isOpen={isSidebarOpen} onClose={() => setIsSidebarOpen(false)} />
|
||||||
Book a Visit
|
</>
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="md:hidden flex items-center gap-4">
|
|
||||||
<ThemeToggle />
|
|
||||||
<button className="text-foreground">
|
|
||||||
<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>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,68 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
|
||||||
|
|
||||||
export default function Hero() {
|
export default function Hero() {
|
||||||
return (
|
return (
|
||||||
<section className="relative h-screen flex flex-col items-center justify-center overflow-hidden">
|
<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">
|
<div className="absolute inset-0 z-0">
|
||||||
<Image
|
<video
|
||||||
src="/hero-image.jpg"
|
autoPlay
|
||||||
alt="Sky and Soil Properties"
|
loop
|
||||||
fill
|
muted
|
||||||
className="object-cover"
|
playsInline
|
||||||
priority
|
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 */}
|
{/* 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>
|
||||||
|
|
||||||
<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">
|
<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" />
|
Sky and Soil <br className="hidden md:block" />
|
||||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-300">
|
<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" }}>
|
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 animate-slide-up opacity-0" style={{ animationDelay: "0.4s" }}>
|
||||||
<Link
|
<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"
|
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
|
Explore Projects
|
||||||
@ -39,13 +84,15 @@ export default function Hero() {
|
|||||||
<button
|
<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"
|
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"
|
||||||
>
|
>
|
||||||
Book a Site Visit
|
<Link href="/contact">
|
||||||
|
Book a Site Visit
|
||||||
|
</Link>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scroll Indicator */}
|
{/* 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">
|
<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" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
121
src/components/InnerBanner.tsx
Normal file
121
src/components/InnerBanner.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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() {
|
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 (
|
return (
|
||||||
<section id="lifestyle" className="py-24 bg-secondary dark:bg-gray-900">
|
<section id="lifestyle" className="py-24 bg-secondary dark:bg-gray-900 relative overflow-hidden">
|
||||||
<div className="max-w-7xl mx-auto px-6">
|
|
||||||
|
{/* 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">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||||
{/* Image Side */}
|
{/* Image Side with Zoom Effect */}
|
||||||
<div className="order-2 lg:order-1 relative h-[600px] rounded-3xl overflow-hidden shadow-2xl group">
|
<div
|
||||||
{/* Placeholder for lifestyle image */}
|
className="order-2 lg:order-1 relative h-[600px] rounded-3xl overflow-hidden shadow-2xl group cursor-crosshair"
|
||||||
<div className="absolute inset-0 bg-gradient-to-tr from-gray-800 to-gray-600 flex items-center justify-center text-white">
|
ref={imageRef}
|
||||||
<span className="text-xl font-light tracking-widest uppercase">Clubhouse & Amenities</span>
|
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>
|
||||||
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/10 transition-colors duration-700" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Side */}
|
{/* Content Side */}
|
||||||
|
|||||||
176
src/components/MouseAnimation.tsx
Normal file
176
src/components/MouseAnimation.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,20 +1,33 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useRef } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { properties, Property } from "@/data/properties";
|
import { properties } from "@/data/properties";
|
||||||
|
|
||||||
type Category = "Apartments" | "Premium Homes" | "Luxury";
|
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 [activeTab, setActiveTab] = useState<Category | "All">("All");
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const filteredProperties = activeTab === "All"
|
const filteredProperties = activeTab === "All"
|
||||||
? properties
|
? properties
|
||||||
: properties.filter((property) => property.category === activeTab);
|
: 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 (
|
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="max-w-7xl mx-auto px-6">
|
||||||
<div className="text-center mb-12">
|
<div className="text-center mb-12">
|
||||||
<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">
|
||||||
@ -43,48 +56,142 @@ export default function Properties() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Grid */}
|
{layout === "slider" ? (
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
/* Slider Layout - 3 cards for All, 1 card for specific tabs */
|
||||||
{filteredProperties.map((property) => (
|
<div className="relative">
|
||||||
<Link
|
{/* Navigation Buttons */}
|
||||||
href={`/properties/${property.id}`}
|
<button
|
||||||
key={property.id}
|
onClick={() => scroll("left")}
|
||||||
className="group 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-sm hover:shadow-xl transition-all duration-300 flex flex-col"
|
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"
|
||||||
>
|
>
|
||||||
{/* Image Placeholder */}
|
<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">
|
||||||
<div className={`h-64 w-full relative overflow-hidden`}>
|
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||||
{/* Using a colored div as placeholder if image fails or while loading, but ideally using Next/Image */}
|
</svg>
|
||||||
<div className={`absolute inset-0 ${property.image.startsWith('/') ? '' : property.image}`} />
|
</button>
|
||||||
{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" />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<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">
|
<button
|
||||||
{property.status}
|
onClick={() => scroll("right")}
|
||||||
</div>
|
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"
|
||||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/5 transition-colors duration-300" />
|
aria-label="Next project"
|
||||||
</div>
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
<div className="p-6 flex flex-col flex-grow">
|
{/* Scrollable Area */}
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div
|
||||||
<span className="text-xs font-medium text-primary bg-blue-50 dark:bg-blue-900/30 px-2 py-1 rounded-md">
|
ref={scrollContainerRef}
|
||||||
{property.location}
|
className={`flex gap-6 overflow-x-auto pb-8 snap-x snap-mandatory hide-scrollbar ${activeTab === "All" ? "px-4" : "px-4 md:px-16"
|
||||||
</span>
|
}`}
|
||||||
</div>
|
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||||
<h3 className="text-xl font-bold text-foreground mb-2">
|
>
|
||||||
{property.title}
|
{filteredProperties.map((property, index) => {
|
||||||
</h3>
|
// Show only first card for specific tabs
|
||||||
<p className="text-gray-600 dark:text-gray-400 text-sm mb-6 flex-grow line-clamp-2">
|
if (activeTab !== "All" && index > 0) return null;
|
||||||
{property.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="w-full py-3 text-center text-sm font-medium text-foreground border border-gray-200 dark:border-gray-700 rounded-xl group-hover:bg-foreground group-hover:text-white dark:group-hover:bg-white dark:group-hover:text-black transition-colors">
|
return (
|
||||||
View Details
|
<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
|
||||||
|
href={`/properties/${property.id}`}
|
||||||
|
key={property.id}
|
||||||
|
className="group 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-sm hover:shadow-xl transition-all duration-300 flex flex-col"
|
||||||
|
>
|
||||||
|
{/* Image Placeholder */}
|
||||||
|
<div className={`h-64 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: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:bg-black/5 transition-colors duration-300" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Link>
|
<div className="p-6 flex flex-col flex-grow">
|
||||||
))}
|
<div className="flex items-center justify-between mb-2">
|
||||||
</div>
|
<span className="text-xs font-medium text-primary bg-blue-50 dark:bg-blue-900/30 px-2 py-1 rounded-md">
|
||||||
|
{property.location}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-xl font-bold text-foreground mb-2">
|
||||||
|
{property.title}
|
||||||
|
</h3>
|
||||||
|
<p className="text-gray-600 dark:text-gray-400 text-sm mb-6 flex-grow line-clamp-2">
|
||||||
|
{property.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="w-full py-3 text-center text-sm font-medium text-foreground border border-gray-200 dark:border-gray-700 rounded-xl group-hover:bg-foreground group-hover:text-white dark:group-hover:bg-white dark:group-hover:text-black transition-colors">
|
||||||
|
View Details
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
103
src/components/PropertiesClient.tsx
Normal file
103
src/components/PropertiesClient.tsx
Normal 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 >
|
||||||
|
);
|
||||||
|
}
|
||||||
50
src/components/PropertyAnimations.tsx
Normal file
50
src/components/PropertyAnimations.tsx
Normal 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'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
161
src/components/PropertyCard.tsx
Normal file
161
src/components/PropertyCard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
186
src/components/PropertyFilters.tsx
Normal file
186
src/components/PropertyFilters.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
151
src/components/PropertyGallery.tsx
Normal file
151
src/components/PropertyGallery.tsx
Normal 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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
66
src/components/PropertyNav.tsx
Normal file
66
src/components/PropertyNav.tsx
Normal 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
111
src/components/Sidebar.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,49 +1,169 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { GrowingBuilding } from "./PropertyAnimations";
|
||||||
|
|
||||||
export default function Testimonials() {
|
export default function Testimonials() {
|
||||||
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const testimonials = [
|
const testimonials = [
|
||||||
{
|
{
|
||||||
quote: "Aurora Springs transformed our idea of a dream home into reality. The attention to detail is unmatched.",
|
quote: "Aurora Springs transformed our idea of a dream home into reality. The attention to detail is unmatched.",
|
||||||
author: "Rajesh Kumar",
|
author: "Rajesh Kumar",
|
||||||
location: "Owner at Aurora Heights",
|
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.",
|
quote: "The transparency and professionalism shown by the team made the entire buying process seamless.",
|
||||||
author: "Priya Sharma",
|
author: "Priya Sharma",
|
||||||
location: "Owner at Serene Meadows",
|
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.",
|
quote: "Living here feels like a permanent vacation. The amenities and the community are world-class.",
|
||||||
author: "David Miller",
|
author: "David Miller",
|
||||||
location: "Resident at The Grandeur",
|
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 (
|
||||||
|
<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 (
|
return (
|
||||||
<section className="py-24 bg-white dark:bg-black">
|
<section className="py-24 bg-white dark:bg-black relative overflow-hidden">
|
||||||
<div className="max-w-7xl mx-auto px-6">
|
|
||||||
|
{/* 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">
|
<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">
|
||||||
Stories of Satisfaction
|
Stories of Satisfaction
|
||||||
</h2>
|
</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>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
{/* Slider Container */}
|
||||||
{testimonials.map((item, index) => (
|
<div className="relative group">
|
||||||
<div
|
{/* Navigation Buttons */}
|
||||||
key={index}
|
<button
|
||||||
className="bg-secondary dark:bg-gray-900 p-8 rounded-2xl relative hover:-translate-y-1 transition-transform duration-300"
|
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"
|
||||||
<div className="absolute top-6 left-6 text-4xl text-primary opacity-20 font-serif">
|
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="min-w-[350px] md:min-w-[400px] snap-center"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
<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.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>
|
||||||
<p className="text-lg text-gray-700 dark:text-gray-300 italic mb-6 relative z-10 pt-4">
|
))}
|
||||||
{item.quote}
|
</div>
|
||||||
</p>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-semibold text-foreground">{item.author}</h4>
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">{item.location}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -1,4 +1,12 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useRef } from "react";
|
||||||
|
import { FloatingHouse, RotatingKey, GrowingBuilding } from "./PropertyAnimations";
|
||||||
|
|
||||||
export default function WhyChooseUs() {
|
export default function WhyChooseUs() {
|
||||||
|
const [visibleCards, setVisibleCards] = useState<boolean[]>([false, false, false, false]);
|
||||||
|
const sectionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
title: "Prime Locations",
|
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 (
|
return (
|
||||||
<section className="py-24 bg-secondary dark:bg-gray-900/50">
|
<section ref={sectionRef} className="py-24 bg-secondary dark:bg-gray-900/50 relative overflow-hidden">
|
||||||
<div className="max-w-7xl mx-auto px-6">
|
|
||||||
|
{/* 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">
|
<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?
|
Why Sky and Soil?
|
||||||
</h2>
|
</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.
|
We bridge the gap between your dreams and reality with premium properties and unmatched service.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -55,9 +107,15 @@ export default function WhyChooseUs() {
|
|||||||
{features.map((feature, index) => (
|
{features.map((feature, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
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}
|
{feature.icon}
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-xl font-semibold text-foreground mb-3">
|
<h3 className="text-xl font-semibold text-foreground mb-3">
|
||||||
|
|||||||
71
src/context/CompareContext.tsx
Normal file
71
src/context/CompareContext.tsx
Normal 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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user