profile + authentication

This commit is contained in:
2025-11-30 15:34:37 +00:00
parent 7d64b6799b
commit e15a78e8b8
24 changed files with 2439 additions and 576 deletions

View File

@@ -1,2 +1,8 @@
@import "tailwindcss";
@font-face {
font-family: 'Figtree';
src: url('/Fonts/figtree/figtree.ttf') format('truetype');
font-weight: 100 900;
font-style: normal;
}

View File

@@ -10,12 +10,18 @@ import SearchBy from "@/components/search-results/search";
import Results from "@/components/search-results/results";
import Features from "@/components/details/features";
import List from "@/components/list-property/list";
import ManageProperty from "@/components/manage-property/manage";
import Login from "@/components/authentication/login";
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 />
<Features />
<Profile />
<Footer />
</div>
</>

View File

@@ -0,0 +1,213 @@
# Profile Component Documentation
## Overview
The Profile component is a comprehensive user profile section for PakStay that supports both **Host** and **Guest** profiles with clear visual differentiation, similar to Fiverr's approach.
## Design Consistency
- **Font**: Figtree (matching landing page)
- **Borders**: Bold 4px black borders
- **Shadows**: `shadow-[8px_8px_0px_rgba(0,0,0,1)]` effect
- **Accent Color**: Lime green `#E7FE78`
- **Animations**: GSAP-powered smooth transitions
- **Hover Effects**: Scale and shadow transformations
## Features
### 1. Profile Header
- **Avatar**: Circular profile picture with camera upload button
- **Verification Badge**: Blue checkmark for verified users
- **Superhost Badge**: Lime green badge for top-rated hosts
- **Contact Info**: Email and phone display
- **Verification Badges**: Email, ID, and Phone verification status
- **Edit Profile Button**: Quick access to profile editing
### 2. Host/Guest Toggle (Fiverr-Style)
For users who are both hosts and guests:
- **Toggle Buttons**: Switch between "Guest Profile" and "Host Dashboard"
- **Smooth Transitions**: GSAP animations when switching views
- **Visual Indicator**: Active tab highlighted with lime green background
### 3. Guest Profile View
#### Stats Cards (3 columns)
- **Reviews**: Total reviews with average rating
- **Total Bookings**: Number of completed stays
- **Wishlist**: Saved properties count
#### Recent Bookings Section
- Property name and location
- Booking dates
- Status badges (Upcoming/Completed)
- Hover effects
#### Quick Actions Sidebar
- Payment Methods
- Notifications
- Privacy & Security
- Language & Region
- Messages
- Sign Out (red variant)
#### Reviews from Hosts
- Host name and property
- Star rating (1-5)
- Review date
- Comment text
### 4. Host Dashboard View
#### Host Stats (4 columns)
- **Properties**: Number of active listings
- **Rating**: Average rating with review count
- **Response Rate**: Percentage with response time
- **Total Bookings**: All-time booking count
#### My Properties Section
- Property cards with:
- Name and location
- Rating and review count
- Total bookings
- Status (Active/Inactive)
- **Add New Property** button
#### Host Tools Sidebar
- Calendar
- Analytics
- Earnings
- Messages
- Notifications
- Settings
#### Achievement Badge
- Superhost medal
- Achievement description
- Gradient background
#### Recent Guest Reviews
- Guest name and property
- Star rating
- Review date
- Comment
## Component Structure
```
Profile (Main Component)
├── ProfileData Interface
├── Header Section
│ ├── Avatar with verification
│ ├── Profile Info
│ ├── Contact Details
│ └── Verification Badges
├── Host/Guest Toggle
└── Content Area
├── GuestProfile Component
│ ├── Stats Cards
│ ├── Recent Bookings
│ ├── Quick Actions
│ └── Reviews
└── HostDashboard Component
├── Host Stats
├── My Properties
├── Host Tools
├── Achievement Badge
└── Guest Reviews
Reusable Components:
├── StatCard
├── SectionCard
├── QuickActionButton
├── BookingItem
├── PropertyItem
└── ReviewItem
```
## Data Structure
```typescript
interface ProfileData {
name: string;
email: string;
phone: string;
location: string;
joinDate: string;
bio: string;
avatar: string;
isVerified: boolean;
isHost: boolean;
stats: {
totalReviews: number;
rating: number;
responseRate?: number;
responseTime?: string;
properties?: number;
totalBookings?: number;
wishlistCount?: number;
};
}
```
## Usage
```tsx
import Profile from '@/components/Profile/profile';
function ProfilePage() {
return (
<div>
<Profile />
</div>
);
}
```
## Customization
### Mock Data
The component currently uses mock data. Replace the `profileData` state with actual data from your backend:
```tsx
const [profileData, setProfileData] = useState<ProfileData>({
// Your data from API
});
```
### Styling
All styling uses Tailwind CSS classes consistent with the PakStay design system. Key classes:
- `border-4 border-black` - Bold borders
- `shadow-[8px_8px_0px_rgba(0,0,0,1)]` - Signature shadow
- `bg-[#E7FE78]` - Lime green accent
- `hover:scale-105` - Hover animation
- `transition-all duration-300` - Smooth transitions
## Responsive Design
- **Mobile**: Single column layout
- **Tablet**: 2-column grid for some sections
- **Desktop**: Full 3-column layout with sidebar
## Animations
- **Page Load**: Fade in from bottom (GSAP)
- **Tab Switch**: Fade out/in transition (GSAP)
- **Hover**: Scale and shadow effects (CSS)
## Icons
Uses **Phosphor Icons** for all iconography:
- User, Star, MapPin, Calendar, Heart, House
- CheckCircle, ShieldCheck, Envelope, Phone
- And many more...
## Future Enhancements
1. **Edit Mode**: Implement actual profile editing functionality
2. **Image Upload**: Connect camera button to file upload
3. **API Integration**: Replace mock data with real backend calls
4. **Settings Pages**: Create dedicated pages for each quick action
5. **Analytics Dashboard**: Expand host analytics section
6. **Messaging**: Integrate real-time messaging
7. **Notifications**: Add notification center
8. **Payment Integration**: Connect payment methods management
## Notes
- The component is fully typed with TypeScript
- All text uses the Figtree font for consistency
- The design follows the PakStay brand guidelines
- Accessibility features can be enhanced with ARIA labels

View File

@@ -0,0 +1,634 @@
'use client';
import { useState, useRef } from 'react';
import Image from 'next/image';
import localFont from 'next/font/local';
import { useGSAP } from '@gsap/react';
import gsap from 'gsap';
import {
User,
Star,
MapPin,
Calendar,
Heart,
House,
CheckCircle,
ShieldCheck,
Envelope,
Phone,
PencilSimple,
Camera,
Buildings,
Bed,
ChartLine,
Gear,
SignOut,
IdentificationCard,
CreditCard,
Bell,
Lock,
Globe,
ChatCircle,
Medal,
} from '@phosphor-icons/react';
const figtree = localFont({
src: [
{
path: '../../../public/Fonts/figtree/figtree.ttf',
},
],
});
interface ProfileData {
name: string;
email: string;
phone: string;
location: string;
joinDate: string;
bio: string;
avatar: string;
isVerified: boolean;
isHost: boolean;
stats: {
totalReviews: number;
rating: number;
responseRate?: number;
responseTime?: string;
properties?: number;
totalBookings?: number;
wishlistCount?: number;
};
}
function Profile() {
const [activeTab, setActiveTab] = useState<'host' | 'guest'>('guest');
const [editMode, setEditMode] = useState(false);
const profileRef = useRef<HTMLDivElement>(null);
const tabsRef = useRef<HTMLDivElement>(null);
// Mock profile data - replace with actual data from your backend
const [profileData, setProfileData] = useState<ProfileData>({
name: 'Asa Bizanjo',
email: 'asabizanjo@gmail.com',
phone: '+92 300 1234567',
location: 'Karachi, Pakistan',
joinDate: 'January 2024',
bio: 'Travel enthusiast and property host. Love meeting new people and sharing the beauty of Pakistan!',
avatar: '/avatar-placeholder.png',
isVerified: true,
isHost: true,
stats: {
totalReviews: 47,
rating: 4.8,
responseRate: 98,
responseTime: 'within an hour',
properties: 3,
totalBookings: 156,
wishlistCount: 12,
},
});
useGSAP(() => {
gsap.fromTo(
profileRef.current,
{ opacity: 0, y: 30 },
{ opacity: 1, y: 0, duration: 0.8, ease: 'power3.out' }
);
});
const handleTabSwitch = (tab: 'host' | 'guest') => {
gsap.to(tabsRef.current, {
opacity: 0,
y: -10,
duration: 0.2,
onComplete: () => {
setActiveTab(tab);
gsap.to(tabsRef.current, {
opacity: 1,
y: 0,
duration: 0.3,
});
},
});
};
return (
<div ref={profileRef} className={`${figtree.className} max-w-7xl mx-auto px-4 py-8`}>
{/* Header Section with Profile Info */}
<div className="border-4 border-black rounded-xl p-8 mb-8 shadow-[8px_8px_0px_rgba(0,0,0,1)] bg-white hover:shadow-[12px_12px_0px_rgba(0,0,0,1)] transition-all duration-300">
<div className="flex flex-col md:flex-row gap-8 items-start">
{/* Avatar Section */}
<div className="relative group">
<div className="w-40 h-40 rounded-full border-4 border-black overflow-hidden bg-gray-200 shadow-[4px_4px_0px_rgba(0,0,0,1)]">
<div className="w-full h-full flex items-center justify-center bg-[#E7FE78]">
<User size={80} weight="bold" />
</div>
</div>
<button className="absolute bottom-0 right-0 bg-[#E7FE78] border-2 border-black rounded-full p-2 shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:scale-110 transition-all duration-300">
<Camera size={20} weight="bold" />
</button>
{profileData.isVerified && (
<div className="absolute -top-2 -right-2 bg-blue-500 border-2 border-black rounded-full p-1">
<CheckCircle size={24} weight="fill" className="text-white" />
</div>
)}
</div>
{/* Profile Info */}
<div className="flex-1">
<div className="flex items-start justify-between mb-4">
<div>
<div className="flex items-center gap-3 mb-2">
<h1 className="text-4xl font-bold">{profileData.name}</h1>
{profileData.isHost && (
<span className="bg-[#E7FE78] border-2 border-black px-3 py-1 rounded-lg text-sm font-bold shadow-[2px_2px_0px_rgba(0,0,0,1)]">
SUPERHOST
</span>
)}
</div>
<div className="flex items-center gap-4 text-gray-600 mb-3">
<div className="flex items-center gap-1">
<MapPin size={18} weight="bold" />
<span>{profileData.location}</span>
</div>
<div className="flex items-center gap-1">
<Calendar size={18} weight="bold" />
<span>Joined {profileData.joinDate}</span>
</div>
</div>
</div>
<button
onClick={() => setEditMode(!editMode)}
className="bg-white border-2 border-black px-4 py-2 rounded-lg shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:scale-105 transition-all duration-300 font-bold"
>
<PencilSimple size={20} weight="bold" className="inline mr-2" />
Edit Profile
</button>
</div>
{/* Bio */}
<p className="text-gray-700 mb-4">{profileData.bio}</p>
{/* Contact Info */}
<div className="flex flex-wrap gap-4 mb-4">
<div className="flex items-center gap-2 bg-gray-50 border-2 border-black px-3 py-2 rounded-lg">
<Envelope size={18} weight="bold" />
<span className="text-sm">{profileData.email}</span>
</div>
<div className="flex items-center gap-2 bg-gray-50 border-2 border-black px-3 py-2 rounded-lg">
<Phone size={18} weight="bold" />
<span className="text-sm">{profileData.phone}</span>
</div>
</div>
{/* Verification Badges */}
<div className="flex flex-wrap gap-3">
<div className="flex items-center gap-2 bg-green-50 border-2 border-black px-3 py-2 rounded-lg">
<ShieldCheck size={18} weight="fill" className="text-green-600" />
<span className="text-sm font-bold">Email Verified</span>
</div>
<div className="flex items-center gap-2 bg-green-50 border-2 border-black px-3 py-2 rounded-lg">
<IdentificationCard size={18} weight="fill" className="text-green-600" />
<span className="text-sm font-bold">ID Verified</span>
</div>
<div className="flex items-center gap-2 bg-green-50 border-2 border-black px-3 py-2 rounded-lg">
<Phone size={18} weight="fill" className="text-green-600" />
<span className="text-sm font-bold">Phone Verified</span>
</div>
</div>
</div>
</div>
</div>
{/* Host/Guest Toggle - Fiverr Style */}
{profileData.isHost && (
<div className="border-4 border-black rounded-xl p-2 mb-8 shadow-[8px_8px_0px_rgba(0,0,0,1)] bg-white inline-flex gap-2">
<button
onClick={() => handleTabSwitch('guest')}
className={`px-6 py-3 rounded-lg font-bold transition-all duration-300 ${activeTab === 'guest'
? 'bg-[#E7FE78] border-2 border-black shadow-[2px_2px_0px_rgba(0,0,0,1)]'
: 'bg-white hover:bg-gray-50'
}`}
>
<User size={20} weight="bold" className="inline mr-2" />
Guest Profile
</button>
<button
onClick={() => handleTabSwitch('host')}
className={`px-6 py-3 rounded-lg font-bold transition-all duration-300 ${activeTab === 'host'
? 'bg-[#E7FE78] border-2 border-black shadow-[2px_2px_0px_rgba(0,0,0,1)]'
: 'bg-white hover:bg-gray-50'
}`}
>
<House size={20} weight="bold" className="inline mr-2" />
Host Dashboard
</button>
</div>
)}
{/* Content Area */}
<div ref={tabsRef}>
{activeTab === 'guest' ? (
<GuestProfile profileData={profileData} />
) : (
<HostDashboard profileData={profileData} />
)}
</div>
</div>
);
}
// Guest Profile Component
function GuestProfile({ profileData }: { profileData: ProfileData }) {
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Stats Cards */}
<div className="lg:col-span-3 grid grid-cols-1 md:grid-cols-3 gap-6">
<StatCard
icon={<Star size={32} weight="fill" className="text-yellow-500" />}
title="Reviews"
value={profileData.stats.totalReviews.toString()}
subtitle={`${profileData.stats.rating} average rating`}
/>
<StatCard
icon={<Bed size={32} weight="bold" />}
title="Total Bookings"
value={profileData.stats.totalBookings?.toString() || '0'}
subtitle="Completed stays"
/>
<StatCard
icon={<Heart size={32} weight="fill" className="text-red-500" />}
title="Wishlist"
value={profileData.stats.wishlistCount?.toString() || '0'}
subtitle="Saved properties"
/>
</div>
{/* Recent Bookings */}
<div className="lg:col-span-2">
<SectionCard title="Recent Bookings" icon={<Calendar size={24} weight="bold" />}>
<div className="space-y-4">
<BookingItem
propertyName="Luxury Villa in Clifton"
location="Karachi, Pakistan"
dates="Dec 15-20, 2024"
status="Upcoming"
/>
<BookingItem
propertyName="Cozy Apartment in F-7"
location="Islamabad, Pakistan"
dates="Nov 10-15, 2024"
status="Completed"
/>
<BookingItem
propertyName="Beach House in Hawksbay"
location="Karachi, Pakistan"
dates="Oct 5-8, 2024"
status="Completed"
/>
</div>
</SectionCard>
</div>
{/* Quick Actions */}
<div className="lg:col-span-1">
<SectionCard title="Quick Actions" icon={<Gear size={24} weight="bold" />}>
<div className="space-y-3">
<QuickActionButton icon={<CreditCard size={20} />} text="Payment Methods" />
<QuickActionButton icon={<Bell size={20} />} text="Notifications" />
<QuickActionButton icon={<Lock size={20} />} text="Privacy & Security" />
<QuickActionButton icon={<Globe size={20} />} text="Language & Region" />
<QuickActionButton icon={<ChatCircle size={20} />} text="Messages" />
<QuickActionButton
icon={<SignOut size={20} />}
text="Sign Out"
variant="danger"
/>
</div>
</SectionCard>
</div>
{/* Reviews */}
<div className="lg:col-span-3">
<SectionCard title="Reviews from Hosts" icon={<Star size={24} weight="fill" />}>
<div className="space-y-4">
<ReviewItem
reviewerName="Ahmed Khan"
propertyName="Luxury Villa in Clifton"
rating={5}
date="Nov 2024"
comment="Excellent guest! Very respectful and left the property in great condition."
/>
<ReviewItem
reviewerName="Sara Ali"
propertyName="Cozy Apartment in F-7"
rating={5}
date="Oct 2024"
comment="Perfect guest, would definitely host again!"
/>
</div>
</SectionCard>
</div>
</div>
);
}
// Host Dashboard Component
function HostDashboard({ profileData }: { profileData: ProfileData }) {
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Host Stats */}
<div className="lg:col-span-3 grid grid-cols-1 md:grid-cols-4 gap-6">
<StatCard
icon={<Buildings size={32} weight="bold" />}
title="Properties"
value={profileData.stats.properties?.toString() || '0'}
subtitle="Active listings"
/>
<StatCard
icon={<Star size={32} weight="fill" className="text-yellow-500" />}
title="Rating"
value={profileData.stats.rating.toString()}
subtitle={`${profileData.stats.totalReviews} reviews`}
/>
<StatCard
icon={<ChatCircle size={32} weight="bold" />}
title="Response Rate"
value={`${profileData.stats.responseRate}%`}
subtitle={profileData.stats.responseTime || 'N/A'}
/>
<StatCard
icon={<ChartLine size={32} weight="bold" className="text-green-600" />}
title="Total Bookings"
value={profileData.stats.totalBookings?.toString() || '0'}
subtitle="All time"
/>
</div>
{/* My Properties */}
<div className="lg:col-span-2">
<SectionCard title="My Properties" icon={<House size={24} weight="bold" />}>
<div className="space-y-4">
<PropertyItem
name="Luxury Villa in Clifton"
location="Karachi, Pakistan"
rating={4.9}
reviews={23}
bookings={45}
status="Active"
/>
<PropertyItem
name="Modern Apartment in DHA"
location="Karachi, Pakistan"
rating={4.7}
reviews={18}
bookings={32}
status="Active"
/>
<PropertyItem
name="Beach House in Hawksbay"
location="Karachi, Pakistan"
rating={4.8}
reviews={15}
bookings={28}
status="Inactive"
/>
</div>
<button className="w-full mt-4 bg-[#E7FE78] border-2 border-black px-4 py-3 rounded-lg shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:scale-105 transition-all duration-300 font-bold">
+ Add New Property
</button>
</SectionCard>
</div>
{/* Host Tools */}
<div className="lg:col-span-1">
<SectionCard title="Host Tools" icon={<Gear size={24} weight="bold" />}>
<div className="space-y-3">
<QuickActionButton icon={<Calendar size={20} />} text="Calendar" />
<QuickActionButton icon={<ChartLine size={20} />} text="Analytics" />
<QuickActionButton icon={<CreditCard size={20} />} text="Earnings" />
<QuickActionButton icon={<ChatCircle size={20} />} text="Messages" />
<QuickActionButton icon={<Bell size={20} />} text="Notifications" />
<QuickActionButton icon={<Gear size={20} />} text="Settings" />
</div>
</SectionCard>
{/* Achievement Badge */}
<div className="mt-6 border-4 border-black rounded-xl p-6 bg-gradient-to-br from-yellow-100 to-yellow-200 shadow-[8px_8px_0px_rgba(0,0,0,1)] text-center">
<Medal size={48} weight="fill" className="mx-auto mb-3 text-yellow-600" />
<h3 className="font-bold text-xl mb-2">Superhost</h3>
<p className="text-sm text-gray-700">
You&apos;re in the top 10% of hosts in Karachi!
</p>
</div>
</div>
{/* Recent Reviews */}
<div className="lg:col-span-3">
<SectionCard title="Recent Guest Reviews" icon={<Star size={24} weight="fill" />}>
<div className="space-y-4">
<ReviewItem
reviewerName="John Doe"
propertyName="Luxury Villa in Clifton"
rating={5}
date="Dec 2024"
comment="Amazing property! The host was very responsive and helpful. Highly recommend!"
/>
<ReviewItem
reviewerName="Jane Smith"
propertyName="Modern Apartment in DHA"
rating={4}
date="Nov 2024"
comment="Great location and clean apartment. Had a wonderful stay!"
/>
</div>
</SectionCard>
</div>
</div>
);
}
// Reusable Components
function StatCard({
icon,
title,
value,
subtitle,
}: {
icon: React.ReactNode;
title: string;
value: string;
subtitle: string;
}) {
return (
<div className="border-4 border-black rounded-xl p-6 bg-white shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:shadow-[8px_8px_0px_rgba(0,0,0,1)] hover:scale-105 transition-all duration-300">
<div className="flex items-center gap-4 mb-3">
{icon}
<h3 className="font-bold text-lg">{title}</h3>
</div>
<p className="text-3xl font-bold mb-1">{value}</p>
<p className="text-sm text-gray-600">{subtitle}</p>
</div>
);
}
function SectionCard({
title,
icon,
children,
}: {
title: string;
icon: React.ReactNode;
children: React.ReactNode;
}) {
return (
<div className="border-4 border-black rounded-xl p-6 bg-white shadow-[4px_4px_0px_rgba(0,0,0,1)]">
<div className="flex items-center gap-3 mb-6 pb-4 border-b-2 border-black">
{icon}
<h2 className="text-2xl font-bold">{title}</h2>
</div>
{children}
</div>
);
}
function QuickActionButton({
icon,
text,
variant = 'default',
}: {
icon: React.ReactNode;
text: string;
variant?: 'default' | 'danger';
}) {
const bgColor = variant === 'danger' ? 'bg-red-50 hover:bg-red-100' : 'bg-gray-50 hover:bg-gray-100';
const textColor = variant === 'danger' ? 'text-red-600' : 'text-black';
return (
<button
className={`w-full flex items-center gap-3 ${bgColor} border-2 border-black px-4 py-3 rounded-lg shadow-[2px_2px_0px_rgba(0,0,0,1)] hover:shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:scale-105 transition-all duration-300 font-bold ${textColor}`}
>
{icon}
<span>{text}</span>
</button>
);
}
function BookingItem({
propertyName,
location,
dates,
status,
}: {
propertyName: string;
location: string;
dates: string;
status: string;
}) {
const statusColor = status === 'Upcoming' ? 'bg-blue-100 text-blue-700' : 'bg-green-100 text-green-700';
return (
<div className="border-2 border-black rounded-lg p-4 bg-gray-50 hover:bg-gray-100 transition-all duration-300">
<div className="flex justify-between items-start mb-2">
<h4 className="font-bold text-lg">{propertyName}</h4>
<span className={`px-3 py-1 rounded-lg text-xs font-bold border-2 border-black ${statusColor}`}>
{status}
</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600 mb-1">
<MapPin size={16} weight="bold" />
<span>{location}</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600">
<Calendar size={16} weight="bold" />
<span>{dates}</span>
</div>
</div>
);
}
function PropertyItem({
name,
location,
rating,
reviews,
bookings,
status,
}: {
name: string;
location: string;
rating: number;
reviews: number;
bookings: number;
status: string;
}) {
const statusColor = status === 'Active' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700';
return (
<div className="border-2 border-black rounded-lg p-4 bg-gray-50 hover:bg-gray-100 transition-all duration-300">
<div className="flex justify-between items-start mb-2">
<h4 className="font-bold text-lg">{name}</h4>
<span className={`px-3 py-1 rounded-lg text-xs font-bold border-2 border-black ${statusColor}`}>
{status}
</span>
</div>
<div className="flex items-center gap-2 text-sm text-gray-600 mb-3">
<MapPin size={16} weight="bold" />
<span>{location}</span>
</div>
<div className="flex gap-4 text-sm">
<div className="flex items-center gap-1">
<Star size={16} weight="fill" className="text-yellow-500" />
<span className="font-bold">{rating}</span>
<span className="text-gray-600">({reviews})</span>
</div>
<div className="flex items-center gap-1">
<Bed size={16} weight="bold" />
<span className="font-bold">{bookings}</span>
<span className="text-gray-600">bookings</span>
</div>
</div>
</div>
);
}
function ReviewItem({
reviewerName,
propertyName,
rating,
date,
comment,
}: {
reviewerName: string;
propertyName: string;
rating: number;
date: string;
comment: string;
}) {
return (
<div className="border-2 border-black rounded-lg p-4 bg-gray-50">
<div className="flex justify-between items-start mb-3">
<div>
<h4 className="font-bold text-lg">{reviewerName}</h4>
<p className="text-sm text-gray-600">{propertyName}</p>
</div>
<div className="text-right">
<div className="flex items-center gap-1 mb-1">
{[...Array(5)].map((_, i) => (
<Star
key={i}
size={16}
weight="fill"
className={i < rating ? 'text-yellow-500' : 'text-gray-300'}
/>
))}
</div>
<p className="text-xs text-gray-600">{date}</p>
</div>
</div>
<p className="text-gray-700">{comment}</p>
</div>
);
}
export default Profile;

View File

@@ -1,5 +1,12 @@
import localFont from "next/font/local";
"use client";
import { useState, useRef } from "react";
import Image from "next/image";
import localFont from "next/font/local";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { Envelope, Lock, ShieldCheck, ArrowRight, CircleNotch, Eye, EyeSlash } from "@phosphor-icons/react";
import logo from '../../../public/logo.png';
const figtree = localFont({
src: [
@@ -7,14 +14,379 @@ const figtree = localFont({
path: '../../../public/Fonts/figtree/figtree.ttf',
},
],
})
});
// Reusing styles for consistency
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 bg-white`;
const buttonStyle = `${figtree.className} w-full flex items-center justify-center gap-2 px-8 py-4 border-2 border-black bg-[#E7FE78] text-lg font-bold 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 labelStyle = `${figtree.className} text-sm font-bold uppercase tracking-wider mb-2 block`;
function Login() {
const [step, setStep] = useState<'credentials' | '2fa' | 'forgot-password' | 'recovery-sent'>('credentials');
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
email: '',
password: '',
otp: ''
});
const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const formRef = useRef<HTMLDivElement>(null);
const imageRef = useRef<HTMLDivElement>(null);
useGSAP(() => {
// Animate image side
gsap.fromTo(imageRef.current,
{ x: -50, opacity: 0 },
{ x: 0, opacity: 1, duration: 1, ease: "power3.out" }
);
// Animate form side
gsap.fromTo(formRef.current,
{ x: 50, opacity: 0 },
{ x: 0, opacity: 1, duration: 1, delay: 0.2, ease: "power3.out" }
);
}, []);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
// Simulate API call
setTimeout(() => {
setIsLoading(false);
if (formData.email === 'asabizanjo@gmail.com' && formData.password === 'Balouch@12') {
// Transition to 2FA
gsap.to(formRef.current, {
x: -20,
opacity: 0,
duration: 0.3,
onComplete: () => {
setStep('2fa');
gsap.fromTo(formRef.current,
{ x: 20, opacity: 0 },
{ x: 0, opacity: 1, duration: 0.3 }
);
}
});
} else {
setError('Invalid email or password');
}
}, 1000);
};
const handleVerify = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
// Simulate API call
setTimeout(() => {
setIsLoading(false);
if (formData.otp === '0000') {
alert('Login Successful! (JWT would be stored here)');
// Here you would store the JWT
} else {
setError('Invalid OTP. Please try again (Hint: 0000)');
}
}, 1000);
};
const handleForgotPasswordSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
// Simulate API call
setTimeout(() => {
setIsLoading(false);
if (formData.email) {
gsap.to(formRef.current, {
x: -20,
opacity: 0,
duration: 0.3,
onComplete: () => {
setStep('recovery-sent');
gsap.fromTo(formRef.current,
{ x: 20, opacity: 0 },
{ x: 0, opacity: 1, duration: 0.3 }
);
}
});
} else {
setError('Please enter your email address');
}
}, 1000);
};
const Login = () => {
return (
<div>
<h1 className={figtree.className}>Login</h1>
<div className="flex min-h-screen w-full bg-[#FAFAFA] overflow-hidden">
{/* Left Side - Visual/Logo */}
<div className="hidden lg:flex w-1/2 bg-[#E7FE78] border-r-4 border-black flex-col items-center justify-center p-12 relative">
<div className="absolute inset-0 opacity-10"
style={{
backgroundImage: `radial-gradient(circle at 2px 2px, black 1px, transparent 0)`,
backgroundSize: '32px 32px'
}}
/>
<div ref={imageRef} className="relative z-10 flex flex-col items-center text-center">
<div className="w-64 h-64 bg-white border-4 border-black rounded-full flex items-center justify-center mb-12 shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
<Image
src={logo}
alt="Logo"
width={180}
height={180}
className="object-contain"
/>
</div>
)
<h1 className={`${figtree.className} text-5xl font-black mb-6 tracking-tight`}>
Welcome Back
</h1>
<p className={`${figtree.className} text-xl font-medium max-w-md leading-relaxed`}>
Your Home Away From Home
</p>
</div>
</div>
{/* Right Side - Form */}
<div className="w-full lg:w-1/2 flex flex-col items-center justify-center p-8 lg:p-24 bg-white relative">
<div ref={formRef} className="w-full max-w-md">
{step === 'credentials' && (
<>
<div className="mb-10">
<h2 className={`${figtree.className} text-4xl font-bold mb-3`}>Sign In</h2>
<p className={`${figtree.className} text-gray-500 text-lg`}>Enter your details to access your account</p>
</div>
<form onSubmit={handleLogin} className="flex flex-col gap-6">
<div>
<label className={labelStyle}>Email Address</label>
<div className="relative">
<input
type="email"
placeholder="john@example.com"
className={inputStyle}
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
<Envelope className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" size={24} />
</div>
</div>
<div>
<div className="flex justify-between items-center">
<label className={labelStyle}>Password</label>
<button
type="button"
className={`${figtree.className} text-sm font-bold text-gray-500 hover:text-black transition-colors mb-2`}
onClick={() => {
setError('');
gsap.to(formRef.current, {
x: -20,
opacity: 0,
duration: 0.3,
onComplete: () => {
setStep('forgot-password');
gsap.fromTo(formRef.current,
{ x: 20, opacity: 0 },
{ x: 0, opacity: 1, duration: 0.3 }
);
}
});
}}
>
Forgot Password?
</button>
</div>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
placeholder="••••••••"
className={inputStyle}
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
>
{showPassword ? <EyeSlash size={24} /> : <Eye size={24} />}
</button>
</div>
</div>
{error && (
<div className={`${figtree.className} p-4 bg-red-50 border-2 border-red-500 text-red-500 font-bold flex items-center gap-2`}>
<ShieldCheck size={20} />
{error}
</div>
)}
<button
type="submit"
disabled={isLoading}
className={buttonStyle}
>
{isLoading ? (
<CircleNotch className="animate-spin" size={24} />
) : (
<>Sign In <ArrowRight size={20} weight="bold" /></>
)}
</button>
<div className="text-center mt-4">
<p className={`${figtree.className} text-gray-500`}>
Don't have an account?{' '}
<a href="/signup" className="text-black font-bold hover:underline">
Sign Up
</a>
</p>
</div>
</form>
</>
)}
{step === '2fa' && (
<>
<div className="mb-10">
<h2 className={`${figtree.className} text-4xl font-bold mb-3`}>Two-Factor Auth</h2>
<p className={`${figtree.className} text-gray-500 text-lg`}>Enter the code sent to your device</p>
</div>
<form onSubmit={handleVerify} className="flex flex-col gap-6">
<div>
<label className={labelStyle}>Authentication Code</label>
<div className="relative">
<input
type="text"
placeholder="0000"
maxLength={4}
className={`${inputStyle} text-center text-3xl tracking-[1em] font-bold`}
value={formData.otp}
onChange={(e) => setFormData({ ...formData, otp: e.target.value })}
/>
</div>
<p className={`${figtree.className} text-xs text-gray-400 mt-2 text-center`}>Use '0000' for testing</p>
</div>
{error && (
<div className={`${figtree.className} p-4 bg-red-50 border-2 border-red-500 text-red-500 font-bold flex items-center gap-2`}>
<ShieldCheck size={20} />
{error}
</div>
)}
<button
type="submit"
disabled={isLoading}
className={buttonStyle}
>
{isLoading ? (
<CircleNotch className="animate-spin" size={24} />
) : (
<>Verify & Login <ShieldCheck size={20} weight="bold" /></>
)}
</button>
<button
type="button"
onClick={() => {
setStep('credentials');
setError('');
}}
className={`${figtree.className} text-gray-500 hover:text-black hover:underline text-center mt-4`}
>
Back to Login
</button>
</form>
</>
)}
{step === 'forgot-password' && (
<>
<div className="mb-10">
<h2 className={`${figtree.className} text-4xl font-bold mb-3`}>Reset Password</h2>
<p className={`${figtree.className} text-gray-500 text-lg`}>Enter your email to receive a recovery link</p>
</div>
<form onSubmit={handleForgotPasswordSubmit} className="flex flex-col gap-6">
<div>
<label className={labelStyle}>Email Address</label>
<div className="relative">
<input
type="email"
placeholder="john@example.com"
className={inputStyle}
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
<Envelope className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" size={24} />
</div>
</div>
{error && (
<div className={`${figtree.className} p-4 bg-red-50 border-2 border-red-500 text-red-500 font-bold flex items-center gap-2`}>
<ShieldCheck size={20} />
{error}
</div>
)}
<button
type="submit"
disabled={isLoading}
className={buttonStyle}
>
{isLoading ? (
<CircleNotch className="animate-spin" size={24} />
) : (
<>Send Recovery Link <ArrowRight size={20} weight="bold" /></>
)}
</button>
<button
type="button"
onClick={() => {
setStep('credentials');
setError('');
}}
className={`${figtree.className} text-gray-500 hover:text-black hover:underline text-center mt-4`}
>
Back to Login
</button>
</form>
</>
)}
{step === 'recovery-sent' && (
<div className="text-center">
<div className="w-20 h-20 bg-[#E7FE78] rounded-full flex items-center justify-center mx-auto mb-6 border-2 border-black shadow-[4px_4px_0px_0px_rgba(0,0,0,1)]">
<Envelope size={40} weight="fill" />
</div>
<h2 className={`${figtree.className} text-3xl font-bold mb-4`}>Check Your Email</h2>
<p className={`${figtree.className} text-gray-500 text-lg mb-8`}>
We've sent a password recovery link to <br />
<span className="font-bold text-black">{formData.email}</span>
</p>
<button
type="button"
onClick={() => {
setStep('credentials');
setError('');
}}
className={buttonStyle}
>
Back to Login
</button>
</div>
)}
</div>
</div>
</div>
);
}
export default Login
export default Login;

View File

@@ -1,5 +1,12 @@
import localFont from "next/font/local";
"use client";
import { useState, useRef } from "react";
import Image from "next/image";
import localFont from "next/font/local";
import { useGSAP } from "@gsap/react";
import gsap from "gsap";
import { Envelope, Lock, ShieldCheck, ArrowRight, CircleNotch, Eye, EyeSlash, User, Phone } from "@phosphor-icons/react";
import logo from '../../../public/logo.png';
const figtree = localFont({
src: [
@@ -7,14 +14,367 @@ const figtree = localFont({
path: '../../../public/Fonts/figtree/figtree.ttf',
},
],
})
});
// Reusing styles for consistency
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 bg-white`;
const buttonStyle = `${figtree.className} w-full flex items-center justify-center gap-2 px-8 py-4 border-2 border-black bg-[#E7FE78] text-lg font-bold 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 labelStyle = `${figtree.className} text-sm font-bold uppercase tracking-wider mb-2 block`;
const countries = [
{ code: 'PK', name: 'Pakistan', dial_code: '+92' },
{ code: 'US', name: 'United States', dial_code: '+1' },
{ code: 'GB', name: 'United Kingdom', dial_code: '+44' },
{ code: 'CA', name: 'Canada', dial_code: '+1' },
{ code: 'AU', name: 'Australia', dial_code: '+61' },
{ code: 'UAE', name: 'UAE', dial_code: '+971' },
{ code: 'SA', name: 'Saudi Arabia', dial_code: '+966' },
];
function Signup() {
const [step, setStep] = useState<'details' | 'verification'>('details');
const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
countryCode: '+92',
phone: '',
password: '',
confirmPassword: '',
emailOtp: '',
phoneOtp: ''
});
const [error, setError] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const formRef = useRef<HTMLDivElement>(null);
const imageRef = useRef<HTMLDivElement>(null);
useGSAP(() => {
// Animate image side
gsap.fromTo(imageRef.current,
{ x: -50, opacity: 0 },
{ x: 0, opacity: 1, duration: 1, ease: "power3.out" }
);
// Animate form side
gsap.fromTo(formRef.current,
{ x: 50, opacity: 0 },
{ x: 0, opacity: 1, duration: 1, delay: 0.2, ease: "power3.out" }
);
}, []);
const handleSignup = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
// Password validation
const passwordRegex = /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{8,}$/;
if (!passwordRegex.test(formData.password)) {
setError('Password must be at least 8 characters with a number and special character');
setIsLoading(false);
return;
}
if (formData.password !== formData.confirmPassword) {
setError('Passwords do not match');
setIsLoading(false);
return;
}
// Simulate API call
setTimeout(() => {
setIsLoading(false);
if (formData.name && formData.email && formData.phone && formData.password) {
// Transition to Verification
gsap.to(formRef.current, {
x: -20,
opacity: 0,
duration: 0.3,
onComplete: () => {
setStep('verification');
gsap.fromTo(formRef.current,
{ x: 20, opacity: 0 },
{ x: 0, opacity: 1, duration: 0.3 }
);
}
});
} else {
setError('Please fill in all fields');
}
}, 1000);
};
const handleVerify = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
// Simulate API call
setTimeout(() => {
setIsLoading(false);
if (formData.emailOtp === '0000' && formData.phoneOtp === '0000') {
alert('Signup Successful! Account Created.');
// Here you would redirect or store token
} else {
setError('Invalid OTPs. Please try again (Hint: 0000 for both)');
}
}, 1000);
};
const Signup = () => {
return (
<div>
<h1 className={`${figtree.className} text-4xl font-bold`}>Signup</h1>
<div className="flex min-h-screen w-full bg-[#FAFAFA] overflow-hidden">
{/* Left Side - Visual/Logo */}
<div className="hidden lg:flex w-1/2 bg-[#E7FE78] border-r-4 border-black flex-col items-center justify-center p-12 relative">
<div className="absolute inset-0 opacity-10"
style={{
backgroundImage: `radial-gradient(circle at 2px 2px, black 1px, transparent 0)`,
backgroundSize: '32px 32px'
}}
/>
<div ref={imageRef} className="relative z-10 flex flex-col items-center text-center">
<div className="w-64 h-64 bg-white border-4 border-black rounded-full flex items-center justify-center mb-12 shadow-[8px_8px_0px_0px_rgba(0,0,0,1)]">
<Image
src={logo}
alt="Logo"
width={180}
height={180}
className="object-contain"
/>
</div>
)
<h1 className={`${figtree.className} text-5xl font-black mb-6 tracking-tight`}>
Join Us Today
</h1>
<p className={`${figtree.className} text-xl font-medium max-w-md leading-relaxed`}>
Start Your Journey With Us
</p>
</div>
</div>
{/* Right Side - Form */}
<div className="w-full lg:w-1/2 flex flex-col items-center justify-center p-8 lg:p-24 bg-white relative">
<div ref={formRef} className="w-full max-w-md">
{step === 'details' ? (
<>
<div className="mb-10">
<h2 className={`${figtree.className} text-4xl font-bold mb-3`}>Create Account</h2>
<p className={`${figtree.className} text-gray-500 text-lg`}>Enter your details to get started</p>
</div>
<form onSubmit={handleSignup} className="flex flex-col gap-6">
<div>
<label className={labelStyle}>Full Name</label>
<div className="relative">
<input
type="text"
placeholder="Abrar Malik"
className={inputStyle}
value={formData.name}
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
/>
<User className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" size={24} />
</div>
</div>
<div>
<label className={labelStyle}>Email Address</label>
<div className="relative">
<input
type="email"
placeholder="abrar@example.com"
className={inputStyle}
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
/>
<Envelope className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" size={24} />
</div>
</div>
<div>
<label className={labelStyle}>Phone Number</label>
<div className="flex gap-2">
<div className="relative w-32 shrink-0">
<select
className={`${inputStyle} pr-8 appearance-none cursor-pointer`}
value={formData.countryCode}
onChange={(e) => setFormData({ ...formData, countryCode: e.target.value })}
>
{countries.map((country) => (
<option key={`${country.code}-${country.dial_code}`} value={country.dial_code}>
{country.code} {country.dial_code}
</option>
))}
</select>
<div className="absolute right-2 top-1/2 -translate-y-1/2 pointer-events-none">
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.5 4.5L6 8L9.5 4.5" stroke="black" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
</div>
<div className="relative flex-1">
<input
type="tel"
placeholder="3333576756"
className={inputStyle}
value={formData.phone}
onChange={(e) => setFormData({ ...formData, phone: e.target.value })}
/>
<Phone className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" size={24} />
</div>
</div>
</div>
<div>
<label className={labelStyle}>Password</label>
<div className="relative">
<input
type={showPassword ? "text" : "password"}
placeholder="••••••••"
className={inputStyle}
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
>
{showPassword ? <EyeSlash size={24} /> : <Eye size={24} />}
</button>
</div>
<p className={`${figtree.className} text-xs text-gray-400 mt-1`}>
Min. 8 characters, 1 number, 1 special character
</p>
</div>
<div>
<label className={labelStyle}>Confirm Password</label>
<div className="relative">
<input
type={showConfirmPassword ? "text" : "password"}
placeholder="••••••••"
className={inputStyle}
value={formData.confirmPassword}
onChange={(e) => setFormData({ ...formData, confirmPassword: e.target.value })}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 transition-colors"
>
{showConfirmPassword ? <EyeSlash size={24} /> : <Eye size={24} />}
</button>
</div>
</div>
{error && (
<div className={`${figtree.className} p-4 bg-red-50 border-2 border-red-500 text-red-500 font-bold flex items-center gap-2`}>
<ShieldCheck size={20} />
{error}
</div>
)}
<button
type="submit"
disabled={isLoading}
className={buttonStyle}
>
{isLoading ? (
<CircleNotch className="animate-spin" size={24} />
) : (
<>Sign Up <ArrowRight size={20} weight="bold" /></>
)}
</button>
<div className="text-center mt-4">
<p className={`${figtree.className} text-gray-500`}>
Already have an account?{' '}
<a href="/login" className="text-black font-bold hover:underline">
Log in
</a>
</p>
</div>
</form>
</>
) : (
<>
<div className="mb-10">
<h2 className={`${figtree.className} text-4xl font-bold mb-3`}>Verify Account</h2>
<p className={`${figtree.className} text-gray-500 text-lg`}>Enter the codes sent to your email and phone</p>
</div>
<form onSubmit={handleVerify} className="flex flex-col gap-6">
<div>
<label className={labelStyle}>Email OTP</label>
<div className="relative">
<input
type="text"
placeholder="0000"
maxLength={4}
className={`${inputStyle} text-center text-2xl tracking-[0.5em] font-bold`}
value={formData.emailOtp}
onChange={(e) => setFormData({ ...formData, emailOtp: e.target.value })}
/>
<Envelope className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" size={24} />
</div>
</div>
<div>
<label className={labelStyle}>Phone OTP</label>
<div className="relative">
<input
type="text"
placeholder="0000"
maxLength={4}
className={`${inputStyle} text-center text-2xl tracking-[0.5em] font-bold`}
value={formData.phoneOtp}
onChange={(e) => setFormData({ ...formData, phoneOtp: e.target.value })}
/>
<Phone className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400" size={24} />
</div>
<p className={`${figtree.className} text-xs text-gray-400 mt-2 text-center`}>Use '0000' for testing both</p>
</div>
{error && (
<div className={`${figtree.className} p-4 bg-red-50 border-2 border-red-500 text-red-500 font-bold flex items-center gap-2`}>
<ShieldCheck size={20} />
{error}
</div>
)}
<button
type="submit"
disabled={isLoading}
className={buttonStyle}
>
{isLoading ? (
<CircleNotch className="animate-spin" size={24} />
) : (
<>Verify & Create Account <ShieldCheck size={20} weight="bold" /></>
)}
</button>
<button
type="button"
onClick={() => {
setStep('details');
setError('');
}}
className={`${figtree.className} text-gray-500 hover:text-black hover:underline text-center mt-4`}
>
Back to Details
</button>
</form>
</>
)}
</div>
</div>
</div>
);
}
export default Signup
export default Signup;

View File

@@ -1,32 +1,24 @@
import localFont from "next/font/local";
'use client';
import Image from "next/image";
import badge from '../../../public/badge-ticket.png'
import arrow from '../../../public/arrow.png'
const figtree = localFont({
src: [
{
path: '../../../public/Fonts/figtree/figtree.ttf',
},
],
})
function Hero() {
return (
<div>
<div className="flex flex-col w-full items-center justify-center mb-16">
<div className="flex flex-row items-center justify-between gap-4">
<h1 className={`${figtree.className} text-7xl font-medium`}>Find Your PakStay Home in</h1>
<h1 style={{ fontFamily: 'Figtree, sans-serif' }} className="text-7xl font-medium">Find Your PakStay Home in</h1>
<div className="relative">
<Image src={badge} alt="badge" width={400} height={400} />
<div className="absolute top-1/2 left-[52%] transform -translate-x-1/2 -translate-y-1/2 rotate-[-8deg]">
<h2 className={`${figtree.className} text-6xl font-bold text-black uppercase`}>Karachi</h2>
<h2 style={{ fontFamily: 'Figtree, sans-serif' }} className="text-6xl font-bold text-black uppercase">Karachi</h2>
</div>
</div>
</div>
<div>
<h3 className={`${figtree.className} text-xl font-bold`}>Travel Karo, Tension Free!</h3>
<h3 style={{ fontFamily: 'Figtree, sans-serif' }} className="text-xl font-bold">Travel Karo, Tension Free!</h3>
</div>
</div>
</div>

View File

@@ -0,0 +1,86 @@
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>
);
};

View File

@@ -0,0 +1,42 @@
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>
)
}

View File

@@ -0,0 +1,48 @@
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>
)
}

View File

@@ -0,0 +1,32 @@
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>
)
}

View File

@@ -0,0 +1,35 @@
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>
)
}

View File

@@ -0,0 +1,29 @@
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>
)
}

View File

@@ -0,0 +1,94 @@
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>
)
}

View File

@@ -0,0 +1,34 @@
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>
)
}

View File

@@ -0,0 +1,24 @@
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>
)
}

View File

@@ -0,0 +1,32 @@
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>
)
}

View File

@@ -0,0 +1,43 @@
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>
)
}

View File

@@ -1,528 +1,24 @@
"use client";
import { useState, useRef, useEffect } from "react";
import localFont from "next/font/local";
import { useState, useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
import Image from "next/image";
import {
House,
Buildings,
Farm,
Bed,
MapPin,
Camera,
WifiHigh,
Article,
CurrencyDollar,
CheckCircle,
CalendarBlank,
ShieldCheck,
CaretLeft,
CaretRight,
Star,
SwimmingPool,
Car,
CookingPot,
TelevisionSimple,
Wind,
X,
WarningCircle,
} from "@phosphor-icons/react/dist/ssr";
import { CaretLeft, CaretRight } from "@phosphor-icons/react/dist/ssr";
const figtree = localFont({
src: [
{
path: '../../../public/Fonts/figtree/figtree.ttf',
},
],
})
import { FormData, FormValue } from "./types";
import { containerStyle, buttonStyle, secondaryButtonStyle, figtree } from "./styles";
// Shared styles matching search.tsx and results.tsx
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-4xl font-bold mb-2`;
const subtitleStyle = `${figtree.className} text-lg text-gray-500 mb-8`;
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 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) =>
`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 {
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; // Added title for preview
photos?: string[];
}
type FormValue = string | number | boolean | string[] | undefined;
interface StepProps {
data: FormData;
updateData: (key: keyof FormData, value: FormValue) => void;
}
/* --- Live Preview Component --- */
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 = [
{ 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>
)
}
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>
)
}
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>
)
}
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>
)
}
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>
)
}
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>
)
}
const PropertyPhotos = ({ 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 (
<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>
)}
</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>
<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>
)
}
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.</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. You will be notified of each booking request.</span>
</div>
</button>
</div>
</div>
)
}
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>
)
}
/* --- Main List Component --- */
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";
function List() {
const [step, setStep] = useState(0);
@@ -548,9 +44,17 @@ function List() {
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 progress = ((step + 1) / totalSteps) * 100;
const updateData = (key: keyof FormData, value: FormValue) => {
setFormData(prev => ({ ...prev, [key]: value }));
@@ -588,31 +92,43 @@ function List() {
<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 */}
<div className="px-8 py-6 border-b-2 border-gray-100 flex justify-between items-center bg-white z-10">
{/* 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`}>{
step === 0 ? "Basics" :
step < 4 ? "Details" :
step < 7 ? "Description" : "Finish"
}</span>
<span className={`${figtree.className} text-xs text-gray-500`}>{currentPhase.name}</span>
</div>
</div>
<button className="text-sm font-bold underline decoration-2 underline-offset-4 hover:text-gray-600">Save & Exit</button>
</div>
{/* Progress Bar */}
<div className="w-full h-1 bg-gray-100">
{/* 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">
<div
className="h-full bg-[#E7FE78] transition-all duration-500 ease-out"
style={{ width: `${progress}%` }}
className="absolute top-0 left-0 h-full bg-[#E7FE78] transition-all duration-500 ease-out"
style={{ width: `${fill}%` }}
/>
</div>
);
})}
</div>
</div>
{/* Scrollable Content */}
<div className="flex-1 overflow-y-auto p-8 md:p-12">

View File

@@ -0,0 +1,18 @@
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'}`;

View File

@@ -0,0 +1,23 @@
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;
}

View File

@@ -0,0 +1,188 @@
"use client";
import { useState } from "react";
import localFont from "next/font/local";
import {
House,
MapPin,
List,
Image as ImageIcon,
CurrencyDollar,
ShieldCheck,
CaretLeft,
Check,
Info
} from "@phosphor-icons/react/dist/ssr";
import { FormData, FormValue } from "../list-property/types";
import { PropertyType } from "../list-property/PropertyType";
import { PropertyPlaceType } from "../list-property/PropertyPlaceType";
import { PropertyLocation } from "../list-property/PropertyLocation";
import { PropertyCapacity } from "../list-property/PropertyCapacity";
import { PropertyAmenities } from "../list-property/PropertyAmenities";
import { PropertyDescription } from "../list-property/PropertyDescription";
import { PropertyPhotos } from "../list-property/PropertyPhotos";
import { PropertyPrice } from "../list-property/PropertyPrice";
import { PropertyInstantApproval } from "../list-property/PropertyInstantApproval";
import { SafetyDetails } from "../list-property/SafetyDetails";
const figtree = localFont({
src: [
{
path: '../../../public/Fonts/figtree/figtree.ttf',
},
],
})
const sidebarItemStyle = (active: boolean) =>
`flex items-center gap-3 p-4 rounded-lg cursor-pointer transition-all ${active ? 'bg-black text-white' : 'hover:bg-gray-100 text-gray-700'}`;
const sectionTitleStyle = `${figtree.className} text-3xl font-bold mb-6`;
export default function ManageProperty() {
const [activeSection, setActiveSection] = useState("details");
const [formData, setFormData] = useState<FormData>({
// Mock existing data
title: "Cozy Cottage in the Hills",
description: "This unique place has a style all its own. Located in the heart of the mountains, this cottage offers a perfect getaway.",
propertyType: "house",
placeType: "entire",
location: "123 Mountain View Rd, Hilltop, CA",
guests: 4,
bedrooms: 2,
beds: 3,
bathrooms: 1,
amenities: ["wifi", "kitchen", "parking"],
price: "15000",
instantBook: true,
safety: ["camera"],
photos: [
"https://images.unsplash.com/photo-1499793983690-e29da59ef1c2?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80",
"https://images.unsplash.com/photo-1502005229762-cf1b2da7c5d6?ixlib=rb-4.0.3&auto=format&fit=crop&w=1974&q=80",
"https://images.unsplash.com/photo-1484154218962-a1c002085d2f?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80",
"https://images.unsplash.com/photo-1513694203232-719a280e022f?ixlib=rb-4.0.3&auto=format&fit=crop&w=2069&q=80",
"https://images.unsplash.com/photo-1505691938895-1758d7feb511?ixlib=rb-4.0.3&auto=format&fit=crop&w=2070&q=80"
]
});
const updateData = (key: keyof FormData, value: FormValue) => {
setFormData(prev => ({ ...prev, [key]: value }));
};
const sections = [
{ id: "details", label: "Property Details", icon: Info },
{ id: "location", label: "Location", icon: MapPin },
{ id: "amenities", label: "Amenities", icon: List },
{ id: "photos", label: "Photos", icon: ImageIcon },
{ id: "pricing", label: "Pricing & Booking", icon: CurrencyDollar },
{ id: "safety", label: "Safety", icon: ShieldCheck },
];
const renderContent = () => {
switch (activeSection) {
case "details":
return (
<div className="flex flex-col gap-12">
<div>
<h2 className={sectionTitleStyle}>Property Details</h2>
<PropertyDescription data={formData} updateData={updateData} />
</div>
<div className="border-t border-gray-200 pt-8">
<PropertyType data={formData} updateData={updateData} />
</div>
<div className="border-t border-gray-200 pt-8">
<PropertyPlaceType data={formData} updateData={updateData} />
</div>
<div className="border-t border-gray-200 pt-8">
<PropertyCapacity data={formData} updateData={updateData} />
</div>
</div>
);
case "location":
return (
<div>
<h2 className={sectionTitleStyle}>Location</h2>
<PropertyLocation data={formData} updateData={updateData} />
</div>
);
case "amenities":
return (
<div>
<h2 className={sectionTitleStyle}>Amenities</h2>
<PropertyAmenities data={formData} updateData={updateData} />
</div>
);
case "photos":
return (
<div>
<h2 className={sectionTitleStyle}>Photos</h2>
<PropertyPhotos data={formData} updateData={updateData} />
</div>
);
case "pricing":
return (
<div className="flex flex-col gap-12">
<div>
<h2 className={sectionTitleStyle}>Pricing</h2>
<PropertyPrice data={formData} updateData={updateData} />
</div>
<div className="border-t border-gray-200 pt-8">
<h2 className={`${figtree.className} text-2xl font-bold mb-6`}>Booking Settings</h2>
<PropertyInstantApproval data={formData} updateData={updateData} />
</div>
</div>
);
case "safety":
return (
<div>
<h2 className={sectionTitleStyle}>Safety</h2>
<SafetyDetails data={formData} updateData={updateData} />
</div>
);
default:
return null;
}
};
return (
<div className="min-h-screen bg-gray-50 flex flex-col">
{/* Header */}
<header className="bg-white border-b border-gray-200 px-8 py-4 flex items-center justify-between sticky top-0 z-20">
<div className="flex items-center gap-4">
<button className="p-2 hover:bg-[#E7FE78] rounded-full transition-colors">
<CaretLeft size={24} />
</button>
<h1 className={`${figtree.className} text-xl font-bold`}>Manage Listing</h1>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-500">Last saved: Just now</span>
<button className={`${figtree.className} flex items-center gap-2 bg-[#E7FE78] text-black px-4 py-2 rounded-lg shadow-[2px_2px_0px_rgba(0,0,0,1)] font-light italic border-2 border-black hover:scale-105 transition-all duration-300 cursor-pointer hover:shadow-[10px_10px_0px_rgba(0,0,0,1]`}>
Save Changes
</button>
</div>
</header>
<div className="flex flex-1 max-w-7xl mx-auto w-full p-8 gap-8">
{/* Sidebar */}
<aside className="w-64 shrink-0 hidden lg:block sticky top-24 h-fit">
<nav className="flex flex-col gap-2">
{sections.map((section) => (
<div
key={section.id}
onClick={() => setActiveSection(section.id)}
className={sidebarItemStyle(activeSection === section.id)}
>
<section.icon size={24} weight={activeSection === section.id ? "fill" : "regular"} color={activeSection === section.id ? "#E7FE78" : "gray"}/>
<span className={`${figtree.className} font-medium`}>{section.label}</span>
</div>
))}
</nav>
</aside>
{/* Main Content */}
<main className="flex-1 bg-white rounded-xl border border-gray-200 shadow-sm p-8 md:p-12 min-h-[600px]">
{renderContent()}
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,24 @@
'use client';
import Hero from "@/components/landing/hero";
import BestPrices from "@/components/landing/best-prices";
import BrowseByCity from "@/components/landing/browse-by-city";
import Review from "@/components/landing/review";
import FAQs from "@/components/landing/faqs";
import Search from "@/components/landing/search";
function Landing() {
return (
<div>
<Hero />
<Search />
<BestPrices />
<BrowseByCity />
<Review />
<FAQs />
</div>
);
}
export default Landing;

View File

@@ -0,0 +1,12 @@
'use client';
import Profile from '@/components/Profile/profile';
function ProfilePage() {
return (
<div className="min-h-screen bg-white">
<Profile />
</div>
);
}
export default ProfilePage;