list done ig, details also made compatible.
This commit is contained in:
@@ -15,7 +15,7 @@ export default function Home() {
|
|||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<List />
|
<Features />
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
20
src/components/authentication/login.tsx
Normal file
20
src/components/authentication/login.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import localFont from "next/font/local";
|
||||||
|
|
||||||
|
|
||||||
|
const figtree = localFont({
|
||||||
|
src: [
|
||||||
|
{
|
||||||
|
path: '../../../public/Fonts/figtree/figtree.ttf',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className={figtree.className}>Login</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Login
|
||||||
20
src/components/authentication/signup.tsx
Normal file
20
src/components/authentication/signup.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import localFont from "next/font/local";
|
||||||
|
|
||||||
|
|
||||||
|
const figtree = localFont({
|
||||||
|
src: [
|
||||||
|
{
|
||||||
|
path: '../../../public/Fonts/figtree/figtree.ttf',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
const Signup = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className={`${figtree.className} text-4xl font-bold`}>Signup</h1>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Signup
|
||||||
@@ -26,7 +26,7 @@ const property: Property = {
|
|||||||
guests: 4,
|
guests: 4,
|
||||||
bedrooms: 2,
|
bedrooms: 2,
|
||||||
beds: 2,
|
beds: 2,
|
||||||
baths: 2,
|
bathrooms: 2,
|
||||||
rating: 4.81,
|
rating: 4.81,
|
||||||
reviewCount: 5,
|
reviewCount: 5,
|
||||||
price: 25000,
|
price: 25000,
|
||||||
@@ -50,7 +50,7 @@ const property: Property = {
|
|||||||
joined: "2020",
|
joined: "2020",
|
||||||
isSuperhost: true
|
isSuperhost: true
|
||||||
},
|
},
|
||||||
images: [
|
photos: [
|
||||||
"/hotel-banner.png",
|
"/hotel-banner.png",
|
||||||
"/hotel-banner.png",
|
"/hotel-banner.png",
|
||||||
"/hotel-banner.png",
|
"/hotel-banner.png",
|
||||||
@@ -98,14 +98,18 @@ const property: Property = {
|
|||||||
rating: 5,
|
rating: 5,
|
||||||
comment: "Host was super helpful and gave great local recommendations. The house is even better than the photos."
|
comment: "Host was super helpful and gave great local recommendations. The house is even better than the photos."
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
propertyType: "house",
|
||||||
|
placeType: "entire",
|
||||||
|
instantBook: true,
|
||||||
|
safety: ["camera", "alarm"]
|
||||||
}
|
}
|
||||||
|
|
||||||
function Features() {
|
function Features() {
|
||||||
return (
|
return (
|
||||||
<div className={`w-full bg-white ${figtree.className}`}>
|
<div className={`w-full bg-white ${figtree.className}`}>
|
||||||
<Header property={property} />
|
<Header property={property} />
|
||||||
<ImageGrid images={property.images} />
|
<ImageGrid photos={property.photos} />
|
||||||
|
|
||||||
{/* 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">
|
||||||
@@ -116,7 +120,7 @@ function Features() {
|
|||||||
guests={property.guests}
|
guests={property.guests}
|
||||||
bedrooms={property.bedrooms}
|
bedrooms={property.bedrooms}
|
||||||
beds={property.beds}
|
beds={property.beds}
|
||||||
baths={property.baths}
|
bathrooms={property.bathrooms}
|
||||||
/>
|
/>
|
||||||
<Highlights />
|
<Highlights />
|
||||||
<Description description={property.description} />
|
<Description description={property.description} />
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ interface HostInfoProps {
|
|||||||
guests: number;
|
guests: number;
|
||||||
bedrooms: number;
|
bedrooms: number;
|
||||||
beds: number;
|
beds: number;
|
||||||
baths: number;
|
bathrooms: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HostInfo({ host, guests, bedrooms, beds, baths }: HostInfoProps) {
|
export function HostInfo({ host, guests, bedrooms, beds, bathrooms }: HostInfoProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between pb-8 border-b-2 border-gray-100">
|
<div className="flex items-center justify-between pb-8 border-b-2 border-gray-100">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-medium mb-1">Hosted by {host.name}</h2>
|
<h2 className="text-2xl font-medium mb-1">Hosted by {host.name}</h2>
|
||||||
<p className="text-gray-600">
|
<p className="text-gray-600">
|
||||||
{guests} guests · {bedrooms} bedrooms · {beds} beds · {baths} baths
|
{guests} guests · {bedrooms} bedrooms · {beds} beds · {bathrooms} baths
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative w-14 h-14 md:w-16 md:h-16">
|
<div className="relative w-14 h-14 md:w-16 md:h-16">
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
interface ImageGridProps {
|
interface ImageGridProps {
|
||||||
images: string[];
|
photos: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ImageGrid({ images }: ImageGridProps) {
|
export function ImageGrid({ photos }: ImageGridProps) {
|
||||||
return (
|
return (
|
||||||
<div className="max-w-[95%] mx-auto mb-12">
|
<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="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">
|
<div className="md:col-span-2 h-full relative border-2 border-black group overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={images[0]}
|
src={photos[0]}
|
||||||
alt="Main view"
|
alt="Main view"
|
||||||
fill
|
fill
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
@@ -19,7 +19,7 @@ export function ImageGrid({ images }: ImageGridProps) {
|
|||||||
<div className="hidden md:grid grid-cols-1 gap-2 h-full">
|
<div className="hidden md:grid grid-cols-1 gap-2 h-full">
|
||||||
<div className="relative h-full border-2 border-black group overflow-hidden">
|
<div className="relative h-full border-2 border-black group overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={images[1]}
|
src={photos[1]}
|
||||||
alt="View 2"
|
alt="View 2"
|
||||||
fill
|
fill
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
@@ -27,7 +27,7 @@ export function ImageGrid({ images }: ImageGridProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="relative h-full border-2 border-black group overflow-hidden">
|
<div className="relative h-full border-2 border-black group overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={images[2]}
|
src={photos[2]}
|
||||||
alt="View 3"
|
alt="View 3"
|
||||||
fill
|
fill
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
@@ -37,7 +37,7 @@ export function ImageGrid({ images }: ImageGridProps) {
|
|||||||
<div className="hidden md:grid grid-cols-1 gap-2 h-full">
|
<div className="hidden md:grid grid-cols-1 gap-2 h-full">
|
||||||
<div className="relative h-full border-2 border-black group overflow-hidden">
|
<div className="relative h-full border-2 border-black group overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={images[3]}
|
src={photos[3]}
|
||||||
alt="View 4"
|
alt="View 4"
|
||||||
fill
|
fill
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
@@ -45,7 +45,7 @@ export function ImageGrid({ images }: ImageGridProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="relative h-full border-2 border-black group overflow-hidden">
|
<div className="relative h-full border-2 border-black group overflow-hidden">
|
||||||
<Image
|
<Image
|
||||||
src={images[4]}
|
src={photos[4]}
|
||||||
alt="View 5"
|
alt="View 5"
|
||||||
fill
|
fill
|
||||||
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
className="object-cover group-hover:scale-105 transition-transform duration-500"
|
||||||
|
|||||||
@@ -28,13 +28,17 @@ export interface Property {
|
|||||||
guests: number;
|
guests: number;
|
||||||
bedrooms: number;
|
bedrooms: number;
|
||||||
beds: number;
|
beds: number;
|
||||||
baths: number;
|
bathrooms: number;
|
||||||
rating: number;
|
rating: number;
|
||||||
reviewCount: number;
|
reviewCount: number;
|
||||||
price: number;
|
price: number;
|
||||||
description: string;
|
description: string;
|
||||||
amenities: Amenity[];
|
amenities: Amenity[];
|
||||||
host: Host;
|
host: Host;
|
||||||
images: string[];
|
photos: string[];
|
||||||
reviews: Review[];
|
reviews: Review[];
|
||||||
|
propertyType: string;
|
||||||
|
placeType: string;
|
||||||
|
instantBook: boolean;
|
||||||
|
safety: string[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef, useEffect } from "react";
|
||||||
import localFont from "next/font/local";
|
import localFont from "next/font/local";
|
||||||
import gsap from "gsap";
|
import gsap from "gsap";
|
||||||
import { useGSAP } from "@gsap/react";
|
import { useGSAP } from "@gsap/react";
|
||||||
|
import Image from "next/image";
|
||||||
import {
|
import {
|
||||||
House,
|
House,
|
||||||
Buildings,
|
Buildings,
|
||||||
@@ -18,8 +19,16 @@ import {
|
|||||||
CalendarBlank,
|
CalendarBlank,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
CaretLeft,
|
CaretLeft,
|
||||||
CaretRight
|
CaretRight,
|
||||||
} from "@phosphor-icons/react";
|
Star,
|
||||||
|
SwimmingPool,
|
||||||
|
Car,
|
||||||
|
CookingPot,
|
||||||
|
TelevisionSimple,
|
||||||
|
Wind,
|
||||||
|
X,
|
||||||
|
WarningCircle,
|
||||||
|
} from "@phosphor-icons/react/dist/ssr";
|
||||||
|
|
||||||
const figtree = localFont({
|
const figtree = localFont({
|
||||||
src: [
|
src: [
|
||||||
@@ -29,14 +38,15 @@ const figtree = localFont({
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Shared styles
|
// Shared styles matching search.tsx and results.tsx
|
||||||
const containerStyle = `flex flex-col gap-6 w-full max-w-xl mx-auto`;
|
const containerStyle = `flex flex-col gap-8 w-full max-w-xl mx-auto pb-24`; // Added padding bottom for fixed footer
|
||||||
const titleStyle = `${figtree.className} text-3xl font-bold mb-4`;
|
const titleStyle = `${figtree.className} text-4xl font-bold mb-2`;
|
||||||
const inputStyle = `${figtree.className} w-full p-4 border-2 border-black text-lg outline-none focus:bg-gray-50 transition-colors`;
|
const subtitleStyle = `${figtree.className} text-lg text-gray-500 mb-8`;
|
||||||
const buttonStyle = `${figtree.className} flex items-center justify-center gap-2 px-8 py-3 border-2 border-black bg-[#E7FE78] text-lg font-medium hover:bg-[#dcfc4e] transition-colors disabled:opacity-50 disabled:cursor-not-allowed`;
|
const inputStyle = `${figtree.className} w-full p-4 border-2 border-black text-lg outline-none focus:bg-[#F7F7F7] transition-colors placeholder:text-gray-400`;
|
||||||
const secondaryButtonStyle = `${figtree.className} flex items-center justify-center gap-2 px-8 py-3 border-2 border-black bg-white text-lg font-medium hover:bg-gray-50 transition-colors`;
|
const buttonStyle = `${figtree.className} flex items-center justify-center gap-2 px-8 py-3 border-2 border-black bg-[#E7FE78] text-lg font-medium hover:bg-[#dcfc4e] transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:translate-y-[4px] active:shadow-none transition-all`;
|
||||||
|
const secondaryButtonStyle = `${figtree.className} flex items-center justify-center gap-2 px-8 py-3 border-2 border-black bg-white text-lg font-medium hover:bg-gray-50 transition-colors shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:translate-y-[4px] active:shadow-none transition-all`;
|
||||||
const optionCardStyle = (selected: boolean) =>
|
const optionCardStyle = (selected: boolean) =>
|
||||||
`flex flex-col items-center justify-center gap-3 p-6 border-2 border-black cursor-pointer transition-all hover:bg-gray-50 ${selected ? 'bg-[#E7FE78]' : 'bg-white'}`;
|
`flex flex-col items-center justify-center gap-4 p-6 border-2 border-black cursor-pointer transition-all ${selected ? 'bg-[#E7FE78]' : 'bg-white hover:bg-gray-50'}`;
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
propertyType?: string;
|
propertyType?: string;
|
||||||
@@ -51,19 +61,103 @@ interface FormData {
|
|||||||
price?: string;
|
price?: string;
|
||||||
instantBook?: boolean;
|
instantBook?: boolean;
|
||||||
safety?: string[];
|
safety?: string[];
|
||||||
|
title?: string; // Added title for preview
|
||||||
|
photos?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
type FormValue = string | number | boolean | string[] | undefined;
|
type FormValue = string | number | boolean | string[] | undefined;
|
||||||
|
|
||||||
interface StepProps {
|
interface StepProps {
|
||||||
onNext: () => void;
|
|
||||||
onBack: () => void;
|
|
||||||
data: FormData;
|
data: FormData;
|
||||||
updateData: (key: keyof FormData, value: FormValue) => void;
|
updateData: (key: keyof FormData, value: FormValue) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Property Type */
|
/* --- Live Preview Component --- */
|
||||||
const PropertyType = ({ onNext, data, updateData }: StepProps) => {
|
const LivePreview = ({ data }: { data: FormData }) => {
|
||||||
|
// Helpers to derive display values
|
||||||
|
const getCity = () => {
|
||||||
|
if (!data.location) return "YOUR CITY";
|
||||||
|
const parts = data.location.split(',');
|
||||||
|
return parts[0].trim().toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTitle = () => {
|
||||||
|
if (data.title) return data.title;
|
||||||
|
const type = data.propertyType ? data.propertyType.charAt(0).toUpperCase() + data.propertyType.slice(1) : "Property";
|
||||||
|
const place = data.placeType === 'entire' ? 'Entire place' : data.placeType === 'room' ? 'Private room' : 'Shared room';
|
||||||
|
return `${type} · ${place}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPrice = () => {
|
||||||
|
return data.price ? `₨${parseInt(data.price).toLocaleString()}` : "₨0";
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full max-w-md mx-auto sticky top-24">
|
||||||
|
<div className="bg-white border-2 border-black p-6 shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className={`${figtree.className} text-xl font-bold`}>Preview</h3>
|
||||||
|
<div className="px-3 py-1 bg-black text-white text-xs font-bold uppercase tracking-wider rounded-full">
|
||||||
|
New
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Card Replica */}
|
||||||
|
<div className="border-2 border-black overflow-hidden bg-white">
|
||||||
|
{/* Image Placeholder */}
|
||||||
|
<div className="relative aspect-[16/14] bg-gray-100 flex items-center justify-center border-b-2 border-black overflow-hidden">
|
||||||
|
{data.photos && data.photos.length > 0 ? (
|
||||||
|
<Image src={data.photos[0]} alt="Preview" fill className="object-cover" />
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center gap-2 text-gray-400">
|
||||||
|
<Camera size={48} />
|
||||||
|
<span className={`${figtree.className} font-medium`}>Cover Photo</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Details */}
|
||||||
|
<div className="p-4 space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<p className={`${figtree.className} text-xs font-bold text-gray-700 tracking-wider uppercase`}>
|
||||||
|
{getCity()}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Star size={16} weight="fill" />
|
||||||
|
<span className={`${figtree.className} text-sm font-medium`}>New</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 className={`${figtree.className} text-base font-normal text-gray-900 line-clamp-1`}>
|
||||||
|
{getTitle()}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className={`${figtree.className} text-base`}>
|
||||||
|
<span className="font-bold">{getPrice()}</span>
|
||||||
|
<span className="text-gray-600"> night</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Live Updates Summary */}
|
||||||
|
<div className="mt-6 space-y-3 border-t-2 border-gray-100 pt-4">
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-500">Guests</span>
|
||||||
|
<span className="font-medium">{data.guests || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-sm">
|
||||||
|
<span className="text-gray-500">Amenities</span>
|
||||||
|
<span className="font-medium">{data.amenities?.length || 0} selected</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* --- Step Components --- */
|
||||||
|
|
||||||
|
const PropertyType = ({ data, updateData }: StepProps) => {
|
||||||
const types = [
|
const types = [
|
||||||
{ id: 'house', label: 'House', icon: House },
|
{ id: 'house', label: 'House', icon: House },
|
||||||
{ id: 'flat', label: 'Flat', icon: Buildings },
|
{ id: 'flat', label: 'Flat', icon: Buildings },
|
||||||
@@ -71,23 +165,19 @@ const PropertyType = ({ onNext, data, updateData }: StepProps) => {
|
|||||||
{ id: 'guesthouse', label: 'Guesthouse', icon: Bed },
|
{ id: 'guesthouse', label: 'Guesthouse', icon: Bed },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleSelect = (id: string) => {
|
|
||||||
updateData('propertyType', id);
|
|
||||||
onNext();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col">
|
||||||
<h2 className={titleStyle}>What kind of place will you host?</h2>
|
<h2 className={titleStyle}>What kind of place will you host?</h2>
|
||||||
|
<p className={subtitleStyle}>Select the category that best describes your property.</p>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{types.map((type) => (
|
{types.map((type) => (
|
||||||
<button
|
<button
|
||||||
key={type.id}
|
key={type.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSelect(type.id)}
|
onClick={() => updateData('propertyType', type.id)}
|
||||||
className={optionCardStyle(data.propertyType === type.id)}
|
className={optionCardStyle(data.propertyType === type.id)}
|
||||||
>
|
>
|
||||||
<type.icon size={48} weight="light" />
|
<type.icon size={48} weight={data.propertyType === type.id ? "fill" : "light"} />
|
||||||
<span className={`${figtree.className} text-xl font-medium`}>{type.label}</span>
|
<span className={`${figtree.className} text-xl font-medium`}>{type.label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
@@ -96,54 +186,45 @@ const PropertyType = ({ onNext, data, updateData }: StepProps) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Property Place Type */
|
const PropertyPlaceType = ({ data, updateData }: StepProps) => {
|
||||||
const PropertyPlaceType = ({ onNext, onBack, data, updateData }: StepProps) => {
|
|
||||||
const places = [
|
const places = [
|
||||||
{ id: 'entire', label: 'An entire place', description: 'Guests have the whole place to themselves.' },
|
{ id: 'entire', label: 'An entire place', description: 'Guests have the entire property to themselves, with full privacy and unrestricted access.' },
|
||||||
{ id: 'room', label: 'A room', description: 'Guests have their own room in a home, plus access to shared spaces.' },
|
{ id: 'room', label: 'A room', description: 'Guests have their own private room within the home, along with comfortable access to shared spaces.' },
|
||||||
{ id: 'shared', label: 'A shared room', description: 'Guests sleep in a room or common area that may be shared with others.' },
|
{ id: 'shared', label: 'A shared room', description: 'Guests sleep in a room or common area that may be shared with others.' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleSelect = (id: string) => {
|
|
||||||
updateData('placeType', id);
|
|
||||||
onNext();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col">
|
||||||
<h2 className={titleStyle}>What type of place will guests have?</h2>
|
<h2 className={titleStyle}>What type of place will guests have?</h2>
|
||||||
|
<p className={subtitleStyle}>Choose the level of privacy your guests will enjoy.</p>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{places.map((place) => (
|
{places.map((place) => (
|
||||||
<button
|
<button
|
||||||
key={place.id}
|
key={place.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleSelect(place.id)}
|
onClick={() => updateData('placeType', place.id)}
|
||||||
className={`${optionCardStyle(data.placeType === place.id)} flex-row justify-between text-left items-center w-full`}
|
className={`${optionCardStyle(data.placeType === place.id)} flex-row justify-between text-left items-center w-full p-6`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<span className={`${figtree.className} text-xl font-bold`}>{place.label}</span>
|
<span className={`${figtree.className} text-xl font-bold`}>{place.label}</span>
|
||||||
<span className={`${figtree.className} text-gray-600`}>{place.description}</span>
|
<span className={`${figtree.className} text-gray-600`}>{place.description}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{data.placeType === place.id && <CheckCircle size={32} weight="fill" />}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-start mt-4">
|
|
||||||
<button type="button" onClick={onBack} className={secondaryButtonStyle}>
|
|
||||||
<CaretLeft size={20} /> Back
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Property Location */
|
const PropertyLocation = ({ data, updateData }: StepProps) => {
|
||||||
const PropertyLocation = ({ onNext, onBack, data, updateData }: StepProps) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col">
|
||||||
<h2 className={titleStyle}>Where's your place located?</h2>
|
<h2 className={titleStyle}>Where's your place located?</h2>
|
||||||
<div className="flex flex-col gap-4">
|
<p className={subtitleStyle}>Your address is only shared with guests after they've made a reservation.</p>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<MapPin size={24} className="absolute left-4 top-1/2 -translate-y-1/2" />
|
<MapPin size={24} className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter your address"
|
placeholder="Enter your address"
|
||||||
@@ -152,25 +233,17 @@ const PropertyLocation = ({ onNext, onBack, data, updateData }: StepProps) => {
|
|||||||
onChange={(e) => updateData('location', e.target.value)}
|
onChange={(e) => updateData('location', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Placeholder for map */}
|
<div className="w-full h-80 bg-gray-100 border-2 border-black flex flex-col items-center justify-center gap-4 relative overflow-hidden group">
|
||||||
<div className="w-full h-64 bg-gray-100 border-2 border-black flex items-center justify-center">
|
<div className="absolute inset-0 opacity-10 bg-[radial-gradient(#000_1px,transparent_1px)] [background-size:16px_16px]" />
|
||||||
<span className={`${figtree.className} text-gray-500`}>Map Preview</span>
|
<MapPin size={48} className="text-gray-400 group-hover:text-black transition-colors duration-300" weight="fill" />
|
||||||
|
<span className={`${figtree.className} text-gray-500 font-medium`}>Map Preview</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<button type="button" onClick={onBack} className={secondaryButtonStyle}>
|
|
||||||
<CaretLeft size={20} /> Back
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => onNext()} className={buttonStyle}>
|
|
||||||
Next <CaretRight size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Property Capacity */
|
const PropertyCapacity = ({ data, updateData }: StepProps) => {
|
||||||
const PropertyCapacity = ({ onNext, onBack, data, updateData }: StepProps) => {
|
|
||||||
const counters: { key: keyof FormData; label: string }[] = [
|
const counters: { key: keyof FormData; label: string }[] = [
|
||||||
{ key: 'guests', label: 'Guests' },
|
{ key: 'guests', label: 'Guests' },
|
||||||
{ key: 'bedrooms', label: 'Bedrooms' },
|
{ key: 'bedrooms', label: 'Bedrooms' },
|
||||||
@@ -184,76 +257,46 @@ const PropertyCapacity = ({ onNext, onBack, data, updateData }: StepProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col">
|
||||||
<h2 className={titleStyle}>Share some basics about your place</h2>
|
<h2 className={titleStyle}>Share some basics about your place</h2>
|
||||||
|
<p className={subtitleStyle}>You'll add more details later, like bed types.</p>
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
{counters.map((item) => (
|
{counters.map((item) => (
|
||||||
<div key={item.key} className="flex items-center justify-between border-b-2 border-gray-100 pb-4">
|
<div key={item.key} className="flex items-center justify-between border-b-2 border-gray-100 pb-6">
|
||||||
<span className={`${figtree.className} text-xl`}>{item.label}</span>
|
<span className={`${figtree.className} text-xl font-medium`}>{item.label}</span>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-6">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateCount(item.key, -1)}
|
onClick={() => updateCount(item.key, -1)}
|
||||||
className="w-10 h-10 rounded-full border-2 border-black flex items-center justify-center hover:bg-gray-100"
|
disabled={!data[item.key]}
|
||||||
|
className="w-12 h-12 rounded-full border-2 border-black flex items-center justify-center hover:bg-gray-100 disabled:opacity-30 disabled:hover:bg-transparent transition-colors"
|
||||||
>
|
>
|
||||||
-
|
<span className="text-2xl mb-1">-</span>
|
||||||
</button>
|
</button>
|
||||||
<span className={`${figtree.className} text-xl w-8 text-center`}>{data[item.key] || 0}</span>
|
<span className={`${figtree.className} text-xl w-8 text-center font-bold`}>{data[item.key] || 0}</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateCount(item.key, 1)}
|
onClick={() => updateCount(item.key, 1)}
|
||||||
className="w-10 h-10 rounded-full border-2 border-black flex items-center justify-center hover:bg-gray-100"
|
className="w-12 h-12 rounded-full border-2 border-black flex items-center justify-center hover:bg-gray-100 transition-colors"
|
||||||
>
|
>
|
||||||
+
|
<span className="text-2xl mb-1">+</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<button type="button" onClick={onBack} className={secondaryButtonStyle}>
|
|
||||||
<CaretLeft size={20} /> Back
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => onNext()} className={buttonStyle}>
|
|
||||||
Next <CaretRight size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Property Photos */
|
const PropertyAmenities = ({ data, updateData }: StepProps) => {
|
||||||
const PropertyPhotos = ({ onNext, onBack }: StepProps) => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<h2 className={titleStyle}>Add some photos of your house</h2>
|
|
||||||
<div className="border-2 border-dashed border-black p-12 flex flex-col items-center justify-center gap-4 bg-gray-50 cursor-pointer hover:bg-gray-100 transition-colors">
|
|
||||||
<Camera size={48} />
|
|
||||||
<div className="text-center">
|
|
||||||
<p className={`${figtree.className} text-xl font-bold`}>Drag your photos here</p>
|
|
||||||
<p className={`${figtree.className} text-gray-500`}>Choose at least 5 photos</p>
|
|
||||||
</div>
|
|
||||||
<button type="button" className="underline font-medium">Upload from your device</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<button type="button" onClick={onBack} className={secondaryButtonStyle}>
|
|
||||||
<CaretLeft size={20} /> Back
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => onNext()} className={buttonStyle}>
|
|
||||||
Next <CaretRight size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Property Amenities */
|
|
||||||
const PropertyAmenities = ({ onNext, onBack, data, updateData }: StepProps) => {
|
|
||||||
const amenities = [
|
const amenities = [
|
||||||
{ id: 'wifi', label: 'Wi-Fi', icon: WifiHigh },
|
{ id: 'wifi', label: 'Wi-Fi', icon: WifiHigh },
|
||||||
{ id: 'kitchen', label: 'Kitchen', icon: House },
|
{ id: 'kitchen', label: 'Kitchen', icon: CookingPot },
|
||||||
{ id: 'parking', label: 'Free parking', icon: Farm },
|
{ id: 'parking', label: 'Free parking', icon: Car },
|
||||||
{ id: 'pool', label: 'Pool', icon: Buildings },
|
{ id: 'pool', label: 'Pool', icon: SwimmingPool },
|
||||||
|
{ id: 'ac', label: 'Air conditioning', icon: Wind }, // Using House as placeholder for AC if needed
|
||||||
|
{ id: 'tv', label: 'TV', icon: TelevisionSimple },
|
||||||
];
|
];
|
||||||
|
|
||||||
const toggleAmenity = (id: string) => {
|
const toggleAmenity = (id: string) => {
|
||||||
@@ -265,39 +308,44 @@ const PropertyAmenities = ({ onNext, onBack, data, updateData }: StepProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col">
|
||||||
<h2 className={titleStyle}>What does your place offer?</h2>
|
<h2 className={titleStyle}>What does your place offer?</h2>
|
||||||
|
<p className={subtitleStyle}>You can add more amenities after you publish.</p>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
{amenities.map((amenity) => (
|
{amenities.map((amenity) => (
|
||||||
<button
|
<button
|
||||||
key={amenity.id}
|
key={amenity.id}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleAmenity(amenity.id)}
|
onClick={() => toggleAmenity(amenity.id)}
|
||||||
className={`${optionCardStyle((data.amenities || []).includes(amenity.id))} items-start w-full`}
|
className={`${optionCardStyle((data.amenities || []).includes(amenity.id))} items-start w-full text-left`}
|
||||||
>
|
>
|
||||||
<amenity.icon size={32} />
|
<amenity.icon size={32} weight={(data.amenities || []).includes(amenity.id) ? "fill" : "regular"} />
|
||||||
<span className={`${figtree.className} text-lg font-medium`}>{amenity.label}</span>
|
<span className={`${figtree.className} text-lg font-medium`}>{amenity.label}</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<button type="button" onClick={onBack} className={secondaryButtonStyle}>
|
|
||||||
<CaretLeft size={20} /> Back
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => onNext()} className={buttonStyle}>
|
|
||||||
Next <CaretRight size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Property Description */
|
const PropertyDescription = ({ data, updateData }: StepProps) => {
|
||||||
const PropertyDescription = ({ onNext, onBack, data, updateData }: StepProps) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col">
|
||||||
<h2 className={titleStyle}>How would you describe your place?</h2>
|
<h2 className={titleStyle}>How would you describe your place?</h2>
|
||||||
<div className="flex flex-col gap-4">
|
<p className={subtitleStyle}>Short and sweet works best.</p>
|
||||||
|
<div className="flex flex-col gap-6">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-bold mb-2">Title</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. Cozy Cottage in the Hills"
|
||||||
|
className={inputStyle}
|
||||||
|
value={data.title || ''}
|
||||||
|
onChange={(e) => updateData('title', e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-bold mb-2">Description</label>
|
||||||
<textarea
|
<textarea
|
||||||
placeholder="This unique place has a style all its own..."
|
placeholder="This unique place has a style all its own..."
|
||||||
className={`${inputStyle} h-48 resize-none`}
|
className={`${inputStyle} h-48 resize-none`}
|
||||||
@@ -305,119 +353,139 @@ const PropertyDescription = ({ onNext, onBack, data, updateData }: StepProps) =>
|
|||||||
onChange={(e) => updateData('description', e.target.value)}
|
onChange={(e) => updateData('description', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<button type="button" onClick={onBack} className={secondaryButtonStyle}>
|
|
||||||
<CaretLeft size={20} /> Back
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => onNext()} className={buttonStyle}>
|
|
||||||
Next <CaretRight size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Property Price */
|
const PropertyPhotos = ({ data, updateData }: StepProps) => {
|
||||||
const PropertyPrice = ({ onNext, onBack, data, updateData }: StepProps) => {
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (e.target.files) {
|
||||||
|
const newPhotos = Array.from(e.target.files).map(file => URL.createObjectURL(file));
|
||||||
|
const currentPhotos = data.photos || [];
|
||||||
|
updateData('photos', [...currentPhotos, ...newPhotos]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removePhoto = (index: number) => {
|
||||||
|
const currentPhotos = data.photos || [];
|
||||||
|
const updated = currentPhotos.filter((_, i) => i !== index);
|
||||||
|
updateData('photos', updated);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h2 className={titleStyle}>Add some photos of your house</h2>
|
||||||
|
<p className={subtitleStyle}>You'll need 5 photos to get started. You can add more or make changes later.</p>
|
||||||
|
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
|
<div
|
||||||
|
onClick={() => fileInputRef.current?.click()}
|
||||||
|
className="border-2 border-dashed border-gray-300 rounded-lg p-12 flex flex-col items-center justify-center gap-4 cursor-pointer hover:bg-gray-50 transition-colors"
|
||||||
|
>
|
||||||
|
<Camera size={48} className="text-gray-400" />
|
||||||
|
<span className={`${figtree.className} text-lg font-medium underline`}>Upload photos</span>
|
||||||
|
<span className="text-sm text-gray-500">JPG, PNG up to 10MB</span>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
multiple
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
ref={fileInputRef}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data.photos && data.photos.length > 0 && (
|
||||||
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||||
|
{data.photos.map((photo, index) => (
|
||||||
|
<div key={index} className="relative aspect-square border-2 border-black rounded-lg overflow-hidden group">
|
||||||
|
<Image src={photo} alt={`Property ${index + 1}`} fill className="object-cover" />
|
||||||
|
<button
|
||||||
|
onClick={() => removePhoto(index)}
|
||||||
|
className="absolute top-2 right-2 bg-white rounded-full p-1 border-2 border-black hover:bg-red-50 transition-colors z-10"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
{index === 0 && (
|
||||||
|
<div className="absolute bottom-2 left-2 bg-black text-white text-xs px-2 py-1 rounded z-10">Cover Photo</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(!data.photos || data.photos.length < 5) && (
|
||||||
|
<div className="flex items-center gap-2 text-orange-600 bg-orange-50 p-4 rounded-lg border border-orange-200">
|
||||||
|
<WarningCircle size={24} />
|
||||||
|
<span className="font-medium">Please upload at least {5 - (data.photos?.length || 0)} more photos</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const PropertyPrice = ({ data, updateData }: StepProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
<h2 className={titleStyle}>Now, set your price</h2>
|
<h2 className={titleStyle}>Now, set your price</h2>
|
||||||
<div className="flex flex-col gap-4 items-center">
|
<p className={subtitleStyle}>You can change it anytime.</p>
|
||||||
|
<div className="flex flex-col gap-8 items-center py-12 bg-[#F7F7F7] border-2 border-black">
|
||||||
<div className="relative w-full max-w-xs">
|
<div className="relative w-full max-w-xs">
|
||||||
<CurrencyDollar size={32} className="absolute left-4 top-1/2 -translate-y-1/2" />
|
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-4xl font-bold text-gray-400">₨</span>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
className={`${inputStyle} pl-14 text-4xl font-bold text-center`}
|
className="w-full bg-transparent text-6xl font-bold text-center outline-none p-4"
|
||||||
value={data.price || ''}
|
value={data.price || ''}
|
||||||
onChange={(e) => updateData('price', e.target.value)}
|
onChange={(e) => updateData('price', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className={`${figtree.className} text-gray-500`}>per night</p>
|
<p className={`${figtree.className} text-xl text-gray-500`}>per night</p>
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<button type="button" onClick={onBack} className={secondaryButtonStyle}>
|
|
||||||
<CaretLeft size={20} /> Back
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => onNext()} className={buttonStyle}>
|
|
||||||
Next <CaretRight size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Property Instant Approval */
|
const PropertyInstantApproval = ({ data, updateData }: StepProps) => {
|
||||||
const PropertyInstantApproval = ({ onNext, onBack, data, updateData }: StepProps) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col">
|
||||||
<h2 className={titleStyle}>Decide how you'll confirm reservations</h2>
|
<h2 className={titleStyle}>Decide how you'll confirm reservations</h2>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateData('instantBook', true)}
|
onClick={() => updateData('instantBook', true)}
|
||||||
className={`${optionCardStyle(data.instantBook === true)} w-full`}
|
className={`${optionCardStyle(data.instantBook === true)} w-full flex-row items-start text-left`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 w-full">
|
<CheckCircle size={32} weight={data.instantBook === true ? "fill" : "regular"} className="shrink-0" />
|
||||||
<CheckCircle size={32} />
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-col text-left">
|
|
||||||
<span className="font-bold text-lg">Use Instant Book</span>
|
<span className="font-bold text-lg">Use Instant Book</span>
|
||||||
<span className="text-gray-600">Guests can book automatically.</span>
|
<span className="text-gray-600">Guests can book automatically. No need to approve reservations.</span>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => updateData('instantBook', false)}
|
onClick={() => updateData('instantBook', false)}
|
||||||
className={`${optionCardStyle(data.instantBook === false)} w-full`}
|
className={`${optionCardStyle(data.instantBook === false)} w-full flex-row items-start text-left`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-4 w-full">
|
<Article size={32} weight={data.instantBook === false ? "fill" : "regular"} className="shrink-0" />
|
||||||
<Article size={32} />
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-col text-left">
|
|
||||||
<span className="font-bold text-lg">Approve manually</span>
|
<span className="font-bold text-lg">Approve manually</span>
|
||||||
<span className="text-gray-600">You approve or decline booking requests.</span>
|
<span className="text-gray-600">You approve or decline booking requests. You will be notified of each booking request.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<button type="button" onClick={onBack} className={secondaryButtonStyle}>
|
|
||||||
<CaretLeft size={20} /> Back
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => onNext()} className={buttonStyle}>
|
|
||||||
Next <CaretRight size={20} />
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Availability Calendar */
|
const SafetyDetails = ({ data, updateData }: StepProps) => {
|
||||||
const AvailabilityCalendar = ({ onNext, onBack }: StepProps) => {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-6">
|
|
||||||
<h2 className={titleStyle}>When is your place available?</h2>
|
|
||||||
<div className="flex flex-col items-center justify-center h-64 border-2 border-black bg-gray-50">
|
|
||||||
<CalendarBlank size={48} className="text-gray-400 mb-2" />
|
|
||||||
<p className={`${figtree.className} text-gray-500`}>Calendar Component Placeholder</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<button type="button" onClick={onBack} className={secondaryButtonStyle}>
|
|
||||||
<CaretLeft size={20} /> Back
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => onNext()} className={buttonStyle}>
|
|
||||||
Next <CaretRight size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Safety Details */
|
|
||||||
const SafetyDetails = ({ onNext, onBack, data, updateData }: StepProps) => {
|
|
||||||
const safetyItems = [
|
const safetyItems = [
|
||||||
{ id: 'camera', label: 'Security camera(s)', icon: Camera },
|
{ id: 'camera', label: 'Security camera(s)', icon: Camera },
|
||||||
{ id: 'weapons', label: 'Weapons', icon: ShieldCheck },
|
{ id: 'weapons', label: 'Dangerous weapons', icon: ShieldCheck },
|
||||||
{ id: 'animals', label: 'Dangerous animals', icon: Farm },
|
{ id: 'animals', label: 'Dangerous animals', icon: Farm },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -430,7 +498,7 @@ const SafetyDetails = ({ onNext, onBack, data, updateData }: StepProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col">
|
||||||
<h2 className={titleStyle}>Does your place have any of these?</h2>
|
<h2 className={titleStyle}>Does your place have any of these?</h2>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
{safetyItems.map((item) => (
|
{safetyItems.map((item) => (
|
||||||
@@ -444,27 +512,27 @@ const SafetyDetails = ({ onNext, onBack, data, updateData }: StepProps) => {
|
|||||||
<item.icon size={32} />
|
<item.icon size={32} />
|
||||||
<span className={`${figtree.className} text-lg font-medium`}>{item.label}</span>
|
<span className={`${figtree.className} text-lg font-medium`}>{item.label}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className={`w-6 h-6 rounded-full border-2 border-black ${(data.safety || []).includes(item.id) ? 'bg-black' : 'bg-transparent'}`} />
|
<div className={`w-6 h-6 rounded-full border-2 border-black flex items-center justify-center transition-colors ${(data.safety || []).includes(item.id) ? 'bg-black' : 'bg-transparent'}`}>
|
||||||
|
{(data.safety || []).includes(item.id) && <div className="w-2 h-2 bg-white rounded-full" />}
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<button type="button" onClick={onBack} className={secondaryButtonStyle}>
|
|
||||||
<CaretLeft size={20} /> Back
|
|
||||||
</button>
|
|
||||||
<button type="button" onClick={() => { }} className={buttonStyle}>
|
|
||||||
Finish <CheckCircle size={20} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Main List Component --- */
|
||||||
|
|
||||||
function List() {
|
function List() {
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
const [direction, setDirection] = useState(1); // 1 for next, -1 for back
|
const [direction, setDirection] = useState(1);
|
||||||
const [formData, setFormData] = useState<FormData>({});
|
const [formData, setFormData] = useState<FormData>({
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
guests: 1,
|
||||||
|
bedrooms: 1,
|
||||||
|
beds: 1,
|
||||||
|
bathrooms: 1,
|
||||||
|
});
|
||||||
const contentRef = useRef<HTMLDivElement>(null);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const steps = [
|
const steps = [
|
||||||
@@ -472,22 +540,29 @@ function List() {
|
|||||||
PropertyPlaceType,
|
PropertyPlaceType,
|
||||||
PropertyLocation,
|
PropertyLocation,
|
||||||
PropertyCapacity,
|
PropertyCapacity,
|
||||||
PropertyPhotos,
|
|
||||||
PropertyAmenities,
|
PropertyAmenities,
|
||||||
PropertyDescription,
|
PropertyDescription,
|
||||||
|
PropertyPhotos,
|
||||||
PropertyPrice,
|
PropertyPrice,
|
||||||
PropertyInstantApproval,
|
PropertyInstantApproval,
|
||||||
AvailabilityCalendar,
|
|
||||||
SafetyDetails
|
SafetyDetails
|
||||||
];
|
];
|
||||||
|
|
||||||
const CurrentStepComponent = steps[step];
|
const CurrentStepComponent = steps[step];
|
||||||
|
const totalSteps = steps.length;
|
||||||
|
const progress = ((step + 1) / totalSteps) * 100;
|
||||||
|
|
||||||
const updateData = (key: keyof FormData, value: FormValue) => {
|
const updateData = (key: keyof FormData, value: FormValue) => {
|
||||||
setFormData(prev => ({ ...prev, [key]: value }));
|
setFormData(prev => ({ ...prev, [key]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
|
// Validation for photos
|
||||||
|
if (step === 6 && (!formData.photos || formData.photos.length < 5)) {
|
||||||
|
alert("Please upload at least 5 photos to continue.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (step < steps.length - 1) {
|
if (step < steps.length - 1) {
|
||||||
setDirection(1);
|
setDirection(1);
|
||||||
setStep(prev => prev + 1);
|
setStep(prev => prev + 1);
|
||||||
@@ -503,81 +578,88 @@ function List() {
|
|||||||
|
|
||||||
useGSAP(() => {
|
useGSAP(() => {
|
||||||
if (!contentRef.current) return;
|
if (!contentRef.current) return;
|
||||||
|
|
||||||
// Animate the content sliding in
|
|
||||||
gsap.fromTo(contentRef.current,
|
gsap.fromTo(contentRef.current,
|
||||||
{
|
{ x: direction * 20, opacity: 0 },
|
||||||
x: direction * 50,
|
{ x: 0, opacity: 1, duration: 0.4, ease: "power2.out" }
|
||||||
opacity: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
x: 0,
|
|
||||||
opacity: 1,
|
|
||||||
duration: 0.4,
|
|
||||||
ease: "power2.out"
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
}, [step]);
|
}, [step]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen w-full bg-white overflow-hidden">
|
<div className="flex min-h-screen w-full bg-white">
|
||||||
{/* Left Panel - Form */}
|
{/* Left Panel - Form */}
|
||||||
<div className="w-full lg:w-1/2 flex flex-col justify-between p-8 md:p-12 overflow-y-auto relative h-screen">
|
<div className="w-full lg:w-1/2 flex flex-col h-screen relative">
|
||||||
{/* Progress Bar - Top */}
|
{/* Header */}
|
||||||
<div className="w-full max-w-xl mx-auto mb-8">
|
<div className="px-8 py-6 border-b-2 border-gray-100 flex justify-between items-center bg-white z-10">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="w-10 h-10 bg-black text-white rounded-full flex items-center justify-center font-bold text-lg">
|
||||||
<div className="w-8 h-8 bg-black text-white rounded-full flex items-center justify-center font-bold text-sm">
|
|
||||||
{step + 1}
|
{step + 1}
|
||||||
</div>
|
</div>
|
||||||
<span className={`${figtree.className} font-bold text-sm uppercase tracking-wider`}>Step {step + 1} of {steps.length}</span>
|
<div className="flex flex-col">
|
||||||
|
<span className={`${figtree.className} font-bold text-sm uppercase tracking-wider`}>Step {step + 1} of {totalSteps}</span>
|
||||||
|
<span className={`${figtree.className} text-xs text-gray-500`}>{
|
||||||
|
step === 0 ? "Basics" :
|
||||||
|
step < 4 ? "Details" :
|
||||||
|
step < 7 ? "Description" : "Finish"
|
||||||
|
}</span>
|
||||||
</div>
|
</div>
|
||||||
<button className="text-sm font-bold underline decoration-2 underline-offset-4 hover:text-gray-600">Exit</button>
|
|
||||||
</div>
|
|
||||||
<div className="h-1 w-full bg-gray-100 rounded-full overflow-hidden">
|
|
||||||
<div
|
|
||||||
className="h-full bg-black transition-all duration-500 ease-out"
|
|
||||||
style={{ width: `${((step + 1) / steps.length) * 100}%` }}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button className="text-sm font-bold underline decoration-2 underline-offset-4 hover:text-gray-600">Save & Exit</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Progress Bar */}
|
||||||
<div className="flex-1 flex flex-col justify-center py-8">
|
<div className="w-full h-1 bg-gray-100">
|
||||||
<div ref={containerRef} className={containerStyle}>
|
<div
|
||||||
<div ref={contentRef}>
|
className="h-full bg-[#E7FE78] transition-all duration-500 ease-out"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Scrollable Content */}
|
||||||
|
<div className="flex-1 overflow-y-auto p-8 md:p-12">
|
||||||
|
<div ref={contentRef} className={containerStyle}>
|
||||||
<CurrentStepComponent
|
<CurrentStepComponent
|
||||||
onNext={handleNext}
|
|
||||||
onBack={handleBack}
|
|
||||||
data={formData}
|
data={formData}
|
||||||
updateData={updateData}
|
updateData={updateData}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Fixed Footer Navigation */}
|
||||||
|
<div className="border-t-2 border-gray-100 p-6 bg-white flex justify-between items-center z-10">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleBack}
|
||||||
|
disabled={step === 0}
|
||||||
|
className={`${secondaryButtonStyle} ${step === 0 ? 'opacity-0 pointer-events-none' : ''}`}
|
||||||
|
>
|
||||||
|
<CaretLeft size={20} /> Back
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleNext}
|
||||||
|
className={buttonStyle}
|
||||||
|
>
|
||||||
|
{step === totalSteps - 1 ? 'Finish' : 'Next'} <CaretRight size={20} />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Panel - Visual */}
|
{/* Right Panel - Live Preview */}
|
||||||
<div className="hidden lg:flex w-1/2 bg-[#F7F7F7] items-center justify-center relative overflow-hidden h-screen border-l-2 border-black">
|
<div className="hidden lg:flex w-1/2 bg-[#F7F7F7] border-l-2 border-black relative flex-col items-center justify-center p-12 overflow-hidden">
|
||||||
{/* Gradient Background */}
|
{/* Background Pattern */}
|
||||||
<div className="absolute inset-0 bg-[#E7FE78]" />
|
<div className="absolute inset-0 opacity-5"
|
||||||
<div className="absolute inset-0 opacity-20"
|
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `radial-gradient(circle at 2px 2px, black 1px, transparent 0)`,
|
backgroundImage: `radial-gradient(circle at 2px 2px, black 1px, transparent 0)`,
|
||||||
backgroundSize: '32px 32px'
|
backgroundSize: '24px 24px'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Decorative Content */}
|
<div className="relative z-10 w-full max-w-lg">
|
||||||
<div className="relative z-10 p-16 max-w-2xl">
|
<div className="mb-8 text-center">
|
||||||
<div className="bg-white border-2 border-black p-8 shadow-[8px_8px_0px_0px_rgba(0,0,0,1)] mb-8 rotate-1 hover:rotate-0 transition-transform duration-500">
|
<h2 className={`${figtree.className} text-3xl font-bold mb-2`}>What guests will see</h2>
|
||||||
<h1 className={`${figtree.className} text-5xl font-bold mb-6 leading-tight`}>
|
<p className={`${figtree.className} text-gray-500`}>As you update your listing, see how it looks in search results.</p>
|
||||||
Open your door<br />to hosting
|
|
||||||
</h1>
|
|
||||||
<p className={`${figtree.className} text-xl text-gray-800 leading-relaxed`}>
|
|
||||||
Earn money, reach millions of travelers, and find your freedom. It's easy to get started.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<LivePreview data={formData} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user