From e15a78e8b89e5b64f08e139eec291ccfd50c0f74 Mon Sep 17 00:00:00 2001 From: asabizanjo Date: Sun, 30 Nov 2025 15:34:37 +0000 Subject: [PATCH] profile + authentication --- src/app/globals.css | 6 + src/app/page.tsx | 8 +- src/components/Profile/README.md | 213 ++++++ src/components/Profile/profile.tsx | 634 ++++++++++++++++++ src/components/authentication/login.tsx | 386 ++++++++++- src/components/authentication/signup.tsx | 374 ++++++++++- src/components/landing/hero.tsx | 32 +- src/components/list-property/LivePreview.tsx | 86 +++ .../list-property/PropertyAmenities.tsx | 42 ++ .../list-property/PropertyCapacity.tsx | 48 ++ .../list-property/PropertyDescription.tsx | 32 + .../list-property/PropertyInstantApproval.tsx | 35 + .../list-property/PropertyLocation.tsx | 29 + .../list-property/PropertyPhotos.tsx | 94 +++ .../list-property/PropertyPlaceType.tsx | 34 + .../list-property/PropertyPrice.tsx | 24 + src/components/list-property/PropertyType.tsx | 32 + .../list-property/SafetyDetails.tsx | 43 ++ src/components/list-property/list.tsx | 598 ++--------------- src/components/list-property/styles.ts | 18 + src/components/list-property/types.ts | 23 + src/components/manage-property/manage.tsx | 188 ++++++ src/pages/landing/page.tsx | 24 + src/pages/profile/page.tsx | 12 + 24 files changed, 2439 insertions(+), 576 deletions(-) create mode 100644 src/components/Profile/README.md create mode 100644 src/components/Profile/profile.tsx create mode 100644 src/components/list-property/LivePreview.tsx create mode 100644 src/components/list-property/PropertyAmenities.tsx create mode 100644 src/components/list-property/PropertyCapacity.tsx create mode 100644 src/components/list-property/PropertyDescription.tsx create mode 100644 src/components/list-property/PropertyInstantApproval.tsx create mode 100644 src/components/list-property/PropertyLocation.tsx create mode 100644 src/components/list-property/PropertyPhotos.tsx create mode 100644 src/components/list-property/PropertyPlaceType.tsx create mode 100644 src/components/list-property/PropertyPrice.tsx create mode 100644 src/components/list-property/PropertyType.tsx create mode 100644 src/components/list-property/SafetyDetails.tsx create mode 100644 src/components/list-property/styles.ts create mode 100644 src/components/list-property/types.ts create mode 100644 src/components/manage-property/manage.tsx create mode 100644 src/pages/landing/page.tsx create mode 100644 src/pages/profile/page.tsx diff --git a/src/app/globals.css b/src/app/globals.css index 3d552a6..c727d38 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,2 +1,8 @@ @import "tailwindcss"; +@font-face { + font-family: 'Figtree'; + src: url('/Fonts/figtree/figtree.ttf') format('truetype'); + font-weight: 100 900; + font-style: normal; +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 0f379ef..0390966 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,12 +10,18 @@ import SearchBy from "@/components/search-results/search"; import Results from "@/components/search-results/results"; import Features from "@/components/details/features"; import List from "@/components/list-property/list"; +import ManageProperty from "@/components/manage-property/manage"; +import Login from "@/components/authentication/login"; +import Signup from "@/components/authentication/signup"; +import Landing from "@/pages/landing/page"; +import Profile from "@/pages/profile/page"; + export default function Home() { return ( <>
- +
diff --git a/src/components/Profile/README.md b/src/components/Profile/README.md new file mode 100644 index 0000000..b3812b8 --- /dev/null +++ b/src/components/Profile/README.md @@ -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 ( +
+ +
+ ); +} +``` + +## Customization + +### Mock Data +The component currently uses mock data. Replace the `profileData` state with actual data from your backend: + +```tsx +const [profileData, setProfileData] = useState({ + // 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 diff --git a/src/components/Profile/profile.tsx b/src/components/Profile/profile.tsx new file mode 100644 index 0000000..6bd710d --- /dev/null +++ b/src/components/Profile/profile.tsx @@ -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(null); + const tabsRef = useRef(null); + + // Mock profile data - replace with actual data from your backend + const [profileData, setProfileData] = useState({ + 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 ( +
+ {/* Header Section with Profile Info */} +
+
+ {/* Avatar Section */} +
+
+
+ +
+
+ + {profileData.isVerified && ( +
+ +
+ )} +
+ + {/* Profile Info */} +
+
+
+
+

{profileData.name}

+ {profileData.isHost && ( + + SUPERHOST + + )} +
+
+
+ + {profileData.location} +
+
+ + Joined {profileData.joinDate} +
+
+
+ +
+ + {/* Bio */} +

{profileData.bio}

+ + {/* Contact Info */} +
+
+ + {profileData.email} +
+
+ + {profileData.phone} +
+
+ + {/* Verification Badges */} +
+
+ + Email Verified +
+
+ + ID Verified +
+
+ + Phone Verified +
+
+
+
+
+ + {/* Host/Guest Toggle - Fiverr Style */} + {profileData.isHost && ( +
+ + +
+ )} + + {/* Content Area */} +
+ {activeTab === 'guest' ? ( + + ) : ( + + )} +
+
+ ); +} + +// Guest Profile Component +function GuestProfile({ profileData }: { profileData: ProfileData }) { + return ( +
+ {/* Stats Cards */} +
+ } + title="Reviews" + value={profileData.stats.totalReviews.toString()} + subtitle={`${profileData.stats.rating} average rating`} + /> + } + title="Total Bookings" + value={profileData.stats.totalBookings?.toString() || '0'} + subtitle="Completed stays" + /> + } + title="Wishlist" + value={profileData.stats.wishlistCount?.toString() || '0'} + subtitle="Saved properties" + /> +
+ + {/* Recent Bookings */} +
+ }> +
+ + + +
+
+
+ + {/* Quick Actions */} +
+ }> +
+ } text="Payment Methods" /> + } text="Notifications" /> + } text="Privacy & Security" /> + } text="Language & Region" /> + } text="Messages" /> + } + text="Sign Out" + variant="danger" + /> +
+
+
+ + {/* Reviews */} +
+ }> +
+ + +
+
+
+
+ ); +} + +// Host Dashboard Component +function HostDashboard({ profileData }: { profileData: ProfileData }) { + return ( +
+ {/* Host Stats */} +
+ } + title="Properties" + value={profileData.stats.properties?.toString() || '0'} + subtitle="Active listings" + /> + } + title="Rating" + value={profileData.stats.rating.toString()} + subtitle={`${profileData.stats.totalReviews} reviews`} + /> + } + title="Response Rate" + value={`${profileData.stats.responseRate}%`} + subtitle={profileData.stats.responseTime || 'N/A'} + /> + } + title="Total Bookings" + value={profileData.stats.totalBookings?.toString() || '0'} + subtitle="All time" + /> +
+ + {/* My Properties */} +
+ }> +
+ + + +
+ +
+
+ + {/* Host Tools */} +
+ }> +
+ } text="Calendar" /> + } text="Analytics" /> + } text="Earnings" /> + } text="Messages" /> + } text="Notifications" /> + } text="Settings" /> +
+
+ + {/* Achievement Badge */} +
+ +

Superhost

+

+ You're in the top 10% of hosts in Karachi! +

+
+
+ + {/* Recent Reviews */} +
+ }> +
+ + +
+
+
+
+ ); +} + +// Reusable Components +function StatCard({ + icon, + title, + value, + subtitle, +}: { + icon: React.ReactNode; + title: string; + value: string; + subtitle: string; +}) { + return ( +
+
+ {icon} +

{title}

+
+

{value}

+

{subtitle}

+
+ ); +} + +function SectionCard({ + title, + icon, + children, +}: { + title: string; + icon: React.ReactNode; + children: React.ReactNode; +}) { + return ( +
+
+ {icon} +

{title}

+
+ {children} +
+ ); +} + +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 ( + + ); +} + +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 ( +
+
+

{propertyName}

+ + {status} + +
+
+ + {location} +
+
+ + {dates} +
+
+ ); +} + +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 ( +
+
+

{name}

+ + {status} + +
+
+ + {location} +
+
+
+ + {rating} + ({reviews}) +
+
+ + {bookings} + bookings +
+
+
+ ); +} + +function ReviewItem({ + reviewerName, + propertyName, + rating, + date, + comment, +}: { + reviewerName: string; + propertyName: string; + rating: number; + date: string; + comment: string; +}) { + return ( +
+
+
+

{reviewerName}

+

{propertyName}

+
+
+
+ {[...Array(5)].map((_, i) => ( + + ))} +
+

{date}

+
+
+

{comment}

+
+ ); +} + +export default Profile; \ No newline at end of file diff --git a/src/components/authentication/login.tsx b/src/components/authentication/login.tsx index 9f1885e..cc8844b 100644 --- a/src/components/authentication/login.tsx +++ b/src/components/authentication/login.tsx @@ -1,5 +1,12 @@ -import localFont from "next/font/local"; +"use client"; +import { useState, useRef } from "react"; +import Image from "next/image"; +import localFont from "next/font/local"; +import { useGSAP } from "@gsap/react"; +import gsap from "gsap"; +import { Envelope, Lock, ShieldCheck, ArrowRight, CircleNotch, Eye, EyeSlash } from "@phosphor-icons/react"; +import logo from '../../../public/logo.png'; const figtree = localFont({ src: [ @@ -7,14 +14,379 @@ const figtree = localFont({ path: '../../../public/Fonts/figtree/figtree.ttf', }, ], -}) +}); + +// Reusing styles for consistency +const inputStyle = `${figtree.className} w-full p-4 border-2 border-black text-lg outline-none focus:bg-[#F7F7F7] transition-colors placeholder:text-gray-400 bg-white`; +const buttonStyle = `${figtree.className} w-full flex items-center justify-center gap-2 px-8 py-4 border-2 border-black bg-[#E7FE78] text-lg font-bold hover:bg-[#dcfc4e] transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:translate-y-[4px] active:shadow-none transition-all`; +const labelStyle = `${figtree.className} text-sm font-bold uppercase tracking-wider mb-2 block`; + +function Login() { + const [step, setStep] = useState<'credentials' | '2fa' | 'forgot-password' | 'recovery-sent'>('credentials'); + const [isLoading, setIsLoading] = useState(false); + const [formData, setFormData] = useState({ + email: '', + password: '', + otp: '' + }); + const [error, setError] = useState(''); + const [showPassword, setShowPassword] = useState(false); + + const formRef = useRef(null); + const imageRef = useRef(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 ( -
-

Login

+
+ {/* Left Side - Visual/Logo */} +
+
+ +
+
+ Logo +
+

+ Welcome Back +

+

+ Your Home Away From Home +

+
+
+ + {/* Right Side - Form */} +
+
+ {step === 'credentials' && ( + <> +
+

Sign In

+

Enter your details to access your account

+
+ +
+
+ +
+ setFormData({ ...formData, email: e.target.value })} + /> + +
+
+ +
+
+ + +
+
+ setFormData({ ...formData, password: e.target.value })} + /> + +
+
+ + {error && ( +
+ + {error} +
+ )} + + + +
+

+ Don't have an account?{' '} + + Sign Up + +

+
+
+ + )} + {step === '2fa' && ( + <> +
+

Two-Factor Auth

+

Enter the code sent to your device

+
+ +
+
+ +
+ setFormData({ ...formData, otp: e.target.value })} + /> +
+

Use '0000' for testing

+
+ + {error && ( +
+ + {error} +
+ )} + + + + +
+ + )} + + {step === 'forgot-password' && ( + <> +
+

Reset Password

+

Enter your email to receive a recovery link

+
+ +
+
+ +
+ setFormData({ ...formData, email: e.target.value })} + /> + +
+
+ + {error && ( +
+ + {error} +
+ )} + + + + +
+ + )} + + {step === 'recovery-sent' && ( +
+
+ +
+

Check Your Email

+

+ We've sent a password recovery link to
+ {formData.email} +

+ + +
+ )} +
+
- ) + ); } -export default Login +export default Login; diff --git a/src/components/authentication/signup.tsx b/src/components/authentication/signup.tsx index 5982ce9..2764ed0 100644 --- a/src/components/authentication/signup.tsx +++ b/src/components/authentication/signup.tsx @@ -1,5 +1,12 @@ -import localFont from "next/font/local"; +"use client"; +import { useState, useRef } from "react"; +import Image from "next/image"; +import localFont from "next/font/local"; +import { useGSAP } from "@gsap/react"; +import gsap from "gsap"; +import { Envelope, Lock, ShieldCheck, ArrowRight, CircleNotch, Eye, EyeSlash, User, Phone } from "@phosphor-icons/react"; +import logo from '../../../public/logo.png'; const figtree = localFont({ src: [ @@ -7,14 +14,367 @@ const figtree = localFont({ path: '../../../public/Fonts/figtree/figtree.ttf', }, ], -}) +}); + +// Reusing styles for consistency +const inputStyle = `${figtree.className} w-full p-4 border-2 border-black text-lg outline-none focus:bg-[#F7F7F7] transition-colors placeholder:text-gray-400 bg-white`; +const buttonStyle = `${figtree.className} w-full flex items-center justify-center gap-2 px-8 py-4 border-2 border-black bg-[#E7FE78] text-lg font-bold hover:bg-[#dcfc4e] transition-colors disabled:opacity-50 disabled:cursor-not-allowed shadow-[4px_4px_0px_0px_rgba(0,0,0,1)] hover:translate-y-[2px] hover:shadow-[2px_2px_0px_0px_rgba(0,0,0,1)] active:translate-y-[4px] active:shadow-none transition-all`; +const labelStyle = `${figtree.className} text-sm font-bold uppercase tracking-wider mb-2 block`; + +const countries = [ + { code: 'PK', name: 'Pakistan', dial_code: '+92' }, + { code: 'US', name: 'United States', dial_code: '+1' }, + { code: 'GB', name: 'United Kingdom', dial_code: '+44' }, + { code: 'CA', name: 'Canada', dial_code: '+1' }, + { code: 'AU', name: 'Australia', dial_code: '+61' }, + { code: 'UAE', name: 'UAE', dial_code: '+971' }, + { code: 'SA', name: 'Saudi Arabia', dial_code: '+966' }, +]; + +function Signup() { + const [step, setStep] = useState<'details' | 'verification'>('details'); + const [isLoading, setIsLoading] = useState(false); + const [formData, setFormData] = useState({ + name: '', + email: '', + countryCode: '+92', + phone: '', + password: '', + confirmPassword: '', + emailOtp: '', + phoneOtp: '' + }); + const [error, setError] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + const formRef = useRef(null); + const imageRef = useRef(null); + + useGSAP(() => { + // Animate image side + gsap.fromTo(imageRef.current, + { x: -50, opacity: 0 }, + { x: 0, opacity: 1, duration: 1, ease: "power3.out" } + ); + + // Animate form side + gsap.fromTo(formRef.current, + { x: 50, opacity: 0 }, + { x: 0, opacity: 1, duration: 1, delay: 0.2, ease: "power3.out" } + ); + }, []); + + const handleSignup = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + // Password validation + const passwordRegex = /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{8,}$/; + + if (!passwordRegex.test(formData.password)) { + setError('Password must be at least 8 characters with a number and special character'); + setIsLoading(false); + return; + } + + if (formData.password !== formData.confirmPassword) { + setError('Passwords do not match'); + setIsLoading(false); + return; + } + + // Simulate API call + setTimeout(() => { + setIsLoading(false); + if (formData.name && formData.email && formData.phone && formData.password) { + // Transition to Verification + gsap.to(formRef.current, { + x: -20, + opacity: 0, + duration: 0.3, + onComplete: () => { + setStep('verification'); + gsap.fromTo(formRef.current, + { x: 20, opacity: 0 }, + { x: 0, opacity: 1, duration: 0.3 } + ); + } + }); + } else { + setError('Please fill in all fields'); + } + }, 1000); + }; + + const handleVerify = async (e: React.FormEvent) => { + e.preventDefault(); + setError(''); + setIsLoading(true); + + // Simulate API call + setTimeout(() => { + setIsLoading(false); + if (formData.emailOtp === '0000' && formData.phoneOtp === '0000') { + alert('Signup Successful! Account Created.'); + // Here you would redirect or store token + } else { + setError('Invalid OTPs. Please try again (Hint: 0000 for both)'); + } + }, 1000); + }; -const Signup = () => { return ( -
-

Signup

+
+ {/* Left Side - Visual/Logo */} +
+
+ +
+
+ Logo +
+

+ Join Us Today +

+

+ Start Your Journey With Us +

+
+
+ + {/* Right Side - Form */} +
+
+ {step === 'details' ? ( + <> +
+

Create Account

+

Enter your details to get started

+
+ +
+
+ +
+ setFormData({ ...formData, name: e.target.value })} + /> + +
+
+ +
+ +
+ setFormData({ ...formData, email: e.target.value })} + /> + +
+
+ +
+ +
+
+ +
+ + + +
+
+
+ setFormData({ ...formData, phone: e.target.value })} + /> + +
+
+
+ +
+ +
+ setFormData({ ...formData, password: e.target.value })} + /> + +
+

+ Min. 8 characters, 1 number, 1 special character +

+
+ +
+ +
+ setFormData({ ...formData, confirmPassword: e.target.value })} + /> + +
+
+ + {error && ( +
+ + {error} +
+ )} + + + +
+

+ Already have an account?{' '} + + Log in + +

+
+
+ + ) : ( + <> +
+

Verify Account

+

Enter the codes sent to your email and phone

+
+ +
+
+ +
+ setFormData({ ...formData, emailOtp: e.target.value })} + /> + +
+
+ +
+ +
+ setFormData({ ...formData, phoneOtp: e.target.value })} + /> + +
+

Use '0000' for testing both

+
+ + {error && ( +
+ + {error} +
+ )} + + + + +
+ + )} +
+
- ) + ); } -export default Signup +export default Signup; diff --git a/src/components/landing/hero.tsx b/src/components/landing/hero.tsx index 316ce71..69dc8ee 100644 --- a/src/components/landing/hero.tsx +++ b/src/components/landing/hero.tsx @@ -1,33 +1,25 @@ -import localFont from "next/font/local"; +'use client'; import Image from "next/image"; import badge from '../../../public/badge-ticket.png' import arrow from '../../../public/arrow.png' -const figtree = localFont({ - src: [ - { - path: '../../../public/Fonts/figtree/figtree.ttf', - }, - ], -}) - function Hero() { return (
-
-

Find Your PakStay Home in

-
- badge -
-

Karachi

-
-
-
-
-

Travel Karo, Tension Free!

+
+

Find Your PakStay Home in

+
+ badge +
+

Karachi

+
+
+
+

Travel Karo, Tension Free!

+
) diff --git a/src/components/list-property/LivePreview.tsx b/src/components/list-property/LivePreview.tsx new file mode 100644 index 0000000..b41ff6e --- /dev/null +++ b/src/components/list-property/LivePreview.tsx @@ -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 ( +
+
+
+

Preview

+
+ New +
+
+ + {/* Card Replica */} +
+ {/* Image Placeholder */} +
+ {data.photos && data.photos.length > 0 ? ( + Preview + ) : ( +
+ + Cover Photo +
+ )} +
+ + {/* Details */} +
+
+

+ {getCity()} +

+
+ + New +
+
+ +

+ {getTitle()} +

+ +

+ {getPrice()} + night +

+
+
+ + {/* Live Updates Summary */} +
+
+ Guests + {data.guests || 0} +
+
+ Amenities + {data.amenities?.length || 0} selected +
+
+
+
+ ); +}; diff --git a/src/components/list-property/PropertyAmenities.tsx b/src/components/list-property/PropertyAmenities.tsx new file mode 100644 index 0000000..59f0949 --- /dev/null +++ b/src/components/list-property/PropertyAmenities.tsx @@ -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 ( +
+

What does your place offer?

+

You can add more amenities after you publish.

+
+ {amenities.map((amenity) => ( + + ))} +
+
+ ) +} diff --git a/src/components/list-property/PropertyCapacity.tsx b/src/components/list-property/PropertyCapacity.tsx new file mode 100644 index 0000000..510a841 --- /dev/null +++ b/src/components/list-property/PropertyCapacity.tsx @@ -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 ( +
+

Share some basics about your place

+

You'll add more details later, like bed types.

+
+ {counters.map((item) => ( +
+ {item.label} +
+ + {data[item.key] || 0} + +
+
+ ))} +
+
+ ) +} diff --git a/src/components/list-property/PropertyDescription.tsx b/src/components/list-property/PropertyDescription.tsx new file mode 100644 index 0000000..9344ce5 --- /dev/null +++ b/src/components/list-property/PropertyDescription.tsx @@ -0,0 +1,32 @@ +import { StepProps } from "./types"; +import { titleStyle, subtitleStyle, inputStyle } from "./styles"; + +export const PropertyDescription = ({ data, updateData }: StepProps) => { + return ( +
+

How would you describe your place?

+

Short and sweet works best.

+
+
+ + updateData('title', e.target.value)} + /> +
+
+ +