profile + authentication
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
213
src/components/Profile/README.md
Normal file
213
src/components/Profile/README.md
Normal 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
|
||||
634
src/components/Profile/profile.tsx
Normal file
634
src/components/Profile/profile.tsx
Normal 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'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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
})
|
||||
});
|
||||
|
||||
const Signup = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1 className={`${figtree.className} text-4xl font-bold`}>Signup</h1>
|
||||
</div>
|
||||
)
|
||||
// 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;
|
||||
}
|
||||
|
||||
export default Signup
|
||||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
<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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
86
src/components/list-property/LivePreview.tsx
Normal file
86
src/components/list-property/LivePreview.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
42
src/components/list-property/PropertyAmenities.tsx
Normal file
42
src/components/list-property/PropertyAmenities.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
48
src/components/list-property/PropertyCapacity.tsx
Normal file
48
src/components/list-property/PropertyCapacity.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
32
src/components/list-property/PropertyDescription.tsx
Normal file
32
src/components/list-property/PropertyDescription.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
35
src/components/list-property/PropertyInstantApproval.tsx
Normal file
35
src/components/list-property/PropertyInstantApproval.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
29
src/components/list-property/PropertyLocation.tsx
Normal file
29
src/components/list-property/PropertyLocation.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
94
src/components/list-property/PropertyPhotos.tsx
Normal file
94
src/components/list-property/PropertyPhotos.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
34
src/components/list-property/PropertyPlaceType.tsx
Normal file
34
src/components/list-property/PropertyPlaceType.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
24
src/components/list-property/PropertyPrice.tsx
Normal file
24
src/components/list-property/PropertyPrice.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
32
src/components/list-property/PropertyType.tsx
Normal file
32
src/components/list-property/PropertyType.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
43
src/components/list-property/SafetyDetails.tsx
Normal file
43
src/components/list-property/SafetyDetails.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
18
src/components/list-property/styles.ts
Normal file
18
src/components/list-property/styles.ts
Normal 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'}`;
|
||||
23
src/components/list-property/types.ts
Normal file
23
src/components/list-property/types.ts
Normal 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;
|
||||
}
|
||||
188
src/components/manage-property/manage.tsx
Normal file
188
src/components/manage-property/manage.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
24
src/pages/landing/page.tsx
Normal file
24
src/pages/landing/page.tsx
Normal 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;
|
||||
12
src/pages/profile/page.tsx
Normal file
12
src/pages/profile/page.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user