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 = {
|
||||
title: "Aurora Springs Realty | Redefining Modern Living",
|
||||
description: "Premium villas, plots, and apartments in North Bengaluru. Experience luxury living with Aurora Springs Realty.",
|
||||
title: {
|
||||
default: "Sky and Soil | Premium Real Estate in North Bengaluru",
|
||||
template: "%s | Sky and Soil"
|
||||
},
|
||||
description: "Discover luxury apartments, villas, and plots in North Bengaluru. Sky and Soil connects you with nature-inspired living spaces and Godrej Properties.",
|
||||
keywords: ["Real Estate", "Bengaluru", "Luxury Homes", "Godrej Properties", "North Bengaluru", "Villas", "Apartments"],
|
||||
};
|
||||
|
||||
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||
import { CompareProvider } from "@/context/CompareContext";
|
||||
import CompareBar from "@/components/CompareBar";
|
||||
import MouseAnimation from "@/components/MouseAnimation";
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
@ -30,7 +37,11 @@ export default function RootLayout({
|
||||
enableSystem
|
||||
disableTransitionOnChange
|
||||
>
|
||||
<CompareProvider>
|
||||
{children}
|
||||
<CompareBar />
|
||||
<MouseAnimation />
|
||||
</CompareProvider>
|
||||
</ThemeProvider>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
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 Footer from "@/components/Footer";
|
||||
|
||||
import FAQ from "@/components/FAQ";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Sky and Soil | Luxury Homes & Premium Real Estate",
|
||||
description: "Find your dream home with Sky and Soil. We offer exclusive listings of apartments, villas, and plots in Bangalore's prime locations.",
|
||||
};
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main className="min-h-screen bg-white">
|
||||
@ -15,9 +23,10 @@ export default function Home() {
|
||||
<Hero />
|
||||
<About />
|
||||
<WhyChooseUs />
|
||||
<Properties />
|
||||
<Properties layout="slider" />
|
||||
<Lifestyle />
|
||||
<Testimonials />
|
||||
<FAQ />
|
||||
<ContactCTA />
|
||||
<Footer />
|
||||
</main>
|
||||
|
||||
11
src/app/projects/page.tsx
Normal file
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 PropertyHero from "@/components/property/PropertyHero";
|
||||
import PropertyOverview from "@/components/property/PropertyOverview";
|
||||
import PropertyAmenities from "@/components/property/PropertyAmenities";
|
||||
import PropertyContact from "@/components/property/PropertyContact";
|
||||
import { notFound } from "next/navigation";
|
||||
|
||||
// This is required for static site generation with dynamic routes
|
||||
import { Metadata } from "next";
|
||||
|
||||
// Required for static site generation with dynamic routes
|
||||
export function generateStaticParams() {
|
||||
return properties.map((property) => ({
|
||||
id: property.id.toString(),
|
||||
}));
|
||||
}
|
||||
|
||||
export default async function PropertyPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const { id } = await params;
|
||||
const property = properties.find((p) => p.id.toString() === id);
|
||||
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
||||
const resolvedParams = await params;
|
||||
const property = properties.find(p => p.id === parseInt(resolvedParams.id));
|
||||
|
||||
if (!property) {
|
||||
return {
|
||||
title: "Property Not Found | Sky and Soil",
|
||||
description: "The requested property could not be found."
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: `${property.title} | Sky and Soil Real Estate`,
|
||||
description: `Explore ${property.title} in ${property.location}. ${property.overview.bhk} ${property.category} starting at ${property.price}.`,
|
||||
};
|
||||
}
|
||||
|
||||
const sections = [
|
||||
{ id: "overview", label: "Overview" },
|
||||
{ id: "about", label: "About" },
|
||||
{ id: "amenities", label: "Amenities" },
|
||||
{ id: "floor-plans", label: "Floor Plans" },
|
||||
{ id: "location", label: "Location" },
|
||||
{ id: "pricing", label: "Pricing" },
|
||||
];
|
||||
|
||||
export default async function PropertyDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
const resolvedParams = await params;
|
||||
const property = properties.find(p => p.id === parseInt(resolvedParams.id));
|
||||
|
||||
if (!property) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-background pb-24">
|
||||
<PropertyHero property={property} />
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-black">
|
||||
<Header />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 mt-12">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12">
|
||||
{/* Main Content */}
|
||||
<div className="lg:col-span-2">
|
||||
<PropertyOverview
|
||||
overview={property.overview}
|
||||
description={property.description}
|
||||
<InnerBanner
|
||||
title={property.title}
|
||||
subtitle={property.location}
|
||||
breadcrumbs={[
|
||||
{ label: "Home", href: "/" },
|
||||
{ label: "Properties", href: "/projects" },
|
||||
{ label: property.title }
|
||||
]}
|
||||
backgroundImage={property.image}
|
||||
/>
|
||||
<PropertyAmenities amenities={property.amenities} />
|
||||
|
||||
<div>
|
||||
|
||||
{/* Sticky Navigation */}
|
||||
<PropertyNav sections={sections} />
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 py-8">
|
||||
{/* Property Header */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-2xl p-8 mb-8 shadow-sm border border-gray-200 dark:border-gray-800">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 mb-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<h1 className="text-3xl md:text-4xl font-bold text-foreground">{property.title}</h1>
|
||||
<span className="px-4 py-1.5 bg-gradient-to-r from-green-500 to-emerald-500 text-white rounded-full text-sm font-semibold shadow-md">
|
||||
{property.status}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center text-gray-600 dark:text-gray-400">
|
||||
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
</svg>
|
||||
{property.location}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400 mb-1">Starting from</div>
|
||||
<div className="text-4xl font-bold bg-gradient-to-r from-primary to-blue-600 bg-clip-text text-transparent">
|
||||
{property.price}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 pt-6 border-t border-gray-200 dark:border-gray-800">
|
||||
<div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
|
||||
<div className="text-2xl font-bold text-primary mb-1">{property.overview.bhk}</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Configuration</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
|
||||
<div className="text-2xl font-bold text-primary mb-1">{property.overview.size}</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Area</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
|
||||
<div className="text-2xl font-bold text-primary mb-1">{property.overview.possession}</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Possession</div>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
|
||||
<div className="text-2xl font-bold text-primary mb-1">{property.overview.totalUnits}</div>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">Total Units</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Image Gallery */}
|
||||
<PropertyGallery images={property.images} title={property.title} />
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
{/* Main Content */}
|
||||
<div className="lg:col-span-2 space-y-8">
|
||||
{/* Overview Section */}
|
||||
<div id="overview" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
|
||||
<div className="w-1 h-8 bg-primary rounded-full"></div>
|
||||
Overview
|
||||
</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-6">
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
|
||||
<div className="text-gray-500 dark:text-gray-400 text-sm mb-2">Property Type</div>
|
||||
<div className="text-lg font-semibold text-foreground">{property.category}</div>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
|
||||
<div className="text-gray-500 dark:text-gray-400 text-sm mb-2">RERA Status</div>
|
||||
<div className="text-lg font-semibold text-green-600">Approved</div>
|
||||
</div>
|
||||
<div className="p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
|
||||
<div className="text-gray-500 dark:text-gray-400 text-sm mb-2">Availability</div>
|
||||
<div className="text-lg font-semibold text-foreground">Available</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* About Section */}
|
||||
<div id="about" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
|
||||
<div className="w-1 h-8 bg-primary rounded-full"></div>
|
||||
About this Property
|
||||
</h2>
|
||||
<p className="text-gray-700 dark:text-gray-300 leading-relaxed text-lg">{property.description}</p>
|
||||
</div>
|
||||
|
||||
{/* Amenities Section */}
|
||||
<div id="amenities" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
|
||||
<div className="w-1 h-8 bg-primary rounded-full"></div>
|
||||
Amenities
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{property.amenities.map((amenity, idx) => (
|
||||
<div key={idx} className="flex items-center gap-3 p-4 bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20 rounded-xl hover:shadow-md transition-shadow">
|
||||
<div className="w-10 h-10 bg-primary rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-5 h-5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-gray-700 dark:text-gray-300 font-medium">{amenity}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Floor Plans Section */}
|
||||
<div id="floor-plans" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
|
||||
<div className="w-1 h-8 bg-primary rounded-full"></div>
|
||||
Floor Plans
|
||||
</h2>
|
||||
<div className="bg-gradient-to-br from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900 rounded-2xl p-12 text-center">
|
||||
<svg className="w-20 h-20 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<p className="text-gray-600 dark:text-gray-400 text-lg mb-4">Floor plans available on request</p>
|
||||
<button className="px-6 py-3 bg-primary text-white rounded-lg hover:bg-blue-700 transition-colors font-medium">
|
||||
Request Floor Plans
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location Section */}
|
||||
<div id="location" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
|
||||
<div className="w-1 h-8 bg-primary rounded-full"></div>
|
||||
Location
|
||||
</h2>
|
||||
<div className="bg-gray-100 dark:bg-gray-800 rounded-xl p-8 text-center">
|
||||
<svg className="w-16 h-16 mx-auto text-gray-400 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 20l-5.447-2.724A1 1 0 013 16.382V5.618a1 1 0 011.447-.894L9 7m0 13l6-3m-6 3V7m6 10l4.553 2.276A1 1 0 0021 18.382V7.618a1 1 0 00-.553-.894L15 4m0 13V4m0 0L9 7" />
|
||||
</svg>
|
||||
<p className="text-gray-600 dark:text-gray-400">Interactive map coming soon</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pricing Section */}
|
||||
<div id="pricing" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32">
|
||||
<h2 className="text-2xl font-bold text-foreground mb-6 flex items-center gap-3">
|
||||
<div className="w-1 h-8 bg-primary rounded-full"></div>
|
||||
Pricing Details
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center p-4 bg-gray-50 dark:bg-gray-800 rounded-xl">
|
||||
<span className="text-gray-700 dark:text-gray-300">Base Price</span>
|
||||
<span className="text-xl font-bold text-primary">{property.price}</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">* Prices are subject to change. Please contact us for the latest pricing and offers.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sidebar */}
|
||||
<div className="lg:col-span-1">
|
||||
<PropertyContact />
|
||||
<div className="sticky top-32 space-y-6">
|
||||
{/* Contact Form */}
|
||||
<div className="bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-800 rounded-2xl p-6 shadow-lg">
|
||||
<h3 className="text-xl font-bold text-foreground mb-4">Get in Touch</h3>
|
||||
<form className="space-y-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Your Name"
|
||||
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground transition-all"
|
||||
/>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="Email Address"
|
||||
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground transition-all"
|
||||
/>
|
||||
<input
|
||||
type="tel"
|
||||
placeholder="Phone Number"
|
||||
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground transition-all"
|
||||
/>
|
||||
<textarea
|
||||
rows={4}
|
||||
placeholder="Message (Optional)"
|
||||
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-700 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent bg-white dark:bg-gray-800 text-foreground transition-all"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
className="w-full bg-gradient-to-r from-primary to-blue-600 text-white py-3 rounded-lg font-semibold hover:shadow-lg transition-all transform hover:scale-105"
|
||||
>
|
||||
Request Callback
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 rounded-2xl p-6 border border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-bold text-foreground mb-4">Quick Actions</h3>
|
||||
<div className="space-y-3">
|
||||
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg hover:shadow-md transition-all text-foreground">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
|
||||
</svg>
|
||||
Share Property
|
||||
</button>
|
||||
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg hover:shadow-md transition-all text-foreground">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
|
||||
</svg>
|
||||
Save to Wishlist
|
||||
</button>
|
||||
<button className="w-full flex items-center justify-center gap-2 px-4 py-3 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-lg hover:shadow-md transition-all text-foreground">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 17h2a2 2 0 002-2v-4a2 2 0 00-2-2H5a2 2 0 00-2 2v4a2 2 0 002 2h2m2 4h6a2 2 0 002-2v-4a2 2 0 00-2-2H9a2 2 0 00-2 2v4a2 2 0 002 2zm8-12V5a2 2 0 00-2-2H9a2 2 0 00-2 2v4h10z" />
|
||||
</svg>
|
||||
Print Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,12 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useRef, MouseEvent } from "react";
|
||||
import Image from "next/image";
|
||||
|
||||
import { FloatingHouse, RotatingKey, GrowingBuilding } from "./PropertyAnimations";
|
||||
|
||||
export default function About() {
|
||||
const [rotation, setRotation] = useState({ x: -10, y: 0 });
|
||||
const [isHovering, setIsHovering] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const rect = containerRef.current.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const width = rect.width;
|
||||
const height = rect.height;
|
||||
|
||||
// Calculate rotation based on mouse position
|
||||
// Y-axis rotation (horizontal mouse movement): -180 to 180 degrees
|
||||
const rotateY = ((x / width) * 360) - 180;
|
||||
|
||||
// X-axis rotation (vertical mouse movement): -90 (top view) to 90 (bottom view)
|
||||
// Inverting Y so top of screen corresponds to seeing the top face
|
||||
const rotateX = -(((y / height) * 180) - 90);
|
||||
|
||||
setRotation({ x: rotateX, y: rotateY });
|
||||
};
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovering(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovering(false);
|
||||
setRotation({ x: -10, y: 0 }); // Reset to default angle
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="about" className="py-24 bg-white dark:bg-black">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<section id="about" className="py-24 bg-white dark:bg-black relative overflow-hidden">
|
||||
|
||||
{/* Decorative Animation */}
|
||||
<div className="absolute top-10 left-10 opacity-30 hidden lg:block">
|
||||
<FloatingHouse />
|
||||
</div>
|
||||
<div className="absolute bottom-20 right-10 opacity-20 hidden lg:block">
|
||||
<RotatingKey />
|
||||
</div>
|
||||
<div className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 opacity-10 pointer-events-none hidden lg:block">
|
||||
<GrowingBuilding className="scale-150" />
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-16 items-center">
|
||||
{/* Text Content */}
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-8 z-10">
|
||||
<h2 className="text-4xl font-bold tracking-tight text-foreground">
|
||||
Where the Sky Meets <br />
|
||||
<span className="text-accent dark:text-accent">The Soil.</span>
|
||||
@ -29,16 +80,128 @@ export default function About() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Image Content */}
|
||||
<div className="relative h-[500px] rounded-3xl overflow-hidden shadow-2xl bg-gray-100 dark:bg-gray-800">
|
||||
{/* Placeholder for an actual image. Using a colored div for now if no image is available,
|
||||
but ideally this would be a real image. I'll use a gradient placeholder. */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-gray-200 to-gray-300 dark:from-gray-800 dark:to-gray-900 flex items-center justify-center text-gray-400 dark:text-gray-600">
|
||||
<span className="text-sm">Modern Architecture Image</span>
|
||||
{/* 3D Cube Container */}
|
||||
<div
|
||||
className="h-[500px] w-full flex items-center justify-center perspective-container cursor-move"
|
||||
ref={containerRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div
|
||||
className={`cube relative w-64 h-64 md:w-80 md:h-80 transform-style-3d ${!isHovering ? 'animate-spin-slow' : ''}`}
|
||||
style={isHovering ? { transform: `rotateX(${rotation.x}deg) rotateY(${rotation.y}deg)` } : {}}
|
||||
>
|
||||
{/* Front Face */}
|
||||
<div className="absolute inset-0 transform translate-z-32 md:translate-z-40 bg-white shadow-xl border border-white/20">
|
||||
<Image
|
||||
src="/assets/images/front-side.jfif"
|
||||
alt="Front View"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Front View</div>
|
||||
</div>
|
||||
|
||||
{/* Back Face */}
|
||||
<div className="absolute inset-0 transform rotate-y-90 translate-z-32 md:translate-z-40 bg-white shadow-xl border border-white/20">
|
||||
<Image
|
||||
src="/assets/images/back-side.jfif"
|
||||
alt="Back View"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Back View</div>
|
||||
</div>
|
||||
|
||||
{/* Right Face */}
|
||||
<div className="absolute inset-0 transform rotate-y-90 translate-z-32 md:translate-z-40 bg-white shadow-xl border border-white/20">
|
||||
<Image
|
||||
src="/assets/images/right-side.jfif"
|
||||
alt="Right View"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Right View</div>
|
||||
</div>
|
||||
|
||||
{/* Left Face */}
|
||||
<div className="absolute inset-0 transform -rotate-y-90 translate-z-32 md:translate-z-40 bg-white shadow-xl border border-white/20">
|
||||
<Image
|
||||
src="/assets/images/left-side.jfif"
|
||||
alt="Left View"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Left View</div>
|
||||
</div>
|
||||
|
||||
{/* Top Face */}
|
||||
<div className="absolute inset-0 transform rotate-x-90 translate-z-32 md:translate-z-40 bg-gray-100 border border-white/20 flex items-center justify-center">
|
||||
<Image
|
||||
src="/assets/images/top-side.jfif"
|
||||
alt="Top View"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Top View</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom Face */}
|
||||
<div className="absolute inset-0 transform -rotate-x-90 translate-z-32 md:translate-z-40 bg-gray-100 border border-white/20 flex items-center justify-center shadow-2xl">
|
||||
<Image
|
||||
src="/assets/images/bottom-side.jfif"
|
||||
alt="Bottom View"
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-4 left-4 bg-black/70 text-white text-xs px-2 py-1 rounded">Bottom View</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx global>{`
|
||||
.perspective-container {
|
||||
perspective: 1000px;
|
||||
}
|
||||
.transform-style-3d {
|
||||
transform-style: preserve-3d;
|
||||
transition: transform 0.1s ease-out; /* Smooth transition for mouse movement */
|
||||
}
|
||||
.translate-z-32 {
|
||||
transform: rotateY(0deg) translateZ(8rem);
|
||||
}
|
||||
.translate-z-40 {
|
||||
transform: rotateY(0deg) translateZ(10rem);
|
||||
}
|
||||
|
||||
/* Custom transforms for faces */
|
||||
.cube > div:nth-child(1) { transform: rotateY(0deg) translateZ(128px); }
|
||||
.cube > div:nth-child(2) { transform: rotateY(180deg) translateZ(128px); }
|
||||
.cube > div:nth-child(3) { transform: rotateY(90deg) translateZ(128px); }
|
||||
.cube > div:nth-child(4) { transform: rotateY(-90deg) translateZ(128px); }
|
||||
.cube > div:nth-child(5) { transform: rotateX(90deg) translateZ(128px); }
|
||||
.cube > div:nth-child(6) { transform: rotateX(-90deg) translateZ(128px); }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.cube > div:nth-child(1) { transform: rotateY(0deg) translateZ(160px); }
|
||||
.cube > div:nth-child(2) { transform: rotateY(180deg) translateZ(160px); }
|
||||
.cube > div:nth-child(3) { transform: rotateY(90deg) translateZ(160px); }
|
||||
.cube > div:nth-child(4) { transform: rotateY(-90deg) translateZ(160px); }
|
||||
.cube > div:nth-child(5) { transform: rotateX(90deg) translateZ(160px); }
|
||||
.cube > div:nth-child(6) { transform: rotateX(-90deg) translateZ(160px); }
|
||||
}
|
||||
|
||||
@keyframes spin-slow {
|
||||
from { transform: rotateX(-10deg) rotateY(0deg); }
|
||||
to { transform: rotateX(-10deg) rotateY(360deg); }
|
||||
}
|
||||
.animate-spin-slow {
|
||||
animation: spin-slow 20s linear infinite;
|
||||
}
|
||||
`}</style>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
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 Link from "next/link";
|
||||
|
||||
import Image from "next/image";
|
||||
import { ThemeToggle } from "@/components/ThemeToggle";
|
||||
import { properties } from "@/data/properties";
|
||||
import Sidebar from "@/components/Sidebar";
|
||||
|
||||
export default function Header() {
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
@ -20,6 +19,7 @@ export default function Header() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${isScrolled
|
||||
? "bg-white/80 dark:bg-black/80 backdrop-blur-md shadow-sm py-4"
|
||||
@ -36,80 +36,24 @@ export default function Header() {
|
||||
className="object-contain p-1"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-2xl font-semibold tracking-tight text-foreground group-hover:text-primary transition-colors">
|
||||
<span className={`text-2xl font-semibold tracking-tight group-hover:text-primary transition-colors ${isScrolled ? "text-foreground" : "text-white"
|
||||
}`}>
|
||||
Sky and Soil
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
<nav className="hidden md:flex items-center space-x-8">
|
||||
<Link href="#about" className="text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-foreground transition-colors">
|
||||
About
|
||||
</Link>
|
||||
|
||||
{/* Projects Mega Menu */}
|
||||
<div className="group relative h-full flex items-center">
|
||||
<Link href="#projects" className="text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-foreground transition-colors py-6">
|
||||
Projects
|
||||
</Link>
|
||||
|
||||
{/* Dropdown */}
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 w-[800px] bg-white/95 dark:bg-black/95 backdrop-blur-md border border-gray-100 dark:border-gray-800 rounded-2xl shadow-2xl opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-300 transform translate-y-2 group-hover:translate-y-0 p-6 grid grid-cols-3 gap-6 z-50">
|
||||
{properties.slice(0, 3).map((property) => (
|
||||
<Link
|
||||
key={property.id}
|
||||
href={`/properties/${property.id}`}
|
||||
className="group/card block"
|
||||
>
|
||||
<div className="relative h-32 w-full rounded-lg overflow-hidden mb-3">
|
||||
<div className={`absolute inset-0 ${property.image.startsWith('/') ? '' : property.image}`} />
|
||||
{property.image.startsWith('/') && (
|
||||
<img src={property.image} alt={property.title} className="w-full h-full object-cover transition-transform duration-500 group-hover/card:scale-110" />
|
||||
)}
|
||||
<div className="absolute top-2 left-2 bg-white/90 dark:bg-black/80 backdrop-blur-sm px-2 py-0.5 rounded-full text-[10px] font-semibold text-foreground uppercase tracking-wider">
|
||||
{property.status}
|
||||
</div>
|
||||
</div>
|
||||
<h4 className="text-sm font-bold text-foreground mb-1 group-hover/card:text-primary transition-colors">
|
||||
{property.title}
|
||||
</h4>
|
||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
||||
{property.location}
|
||||
</p>
|
||||
<p className="text-xs font-semibold text-accent">
|
||||
{property.price}
|
||||
</p>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
<div className="col-span-3 pt-4 border-t border-gray-100 dark:border-gray-800 text-center">
|
||||
<Link href="#projects" className="text-sm font-medium text-primary hover:underline">
|
||||
View All Projects →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Link href="#lifestyle" className="text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-foreground transition-colors">
|
||||
Lifestyle
|
||||
</Link>
|
||||
<Link href="#contact" className="text-sm font-medium text-gray-600 dark:text-gray-300 hover:text-foreground transition-colors">
|
||||
Contact
|
||||
</Link>
|
||||
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
|
||||
<div className="hidden md:flex items-center gap-4">
|
||||
{/* Mobile menu button logic would go here if we were implementing full mobile menu */}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<Link href="/contact">
|
||||
<button className="hidden md:block px-5 py-2 text-sm font-medium text-white bg-primary rounded-full hover:bg-blue-600 transition-colors shadow-sm hover:shadow-md active:scale-95 transform duration-200">
|
||||
Book a Visit
|
||||
</button>
|
||||
</Link>
|
||||
|
||||
<div className="md:hidden flex items-center gap-4">
|
||||
<ThemeToggle />
|
||||
<button className="text-foreground">
|
||||
<button
|
||||
className={`p-2 -mr-2 transition-colors ${isScrolled ? "text-foreground" : "text-white"
|
||||
}`}
|
||||
onClick={() => setIsSidebarOpen(true)}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
@ -117,5 +61,8 @@ export default function Header() {
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<Sidebar isOpen={isSidebarOpen} onClose={() => setIsSidebarOpen(false)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,23 +1,68 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section className="relative h-screen flex flex-col items-center justify-center overflow-hidden">
|
||||
{/* Background Image */}
|
||||
{/* Video Background */}
|
||||
<div className="absolute inset-0 z-0">
|
||||
<Image
|
||||
src="/hero-image.jpg"
|
||||
alt="Sky and Soil Properties"
|
||||
fill
|
||||
className="object-cover"
|
||||
priority
|
||||
/>
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="absolute inset-0 w-full h-full object-cover"
|
||||
poster="/assets/images/banglore-nit-view.mp4" // Fallback image
|
||||
>
|
||||
<source src="/assets/images/banglore-nit-view.mp4" type="video/mp4" />
|
||||
{/* Note: This is a placeholder video. Replace with your Bangalore drone shot. */}
|
||||
</video>
|
||||
{/* Overlay */}
|
||||
<div className="absolute inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-[2px]"></div>
|
||||
<div className="absolute inset-0 bg-black/50 dark:bg-black/60 backdrop-blur-[1px]"></div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-10 max-w-5xl mx-auto px-6 text-center">
|
||||
{/* Location Pins (Decorative) */}
|
||||
<div className="absolute inset-0 z-10 pointer-events-none hidden md:block">
|
||||
{/* Pin 1: Hebbal */}
|
||||
<div className="absolute top-[30%] left-[20%] animate-bounce" style={{ animationDuration: '3s' }}>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="bg-white/90 backdrop-blur-md px-3 py-1 rounded-lg shadow-lg mb-2">
|
||||
<span className="text-xs font-bold text-black">Hebbal</span>
|
||||
</div>
|
||||
<div className="w-4 h-4 bg-primary rounded-full border-2 border-white shadow-lg relative">
|
||||
<div className="absolute inset-0 bg-primary rounded-full animate-ping opacity-75"></div>
|
||||
</div>
|
||||
<div className="h-16 w-0.5 bg-gradient-to-b from-white/50 to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pin 2: Airport */}
|
||||
<div className="absolute top-[20%] right-[25%] animate-bounce" style={{ animationDuration: '4s', animationDelay: '1s' }}>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="bg-white/90 backdrop-blur-md px-3 py-1 rounded-lg shadow-lg mb-2">
|
||||
<span className="text-xs font-bold text-black">Airport</span>
|
||||
</div>
|
||||
<div className="w-4 h-4 bg-primary rounded-full border-2 border-white shadow-lg relative">
|
||||
<div className="absolute inset-0 bg-primary rounded-full animate-ping opacity-75"></div>
|
||||
</div>
|
||||
<div className="h-24 w-0.5 bg-gradient-to-b from-white/50 to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pin 3: Whitefield */}
|
||||
<div className="absolute bottom-[30%] right-[15%] animate-bounce" style={{ animationDuration: '3.5s', animationDelay: '0.5s' }}>
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="bg-white/90 backdrop-blur-md px-3 py-1 rounded-lg shadow-lg mb-2">
|
||||
<span className="text-xs font-bold text-black">Whitefield</span>
|
||||
</div>
|
||||
<div className="w-4 h-4 bg-primary rounded-full border-2 border-white shadow-lg relative">
|
||||
<div className="absolute inset-0 bg-primary rounded-full animate-ping opacity-75"></div>
|
||||
</div>
|
||||
<div className="h-12 w-0.5 bg-gradient-to-b from-white/50 to-transparent"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative z-20 max-w-5xl mx-auto px-6 text-center">
|
||||
<h1 className="text-5xl md:text-7xl lg:text-8xl font-bold tracking-tight text-white mb-6 animate-fade-in drop-shadow-lg">
|
||||
Sky and Soil <br className="hidden md:block" />
|
||||
<span className="text-transparent bg-clip-text bg-gradient-to-r from-white to-gray-300">
|
||||
@ -31,7 +76,7 @@ export default function Hero() {
|
||||
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4 animate-slide-up opacity-0" style={{ animationDelay: "0.4s" }}>
|
||||
<Link
|
||||
href="#projects"
|
||||
href="/projects"
|
||||
className="px-8 py-4 text-base font-medium text-primary bg-white rounded-full hover:bg-gray-100 transition-all shadow-lg hover:shadow-xl hover:-translate-y-0.5 active:scale-95"
|
||||
>
|
||||
Explore Projects
|
||||
@ -39,13 +84,15 @@ export default function Hero() {
|
||||
<button
|
||||
className="px-8 py-4 text-base font-medium text-white border border-white/30 bg-white/10 backdrop-blur-sm rounded-full hover:bg-white/20 transition-all shadow-sm hover:shadow-md active:scale-95"
|
||||
>
|
||||
<Link href="/contact">
|
||||
Book a Site Visit
|
||||
</Link>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll Indicator */}
|
||||
<div className="absolute bottom-10 left-1/2 transform -translate-x-1/2 animate-bounce z-10">
|
||||
<div className="absolute bottom-10 left-1/2 transform -translate-x-1/2 animate-bounce z-20">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="w-6 h-6 text-white/80">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
|
||||
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() {
|
||||
const [zoomProps, setZoomProps] = useState({ x: 0, y: 0, show: false });
|
||||
const imageRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleMouseMove = (e: MouseEvent<HTMLDivElement>) => {
|
||||
if (!imageRef.current) return;
|
||||
|
||||
const { left, top, width, height } = imageRef.current.getBoundingClientRect();
|
||||
const x = ((e.clientX - left) / width) * 100;
|
||||
const y = ((e.clientY - top) / height) * 100;
|
||||
|
||||
setZoomProps({ x, y, show: true });
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setZoomProps((prev) => ({ ...prev, show: false }));
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="lifestyle" className="py-24 bg-secondary dark:bg-gray-900">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
{/* Image Side */}
|
||||
<div className="order-2 lg:order-1 relative h-[600px] rounded-3xl overflow-hidden shadow-2xl group">
|
||||
{/* Placeholder for lifestyle image */}
|
||||
<div className="absolute inset-0 bg-gradient-to-tr from-gray-800 to-gray-600 flex items-center justify-center text-white">
|
||||
<span className="text-xl font-light tracking-widest uppercase">Clubhouse & Amenities</span>
|
||||
<section id="lifestyle" className="py-24 bg-secondary dark:bg-gray-900 relative overflow-hidden">
|
||||
|
||||
{/* Decorative Animations */}
|
||||
<div className="absolute top-20 right-10 opacity-30 hidden lg:block">
|
||||
<FloatingHouse />
|
||||
</div>
|
||||
<div className="absolute bottom-20 left-10 opacity-20 hidden lg:block">
|
||||
<RotatingKey />
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
|
||||
{/* Image Side with Zoom Effect */}
|
||||
<div
|
||||
className="order-2 lg:order-1 relative h-[600px] rounded-3xl overflow-hidden shadow-2xl group cursor-crosshair"
|
||||
ref={imageRef}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Image
|
||||
src="/hero-image.jpg"
|
||||
alt="Lifestyle"
|
||||
fill
|
||||
className={`object-cover transition-transform duration-200 ease-out ${zoomProps.show ? 'scale-[2.5]' : 'scale-100'}`}
|
||||
style={{
|
||||
transformOrigin: `${zoomProps.x}% ${zoomProps.y}%`
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Overlay Text - Hidden on Hover to see details */}
|
||||
<div className={`absolute inset-0 bg-gradient-to-tr from-gray-800/60 to-transparent flex items-center justify-center pointer-events-none transition-opacity duration-300 ${zoomProps.show ? 'opacity-0' : 'opacity-100'}`}>
|
||||
<span className="text-xl font-light tracking-widest uppercase text-white border border-white/30 px-6 py-2 rounded-full backdrop-blur-sm">
|
||||
Clubhouse & Amenities
|
||||
</span>
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-black/20 group-hover:bg-black/10 transition-colors duration-700" />
|
||||
</div>
|
||||
|
||||
{/* Content Side */}
|
||||
|
||||
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";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useRef } from "react";
|
||||
import Link from "next/link";
|
||||
import { properties, Property } from "@/data/properties";
|
||||
import { properties } from "@/data/properties";
|
||||
|
||||
type Category = "Apartments" | "Premium Homes" | "Luxury";
|
||||
|
||||
export default function Properties() {
|
||||
interface PropertiesProps {
|
||||
layout?: "slider" | "grid";
|
||||
}
|
||||
|
||||
export default function Properties({ layout = "slider" }: PropertiesProps) {
|
||||
const [activeTab, setActiveTab] = useState<Category | "All">("All");
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const filteredProperties = activeTab === "All"
|
||||
? properties
|
||||
: properties.filter((property) => property.category === activeTab);
|
||||
|
||||
const scroll = (direction: "left" | "right") => {
|
||||
if (scrollContainerRef.current) {
|
||||
const container = scrollContainerRef.current;
|
||||
const scrollAmount = direction === "left" ? -400 : 400;
|
||||
container.scrollBy({ left: scrollAmount, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="projects" className="py-24 bg-white dark:bg-black">
|
||||
<section id="projects" className="py-24 bg-white dark:bg-black overflow-hidden">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
|
||||
@ -43,7 +56,101 @@ export default function Properties() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
{layout === "slider" ? (
|
||||
/* Slider Layout - 3 cards for All, 1 card for specific tabs */
|
||||
<div className="relative">
|
||||
{/* Navigation Buttons */}
|
||||
<button
|
||||
onClick={() => scroll("left")}
|
||||
className="absolute left-0 md:left-4 top-1/2 -translate-y-1/2 z-10 bg-white dark:bg-gray-800 p-3 rounded-full shadow-lg hover:scale-110 transition-all duration-300"
|
||||
aria-label="Previous project"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5 text-foreground">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => scroll("right")}
|
||||
className="absolute right-0 md:right-4 top-1/2 -translate-y-1/2 z-10 bg-white dark:bg-gray-800 p-3 rounded-full shadow-lg hover:scale-110 transition-all duration-300"
|
||||
aria-label="Next project"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5 text-foreground">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Scrollable Area */}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className={`flex gap-6 overflow-x-auto pb-8 snap-x snap-mandatory hide-scrollbar ${activeTab === "All" ? "px-4" : "px-4 md:px-16"
|
||||
}`}
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
{filteredProperties.map((property, index) => {
|
||||
// Show only first card for specific tabs
|
||||
if (activeTab !== "All" && index > 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={property.id}
|
||||
className={activeTab === "All"
|
||||
? "min-w-[300px] md:min-w-[380px] snap-center"
|
||||
: "flex-shrink-0 w-full max-w-md mx-auto snap-center"
|
||||
}
|
||||
>
|
||||
<Link
|
||||
href={`/properties/${property.id}`}
|
||||
className="group/card block bg-white dark:bg-gray-900 rounded-2xl overflow-hidden border border-gray-100 dark:border-gray-800 hover:border-gray-200 dark:hover:border-gray-700 shadow-lg hover:shadow-2xl transition-all duration-300 h-full flex flex-col"
|
||||
>
|
||||
{/* Image */}
|
||||
<div className={activeTab === "All" ? "h-64 w-full relative overflow-hidden" : "h-80 w-full relative overflow-hidden"}>
|
||||
<div className={`absolute inset-0 ${property.image.startsWith('/') ? '' : property.image}`} />
|
||||
{property.image.startsWith('/') && (
|
||||
<img src={property.image} alt={property.title} className="w-full h-full object-cover transition-transform duration-500 group-hover/card:scale-110" />
|
||||
)}
|
||||
|
||||
<div className="absolute top-4 left-4 bg-white/90 dark:bg-black/80 backdrop-blur-sm px-3 py-1 rounded-full text-xs font-semibold text-foreground uppercase tracking-wider">
|
||||
{property.status}
|
||||
</div>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover/card:bg-black/5 transition-colors duration-300" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className={activeTab === "All" ? "p-6 flex flex-col flex-grow" : "p-8 flex flex-col flex-grow"}>
|
||||
<div className={activeTab === "All" ? "flex items-center justify-between mb-2" : "flex items-center justify-between mb-3"}>
|
||||
<span className={activeTab === "All"
|
||||
? "text-xs font-medium text-primary bg-blue-50 dark:bg-blue-900/30 px-2 py-1 rounded-md"
|
||||
: "text-sm font-medium text-primary bg-blue-50 dark:bg-blue-900/30 px-3 py-1.5 rounded-md"
|
||||
}>
|
||||
{property.location}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className={activeTab === "All" ? "text-xl font-bold text-foreground mb-2" : "text-2xl font-bold text-foreground mb-3"}>
|
||||
{property.title}
|
||||
</h3>
|
||||
<p className={activeTab === "All"
|
||||
? "text-gray-600 dark:text-gray-400 text-sm mb-6 flex-grow line-clamp-2"
|
||||
: "text-gray-600 dark:text-gray-400 text-base mb-6 flex-grow line-clamp-3"
|
||||
}>
|
||||
{property.description}
|
||||
</p>
|
||||
|
||||
<div className={activeTab === "All"
|
||||
? "w-full py-3 text-center text-sm font-medium text-foreground border border-gray-200 dark:border-gray-700 rounded-xl group-hover/card:bg-foreground group-hover/card:text-white dark:group-hover/card:bg-white dark:group-hover/card:text-black transition-colors"
|
||||
: "w-full py-3 text-center text-base font-medium text-foreground border-2 border-gray-200 dark:border-gray-700 rounded-xl group-hover/card:bg-foreground group-hover/card:text-white dark:group-hover/card:bg-white dark:group-hover/card:text-black transition-colors"
|
||||
}>
|
||||
View Details
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
/* Grid Layout */
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{filteredProperties.map((property) => (
|
||||
<Link
|
||||
@ -53,7 +160,6 @@ export default function Properties() {
|
||||
>
|
||||
{/* Image Placeholder */}
|
||||
<div className={`h-64 w-full relative overflow-hidden`}>
|
||||
{/* Using a colored div as placeholder if image fails or while loading, but ideally using Next/Image */}
|
||||
<div className={`absolute inset-0 ${property.image.startsWith('/') ? '' : property.image}`} />
|
||||
{property.image.startsWith('/') && (
|
||||
<img src={property.image} alt={property.title} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
|
||||
@ -85,6 +191,7 @@ export default function Properties() {
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
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,51 +1,171 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { GrowingBuilding } from "./PropertyAnimations";
|
||||
|
||||
export default function Testimonials() {
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
quote: "Aurora Springs transformed our idea of a dream home into reality. The attention to detail is unmatched.",
|
||||
author: "Rajesh Kumar",
|
||||
location: "Owner at Aurora Heights",
|
||||
rating: 5,
|
||||
date: "2 months ago"
|
||||
},
|
||||
{
|
||||
quote: "The transparency and professionalism shown by the team made the entire buying process seamless.",
|
||||
author: "Priya Sharma",
|
||||
location: "Owner at Serene Meadows",
|
||||
rating: 5,
|
||||
date: "3 months ago"
|
||||
},
|
||||
{
|
||||
quote: "Living here feels like a permanent vacation. The amenities and the community are world-class.",
|
||||
author: "David Miller",
|
||||
location: "Resident at The Grandeur",
|
||||
rating: 5,
|
||||
date: "1 month ago"
|
||||
},
|
||||
{
|
||||
quote: "Exceptional service from start to finish. The team went above and beyond to ensure we found our perfect home.",
|
||||
author: "Anita Desai",
|
||||
location: "Owner at Green Valley",
|
||||
rating: 5,
|
||||
date: "4 months ago"
|
||||
},
|
||||
{
|
||||
quote: "The quality of construction and the beautiful surroundings make this the best investment we've ever made.",
|
||||
author: "Michael Chen",
|
||||
location: "Resident at Skyline Towers",
|
||||
rating: 5,
|
||||
date: "5 months ago"
|
||||
}
|
||||
];
|
||||
|
||||
const scroll = (direction: "left" | "right") => {
|
||||
if (scrollContainerRef.current) {
|
||||
const scrollAmount = direction === "left" ? -400 : 400;
|
||||
scrollContainerRef.current.scrollBy({ left: scrollAmount, behavior: "smooth" });
|
||||
}
|
||||
};
|
||||
|
||||
const renderStars = (rating: number) => {
|
||||
return (
|
||||
<section className="py-24 bg-white dark:bg-black">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<div className="flex gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<svg
|
||||
key={i}
|
||||
className={`w-5 h-5 ${i < rating ? 'text-yellow-400' : 'text-gray-300'}`}
|
||||
fill="currentColor"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M9.049 2.927c.3-.921 1.603-.921 1.902 0l1.07 3.292a1 1 0 00.95.69h3.462c.969 0 1.371 1.24.588 1.81l-2.8 2.034a1 1 0 00-.364 1.118l1.07 3.292c.3.921-.755 1.688-1.54 1.118l-2.8-2.034a1 1 0 00-1.175 0l-2.8 2.034c-.784.57-1.838-.197-1.539-1.118l1.07-3.292a1 1 0 00-.364-1.118L2.98 8.72c-.783-.57-.38-1.81.588-1.81h3.461a1 1 0 00.951-.69l1.07-3.292z" />
|
||||
</svg>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
return name.split(' ').map(n => n[0]).join('').toUpperCase();
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-white dark:bg-black relative overflow-hidden">
|
||||
|
||||
{/* Decorative Animations */}
|
||||
<div className="absolute top-1/2 left-10 -translate-y-1/2 opacity-20 hidden lg:block">
|
||||
<GrowingBuilding />
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
|
||||
Stories of Satisfaction
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-2 mt-4">
|
||||
<div className="flex">
|
||||
{renderStars(5)}
|
||||
</div>
|
||||
<span className="text-lg font-semibold text-foreground">5.0</span>
|
||||
<span className="text-gray-500">• Based on {testimonials.length} reviews</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
|
||||
{/* Slider Container */}
|
||||
<div className="relative group">
|
||||
{/* Navigation Buttons */}
|
||||
<button
|
||||
onClick={() => scroll("left")}
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 -translate-x-4 z-10 bg-white dark:bg-gray-800 p-3 rounded-full shadow-lg opacity-0 group-hover:opacity-100 transition-all duration-300 hover:scale-110 hidden md:block border border-gray-200 dark:border-gray-700"
|
||||
aria-label="Previous reviews"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5 text-foreground">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 19.5L8.25 12l7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => scroll("right")}
|
||||
className="absolute right-0 top-1/2 -translate-y-1/2 translate-x-4 z-10 bg-white dark:bg-gray-800 p-3 rounded-full shadow-lg opacity-0 group-hover:opacity-100 transition-all duration-300 hover:scale-110 hidden md:block border border-gray-200 dark:border-gray-700"
|
||||
aria-label="Next reviews"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor" className="w-5 h-5 text-foreground">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 4.5l7.5 7.5-7.5 7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{/* Scrollable Reviews */}
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
className="flex gap-6 overflow-x-auto pb-8 snap-x snap-mandatory hide-scrollbar"
|
||||
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
|
||||
>
|
||||
{testimonials.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-secondary dark:bg-gray-900 p-8 rounded-2xl relative hover:-translate-y-1 transition-transform duration-300"
|
||||
className="min-w-[350px] md:min-w-[400px] snap-center"
|
||||
>
|
||||
<div className="absolute top-6 left-6 text-4xl text-primary opacity-20 font-serif">
|
||||
“
|
||||
<div className="bg-white dark:bg-gray-900 p-6 rounded-2xl border border-gray-200 dark:border-gray-800 hover:shadow-xl transition-all duration-300 h-full flex flex-col">
|
||||
{/* Header with Avatar and Info */}
|
||||
<div className="flex items-start gap-4 mb-4">
|
||||
<div className="w-12 h-12 rounded-full bg-primary text-white flex items-center justify-center font-bold text-lg flex-shrink-0">
|
||||
{getInitials(item.author)}
|
||||
</div>
|
||||
<p className="text-lg text-gray-700 dark:text-gray-300 italic mb-6 relative z-10 pt-4">
|
||||
{item.quote}
|
||||
</p>
|
||||
<div>
|
||||
<div className="flex-1">
|
||||
<h4 className="font-semibold text-foreground">{item.author}</h4>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">{item.location}</p>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400">{item.date}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Star Rating */}
|
||||
<div className="mb-4">
|
||||
{renderStars(item.rating)}
|
||||
</div>
|
||||
|
||||
{/* Review Text */}
|
||||
<p className="text-gray-700 dark:text-gray-300 leading-relaxed mb-4 flex-grow">
|
||||
"{item.quote}"
|
||||
</p>
|
||||
|
||||
{/* Location Badge */}
|
||||
<div className="pt-4 border-t border-gray-100 dark:border-gray-800">
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 flex items-center gap-2">
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 11a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
{item.location}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,4 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { FloatingHouse, RotatingKey, GrowingBuilding } from "./PropertyAnimations";
|
||||
|
||||
export default function WhyChooseUs() {
|
||||
const [visibleCards, setVisibleCards] = useState<boolean[]>([false, false, false, false]);
|
||||
const sectionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const features = [
|
||||
{
|
||||
title: "Prime Locations",
|
||||
@ -39,14 +47,58 @@ export default function WhyChooseUs() {
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
// Trigger cards to appear one by one with delay
|
||||
features.forEach((_, index) => {
|
||||
setTimeout(() => {
|
||||
setVisibleCards((prev) => {
|
||||
const newVisible = [...prev];
|
||||
newVisible[index] = true;
|
||||
return newVisible;
|
||||
});
|
||||
}, index * 150); // 150ms delay between each card
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
{ threshold: 0.2 }
|
||||
);
|
||||
|
||||
if (sectionRef.current) {
|
||||
observer.observe(sectionRef.current);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (sectionRef.current) {
|
||||
observer.unobserve(sectionRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="py-24 bg-secondary dark:bg-gray-900/50">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<section ref={sectionRef} className="py-24 bg-secondary dark:bg-gray-900/50 relative overflow-hidden">
|
||||
|
||||
{/* Decorative Animations */}
|
||||
<div className="absolute top-20 left-10 opacity-50 hidden lg:block">
|
||||
<FloatingHouse />
|
||||
</div>
|
||||
<div className="absolute top-1/2 right-10 -translate-y-1/2 opacity-40 hidden lg:block">
|
||||
<RotatingKey />
|
||||
</div>
|
||||
<div className="absolute bottom-10 left-1/4 opacity-30 hidden lg:block">
|
||||
<GrowingBuilding />
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 relative z-10">
|
||||
<div className="text-center mb-16">
|
||||
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4">
|
||||
<h2 className="text-3xl md:text-4xl font-bold tracking-tight text-foreground mb-4 animate-fade-in">
|
||||
Why Sky and Soil?
|
||||
</h2>
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto">
|
||||
<p className="text-lg text-gray-600 dark:text-gray-400 max-w-2xl mx-auto animate-slide-up">
|
||||
We bridge the gap between your dreams and reality with premium properties and unmatched service.
|
||||
</p>
|
||||
</div>
|
||||
@ -55,9 +107,15 @@ export default function WhyChooseUs() {
|
||||
{features.map((feature, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="bg-white dark:bg-gray-800 p-8 rounded-2xl shadow-sm hover:shadow-md transition-shadow duration-300 flex flex-col items-center text-center group"
|
||||
className={`bg-white dark:bg-gray-800 p-8 rounded-2xl shadow-sm hover:shadow-xl transition-all duration-500 flex flex-col items-center text-center group transform ${visibleCards[index]
|
||||
? 'translate-y-0 opacity-100'
|
||||
: 'translate-y-10 opacity-0'
|
||||
}`}
|
||||
style={{
|
||||
transitionDelay: `${index * 100}ms`
|
||||
}}
|
||||
>
|
||||
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900/30 text-primary rounded-full group-hover:scale-110 transition-transform duration-300">
|
||||
<div className="mb-6 p-4 bg-blue-50 dark:bg-blue-900/30 text-primary rounded-full group-hover:scale-110 group-hover:rotate-6 transition-all duration-300">
|
||||
{feature.icon}
|
||||
</div>
|
||||
<h3 className="text-xl font-semibold text-foreground mb-3">
|
||||
|
||||
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