features components
This commit is contained in:
79
src/components/details/amenities.tsx
Normal file
79
src/components/details/amenities.tsx
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
import { Amenity } from "./types";
|
||||||
|
|
||||||
|
interface AmenitiesProps {
|
||||||
|
amenities: Amenity[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Amenities({ amenities }: AmenitiesProps) {
|
||||||
|
const [showAmenities, setShowAmenities] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showAmenities) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
};
|
||||||
|
}, [showAmenities]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="pb-8 border-b-2 border-gray-100">
|
||||||
|
<h3 className="text-2xl font-medium mb-6">What this place offers</h3>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{amenities.slice(0, 6).map((item, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-4 p-4 border-2 border-gray-100 hover:border-black transition-colors">
|
||||||
|
<item.icon className="w-6 h-6" />
|
||||||
|
<span className="text-lg font-light">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAmenities(true)}
|
||||||
|
className="mt-6 px-6 py-3 border-2 border-black font-medium hover:bg-[#E7FE78] transition-colors"
|
||||||
|
>
|
||||||
|
Show all {amenities.length} amenities
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Amenities Modal */}
|
||||||
|
{showAmenities && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setShowAmenities(false)} />
|
||||||
|
<div className="relative bg-white w-full max-w-2xl max-h-[85vh] rounded-xl shadow-2xl flex flex-col animate-in fade-in zoom-in duration-200">
|
||||||
|
<div className="flex items-center justify-between p-6 border-b">
|
||||||
|
<h2 className="text-2xl font-medium">What this place offers</h2>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowAmenities(false)}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-6 overflow-y-auto flex-1">
|
||||||
|
<div className="space-y-8">
|
||||||
|
{Array.from(new Set(amenities.map(a => a.category))).map(category => (
|
||||||
|
<div key={category}>
|
||||||
|
<h3 className="font-medium text-lg mb-4">{category}</h3>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{amenities.filter(a => a.category === category).map((item, idx) => (
|
||||||
|
<div key={idx} className="flex items-center gap-4 pb-4 border-b border-gray-50 last:border-0">
|
||||||
|
<item.icon className="w-6 h-6 text-gray-600" />
|
||||||
|
<span className="text-lg font-light">{item.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
72
src/components/details/booking-card.tsx
Normal file
72
src/components/details/booking-card.tsx
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import { Star } from "lucide-react";
|
||||||
|
|
||||||
|
interface BookingCardProps {
|
||||||
|
price: number;
|
||||||
|
rating: number;
|
||||||
|
reviewCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BookingCard({ price, rating, reviewCount }: BookingCardProps) {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
<div className="sticky top-24 border-2 border-black p-6 bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
|
||||||
|
<div className="flex items-end justify-between mb-6">
|
||||||
|
<div>
|
||||||
|
<span className="text-2xl font-bold">₨{price.toLocaleString()}</span>
|
||||||
|
<span className="text-gray-600"> / night</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-sm font-medium">
|
||||||
|
<Star className="w-4 h-4 fill-black" />
|
||||||
|
{rating} · <span className="text-gray-500 underline">{reviewCount} reviews</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="border-2 border-black mb-4">
|
||||||
|
<div className="grid grid-cols-2 border-b-2 border-black">
|
||||||
|
<div className="p-3 border-r-2 border-black">
|
||||||
|
<label className="block text-xs font-bold uppercase tracking-wider mb-1">Check-in</label>
|
||||||
|
<input type="date" className="w-full outline-none text-sm bg-transparent" />
|
||||||
|
</div>
|
||||||
|
<div className="p-3">
|
||||||
|
<label className="block text-xs font-bold uppercase tracking-wider mb-1">Check-out</label>
|
||||||
|
<input type="date" className="w-full outline-none text-sm bg-transparent" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="p-3">
|
||||||
|
<label className="block text-xs font-bold uppercase tracking-wider mb-1">Guests</label>
|
||||||
|
<select className="w-full outline-none text-sm bg-transparent">
|
||||||
|
<option>1 guest</option>
|
||||||
|
<option>2 guests</option>
|
||||||
|
<option>3 guests</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button className="w-full py-4 bg-black text-white font-medium text-lg hover:bg-[#E7FE78] hover:text-black border-2 border-black transition-all duration-300 mb-4">
|
||||||
|
Reserve
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<p className="text-center text-sm text-gray-500 mb-4">You won't be charged yet</p>
|
||||||
|
|
||||||
|
<div className="space-y-3 text-sm">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="underline">₨{price.toLocaleString()} x 5 nights</span>
|
||||||
|
<span>₨{(price * 5).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="underline">Cleaning fee</span>
|
||||||
|
<span>₨5,000</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="underline">Service fee</span>
|
||||||
|
<span>₨8,000</span>
|
||||||
|
</div>
|
||||||
|
<div className="pt-4 border-t-2 border-gray-100 flex justify-between font-bold text-lg">
|
||||||
|
<span>Total</span>
|
||||||
|
<span>₨{(price * 5 + 13000).toLocaleString()}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
14
src/components/details/description.tsx
Normal file
14
src/components/details/description.tsx
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
interface DescriptionProps {
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Description({ description }: DescriptionProps) {
|
||||||
|
return (
|
||||||
|
<div className="pb-8 border-b-2 border-gray-100">
|
||||||
|
<h3 className="text-2xl font-medium mb-4">About this place</h3>
|
||||||
|
<p className="text-gray-700 leading-relaxed text-lg font-light">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,16 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { Star, MapPin, Share, Heart, Wifi, Car, Utensils, Wind, Monitor, X, Waves, Dumbbell, Coffee, Briefcase, Droplets, Tv } from "lucide-react";
|
import { Wifi, Car, Utensils, Wind, Monitor, Waves, Dumbbell, Coffee, Briefcase, Droplets, Tv } from "lucide-react";
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
import Image from "next/image";
|
import { Header } from "./header";
|
||||||
|
import { ImageGrid } from "./image-grid";
|
||||||
|
import { HostInfo } from "./host-info";
|
||||||
|
import { Highlights } from "./highlights";
|
||||||
|
import { Description } from "./description";
|
||||||
|
import { Amenities } from "./amenities";
|
||||||
|
import { BookingCard } from "./booking-card";
|
||||||
|
import { Reviews } from "./reviews";
|
||||||
|
import { Property } from "./types";
|
||||||
|
|
||||||
const figtree = localFont({
|
const figtree = localFont({
|
||||||
src: [
|
src: [
|
||||||
@@ -13,7 +20,7 @@ const figtree = localFont({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
const property = {
|
const property: Property = {
|
||||||
title: "Luxury Villa with Panoramic Mountain Views",
|
title: "Luxury Villa with Panoramic Mountain Views",
|
||||||
location: "Islamabad, Pakistan",
|
location: "Islamabad, Pakistan",
|
||||||
guests: 4,
|
guests: 4,
|
||||||
@@ -95,368 +102,40 @@ const property = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Features() {
|
function Features() {
|
||||||
const [showAmenities, setShowAmenities] = useState(false);
|
|
||||||
const [showReviews, setShowReviews] = useState(false);
|
|
||||||
|
|
||||||
// Prevent body scroll when modal is open
|
|
||||||
useEffect(() => {
|
|
||||||
if (showAmenities || showReviews) {
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
} else {
|
|
||||||
document.body.style.overflow = 'unset';
|
|
||||||
}
|
|
||||||
return () => {
|
|
||||||
document.body.style.overflow = 'unset';
|
|
||||||
};
|
|
||||||
}, [showAmenities, showReviews]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`w-full bg-white ${figtree.className}`}>
|
<div className={`w-full bg-white ${figtree.className}`}>
|
||||||
{/* Header Section */}
|
<Header property={property} />
|
||||||
<div className="max-w-[95%] mx-auto pt-8 pb-6">
|
<ImageGrid images={property.images} />
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<h1 className="text-4xl font-medium uppercase tracking-tight">{property.title}</h1>
|
|
||||||
<div className="flex flex-row items-center justify-between w-full">
|
|
||||||
<div className="flex items-center gap-4 text-sm md:text-base">
|
|
||||||
<span className="flex items-center gap-1 font-medium underline cursor-pointer hover:text-gray-600">
|
|
||||||
<Star className="w-4 h-4 fill-black" />
|
|
||||||
{property.rating} · {property.reviewCount} reviews
|
|
||||||
</span>
|
|
||||||
<span className="hidden md:inline text-gray-400">|</span>
|
|
||||||
<span className="flex items-center gap-1 font-medium underline cursor-pointer hover:text-gray-600">
|
|
||||||
<MapPin className="w-4 h-4" />
|
|
||||||
{property.location}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<button className="flex items-center gap-2 px-4 py-2 text-sm font-medium border-2 border-transparent hover:bg-gray-100 transition-colors rounded-full md:rounded-none md:border-black md:hover:bg-[#E7FE78]">
|
|
||||||
<Share className="w-4 h-4" />
|
|
||||||
<span className="hidden md:inline">Share</span>
|
|
||||||
</button>
|
|
||||||
<button className="flex items-center gap-2 px-4 py-2 text-sm font-medium border-2 border-transparent hover:bg-gray-100 transition-colors rounded-full md:rounded-none md:border-black md:hover:bg-[#E7FE78]">
|
|
||||||
<Heart className="w-4 h-4" />
|
|
||||||
<span className="hidden md:inline">Save</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Image Grid */}
|
|
||||||
<div className="max-w-[95%] mx-auto mb-12">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-2 h-[400px] md:h-[500px] rounded-xl overflow-hidden md:rounded-none">
|
|
||||||
<div className="md:col-span-2 h-full relative border-2 border-black group overflow-hidden">
|
|
||||||
<Image
|
|
||||||
src={property.images[0]}
|
|
||||||
alt="Main view"
|
|
||||||
fill
|
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="hidden md:grid grid-cols-1 gap-2 h-full">
|
|
||||||
<div className="relative h-full border-2 border-black group overflow-hidden">
|
|
||||||
<Image
|
|
||||||
src={property.images[1]}
|
|
||||||
alt="View 2"
|
|
||||||
fill
|
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="relative h-full border-2 border-black group overflow-hidden">
|
|
||||||
<Image
|
|
||||||
src={property.images[2]}
|
|
||||||
alt="View 3"
|
|
||||||
fill
|
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="hidden md:grid grid-cols-1 gap-2 h-full">
|
|
||||||
<div className="relative h-full border-2 border-black group overflow-hidden">
|
|
||||||
<Image
|
|
||||||
src={property.images[3]}
|
|
||||||
alt="View 4"
|
|
||||||
fill
|
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="relative h-full border-2 border-black group overflow-hidden">
|
|
||||||
<Image
|
|
||||||
src={property.images[4]}
|
|
||||||
alt="View 5"
|
|
||||||
fill
|
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
|
||||||
/>
|
|
||||||
<button className="absolute bottom-4 right-4 bg-white border-2 border-black px-4 py-2 text-sm font-medium hover:bg-[#E7FE78] transition-colors">
|
|
||||||
Show all photos
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="max-w-[95%] mx-auto grid grid-cols-1 lg:grid-cols-3 gap-12 mb-20">
|
<div className="max-w-[95%] mx-auto grid grid-cols-1 lg:grid-cols-3 gap-12 mb-20">
|
||||||
|
|
||||||
{/* Left Column */}
|
{/* Left Column */}
|
||||||
<div className="lg:col-span-2 space-y-10">
|
<div className="lg:col-span-2 space-y-10">
|
||||||
|
<HostInfo
|
||||||
{/* Host Info & Stats */}
|
host={property.host}
|
||||||
<div className="flex items-center justify-between pb-8 border-b-2 border-gray-100">
|
guests={property.guests}
|
||||||
<div>
|
bedrooms={property.bedrooms}
|
||||||
<h2 className="text-2xl font-medium mb-1">Hosted by {property.host.name}</h2>
|
beds={property.beds}
|
||||||
<p className="text-gray-600">
|
baths={property.baths}
|
||||||
{property.guests} guests · {property.bedrooms} bedrooms · {property.beds} beds · {property.baths} baths
|
/>
|
||||||
</p>
|
<Highlights />
|
||||||
</div>
|
<Description description={property.description} />
|
||||||
<div className="relative w-14 h-14 md:w-16 md:h-16">
|
<Amenities amenities={property.amenities} />
|
||||||
<Image
|
|
||||||
src={property.host.image}
|
|
||||||
alt={property.host.name}
|
|
||||||
fill
|
|
||||||
className="rounded-full object-cover border-2 border-black"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Highlights */}
|
|
||||||
<div className="space-y-6 pb-8 border-b-2 border-gray-100">
|
|
||||||
<div className="flex gap-4 items-start">
|
|
||||||
<div className="p-2 border-2 border-black bg-[#E7FE78]">
|
|
||||||
<Star className="w-6 h-6" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-lg">Top rated host</h3>
|
|
||||||
<p className="text-gray-600 text-sm">John has received 5-star ratings from 95% of recent guests.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-4 items-start">
|
|
||||||
<div className="p-2 border-2 border-black bg-white">
|
|
||||||
<MapPin className="w-6 h-6" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-medium text-lg">Great location</h3>
|
|
||||||
<p className="text-gray-600 text-sm">100% of recent guests gave the location a 5-star rating.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<div className="pb-8 border-b-2 border-gray-100">
|
|
||||||
<h3 className="text-2xl font-medium mb-4">About this place</h3>
|
|
||||||
<p className="text-gray-700 leading-relaxed text-lg font-light">
|
|
||||||
{property.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Amenities */}
|
|
||||||
<div className="pb-8 border-b-2 border-gray-100">
|
|
||||||
<h3 className="text-2xl font-medium mb-6">What this place offers</h3>
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
{property.amenities.slice(0, 6).map((item, idx) => (
|
|
||||||
<div key={idx} className="flex items-center gap-4 p-4 border-2 border-gray-100 hover:border-black transition-colors">
|
|
||||||
<item.icon className="w-6 h-6" />
|
|
||||||
<span className="text-lg font-light">{item.label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowAmenities(true)}
|
|
||||||
className="mt-6 px-6 py-3 border-2 border-black font-medium hover:bg-[#E7FE78] transition-colors"
|
|
||||||
>
|
|
||||||
Show all {property.amenities.length} amenities
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column - Sticky Booking Card */}
|
{/* Right Column - Sticky Booking Card */}
|
||||||
<div className="relative">
|
<BookingCard
|
||||||
<div className="sticky top-24 border-2 border-black p-6 bg-white shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
|
price={property.price}
|
||||||
<div className="flex items-end justify-between mb-6">
|
rating={property.rating}
|
||||||
<div>
|
reviewCount={property.reviewCount}
|
||||||
<span className="text-2xl font-bold">₨{property.price.toLocaleString()}</span>
|
/>
|
||||||
<span className="text-gray-600"> / night</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1 text-sm font-medium">
|
|
||||||
<Star className="w-4 h-4 fill-black" />
|
|
||||||
{property.rating} · <span className="text-gray-500 underline">{property.reviewCount} reviews</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="border-2 border-black mb-4">
|
|
||||||
<div className="grid grid-cols-2 border-b-2 border-black">
|
|
||||||
<div className="p-3 border-r-2 border-black">
|
|
||||||
<label className="block text-xs font-bold uppercase tracking-wider mb-1">Check-in</label>
|
|
||||||
<input type="date" className="w-full outline-none text-sm bg-transparent" />
|
|
||||||
</div>
|
|
||||||
<div className="p-3">
|
|
||||||
<label className="block text-xs font-bold uppercase tracking-wider mb-1">Check-out</label>
|
|
||||||
<input type="date" className="w-full outline-none text-sm bg-transparent" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="p-3">
|
|
||||||
<label className="block text-xs font-bold uppercase tracking-wider mb-1">Guests</label>
|
|
||||||
<select className="w-full outline-none text-sm bg-transparent">
|
|
||||||
<option>1 guest</option>
|
|
||||||
<option>2 guests</option>
|
|
||||||
<option>3 guests</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button className="w-full py-4 bg-black text-white font-medium text-lg hover:bg-[#E7FE78] hover:text-black border-2 border-black transition-all duration-300 mb-4">
|
|
||||||
Reserve
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<p className="text-center text-sm text-gray-500 mb-4">You won't be charged yet</p>
|
|
||||||
|
|
||||||
<div className="space-y-3 text-sm">
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="underline">₨{property.price.toLocaleString()} x 5 nights</span>
|
|
||||||
<span>₨{(property.price * 5).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="underline">Cleaning fee</span>
|
|
||||||
<span>₨5,000</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<span className="underline">Service fee</span>
|
|
||||||
<span>₨8,000</span>
|
|
||||||
</div>
|
|
||||||
<div className="pt-4 border-t-2 border-gray-100 flex justify-between font-bold text-lg">
|
|
||||||
<span>Total</span>
|
|
||||||
<span>₨{(property.price * 5 + 13000).toLocaleString()}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Reviews Section */}
|
<Reviews
|
||||||
<div className="max-w-[95%] mx-auto pb-20 border-t-2 border-gray-100 pt-12">
|
reviews={property.reviews}
|
||||||
<div className="flex items-center gap-2 mb-8">
|
rating={property.rating}
|
||||||
<Star className="w-6 h-6 fill-[#E7FE78] text-black" />
|
reviewCount={property.reviewCount}
|
||||||
<h2 className="text-2xl font-medium">{property.rating} · {property.reviewCount} reviews</h2>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
||||||
{property.reviews.slice(0, 4).map((review) => (
|
|
||||||
<div key={review.id} className="p-6 border-2 border-black hover:bg-gray-50 transition-colors">
|
|
||||||
<div className="flex items-center gap-4 mb-4">
|
|
||||||
<Image
|
|
||||||
src={review.avatar}
|
|
||||||
alt={review.user}
|
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
className="rounded-full border border-black"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium">{review.user}</h4>
|
|
||||||
<p className="text-sm text-gray-500">{review.date}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-700 font-light leading-relaxed">
|
|
||||||
"{review.comment}"
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowReviews(true)}
|
|
||||||
className="mt-8 px-8 py-3 border-2 border-black font-medium hover:bg-[#E7FE78] transition-colors"
|
|
||||||
>
|
|
||||||
Show all {property.reviewCount} reviews
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Amenities Modal */}
|
|
||||||
{showAmenities && (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setShowAmenities(false)} />
|
|
||||||
<div className="relative bg-white w-full max-w-2xl max-h-[85vh] rounded-xl shadow-2xl flex flex-col animate-in fade-in zoom-in duration-200">
|
|
||||||
<div className="flex items-center justify-between p-6 border-b">
|
|
||||||
<h2 className="text-2xl font-medium">What this place offers</h2>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowAmenities(false)}
|
|
||||||
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="p-6 overflow-y-auto flex-1">
|
|
||||||
<div className="space-y-8">
|
|
||||||
{Array.from(new Set(property.amenities.map(a => a.category))).map(category => (
|
|
||||||
<div key={category}>
|
|
||||||
<h3 className="font-medium text-lg mb-4">{category}</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{property.amenities.filter(a => a.category === category).map((item, idx) => (
|
|
||||||
<div key={idx} className="flex items-center gap-4 pb-4 border-b border-gray-50 last:border-0">
|
|
||||||
<item.icon className="w-6 h-6 text-gray-600" />
|
|
||||||
<span className="text-lg font-light">{item.label}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Reviews Modal */}
|
|
||||||
{showReviews && (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setShowReviews(false)} />
|
|
||||||
<div className="relative bg-white w-full max-w-4xl max-h-[85vh] rounded-xl shadow-2xl flex flex-col animate-in fade-in zoom-in duration-200">
|
|
||||||
<div className="flex items-center justify-between p-6 border-b">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Star className="w-5 h-5 fill-black" />
|
|
||||||
<h2 className="text-2xl font-medium">{property.rating} · {property.reviewCount} reviews</h2>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={() => setShowReviews(false)}
|
|
||||||
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
|
||||||
>
|
|
||||||
<X className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="p-8 overflow-y-auto flex-1">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
||||||
{property.reviews.map((review) => (
|
|
||||||
<div key={review.id} className="space-y-4">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<Image
|
|
||||||
src={review.avatar}
|
|
||||||
alt={review.user}
|
|
||||||
width={48}
|
|
||||||
height={48}
|
|
||||||
className="rounded-full border border-gray-200"
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<h4 className="font-medium">{review.user}</h4>
|
|
||||||
<p className="text-sm text-gray-500">{review.date}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
{[...Array(5)].map((_, i) => (
|
|
||||||
<Star
|
|
||||||
key={i}
|
|
||||||
className={`w-4 h-4 ${i < Math.floor(review.rating) ? 'fill-black text-black' : 'text-gray-300'}`}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-700 leading-relaxed">
|
|
||||||
{review.comment}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/components/details/header.tsx
Normal file
39
src/components/details/header.tsx
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Star, MapPin, Share, Heart } from "lucide-react";
|
||||||
|
import { Property } from "./types";
|
||||||
|
|
||||||
|
interface HeaderProps {
|
||||||
|
property: Property;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Header({ property }: HeaderProps) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-[95%] mx-auto pt-8 pb-6">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<h1 className="text-4xl font-medium uppercase tracking-tight">{property.title}</h1>
|
||||||
|
<div className="flex flex-row items-center justify-between w-full">
|
||||||
|
<div className="flex items-center gap-4 text-sm md:text-base">
|
||||||
|
<span className="flex items-center gap-1 font-medium underline cursor-pointer hover:text-gray-600">
|
||||||
|
<Star className="w-4 h-4 fill-black" />
|
||||||
|
{property.rating} · {property.reviewCount} reviews
|
||||||
|
</span>
|
||||||
|
<span className="hidden md:inline text-gray-400">|</span>
|
||||||
|
<span className="flex items-center gap-1 font-medium underline cursor-pointer hover:text-gray-600">
|
||||||
|
<MapPin className="w-4 h-4" />
|
||||||
|
{property.location}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button className="flex items-center gap-2 px-4 py-2 text-sm font-medium border-2 border-transparent hover:bg-gray-100 transition-colors rounded-full md:rounded-none md:border-black md:hover:bg-[#E7FE78]">
|
||||||
|
<Share className="w-4 h-4" />
|
||||||
|
<span className="hidden md:inline">Share</span>
|
||||||
|
</button>
|
||||||
|
<button className="flex items-center gap-2 px-4 py-2 text-sm font-medium border-2 border-transparent hover:bg-gray-100 transition-colors rounded-full md:rounded-none md:border-black md:hover:bg-[#E7FE78]">
|
||||||
|
<Heart className="w-4 h-4" />
|
||||||
|
<span className="hidden md:inline">Save</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
src/components/details/highlights.tsx
Normal file
26
src/components/details/highlights.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Star, MapPin } from "lucide-react";
|
||||||
|
|
||||||
|
export function Highlights() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 pb-8 border-b-2 border-gray-100">
|
||||||
|
<div className="flex gap-4 items-start">
|
||||||
|
<div className="p-2 border-2 border-black bg-[#E7FE78]">
|
||||||
|
<Star className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-lg">Top rated host</h3>
|
||||||
|
<p className="text-gray-600 text-sm">John has received 5-star ratings from 95% of recent guests.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 items-start">
|
||||||
|
<div className="p-2 border-2 border-black bg-white">
|
||||||
|
<MapPin className="w-6 h-6" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-medium text-lg">Great location</h3>
|
||||||
|
<p className="text-gray-600 text-sm">100% of recent guests gave the location a 5-star rating.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
31
src/components/details/host-info.tsx
Normal file
31
src/components/details/host-info.tsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import { Host } from "./types";
|
||||||
|
|
||||||
|
interface HostInfoProps {
|
||||||
|
host: Host;
|
||||||
|
guests: number;
|
||||||
|
bedrooms: number;
|
||||||
|
beds: number;
|
||||||
|
baths: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HostInfo({ host, guests, bedrooms, beds, baths }: HostInfoProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between pb-8 border-b-2 border-gray-100">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-medium mb-1">Hosted by {host.name}</h2>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
{guests} guests · {bedrooms} bedrooms · {beds} beds · {baths} baths
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="relative w-14 h-14 md:w-16 md:h-16">
|
||||||
|
<Image
|
||||||
|
src={host.image}
|
||||||
|
alt={host.name}
|
||||||
|
fill
|
||||||
|
className="rounded-full object-cover border-2 border-black"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
61
src/components/details/image-grid.tsx
Normal file
61
src/components/details/image-grid.tsx
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
interface ImageGridProps {
|
||||||
|
images: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ImageGrid({ images }: ImageGridProps) {
|
||||||
|
return (
|
||||||
|
<div className="max-w-[95%] mx-auto mb-12">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-2 h-[400px] md:h-[500px] rounded-xl overflow-hidden md:rounded-none">
|
||||||
|
<div className="md:col-span-2 h-full relative border-2 border-black group overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={images[0]}
|
||||||
|
alt="Main view"
|
||||||
|
fill
|
||||||
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:grid grid-cols-1 gap-2 h-full">
|
||||||
|
<div className="relative h-full border-2 border-black group overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={images[1]}
|
||||||
|
alt="View 2"
|
||||||
|
fill
|
||||||
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative h-full border-2 border-black group overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={images[2]}
|
||||||
|
alt="View 3"
|
||||||
|
fill
|
||||||
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:grid grid-cols-1 gap-2 h-full">
|
||||||
|
<div className="relative h-full border-2 border-black group overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={images[3]}
|
||||||
|
alt="View 4"
|
||||||
|
fill
|
||||||
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="relative h-full border-2 border-black group overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={images[4]}
|
||||||
|
alt="View 5"
|
||||||
|
fill
|
||||||
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
<button className="absolute bottom-4 right-4 bg-white border-2 border-black px-4 py-2 text-sm font-medium hover:bg-[#E7FE78] transition-colors">
|
||||||
|
Show all photos
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
118
src/components/details/reviews.tsx
Normal file
118
src/components/details/reviews.tsx
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Star, X } from "lucide-react";
|
||||||
|
import Image from "next/image";
|
||||||
|
import { Review } from "./types";
|
||||||
|
|
||||||
|
interface ReviewsProps {
|
||||||
|
reviews: Review[];
|
||||||
|
rating: number;
|
||||||
|
reviewCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Reviews({ reviews, rating, reviewCount }: ReviewsProps) {
|
||||||
|
const [showReviews, setShowReviews] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showReviews) {
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = 'unset';
|
||||||
|
};
|
||||||
|
}, [showReviews]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-[95%] mx-auto pb-20 border-t-2 border-gray-100 pt-12">
|
||||||
|
<div className="flex items-center gap-2 mb-8">
|
||||||
|
<Star className="w-6 h-6 fill-[#E7FE78] text-black" />
|
||||||
|
<h2 className="text-2xl font-medium">{rating} · {reviewCount} reviews</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
{reviews.slice(0, 4).map((review) => (
|
||||||
|
<div key={review.id} className="p-6 border-2 border-black hover:bg-gray-50 transition-colors">
|
||||||
|
<div className="flex items-center gap-4 mb-4">
|
||||||
|
<Image
|
||||||
|
src={review.avatar}
|
||||||
|
alt={review.user}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
className="rounded-full border border-black"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">{review.user}</h4>
|
||||||
|
<p className="text-sm text-gray-500">{review.date}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-700 font-light leading-relaxed">
|
||||||
|
"{review.comment}"
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowReviews(true)}
|
||||||
|
className="mt-8 px-8 py-3 border-2 border-black font-medium hover:bg-[#E7FE78] transition-colors"
|
||||||
|
>
|
||||||
|
Show all {reviewCount} reviews
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Reviews Modal */}
|
||||||
|
{showReviews && (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
||||||
|
<div className="absolute inset-0 bg-black/50 backdrop-blur-sm" onClick={() => setShowReviews(false)} />
|
||||||
|
<div className="relative bg-white w-full max-w-4xl max-h-[85vh] rounded-xl shadow-2xl flex flex-col animate-in fade-in zoom-in duration-200">
|
||||||
|
<div className="flex items-center justify-between p-6 border-b">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Star className="w-5 h-5 fill-black" />
|
||||||
|
<h2 className="text-2xl font-medium">{rating} · {reviewCount} reviews</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowReviews(false)}
|
||||||
|
className="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="p-8 overflow-y-auto flex-1">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||||
|
{reviews.map((review) => (
|
||||||
|
<div key={review.id} className="space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<Image
|
||||||
|
src={review.avatar}
|
||||||
|
alt={review.user}
|
||||||
|
width={48}
|
||||||
|
height={48}
|
||||||
|
className="rounded-full border border-gray-200"
|
||||||
|
/>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-medium">{review.user}</h4>
|
||||||
|
<p className="text-sm text-gray-500">{review.date}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
{[...Array(5)].map((_, i) => (
|
||||||
|
<Star
|
||||||
|
key={i}
|
||||||
|
className={`w-4 h-4 ${i < Math.floor(review.rating) ? 'fill-black text-black' : 'text-gray-300'}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="text-gray-700 leading-relaxed">
|
||||||
|
{review.comment}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
src/components/details/types.ts
Normal file
40
src/components/details/types.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
export interface Amenity {
|
||||||
|
icon: LucideIcon;
|
||||||
|
label: string;
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Review {
|
||||||
|
id: number;
|
||||||
|
user: string;
|
||||||
|
avatar: string;
|
||||||
|
date: string;
|
||||||
|
rating: number;
|
||||||
|
comment: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Host {
|
||||||
|
name: string;
|
||||||
|
image: string;
|
||||||
|
joined: string;
|
||||||
|
isSuperhost: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Property {
|
||||||
|
title: string;
|
||||||
|
location: string;
|
||||||
|
guests: number;
|
||||||
|
bedrooms: number;
|
||||||
|
beds: number;
|
||||||
|
baths: number;
|
||||||
|
rating: number;
|
||||||
|
reviewCount: number;
|
||||||
|
price: number;
|
||||||
|
description: string;
|
||||||
|
amenities: Amenity[];
|
||||||
|
host: Host;
|
||||||
|
images: string[];
|
||||||
|
reviews: Review[];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user