features components

This commit is contained in:
2025-11-20 20:32:35 +00:00
parent b7405d787c
commit f7fb8539d1
10 changed files with 513 additions and 354 deletions

View 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>
);
}

View 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>
);
}

View 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>
);
}

View File

@@ -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>
)
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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[];
}