profile + authentication
This commit is contained in:
@@ -1,2 +1,8 @@
|
|||||||
@import "tailwindcss";
|
@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 Results from "@/components/search-results/results";
|
||||||
import Features from "@/components/details/features";
|
import Features from "@/components/details/features";
|
||||||
import List from "@/components/list-property/list";
|
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() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<Features />
|
<Profile />
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</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({
|
const figtree = localFont({
|
||||||
src: [
|
src: [
|
||||||
@@ -7,14 +14,379 @@ const figtree = localFont({
|
|||||||
path: '../../../public/Fonts/figtree/figtree.ttf',
|
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 (
|
return (
|
||||||
<div>
|
<div className="flex min-h-screen w-full bg-[#FAFAFA] overflow-hidden">
|
||||||
<h1 className={figtree.className}>Login</h1>
|
{/* 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>
|
</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({
|
const figtree = localFont({
|
||||||
src: [
|
src: [
|
||||||
@@ -7,14 +14,367 @@ const figtree = localFont({
|
|||||||
path: '../../../public/Fonts/figtree/figtree.ttf',
|
path: '../../../public/Fonts/figtree/figtree.ttf',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
});
|
||||||
|
|
||||||
const Signup = () => {
|
// Reusing styles for consistency
|
||||||
return (
|
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`;
|
||||||
<div>
|
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`;
|
||||||
<h1 className={`${figtree.className} text-4xl font-bold`}>Signup</h1>
|
const labelStyle = `${figtree.className} text-sm font-bold uppercase tracking-wider mb-2 block`;
|
||||||
</div>
|
|
||||||
)
|
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 Image from "next/image";
|
||||||
import badge from '../../../public/badge-ticket.png'
|
import badge from '../../../public/badge-ticket.png'
|
||||||
import arrow from '../../../public/arrow.png'
|
import arrow from '../../../public/arrow.png'
|
||||||
|
|
||||||
const figtree = localFont({
|
|
||||||
src: [
|
|
||||||
{
|
|
||||||
path: '../../../public/Fonts/figtree/figtree.ttf',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
function Hero() {
|
function Hero() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col w-full items-center justify-center mb-16">
|
<div className="flex flex-col w-full items-center justify-center mb-16">
|
||||||
<div className="flex flex-row items-center justify-between gap-4">
|
<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">
|
<div className="relative">
|
||||||
<Image src={badge} alt="badge" width={400} height={400} />
|
<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]">
|
<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>
|
||||||
</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>
|
</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";
|
"use client";
|
||||||
|
|
||||||
import { useState, useRef, useEffect } from "react";
|
import { useState, useRef } from "react";
|
||||||
import localFont from "next/font/local";
|
|
||||||
import gsap from "gsap";
|
import gsap from "gsap";
|
||||||
import { useGSAP } from "@gsap/react";
|
import { useGSAP } from "@gsap/react";
|
||||||
import Image from "next/image";
|
import { CaretLeft, CaretRight } from "@phosphor-icons/react/dist/ssr";
|
||||||
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";
|
|
||||||
|
|
||||||
const figtree = localFont({
|
import { FormData, FormValue } from "./types";
|
||||||
src: [
|
import { containerStyle, buttonStyle, secondaryButtonStyle, figtree } from "./styles";
|
||||||
{
|
|
||||||
path: '../../../public/Fonts/figtree/figtree.ttf',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
// Shared styles matching search.tsx and results.tsx
|
import { PropertyType } from "./PropertyType";
|
||||||
const containerStyle = `flex flex-col gap-8 w-full max-w-xl mx-auto pb-24`; // Added padding bottom for fixed footer
|
import { PropertyPlaceType } from "./PropertyPlaceType";
|
||||||
const titleStyle = `${figtree.className} text-4xl font-bold mb-2`;
|
import { PropertyLocation } from "./PropertyLocation";
|
||||||
const subtitleStyle = `${figtree.className} text-lg text-gray-500 mb-8`;
|
import { PropertyCapacity } from "./PropertyCapacity";
|
||||||
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`;
|
import { PropertyAmenities } from "./PropertyAmenities";
|
||||||
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`;
|
import { PropertyDescription } from "./PropertyDescription";
|
||||||
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`;
|
import { PropertyPhotos } from "./PropertyPhotos";
|
||||||
const optionCardStyle = (selected: boolean) =>
|
import { PropertyPrice } from "./PropertyPrice";
|
||||||
`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'}`;
|
import { PropertyInstantApproval } from "./PropertyInstantApproval";
|
||||||
|
import { SafetyDetails } from "./SafetyDetails";
|
||||||
interface FormData {
|
import { LivePreview } from "./LivePreview";
|
||||||
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 --- */
|
|
||||||
|
|
||||||
function List() {
|
function List() {
|
||||||
const [step, setStep] = useState(0);
|
const [step, setStep] = useState(0);
|
||||||
@@ -548,9 +44,17 @@ function List() {
|
|||||||
SafetyDetails
|
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 CurrentStepComponent = steps[step];
|
||||||
const totalSteps = steps.length;
|
const totalSteps = steps.length;
|
||||||
const progress = ((step + 1) / totalSteps) * 100;
|
|
||||||
|
|
||||||
const updateData = (key: keyof FormData, value: FormValue) => {
|
const updateData = (key: keyof FormData, value: FormValue) => {
|
||||||
setFormData(prev => ({ ...prev, [key]: value }));
|
setFormData(prev => ({ ...prev, [key]: value }));
|
||||||
@@ -588,31 +92,43 @@ function List() {
|
|||||||
<div className="flex min-h-screen w-full bg-white">
|
<div className="flex min-h-screen w-full bg-white">
|
||||||
{/* Left Panel - Form */}
|
{/* Left Panel - Form */}
|
||||||
<div className="w-full lg:w-1/2 flex flex-col h-screen relative">
|
<div className="w-full lg:w-1/2 flex flex-col h-screen relative">
|
||||||
{/* Header */}
|
{/* Header Section */}
|
||||||
<div className="px-8 py-6 border-b-2 border-gray-100 flex justify-between items-center bg-white z-10">
|
<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="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">
|
<div className="w-10 h-10 bg-black text-white rounded-full flex items-center justify-center font-bold text-lg">
|
||||||
{step + 1}
|
{step + 1}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<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} font-bold text-sm uppercase tracking-wider`}>Step {step + 1} of {totalSteps}</span>
|
||||||
<span className={`${figtree.className} text-xs text-gray-500`}>{
|
<span className={`${figtree.className} text-xs text-gray-500`}>{currentPhase.name}</span>
|
||||||
step === 0 ? "Basics" :
|
|
||||||
step < 4 ? "Details" :
|
|
||||||
step < 7 ? "Description" : "Finish"
|
|
||||||
}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button className="text-sm font-bold underline decoration-2 underline-offset-4 hover:text-gray-600">Save & Exit</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Segmented Progress Bar */}
|
||||||
<div className="w-full h-1 bg-gray-100">
|
<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
|
<div
|
||||||
className="h-full bg-[#E7FE78] transition-all duration-500 ease-out"
|
className="absolute top-0 left-0 h-full bg-[#E7FE78] transition-all duration-500 ease-out"
|
||||||
style={{ width: `${progress}%` }}
|
style={{ width: `${fill}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Scrollable Content */}
|
{/* Scrollable Content */}
|
||||||
<div className="flex-1 overflow-y-auto p-8 md:p-12">
|
<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