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"
|
||||
|
||||
import { Star, MapPin, Share, Heart, Wifi, Car, Utensils, Wind, Monitor, X, Waves, Dumbbell, Coffee, Briefcase, Droplets, Tv } from "lucide-react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Wifi, Car, Utensils, Wind, Monitor, Waves, Dumbbell, Coffee, Briefcase, Droplets, Tv } from "lucide-react";
|
||||
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({
|
||||
src: [
|
||||
@@ -13,7 +20,7 @@ const figtree = localFont({
|
||||
],
|
||||
})
|
||||
|
||||
const property = {
|
||||
const property: Property = {
|
||||
title: "Luxury Villa with Panoramic Mountain Views",
|
||||
location: "Islamabad, Pakistan",
|
||||
guests: 4,
|
||||
@@ -95,368 +102,40 @@ const property = {
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={`w-full bg-white ${figtree.className}`}>
|
||||
{/* Header Section */}
|
||||
<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>
|
||||
|
||||
{/* 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>
|
||||
<Header property={property} />
|
||||
<ImageGrid images={property.images} />
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="max-w-[95%] mx-auto grid grid-cols-1 lg:grid-cols-3 gap-12 mb-20">
|
||||
|
||||
{/* Left Column */}
|
||||
<div className="lg:col-span-2 space-y-10">
|
||||
|
||||
{/* Host Info & Stats */}
|
||||
<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 {property.host.name}</h2>
|
||||
<p className="text-gray-600">
|
||||
{property.guests} guests · {property.bedrooms} bedrooms · {property.beds} beds · {property.baths} baths
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative w-14 h-14 md:w-16 md:h-16">
|
||||
<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>
|
||||
|
||||
<HostInfo
|
||||
host={property.host}
|
||||
guests={property.guests}
|
||||
bedrooms={property.bedrooms}
|
||||
beds={property.beds}
|
||||
baths={property.baths}
|
||||
/>
|
||||
<Highlights />
|
||||
<Description description={property.description} />
|
||||
<Amenities amenities={property.amenities} />
|
||||
</div>
|
||||
|
||||
{/* Right Column - Sticky Booking Card */}
|
||||
<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">₨{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>
|
||||
<BookingCard
|
||||
price={property.price}
|
||||
rating={property.rating}
|
||||
reviewCount={property.reviewCount}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Reviews Section */}
|
||||
<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">{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>
|
||||
)}
|
||||
<Reviews
|
||||
reviews={property.reviews}
|
||||
rating={property.rating}
|
||||
reviewCount={property.reviewCount}
|
||||
/>
|
||||
</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