new listing
This commit is contained in:
@@ -16,12 +16,13 @@ import Signup from "@/components/authentication/signup";
|
||||
import Landing from "@/pages/landing/page";
|
||||
import Profile from "@/pages/profile/page";
|
||||
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Navigation />
|
||||
<Profile />
|
||||
<List />
|
||||
<Footer />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
import Image from "next/image";
|
||||
import { Camera, Star } from "@phosphor-icons/react/dist/ssr";
|
||||
import { FormData } from "./types";
|
||||
import { figtree } from "./styles";
|
||||
|
||||
export 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>
|
||||
);
|
||||
};
|
||||
@@ -1,42 +0,0 @@
|
||||
import { WifiHigh, CookingPot, Car, SwimmingPool, Wind, TelevisionSimple } from "@phosphor-icons/react/dist/ssr";
|
||||
import { StepProps } from "./types";
|
||||
import { titleStyle, subtitleStyle, optionCardStyle, figtree } from "./styles";
|
||||
|
||||
export const PropertyAmenities = ({ data, updateData }: StepProps) => {
|
||||
const amenities = [
|
||||
{ id: 'wifi', label: 'Wi-Fi', icon: WifiHigh },
|
||||
{ id: 'kitchen', label: 'Kitchen', icon: CookingPot },
|
||||
{ id: 'parking', label: 'Free parking', icon: Car },
|
||||
{ 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 current = data.amenities || [];
|
||||
const updated = current.includes(id)
|
||||
? current.filter((item: string) => item !== id)
|
||||
: [...current, id];
|
||||
updateData('amenities', updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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">
|
||||
{amenities.map((amenity) => (
|
||||
<button
|
||||
key={amenity.id}
|
||||
type="button"
|
||||
onClick={() => toggleAmenity(amenity.id)}
|
||||
className={`${optionCardStyle((data.amenities || []).includes(amenity.id))} items-start w-full text-left`}
|
||||
>
|
||||
<amenity.icon size={32} weight={(data.amenities || []).includes(amenity.id) ? "fill" : "regular"} />
|
||||
<span className={`${figtree.className} text-lg font-medium`}>{amenity.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { StepProps, FormData } from "./types";
|
||||
import { titleStyle, subtitleStyle, figtree } from "./styles";
|
||||
|
||||
export const PropertyCapacity = ({ data, updateData }: StepProps) => {
|
||||
const counters: { key: keyof FormData; label: string }[] = [
|
||||
{ key: 'guests', label: 'Guests' },
|
||||
{ key: 'bedrooms', label: 'Bedrooms' },
|
||||
{ key: 'beds', label: 'Beds' },
|
||||
{ key: 'bathrooms', label: 'Bathrooms' },
|
||||
];
|
||||
|
||||
const updateCount = (key: keyof FormData, delta: number) => {
|
||||
const current = (data[key] as number) || 0;
|
||||
updateData(key, Math.max(0, current + delta));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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">
|
||||
{counters.map((item) => (
|
||||
<div key={item.key} className="flex items-center justify-between border-b-2 border-gray-100 pb-6">
|
||||
<span className={`${figtree.className} text-xl font-medium`}>{item.label}</span>
|
||||
<div className="flex items-center gap-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateCount(item.key, -1)}
|
||||
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>
|
||||
<span className={`${figtree.className} text-xl w-8 text-center font-bold`}>{data[item.key] || 0}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateCount(item.key, 1)}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { StepProps } from "./types";
|
||||
import { titleStyle, subtitleStyle, inputStyle } from "./styles";
|
||||
|
||||
export const PropertyDescription = ({ data, updateData }: StepProps) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<h2 className={titleStyle}>How would you describe your place?</h2>
|
||||
<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
|
||||
placeholder="This unique place has a style all its own..."
|
||||
className={`${inputStyle} h-48 resize-none`}
|
||||
value={data.description || ''}
|
||||
onChange={(e) => updateData('description', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { CheckCircle, Article } from "@phosphor-icons/react/dist/ssr";
|
||||
import { StepProps } from "./types";
|
||||
import { titleStyle, optionCardStyle } from "./styles";
|
||||
|
||||
export const PropertyInstantApproval = ({ data, updateData }: StepProps) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<h2 className={titleStyle}>Decide how you'll confirm reservations</h2>
|
||||
<div className="flex flex-col gap-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateData('instantBook', true)}
|
||||
className={`${optionCardStyle(data.instantBook === true)} w-full flex-row items-start text-left`}
|
||||
>
|
||||
<CheckCircle size={32} weight={data.instantBook === true ? "fill" : "regular"} className="shrink-0" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-lg">Use Instant Book</span>
|
||||
<span className="text-gray-600">Guests can book automatically. No need to approve reservations. You will be notified of each booking request.</span>
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => updateData('instantBook', false)}
|
||||
className={`${optionCardStyle(data.instantBook === false)} w-full flex-row items-start text-left`}
|
||||
>
|
||||
<Article size={32} weight={data.instantBook === false ? "fill" : "regular"} className="shrink-0" />
|
||||
<div className="flex flex-col">
|
||||
<span className="font-bold text-lg">Approve manually</span>
|
||||
<span className="text-gray-600">You approve or decline booking requests for each reservation. You will be notified of each booking request. </span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { MapPin } from "@phosphor-icons/react/dist/ssr";
|
||||
import { StepProps } from "./types";
|
||||
import { titleStyle, subtitleStyle, inputStyle, figtree } from "./styles";
|
||||
|
||||
export const PropertyLocation = ({ data, updateData }: StepProps) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<h2 className={titleStyle}>Where's your place located?</h2>
|
||||
<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">
|
||||
<MapPin size={24} className="absolute left-4 top-1/2 -translate-y-1/2 text-gray-500" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Enter your address"
|
||||
className={`${inputStyle} pl-12`}
|
||||
value={data.location || ''}
|
||||
onChange={(e) => updateData('location', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<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="absolute inset-0 opacity-10 bg-[radial-gradient(#000_1px,transparent_1px)] [background-size:16px_16px]" />
|
||||
<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>
|
||||
)
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { useRef, ChangeEvent } from "react";
|
||||
import Image from "next/image";
|
||||
import { Camera, X, WarningCircle } from "@phosphor-icons/react/dist/ssr";
|
||||
import { StepProps } from "./types";
|
||||
import { titleStyle, subtitleStyle, figtree } from "./styles";
|
||||
|
||||
export const PropertyPhotos = ({ data, updateData }: StepProps) => {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const handleFileChange = (e: 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);
|
||||
};
|
||||
|
||||
const makeCover = (index: number) => {
|
||||
const currentPhotos = data.photos || [];
|
||||
if (index === 0 || index >= currentPhotos.length) return;
|
||||
|
||||
const newPhotos = [...currentPhotos];
|
||||
const [selectedPhoto] = newPhotos.splice(index, 1);
|
||||
newPhotos.unshift(selectedPhoto);
|
||||
|
||||
updateData('photos', newPhotos);
|
||||
};
|
||||
|
||||
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
|
||||
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>
|
||||
)}
|
||||
{index !== 0 && (
|
||||
<button
|
||||
onClick={() => makeCover(index)}
|
||||
className="absolute bottom-2 left-2 bg-white/90 text-black text-xs font-bold px-2 py-1 rounded border-2 border-black opacity-0 group-hover:opacity-100 transition-opacity z-10 hover:bg-[#E7FE78]"
|
||||
>
|
||||
Make Cover
|
||||
</button>
|
||||
)}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { CheckCircle } from "@phosphor-icons/react/dist/ssr";
|
||||
import { StepProps } from "./types";
|
||||
import { titleStyle, subtitleStyle, optionCardStyle, figtree } from "./styles";
|
||||
|
||||
export const PropertyPlaceType = ({ data, updateData }: StepProps) => {
|
||||
const places = [
|
||||
{ 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 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.' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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">
|
||||
{places.map((place) => (
|
||||
<button
|
||||
key={place.id}
|
||||
type="button"
|
||||
onClick={() => updateData('placeType', place.id)}
|
||||
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">
|
||||
<span className={`${figtree.className} text-xl font-bold`}>{place.label}</span>
|
||||
<span className={`${figtree.className} text-gray-600`}>{place.description}</span>
|
||||
</div>
|
||||
{data.placeType === place.id && <CheckCircle size={32} weight="fill" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { StepProps } from "./types";
|
||||
import { titleStyle, subtitleStyle, figtree } from "./styles";
|
||||
|
||||
export const PropertyPrice = ({ data, updateData }: StepProps) => {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<h2 className={titleStyle}>Now, set your price</h2>
|
||||
<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">
|
||||
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-4xl font-bold text-gray-400">₨</span>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="0"
|
||||
className="w-full bg-transparent text-6xl font-bold text-center outline-none p-4"
|
||||
value={data.price || ''}
|
||||
onChange={(e) => updateData('price', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<p className={`${figtree.className} text-xl text-gray-500`}>per night</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { House, Buildings, Farm, Bed } from "@phosphor-icons/react/dist/ssr";
|
||||
import { StepProps } from "./types";
|
||||
import { titleStyle, subtitleStyle, optionCardStyle, figtree } from "./styles";
|
||||
|
||||
export const PropertyType = ({ data, updateData }: StepProps) => {
|
||||
const types = [
|
||||
{ id: 'house', label: 'House', icon: House },
|
||||
{ id: 'flat', label: 'Flat', icon: Buildings },
|
||||
{ id: 'farmhouse', label: 'Farmhouse', icon: Farm },
|
||||
{ id: 'guesthouse', label: 'Guesthouse', icon: Bed },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<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">
|
||||
{types.map((type) => (
|
||||
<button
|
||||
key={type.id}
|
||||
type="button"
|
||||
onClick={() => updateData('propertyType', type.id)}
|
||||
className={optionCardStyle(data.propertyType === type.id)}
|
||||
>
|
||||
<type.icon size={48} weight={data.propertyType === type.id ? "fill" : "light"} />
|
||||
<span className={`${figtree.className} text-xl font-medium`}>{type.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { Camera, ShieldCheck, Farm } from "@phosphor-icons/react/dist/ssr";
|
||||
import { StepProps } from "./types";
|
||||
import { titleStyle, optionCardStyle, figtree } from "./styles";
|
||||
|
||||
export const SafetyDetails = ({ data, updateData }: StepProps) => {
|
||||
const safetyItems = [
|
||||
{ id: 'camera', label: 'Security camera(s)', icon: Camera },
|
||||
{ id: 'weapons', label: 'Dangerous weapons', icon: ShieldCheck },
|
||||
{ id: 'animals', label: 'Dangerous animals', icon: Farm },
|
||||
];
|
||||
|
||||
const toggleSafety = (id: string) => {
|
||||
const current = data.safety || [];
|
||||
const updated = current.includes(id)
|
||||
? current.filter((item: string) => item !== id)
|
||||
: [...current, id];
|
||||
updateData('safety', updated);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<h2 className={titleStyle}>Does your place have any of these?</h2>
|
||||
<div className="flex flex-col gap-4">
|
||||
{safetyItems.map((item) => (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
onClick={() => toggleSafety(item.id)}
|
||||
className={`${optionCardStyle((data.safety || []).includes(item.id))} flex-row justify-between items-center w-full`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<item.icon size={32} />
|
||||
<span className={`${figtree.className} text-lg font-medium`}>{item.label}</span>
|
||||
</div>
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,185 +1,464 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { useState, useRef } from "react";
|
||||
import gsap from "gsap";
|
||||
import { useGSAP } from "@gsap/react";
|
||||
import { CaretLeft, CaretRight } from "@phosphor-icons/react/dist/ssr";
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useGSAP } from '@gsap/react';
|
||||
import gsap from 'gsap';
|
||||
import { CaretLeft, CaretRight, House, Buildings, Tent, MapPin, Users, Bed, Toilet, CheckCircle, UploadSimple, ShieldCheck, WifiHigh, Car, SwimmingPool, Desktop, Moon, Plant, HouseLine, X, Farm, City, Plus, Minus } from '@phosphor-icons/react/dist/ssr';
|
||||
import localFont from 'next/font/local';
|
||||
import Image from 'next/image';
|
||||
|
||||
import { FormData, FormValue } from "./types";
|
||||
import { containerStyle, buttonStyle, secondaryButtonStyle, figtree } from "./styles";
|
||||
const figtree = localFont({
|
||||
src: [
|
||||
{
|
||||
path: '../../../public/Fonts/figtree/figtree.ttf',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
import { PropertyType } from "./PropertyType";
|
||||
import { PropertyPlaceType } from "./PropertyPlaceType";
|
||||
import { PropertyLocation } from "./PropertyLocation";
|
||||
import { PropertyCapacity } from "./PropertyCapacity";
|
||||
import { PropertyAmenities } from "./PropertyAmenities";
|
||||
import { PropertyDescription } from "./PropertyDescription";
|
||||
import { PropertyPhotos } from "./PropertyPhotos";
|
||||
import { PropertyPrice } from "./PropertyPrice";
|
||||
import { PropertyInstantApproval } from "./PropertyInstantApproval";
|
||||
import { SafetyDetails } from "./SafetyDetails";
|
||||
import { LivePreview } from "./LivePreview";
|
||||
// Types for form data
|
||||
interface FormData {
|
||||
propertyType: string;
|
||||
location: string;
|
||||
description: string;
|
||||
title: string;
|
||||
guests: number;
|
||||
bedrooms: number;
|
||||
beds: number;
|
||||
bathrooms: number;
|
||||
amenities: string[];
|
||||
safety: string[];
|
||||
photos: File[]; // heavily mocked for now
|
||||
instantApproval: boolean;
|
||||
}
|
||||
|
||||
function List() {
|
||||
const PROPERTY_TYPES = [
|
||||
{ id: 'shared_house', label: 'Shared House', icon: Users },
|
||||
{ id: 'entire_house', label: 'Entire House', icon: House },
|
||||
{ id: 'farmhouse', label: 'Farmhouse', icon: Farm },
|
||||
{ id: 'guesthouse', label: 'Guesthouse', icon: HouseLine },
|
||||
{ id: 'apartment', label: 'Apartment', icon: City },
|
||||
{ id: 'cabin', label: 'Cabin', icon: Tent },
|
||||
];
|
||||
|
||||
const AMENITIES_LIST = [
|
||||
{ id: 'wifi', label: 'Wi-Fi', icon: WifiHigh },
|
||||
{ id: 'parking', label: 'Free Parking', icon: Car },
|
||||
{ id: 'pool', label: 'Pool', icon: SwimmingPool },
|
||||
{ id: 'workspace', label: 'Workspace', icon: Desktop },
|
||||
];
|
||||
|
||||
const SAFETY_LIST = [
|
||||
{ id: 'smoke_alarm', label: 'Smoke Alarm', icon: ShieldCheck },
|
||||
{ id: 'first_aid', label: 'First Aid Kit', icon: ShieldCheck },
|
||||
{ id: 'fire_ext', label: 'Fire Extinguisher', icon: ShieldCheck },
|
||||
];
|
||||
|
||||
export default function List() {
|
||||
const [step, setStep] = useState(0);
|
||||
const [direction, setDirection] = useState(1);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const formRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [formData, setFormData] = useState<FormData>({
|
||||
guests: 1,
|
||||
bedrooms: 1,
|
||||
beds: 1,
|
||||
bathrooms: 1,
|
||||
propertyType: '',
|
||||
location: '',
|
||||
description: '',
|
||||
title: '',
|
||||
guests: 4,
|
||||
bedrooms: 2,
|
||||
beds: 2,
|
||||
bathrooms: 1.5,
|
||||
amenities: [],
|
||||
safety: [],
|
||||
photos: [],
|
||||
instantApproval: false,
|
||||
});
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const steps = [
|
||||
PropertyType,
|
||||
PropertyPlaceType,
|
||||
PropertyLocation,
|
||||
PropertyCapacity,
|
||||
PropertyAmenities,
|
||||
PropertyDescription,
|
||||
PropertyPhotos,
|
||||
PropertyPrice,
|
||||
PropertyInstantApproval,
|
||||
SafetyDetails
|
||||
];
|
||||
|
||||
const phases = [
|
||||
{ name: "Basics", start: 0, end: 4 },
|
||||
{ name: "Description", start: 5, end: 6 },
|
||||
{ name: "Publish", start: 7, end: 9 }
|
||||
];
|
||||
|
||||
const currentPhaseIndex = phases.findIndex(p => step >= p.start && step <= p.end);
|
||||
const currentPhase = phases[currentPhaseIndex];
|
||||
|
||||
const CurrentStepComponent = steps[step];
|
||||
const totalSteps = steps.length;
|
||||
|
||||
const updateData = (key: keyof FormData, value: FormValue) => {
|
||||
setFormData(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
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) {
|
||||
setDirection(1);
|
||||
setStep(prev => prev + 1);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (step > 0) {
|
||||
setDirection(-1);
|
||||
setStep(prev => prev - 1);
|
||||
}
|
||||
};
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
// Animation for step transitions
|
||||
useGSAP(() => {
|
||||
if (!contentRef.current) return;
|
||||
gsap.fromTo(contentRef.current,
|
||||
{ x: direction * 20, opacity: 0 },
|
||||
{ x: 0, opacity: 1, duration: 0.4, ease: "power2.out" }
|
||||
gsap.fromTo(formRef.current,
|
||||
{ opacity: 0, x: 20 },
|
||||
{ opacity: 1, x: 0, duration: 0.5, ease: 'power2.out' }
|
||||
);
|
||||
}, [step]);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen w-full bg-white">
|
||||
{/* Left Panel - Form */}
|
||||
<div className="w-full lg:w-1/2 flex flex-col h-screen relative">
|
||||
{/* Header Section */}
|
||||
<div className="bg-white z-10 border-b-2 border-gray-100">
|
||||
<div className="px-8 py-6 flex justify-between items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 bg-black text-white rounded-full flex items-center justify-center font-bold text-lg">
|
||||
{step + 1}
|
||||
</div>
|
||||
<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`}>{currentPhase.name}</span>
|
||||
const handleNext = () => {
|
||||
if (step === 5 && formData.photos.length < 5) {
|
||||
setError('Please upload at least 5 photos to continue.');
|
||||
return;
|
||||
}
|
||||
setError('');
|
||||
setStep((prev) => prev + 1);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setStep((prev) => Math.max(0, prev - 1));
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof FormData, value: any) => {
|
||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const toggleArrayItem = (field: 'amenities' | 'safety', item: string) => {
|
||||
setFormData((prev) => {
|
||||
const list = prev[field];
|
||||
return list.includes(item)
|
||||
? { ...prev, [field]: list.filter((i) => i !== item) }
|
||||
: { ...prev, [field]: [...list, item] };
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setIsSubmitting(true);
|
||||
// Simulate API call
|
||||
setTimeout(() => {
|
||||
setIsSubmitting(false);
|
||||
setIsSuccess(true);
|
||||
}, 2000);
|
||||
};
|
||||
|
||||
// Render Steps
|
||||
const renderStep = () => {
|
||||
if (isSuccess) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full text-center space-y-6 animate-in fade-in zoom-in duration-500">
|
||||
<CheckCircle className="w-24 h-24 text-[#E7FE78] fill-current" weight="fill" />
|
||||
<h2 className={`${figtree.className} text-3xl font-bold`}>Submission Received!</h2>
|
||||
<p className={`${figtree.className} text-xl text-gray-600 max-w-md`}>
|
||||
We are processing your request, your listing will be manually reviewed before going live, this can take upto 24 hours.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => window.location.href = '/'}
|
||||
className={`${figtree.className} mt-8 bg-[#E7FE78] text-black border-2 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)] px-8 py-3 rounded-full hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_rgba(0,0,0,1)] transition-all active:shadow-none active:translate-x-[4px] active:translate-y-[4px]`}
|
||||
>
|
||||
Return Home
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
switch (step) {
|
||||
case 0: // Property Type
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<h2 className={`${figtree.className} text-3xl font-bold`}>What kind of place will you host?</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{PROPERTY_TYPES.map((type) => {
|
||||
const Icon = type.icon;
|
||||
const isSelected = formData.propertyType === type.id;
|
||||
return (
|
||||
<div
|
||||
key={type.id}
|
||||
onClick={() => handleChange('propertyType', type.id)}
|
||||
className={`cursor-pointer p-6 border-2 rounded-xl flex flex-col items-center space-y-4 transition-all duration-300 ${isSelected ? 'border-black bg-[#E7FE78] shadow-[4px_4px_0px_rgba(0,0,0,1)]' : 'border-gray-200 hover:border-black'
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-12 h-12" />
|
||||
<span className={`${figtree.className} font-semibold text-lg`}>{type.label}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 1: // Location
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<h2 className={`${figtree.className} text-3xl font-bold`}>Where's your place located?</h2>
|
||||
<div className="space-y-4">
|
||||
<label className={`${figtree.className} text-gray-600`}>Address</label>
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-4 top-1/2 -translate-y-1/2 w-6 h-6 text-gray-400" />
|
||||
<input
|
||||
type="text"
|
||||
value={formData.location}
|
||||
onChange={(e) => handleChange('location', e.target.value)}
|
||||
placeholder="123 Lakeview Drive, Lakeside, CA"
|
||||
className={`${figtree.className} w-full pl-12 pr-4 py-4 border-2 border-gray-200 rounded-xl focus:border-black focus:outline-none transition-colors text-lg`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Segmented Progress Bar */}
|
||||
<div className="w-full px-8 pb-6 flex gap-2">
|
||||
{phases.map((phase, index) => {
|
||||
let fill = 0;
|
||||
if (index < currentPhaseIndex) {
|
||||
fill = 100;
|
||||
} else if (index === currentPhaseIndex) {
|
||||
const phaseTotal = phase.end - phase.start + 1;
|
||||
const phaseStep = step - phase.start + 1;
|
||||
fill = (phaseStep / phaseTotal) * 100;
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={phase.name} className="h-1 flex-1 bg-gray-100 rounded-full overflow-hidden relative">
|
||||
);
|
||||
case 2: // Basics (Capacity)
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<h2 className={`${figtree.className} text-3xl font-bold`}>Share some basics about your place</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{[
|
||||
{ label: 'Guests', field: 'guests', icon: Users },
|
||||
{ label: 'Bedrooms', field: 'bedrooms', icon: Moon },
|
||||
{ label: 'Beds', field: 'beds', icon: Bed },
|
||||
{ label: 'Bathrooms', field: 'bathrooms', icon: Toilet },
|
||||
].map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<div key={item.field} className="flex items-center justify-between p-4 border-2 border-gray-200 rounded-xl">
|
||||
<div className="flex items-center space-x-3">
|
||||
<Icon className="w-6 h-6 text-gray-500" />
|
||||
<span className={`${figtree.className} text-lg`}>{item.label}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => handleChange(item.field as keyof FormData, Math.max(0, (formData[item.field as keyof FormData] as number) - 1))}
|
||||
className="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center hover:border-black transition-colors cursor-pointer hover:bg-[#E7FE78] hover:border-2"
|
||||
>
|
||||
<Minus />
|
||||
</button>
|
||||
<span className={`${figtree.className} text-xl w-6 text-center`}>{formData[item.field as keyof FormData] as number}</span>
|
||||
<button
|
||||
onClick={() => handleChange(item.field as keyof FormData, (formData[item.field as keyof FormData] as number) + 1)}
|
||||
className="w-8 h-8 rounded-full border border-gray-300 flex items-center justify-center hover:border-black transition-colors cursor-pointer hover:bg-[#E7FE78] hover:border-2"
|
||||
>
|
||||
<Plus />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 3: // Description
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<h2 className={`${figtree.className} text-3xl font-bold`}>Describe your place</h2>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className={`${figtree.className} text-gray-600 block mb-2`}>Title</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => handleChange('title', e.target.value)}
|
||||
placeholder="e.g. Charming Lakeside Cabin"
|
||||
className={`${figtree.className} w-full p-4 border-2 border-gray-200 rounded-xl focus:border-black focus:outline-none transition-colors text-lg`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className={`${figtree.className} text-gray-600 block mb-2`}>Description</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={(e) => handleChange('description', e.target.value)}
|
||||
placeholder="Tell guests what makes your place unique..."
|
||||
rows={5}
|
||||
className={`${figtree.className} w-full p-4 border-2 border-gray-200 rounded-xl focus:border-black focus:outline-none transition-colors text-lg resize-none`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 4: // Amenities
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<h2 className={`${figtree.className} text-3xl font-bold`}>What does it offer?</h2>
|
||||
<div className="space-y-6">
|
||||
<h3 className={`${figtree.className} text-xl font-semibold`}>Amenities</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{AMENITIES_LIST.map((item) => (
|
||||
<div
|
||||
className="absolute top-0 left-0 h-full bg-[#E7FE78] transition-all duration-500 ease-out"
|
||||
style={{ width: `${fill}%` }}
|
||||
key={item.id}
|
||||
onClick={() => toggleArrayItem('amenities', item.id)}
|
||||
className={`cursor-pointer p-4 border-2 rounded-xl flex items-center space-x-3 transition-all ${formData.amenities.includes(item.id) ? 'border-black bg-[#E7FE78]' : 'border-gray-200 hover:border-black'
|
||||
}`}
|
||||
>
|
||||
<item.icon className="w-6 h-6" />
|
||||
<span className={figtree.className}>{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<h3 className={`${figtree.className} text-xl font-semibold`}>Safety</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{SAFETY_LIST.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
onClick={() => toggleArrayItem('safety', item.id)}
|
||||
className={`cursor-pointer p-4 border-2 rounded-xl flex items-center space-x-3 transition-all ${formData.safety.includes(item.id) ? 'border-black bg-[#E7FE78]' : 'border-gray-200 hover:border-black'
|
||||
}`}
|
||||
>
|
||||
<item.icon className="w-6 h-6" />
|
||||
<span className={figtree.className}>{item.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 5: // Photos
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h2 className={`${figtree.className} text-3xl font-bold`}>Add some photos</h2>
|
||||
<p className={`${figtree.className} text-gray-500 mt-2`}>You need at least 5 photos to publish your listing.</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onClick={() => document.getElementById('photo-upload')?.click()}
|
||||
className="border-2 border-dashed border-gray-300 rounded-xl p-12 flex flex-col items-center justify-center space-y-4 hover:border-black transition-colors cursor-pointer bg-gray-50"
|
||||
>
|
||||
<input
|
||||
id="photo-upload"
|
||||
type="file"
|
||||
multiple
|
||||
accept="image/*"
|
||||
className="hidden"
|
||||
onChange={(e) => {
|
||||
if (e.target.files) {
|
||||
const newPhotos = Array.from(e.target.files);
|
||||
setFormData(prev => ({ ...prev, photos: [...prev.photos, ...newPhotos] }));
|
||||
setError('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<UploadSimple className="w-16 h-16 text-gray-400" />
|
||||
<div className="text-center">
|
||||
<p className={`${figtree.className} text-lg font-semibold`}>Click to upload photos</p>
|
||||
<p className={`${figtree.className} text-sm text-gray-500`}>Select multiple files</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-4 bg-red-50 text-red-600 rounded-lg flex items-center gap-2">
|
||||
<ShieldCheck className="w-5 h-5" />
|
||||
<span className={figtree.className}>{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{formData.photos.length > 0 && formData.photos.map((photo, index) => (
|
||||
<div key={index} className="aspect-square relative rounded-lg overflow-hidden group">
|
||||
<Image
|
||||
src={URL.createObjectURL(photo)}
|
||||
alt={`Preview ${index}`}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setFormData(prev => ({
|
||||
...prev,
|
||||
photos: prev.photos.filter((_, i) => i !== index)
|
||||
}))}
|
||||
className="absolute top-2 right-2 bg-white p-1 rounded-full shadow-md opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 6: // Instant Approval
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<h2 className={`${figtree.className} text-3xl font-bold`}>Finish up</h2>
|
||||
<div className="flex items-center justify-between p-6 border-2 border-gray-200 rounded-xl">
|
||||
<div>
|
||||
<h3 className={`${figtree.className} text-xl font-semibold`}>Instant Approval</h3>
|
||||
<p className={`${figtree.className} text-gray-500 max-w-sm`}>Listings with instant approval get 30% more bookings on average.</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleChange('instantApproval', !formData.instantApproval)}
|
||||
className={`w-16 h-8 rounded-full transition-colors relative ${formData.instantApproval ? 'bg-[#E7FE78] border-2 border-black' : 'bg-gray-200 border-2 border-transparent'}`}
|
||||
>
|
||||
<div className={`absolute top-1/2 -translate-y-1/2 w-6 h-6 bg-white rounded-full border-2 border-black transition-all ${formData.instantApproval ? 'left-[calc(100%-1.6rem)]' : 'left-1'}`} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
case 7: // Preview
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<h2 className={`${figtree.className} text-3xl font-bold`}>Review your listing</h2>
|
||||
<div className="border-2 border-black rounded-xl overflow-hidden shadow-[8px_8px_0px_rgba(0,0,0,1)] bg-white">
|
||||
<div className="h-64 bg-gray-200 relative flex items-center justify-center">
|
||||
<Image src="/placeholder-house.jpg" alt="House" width={800} height={400} className="object-cover w-full h-full opacity-50" />
|
||||
<span className={`${figtree.className} absolute text-gray-500`}>Cover Photo</span>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<h3 className={`${figtree.className} text-2xl font-bold`}>{formData.title || 'Untitled Listing'}</h3>
|
||||
<p className={`${figtree.className} text-gray-600 flex items-center gap-2`}>
|
||||
<MapPin weight="fill" /> {formData.location || 'No location set'}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4 text-sm text-gray-600 border-y py-4">
|
||||
<span>{formData.guests} Guests</span> •
|
||||
<span>{formData.bedrooms} Bedrooms</span> •
|
||||
<span>{formData.beds} Beds</span> •
|
||||
<span>{formData.bathrooms} Baths</span>
|
||||
</div>
|
||||
<p className={`${figtree.className} text-gray-700`}>{formData.description || 'No description provided.'}</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{formData.amenities.map(a => (
|
||||
<span key={a} className="px-3 py-1 bg-gray-100 rounded-full text-sm">{AMENITIES_LIST.find(i => i.id === a)?.label}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="min-h-[calc(81vh)] w-full bg-white text-black p-8 flex flex-col">
|
||||
{/* Header / Progress with max-width for better readability */}
|
||||
{!isSuccess && (
|
||||
<div className="w-full max-w-6xl mx-auto mb-4">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<span className={`${figtree.className} font-bold text-gray-400`}>Step {step + 1} of 8</span>
|
||||
<div className="flex gap-1">
|
||||
{Array.from({ length: 8 }).map((_, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`h-2 rounded-full transition-all duration-300 ${i <= step ? 'w-8 bg-[#E7FE78] border-2 border-black' : 'w-2 bg-gray-200'}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Scrollable Content */}
|
||||
<div className="flex-1 overflow-y-auto p-8 md:p-12">
|
||||
<div ref={contentRef} className={containerStyle}>
|
||||
<CurrentStepComponent
|
||||
data={formData}
|
||||
updateData={updateData}
|
||||
/>
|
||||
</div>
|
||||
{/* Content Form */}
|
||||
<div className="flex-1 flex items-center justify-center w-full">
|
||||
<div ref={formRef} className="w-full max-w-4xl">
|
||||
{renderStep()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Fixed Footer Navigation */}
|
||||
<div className="border-t-2 border-gray-100 p-6 bg-white flex justify-between items-center z-10">
|
||||
{/* Footer Navigation */}
|
||||
{!isSuccess && (
|
||||
<div className="flex justify-between items-center pt-8 w-full max-w-6xl mx-auto">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleBack}
|
||||
disabled={step === 0}
|
||||
className={`${secondaryButtonStyle} ${step === 0 ? 'opacity-0 pointer-events-none' : ''}`}
|
||||
className={`${figtree.className} text-black font-semibold underline disabled:opacity-0 hover:text-gray-600 transition-colors`}
|
||||
>
|
||||
<CaretLeft size={20} /> Back
|
||||
Back
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNext}
|
||||
className={buttonStyle}
|
||||
>
|
||||
{step === totalSteps - 1 ? 'Finish' : 'Next'} <CaretRight size={20} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Panel - Live Preview */}
|
||||
<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">
|
||||
{/* Background Pattern */}
|
||||
<div className="absolute inset-0 opacity-5"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle at 2px 2px, black 1px, transparent 0)`,
|
||||
backgroundSize: '24px 24px'
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="relative z-10 w-full max-w-lg">
|
||||
<div className="mb-8 text-center">
|
||||
<h2 className={`${figtree.className} text-3xl font-bold mb-2`}>What guests will see</h2>
|
||||
<p className={`${figtree.className} text-gray-500`}>As you update your listing, see how it looks in search results.</p>
|
||||
</div>
|
||||
<LivePreview data={formData} />
|
||||
{step === 7 ? (
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting}
|
||||
className={`${figtree.className} bg-[#E7FE78] text-black px-8 py-3 rounded-lg font-bold border-2 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_rgba(0,0,0,1)] transition-all active:shadow-none active:translate-x-[4px] active:translate-y-[4px] disabled:opacity-50 disabled:cursor-not-allowed`}
|
||||
>
|
||||
{isSubmitting ? 'Submitting...' : 'Submit Listing'}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={handleNext}
|
||||
className={`${figtree.className} bg-[#E7FE78] text-black px-8 py-3 rounded-lg font-bold border-2 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:translate-x-[2px] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_rgba(0,0,0,1)] transition-all active:shadow-none active:translate-x-[4px] active:translate-y-[4px] flex items-center gap-2`}
|
||||
>
|
||||
Next <CaretRight weight="bold" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default List;
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import localFont from "next/font/local";
|
||||
|
||||
export const figtree = localFont({
|
||||
src: [
|
||||
{
|
||||
path: '../../../public/Fonts/figtree/figtree.ttf',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
export const containerStyle = `flex flex-col gap-8 w-full max-w-xl mx-auto pb-24`; // Added padding bottom for fixed footer
|
||||
export const titleStyle = `${figtree.className} text-4xl font-bold mb-2`;
|
||||
export const subtitleStyle = `${figtree.className} text-lg text-gray-500 mb-8`;
|
||||
export 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`;
|
||||
export 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`;
|
||||
export 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`;
|
||||
export const optionCardStyle = (selected: boolean) =>
|
||||
`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'}`;
|
||||
@@ -1,23 +0,0 @@
|
||||
export interface FormData {
|
||||
propertyType?: string;
|
||||
placeType?: string;
|
||||
location?: string;
|
||||
guests?: number;
|
||||
bedrooms?: number;
|
||||
beds?: number;
|
||||
bathrooms?: number;
|
||||
amenities?: string[];
|
||||
description?: string;
|
||||
price?: string;
|
||||
instantBook?: boolean;
|
||||
safety?: string[];
|
||||
title?: string;
|
||||
photos?: string[];
|
||||
}
|
||||
|
||||
export type FormValue = string | number | boolean | string[] | undefined;
|
||||
|
||||
export interface StepProps {
|
||||
data: FormData;
|
||||
updateData: (key: keyof FormData, value: FormValue) => void;
|
||||
}
|
||||
Reference in New Issue
Block a user