Add enums, AuthService, and integrate backend registration endpoints
All checks were successful
Build frontend / build (push) Successful in 57s

- Add separate enum files: BuildingType, PropertyStatus, BookingStatus, CommissionType, IdentityType, UserRole, City, LoginMethod, OwnerType, CustomerType
- Add AuthService (addToken/getToken/deleteToken)
- Update api.js: use AuthService, add Owner/Add and Customer/Add endpoints
- Update login page to use AuthService for token storage
- Rewrite owner register: 3-step flow with OwnerType dropdown, backend integration, OTP verification
- Rewrite tenant register: 2-step flow with CustomerType dropdown, backend integration, OTP verification
- Update homepage and property detail to use enums instead of hardcoded maps
- Update AddPropertyForm to import from enums directly
- Add console logs and status toasts linked to API response messages
This commit is contained in:
Claw AI
2026-03-27 18:01:42 +00:00
parent 2fb55db360
commit eff0b41b78
20 changed files with 1099 additions and 843 deletions

View File

@ -3,7 +3,7 @@
import { useState } from 'react'; import { useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { useProperties } from '@/app/contexts/PropertyContext'; import { useProperties } from '@/app/contexts/PropertyContext';
import { COMMISSION_TYPE, CITIES } from '@/app/utils/constants'; import { CommissionType, City, CitiesList } from '@/app/enums';
import { X, MapPin, Home, DollarSign, Percent } from 'lucide-react'; import { X, MapPin, Home, DollarSign, Percent } from 'lucide-react';
export default function AddPropertyForm({ onClose, onSuccess }) { export default function AddPropertyForm({ onClose, onSuccess }) {
@ -25,7 +25,7 @@ export default function AddPropertyForm({ onClose, onSuccess }) {
dailyPrice: 0, dailyPrice: 0,
commissionRate: 5, commissionRate: 5,
commissionType: COMMISSION_TYPE.FROM_OWNER, commissionType: CommissionType.FROM_OWNER,
securityDeposit: 0, securityDeposit: 0,
@ -86,11 +86,11 @@ export default function AddPropertyForm({ onClose, onSuccess }) {
const commission = (dailyPrice * commissionRate) / 100; const commission = (dailyPrice * commissionRate) / 100;
switch(commissionType) { switch(commissionType) {
case COMMISSION_TYPE.FROM_TENANT: case CommissionType.FROM_TENANT:
return dailyPrice + commission; return dailyPrice + commission;
case COMMISSION_TYPE.FROM_OWNER: case CommissionType.FROM_OWNER:
return dailyPrice; return dailyPrice;
case COMMISSION_TYPE.FROM_BOTH: case CommissionType.FROM_BOTH:
return dailyPrice + (commission / 2); return dailyPrice + (commission / 2);
default: default:
return dailyPrice; return dailyPrice;
@ -232,8 +232,8 @@ export default function AddPropertyForm({ onClose, onSuccess }) {
<input <input
type="radio" type="radio"
name="commissionType" name="commissionType"
value={COMMISSION_TYPE.FROM_OWNER} value={CommissionType.FROM_OWNER}
checked={formData.commissionType === COMMISSION_TYPE.FROM_OWNER} checked={formData.commissionType === CommissionType.FROM_OWNER}
onChange={(e) => setFormData({...formData, commissionType: e.target.value})} onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
/> />
<span>من المالك</span> <span>من المالك</span>
@ -242,8 +242,8 @@ export default function AddPropertyForm({ onClose, onSuccess }) {
<input <input
type="radio" type="radio"
name="commissionType" name="commissionType"
value={COMMISSION_TYPE.FROM_TENANT} value={CommissionType.FROM_TENANT}
checked={formData.commissionType === COMMISSION_TYPE.FROM_TENANT} checked={formData.commissionType === CommissionType.FROM_TENANT}
onChange={(e) => setFormData({...formData, commissionType: e.target.value})} onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
/> />
<span>من المستأجر</span> <span>من المستأجر</span>
@ -252,8 +252,8 @@ export default function AddPropertyForm({ onClose, onSuccess }) {
<input <input
type="radio" type="radio"
name="commissionType" name="commissionType"
value={COMMISSION_TYPE.FROM_BOTH} value={CommissionType.FROM_BOTH}
checked={formData.commissionType === COMMISSION_TYPE.FROM_BOTH} checked={formData.commissionType === CommissionType.FROM_BOTH}
onChange={(e) => setFormData({...formData, commissionType: e.target.value})} onChange={(e) => setFormData({...formData, commissionType: e.target.value})}
/> />
<span>من الاثنين</span> <span>من الاثنين</span>

View File

@ -0,0 +1,38 @@
/**
* BookingStatus Enum
* Backend values are strings
* Used in: Reservation workflow
*/
const BookingStatus = Object.freeze({
PENDING: 'pending',
OWNER_APPROVED: 'owner_approved',
ADMIN_APPROVED: 'admin_approved',
ACTIVE: 'active',
COMPLETED: 'completed',
REJECTED: 'rejected',
CANCELLED: 'cancelled',
});
// Map status → Arabic label
const BookingStatusLabels = Object.freeze({
[BookingStatus.PENDING]: 'بانتظار الموافقة',
[BookingStatus.OWNER_APPROVED]: 'موافقة المالك',
[BookingStatus.ADMIN_APPROVED]: 'موافقة الإدارة',
[BookingStatus.ACTIVE]: 'إيجار نشط',
[BookingStatus.COMPLETED]: 'منتهي',
[BookingStatus.REJECTED]: 'مرفوض',
[BookingStatus.CANCELLED]: 'ملغي',
});
// Map status → color class (Tailwind bg)
const BookingStatusColors = Object.freeze({
[BookingStatus.PENDING]: 'yellow',
[BookingStatus.OWNER_APPROVED]: 'blue',
[BookingStatus.ADMIN_APPROVED]: 'green',
[BookingStatus.ACTIVE]: 'purple',
[BookingStatus.COMPLETED]: 'gray',
[BookingStatus.REJECTED]: 'red',
[BookingStatus.CANCELLED]: 'red',
});
export { BookingStatus, BookingStatusLabels, BookingStatusColors };

33
app/enums/BuildingType.js Normal file
View File

@ -0,0 +1,33 @@
/**
* BuildingType Enum
* Backend values are numeric (0, 1, 2)
* Used in: PropertyInformation.buildingType
*/
const BuildingType = Object.freeze({
APARTMENT: 0,
VILLA: 1,
HOUSE: 2,
});
// Map numeric value → Arabic label
const BuildingTypeLabels = Object.freeze({
[BuildingType.APARTMENT]: 'شقة',
[BuildingType.VILLA]: 'فيلا',
[BuildingType.HOUSE]: 'بيت',
});
// Map numeric value → English key (for UI filters)
const BuildingTypeKeys = Object.freeze({
[BuildingType.APARTMENT]: 'apartment',
[BuildingType.VILLA]: 'villa',
[BuildingType.HOUSE]: 'house',
});
// Reverse map: English key → numeric value
const BuildingTypeByKey = Object.freeze({
apartment: BuildingType.APARTMENT,
villa: BuildingType.VILLA,
house: BuildingType.HOUSE,
});
export { BuildingType, BuildingTypeLabels, BuildingTypeKeys, BuildingTypeByKey };

38
app/enums/City.js Normal file
View File

@ -0,0 +1,38 @@
/**
* City Enum
* Syrian cities used in property locations
* Used in: Property search filters, location display
*/
const City = Object.freeze({
DAMASCUS: 'دمشق',
ALEPPO: 'حلب',
HOMS: 'حمص',
LATAKIA: 'اللاذقية',
DARAA: 'درعا',
TARTOUS: 'طرطوس',
SUWEIDA: 'السويداء',
DEIR_EZZOR: 'دير الزور',
RAQQA: 'الرقة',
IDLIB: 'إدلب',
HASAKAH: 'الحسكة',
QAMISHLI: 'القامشلي',
RURAL_DAMASCUS: 'ريف دمشق',
});
// All cities as a flat array
const CitiesList = Object.freeze(Object.values(City));
/**
* Extract city name from a full address string
* @param {string} address
* @returns {string}
*/
function extractCity(address) {
if (!address) return '';
for (const city of CitiesList) {
if (address.includes(city)) return city;
}
return '';
}
export { City, CitiesList, extractCity };

View File

@ -0,0 +1,19 @@
/**
* CommissionType Enum
* Defines who pays the platform commission
* Used in: Property pricing, booking financials
*/
const CommissionType = Object.freeze({
FROM_OWNER: 'from_owner',
FROM_TENANT: 'from_tenant',
FROM_BOTH: 'from_both',
});
// Map type → Arabic label
const CommissionTypeLabels = Object.freeze({
[CommissionType.FROM_OWNER]: 'من المالك',
[CommissionType.FROM_TENANT]: 'من المستأجر',
[CommissionType.FROM_BOTH]: 'من الاثنين',
});
export { CommissionType, CommissionTypeLabels };

17
app/enums/CustomerType.js Normal file
View File

@ -0,0 +1,17 @@
/**
* CustomerType Enum
* Backend values for customer sub-types
* Used in: Customer registration (Customer/Add)
*/
const CustomerType = Object.freeze({
PERSONAL: 'Personal',
FAMILY: 'Family',
});
// Map value → Arabic label
const CustomerTypeLabels = Object.freeze({
[CustomerType.PERSONAL]: 'شخصي',
[CustomerType.FAMILY]: 'عائلي',
});
export { CustomerType, CustomerTypeLabels };

23
app/enums/IdentityType.js Normal file
View File

@ -0,0 +1,23 @@
/**
* IdentityType Enum
* Tenant identity document type
* Used in: Property booking, allowedIdentities filter
*/
const IdentityType = Object.freeze({
SYRIAN: 'syrian',
PASSPORT: 'passport',
});
// Map type → Arabic label
const IdentityTypeLabels = Object.freeze({
[IdentityType.SYRIAN]: 'هوية سورية',
[IdentityType.PASSPORT]: 'جواز سفر',
});
// Map type → flag emoji
const IdentityTypeFlags = Object.freeze({
[IdentityType.SYRIAN]: '🇸🇾',
[IdentityType.PASSPORT]: '🛂',
});
export { IdentityType, IdentityTypeLabels, IdentityTypeFlags };

11
app/enums/LoginMethod.js Normal file
View File

@ -0,0 +1,11 @@
/**
* LoginMethod Enum
* Authentication method type
* Used in: Login page, OTP verification
*/
const LoginMethod = Object.freeze({
EMAIL: 'email',
PHONE: 'phone',
});
export { LoginMethod };

17
app/enums/OwnerType.js Normal file
View File

@ -0,0 +1,17 @@
/**
* OwnerType Enum
* Backend values for owner sub-types
* Used in: Owner registration (Owner/Add)
*/
const OwnerType = Object.freeze({
PERSON: 'peerson',
REAL_ESTATE_AGENCY: 'RealEstateAgency',
});
// Map value → Arabic label
const OwnerTypeLabels = Object.freeze({
[OwnerType.PERSON]: 'شخص',
[OwnerType.REAL_ESTATE_AGENCY]: 'وكالة عقارية',
});
export { OwnerType, OwnerTypeLabels };

View File

@ -0,0 +1,33 @@
/**
* PropertyStatus Enum
* Backend values are numeric (0, 1, 2)
* Used in: PropertyInformation.status
*/
const PropertyStatus = Object.freeze({
AVAILABLE: 0,
BOOKED: 1,
MAINTENANCE: 2,
});
// Map numeric value → Arabic label
const PropertyStatusLabels = Object.freeze({
[PropertyStatus.AVAILABLE]: 'متاح',
[PropertyStatus.BOOKED]: 'محجوز',
[PropertyStatus.MAINTENANCE]: 'صيانة',
});
// Map numeric value → English key (for UI filters)
const PropertyStatusKeys = Object.freeze({
[PropertyStatus.AVAILABLE]: 'available',
[PropertyStatus.BOOKED]: 'booked',
[PropertyStatus.MAINTENANCE]: 'maintenance',
});
// Reverse map: English key → numeric value
const PropertyStatusByKey = Object.freeze({
available: PropertyStatus.AVAILABLE,
booked: PropertyStatus.BOOKED,
maintenance: PropertyStatus.MAINTENANCE,
});
export { PropertyStatus, PropertyStatusLabels, PropertyStatusKeys, PropertyStatusByKey };

26
app/enums/UserRole.js Normal file
View File

@ -0,0 +1,26 @@
/**
* UserRole Enum
* User account roles in the system
* Used in: JWT payload, registration, routing
*/
const UserRole = Object.freeze({
OWNER: 'owner',
TENANT: 'tenant',
ADMIN: 'admin',
});
// Map role → Arabic label
const UserRoleLabels = Object.freeze({
[UserRole.OWNER]: 'مالك عقار',
[UserRole.TENANT]: 'مستأجر',
[UserRole.ADMIN]: 'مدير النظام',
});
// Map role → color theme (used in UI)
const UserRoleColors = Object.freeze({
[UserRole.OWNER]: 'amber',
[UserRole.TENANT]: 'blue',
[UserRole.ADMIN]: 'red',
});
export { UserRole, UserRoleLabels, UserRoleColors };

15
app/enums/index.js Normal file
View File

@ -0,0 +1,15 @@
/**
* Enums Index
* Central export for all enum modules
*/
export { BuildingType, BuildingTypeLabels, BuildingTypeKeys, BuildingTypeByKey } from './BuildingType';
export { PropertyStatus, PropertyStatusLabels, PropertyStatusKeys, PropertyStatusByKey } from './PropertyStatus';
export { BookingStatus, BookingStatusLabels, BookingStatusColors } from './BookingStatus';
export { CommissionType, CommissionTypeLabels } from './CommissionType';
export { IdentityType, IdentityTypeLabels, IdentityTypeFlags } from './IdentityType';
export { UserRole, UserRoleLabels, UserRoleColors } from './UserRole';
export { City, CitiesList, extractCity } from './City';
export { LoginMethod } from './LoginMethod';
export { OwnerType, OwnerTypeLabels } from './OwnerType';
export { CustomerType, CustomerTypeLabels } from './CustomerType';

View File

@ -29,6 +29,7 @@ import {
isEmail, isEmail,
isPhoneNumber, isPhoneNumber,
} from '../utils/api'; } from '../utils/api';
import AuthService from '../services/AuthService';
export default function LoginPage() { export default function LoginPage() {
const router = useRouter(); const router = useRouter();
@ -87,10 +88,10 @@ export default function LoginPage() {
console.log('[Login] Response:', result); console.log('[Login] Response:', result);
if (result.status === 200) { if (result.status === 200) {
// Login success — store token // Login success — store token via AuthService
const token = typeof result.data === 'string' ? result.data : result.data?.token || result.data; const token = typeof result.data === 'string' ? result.data : result.data?.token || result.data;
localStorage.setItem('token', token); AuthService.addToken(token);
console.log('[Login] Token stored successfully'); console.log('[Login] Token stored via AuthService');
// Decode token to get user info (basic JWT decode) // Decode token to get user info (basic JWT decode)
try { try {
@ -182,11 +183,11 @@ export default function LoginPage() {
console.log('[OTP] Verify response:', result); console.log('[OTP] Verify response:', result);
if (result.ok) { if (result.ok) {
// Verified — store token if returned // Verified — store token if returned via AuthService
const token = typeof result.data === 'string' ? result.data : result.data?.token || result.data; const token = typeof result.data === 'string' ? result.data : result.data?.token || result.data;
if (token && typeof token === 'string' && token.includes('.')) { if (token && typeof token === 'string' && token.includes('.')) {
localStorage.setItem('token', token); AuthService.addToken(token);
console.log('[OTP] Token stored'); console.log('[OTP] Token stored via AuthService');
} }
localStorage.setItem('user', JSON.stringify({ localStorage.setItem('user', JSON.stringify({

View File

@ -29,6 +29,7 @@ import PropertyMap from './components/home/PropertyMap';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import { getRentProperties, getSaleProperties } from './utils/api'; import { getRentProperties, getSaleProperties } from './utils/api';
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from './enums';
// Map API property data to the format the UI expects // Map API property data to the format the UI expects
// API returns { propertyInformationId, deposit, monthlyRent, dailyRent, rating, propertyInformation: {...}, ... } // API returns { propertyInformationId, deposit, monthlyRent, dailyRent, rating, propertyInformation: {...}, ... }
@ -38,13 +39,8 @@ function mapApiProperty(item, index) {
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0; const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
const monthlyPrice = item.monthlyRent ?? 0; const monthlyPrice = item.monthlyRent ?? 0;
// BuildingType: 0=Apartment, 1=Villa, 2=House const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? 'apartment';
const buildingTypeMap = { 0: 'apartment', 1: 'villa', 2: 'house' }; const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? 'available';
const propType = buildingTypeMap[info.buildingType] ?? buildingTypeMap[item.type] ?? 'apartment';
// Status: 0=Available, 1=Booked, 2=Maintenance
const statusMap = { 0: 'available', 1: 'booked', 2: 'maintenance' };
const status = statusMap[info.status] ?? statusMap[item.status] ?? 'available';
const features = []; const features = [];
if (item.isSmokeAllow) features.push('يسمح بالتدخين'); if (item.isSmokeAllow) features.push('يسمح بالتدخين');
@ -86,14 +82,7 @@ function mapApiProperty(item, index) {
}; };
} }
function extractCity(address) { // extractCity is now imported from @/app/enums
if (!address) return '';
const cities = ['دمشق', 'حلب', 'حمص', 'اللاذقية', 'درعا', 'طرطوس', 'السويداء', 'دير الزور', 'الرقة', 'إدلب', 'الحسكة', 'القامشلي', 'ريف دمشق'];
for (const city of cities) {
if (address.includes(city)) return city;
}
return '';
}
// Fallback dummy data // Fallback dummy data
const FALLBACK_PROPERTIES = [ const FALLBACK_PROPERTIES = [

View File

@ -43,6 +43,7 @@ import {
ArrowLeft ArrowLeft
} from 'lucide-react'; } from 'lucide-react';
import { getRentProperty, getSaleProperty, bookReservation, checkAvailability } from '../../utils/api'; import { getRentProperty, getSaleProperty, bookReservation, checkAvailability } from '../../utils/api';
import { BuildingTypeKeys, PropertyStatusKeys, extractCity } from '../../enums';
// Map API response to the UI format // Map API response to the UI format
function mapApiDetail(item) { function mapApiDetail(item) {
@ -53,11 +54,8 @@ function mapApiDetail(item) {
const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0; const dailyPrice = item.dailyRent ?? item.monthlyRent ?? item.price ?? 0;
const monthlyPrice = item.monthlyRent ?? 0; const monthlyPrice = item.monthlyRent ?? 0;
const buildingTypeMap = { 0: 'apartment', 1: 'villa', 2: 'house' }; const propType = BuildingTypeKeys[info.buildingType] ?? BuildingTypeKeys[item.type] ?? 'apartment';
const propType = buildingTypeMap[info.buildingType] ?? buildingTypeMap[item.type] ?? 'apartment'; const status = PropertyStatusKeys[info.status] ?? PropertyStatusKeys[item.status] ?? 'available';
const statusMap = { 0: 'available', 1: 'booked', 2: 'maintenance' };
const status = statusMap[info.status] ?? statusMap[item.status] ?? 'available';
const features = []; const features = [];
if (item.isSmokeAllow) features.push({ name: 'يسمح بالتدخين', available: true, description: '' }); if (item.isSmokeAllow) features.push({ name: 'يسمح بالتدخين', available: true, description: '' });
@ -122,14 +120,7 @@ function mapApiDetail(item) {
}; };
} }
function extractCity(address) { // extractCity is now imported from @/app/enums
if (!address) return '';
const cities = ['دمشق', 'حلب', 'حمص', 'اللاذقية', 'درعا', 'طرطوس', 'السويداء', 'دير الزور', 'الرقة', 'إدلب', 'الحسكة', 'القامشلي', 'ريف دمشق'];
for (const city of cities) {
if (address.includes(city)) return city;
}
return '';
}
// Fallback data (same as before) // Fallback data (same as before)
const FALLBACK_PROPERTIES = { const FALLBACK_PROPERTIES = {

View File

@ -1,37 +1,27 @@
'use client'; 'use client';
import { useState, useRef, useEffect } from 'react'; import { useState, useRef } from 'react';
import { motion, AnimatePresence } from 'framer-motion'; import { motion, AnimatePresence } from 'framer-motion';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import Image from 'next/image'; import Image from 'next/image';
import { import {
User, User, Mail, Phone, Lock, Eye, EyeOff, MessageCircle,
Mail, Camera, X, CheckCircle, XCircle, ArrowLeft, Building,
Phone, Loader2, Shield, KeyRound
Lock,
Eye,
EyeOff,
MessageCircle,
Camera,
Upload,
X,
CheckCircle,
XCircle,
AlertCircle,
ArrowLeft,
Building,
Loader2,
Home
} from 'lucide-react'; } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from 'react-hot-toast';
import { addOwner, loginWithEmail, sendEmailOTP, verifyEmail } from '../../utils/api';
import AuthService from '../../services/AuthService';
import { OwnerType, OwnerTypeLabels } from '../../enums';
export default function OwnerRegisterPage() { export default function OwnerRegisterPage() {
const router = useRouter(); const router = useRouter();
const [step, setStep] = useState(1); const [step, setStep] = useState(1); // 1=form, 2=id images, 3=OTP
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
email: '', email: '',
@ -39,16 +29,13 @@ export default function OwnerRegisterPage() {
whatsapp: '', whatsapp: '',
password: '', password: '',
confirmPassword: '', confirmPassword: '',
ownerType: OwnerType.PERSON,
agreeTerms: false agreeTerms: false
}); });
const [idImages, setIdImages] = useState({
front: null, const [idImages, setIdImages] = useState({ front: null, back: null });
back: null const [idImagePreviews, setIdImagePreviews] = useState({ front: '', back: '' });
}); const [otpCode, setOtpCode] = useState('');
const [idImagePreviews, setIdImagePreviews] = useState({
front: '',
back: ''
});
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const fileInputFrontRef = useRef(null); const fileInputFrontRef = useRef(null);
@ -56,80 +43,44 @@ export default function OwnerRegisterPage() {
const handleImageUpload = (side, file) => { const handleImageUpload = (side, file) => {
if (!file) return; if (!file) return;
if (!file.type.startsWith('image/')) { if (!file.type.startsWith('image/')) {
toast.error('الرجاء اختيار صورة صالحة'); toast.error('الرجاء اختيار صورة صالحة');
return; return;
} }
if (file.size > 5 * 1024 * 1024) { if (file.size > 5 * 1024 * 1024) {
toast.error('حجم الصورة يجب أن يكون أقل من 5 ميجابايت'); toast.error('حجم الصورة يجب أن يكون أقل من 5 ميجابايت');
return; return;
} }
const reader = new FileReader(); const reader = new FileReader();
reader.onloadend = () => { reader.onloadend = () => {
setIdImagePreviews(prev => ({ setIdImagePreviews(prev => ({ ...prev, [side]: reader.result }));
...prev,
[side]: reader.result
}));
}; };
reader.readAsDataURL(file); reader.readAsDataURL(file);
setIdImages(prev => ({ ...prev, [side]: file }));
setIdImages(prev => ({ console.log('[OwnerRegister] Image uploaded:', side);
...prev, toast.success('تم رفع الصورة بنجاح', { style: { background: '#dcfce7', color: '#166534' } });
[side]: file
}));
toast.success(`تم رفع الصورة بنجاح`, {
style: { background: '#dcfce7', color: '#166534' }
});
}; };
const validateEmail = (email) => { const validateEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const validatePhone = (phone) => /^(09|05)[0-9]{8}$/.test(phone);
return re.test(email);
};
const validatePhone = (phone) => {
const re = /^(09|05)[0-9]{8}$/;
return re.test(phone);
};
const validateStep1 = () => { const validateStep1 = () => {
const newErrors = {}; const newErrors = {};
if (!formData.name) newErrors.name = 'الاسم الكامل مطلوب';
else if (formData.name.length < 3) newErrors.name = 'الاسم يجب أن يكون 3 أحرف على الأقل';
if (!formData.name) { if (!formData.email) newErrors.email = 'البريد الإلكتروني مطلوب';
newErrors.name = 'الاسم الكامل مطلوب'; else if (!validateEmail(formData.email)) newErrors.email = 'البريد الإلكتروني غير صالح';
} else if (formData.name.length < 3) {
newErrors.name = 'الاسم يجب أن يكون 3 أحرف على الأقل';
}
if (!formData.email) { if (!formData.whatsapp) newErrors.whatsapp = 'رقم الواتساب مطلوب';
newErrors.email = 'البريد الإلكتروني مطلوب'; else if (!validatePhone(formData.whatsapp)) newErrors.whatsapp = 'رقم الواتساب غير صالح (يجب أن يبدأ 09 أو 05)';
} else if (!validateEmail(formData.email)) {
newErrors.email = 'البريد الإلكتروني غير صالح';
}
if (!formData.whatsapp) { if (formData.phone && !validatePhone(formData.phone)) newErrors.phone = 'رقم الهاتف غير صالح';
newErrors.whatsapp = 'رقم الواتساب مطلوب';
} else if (!validatePhone(formData.whatsapp)) {
newErrors.whatsapp = 'رقم الواتساب غير صالح (يجب أن يبدأ 09 أو 05)';
}
if (formData.phone && !validatePhone(formData.phone)) { if (!formData.password) newErrors.password = 'كلمة المرور مطلوبة';
newErrors.phone = 'رقم الهاتف غير صالح'; else if (formData.password.length < 6) newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل';
}
if (!formData.password) { if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = 'كلمات المرور غير متطابقة';
newErrors.password = 'كلمة المرور مطلوبة';
} else if (formData.password.length < 6) {
newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'كلمات المرور غير متطابقة';
}
setErrors(newErrors); setErrors(newErrors);
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0;
@ -137,20 +88,15 @@ export default function OwnerRegisterPage() {
const validateStep2 = () => { const validateStep2 = () => {
const newErrors = {}; const newErrors = {};
if (!idImages.front) newErrors.front = 'صورة الوجه الأمامي للهوية مطلوبة';
if (!idImages.front) { if (!idImages.back) newErrors.back = 'صورة الوجه الخلفي للهوية مطلوبة';
newErrors.front = 'صورة الوجه الأمامي للهوية مطلوبة';
}
if (!idImages.back) {
newErrors.back = 'صورة الوجه الخلفي للهوية مطلوبة';
}
setErrors(newErrors); setErrors(newErrors);
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0;
}; };
const handleNextStep = () => { const handleNextStep = () => {
if (validateStep1()) { if (validateStep1()) {
console.log('[OwnerRegister] Step 1 valid, moving to step 2');
setStep(2); setStep(2);
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
} else { } else {
@ -158,6 +104,7 @@ export default function OwnerRegisterPage() {
} }
}; };
// ─── Main signup handler ───
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -165,32 +112,131 @@ export default function OwnerRegisterPage() {
toast.error('يرجى إكمال جميع الصور المطلوبة'); toast.error('يرجى إكمال جميع الصور المطلوبة');
return; return;
} }
if (!formData.agreeTerms) { if (!formData.agreeTerms) {
toast.error('يجب الموافقة على الشروط والأحكام'); toast.error('يجب الموافقة على الشروط والأحكام');
return; return;
} }
setIsLoading(true); setIsLoading(true);
console.log('[OwnerRegister] Submitting owner registration...');
setTimeout(() => { const payload = {
setIsLoading(false);
toast.success('تم إنشاء الحساب بنجاح!', {
style: { background: '#dcfce7', color: '#166534' },
duration: 3000
});
localStorage.setItem('user', JSON.stringify({
name: formData.name, name: formData.name,
email: formData.email, email: formData.email,
role: 'owner', phoneNumber: formData.phone || '',
avatar: formData.name.charAt(0).toUpperCase() whatsAppNumber: formData.whatsapp,
})); password: formData.password,
ownerType: formData.ownerType,
};
try {
const res = await addOwner(payload);
console.log('[OwnerRegister] addOwner response:', res);
if (res.status === 200 || res.ok) {
// ── Store temp token for OTP flow ──
const tempToken = res.data;
if (tempToken) {
AuthService.addToken(tempToken);
console.log('[OwnerRegister] Temp token stored for OTP');
}
const apiMessage = res.message || res.data?.message;
toast.success(apiMessage || 'تم إنشاء الحساب! يرجى التحقق من بريدك الإلكتروني', {
duration: 4000,
});
// ── Auto-login to trigger OTP ──
console.log('[OwnerRegister] Auto-login to send OTP...');
const loginRes = await loginWithEmail(formData.email, formData.password);
console.log('[OwnerRegister] login response:', loginRes);
if (loginRes.status === 206) {
// OTP sent — move to OTP step
const otpToken = loginRes.data;
if (otpToken) {
AuthService.addToken(otpToken);
console.log('[OwnerRegister] OTP token stored');
}
const loginMsg = loginRes.message || loginRes.data?.message;
toast(loginMsg || 'تم إرسال رمز التحقق إلى بريدك الإلكتروني', { icon: '📧' });
setStep(3);
} else if (loginRes.status === 200) {
// Direct login success (no OTP needed)
const loginToken = loginRes.data;
if (loginToken) {
AuthService.addToken(loginToken);
}
toast.success(loginRes.message || 'تم تسجيل الدخول بنجاح!');
router.push('/');
}
} else {
// Registration failed
const errMsg = res.message || res.data?.message || 'فشل في إنشاء الحساب';
console.error('[OwnerRegister] Registration failed:', errMsg);
toast.error(errMsg);
}
} catch (err) {
console.error('[OwnerRegister] Error:', err);
toast.error(err.message || 'حدث خطأ أثناء التسجيل');
} finally {
setIsLoading(false);
}
};
// ─── OTP verification handler ───
const handleVerifyOTP = async () => {
if (!otpCode || otpCode.length < 4) {
toast.error('يرجى إدخال رمز التحقق');
return;
}
setIsLoading(true);
console.log('[OwnerRegister] Verifying OTP:', otpCode);
try {
const res = await verifyEmail(otpCode);
console.log('[OwnerRegister] VerifyEmail response:', res);
if (res.status === 200) {
// ── Verified! Remove temp token, redirect to login ──
AuthService.deleteToken();
console.log('[OwnerRegister] Temp token removed after verification');
toast.success(res.message || 'تم التحقق من البريد الإلكتروني بنجاح!', {
duration: 3000,
});
setTimeout(() => { setTimeout(() => {
router.push('/'); router.push('/login');
}, 1500); }, 1500);
}, 2000); } else {
const errMsg = res.message || res.data?.message || 'رمز التحقق غير صحيح';
console.error('[OwnerRegister] Verification failed:', errMsg);
toast.error(errMsg);
}
} catch (err) {
console.error('[OwnerRegister] Verify error:', err);
toast.error(err.message || 'حدث خطأ أثناء التحقق');
} finally {
setIsLoading(false);
}
};
// ─── Resend OTP ───
const handleResendOTP = async () => {
setIsLoading(true);
console.log('[OwnerRegister] Resending email OTP...');
try {
await sendEmailOTP();
toast.success('تم إرسال رمز تحقق جديد');
} catch (err) {
console.error('[OwnerRegister] Resend OTP error:', err);
toast.error('فشل في إرسال الرمز');
} finally {
setIsLoading(false);
}
}; };
const fadeInUp = { const fadeInUp = {
@ -200,37 +246,25 @@ export default function OwnerRegisterPage() {
}; };
const staggerContainer = { const staggerContainer = {
animate: { animate: { transition: { staggerChildren: 0.1 } }
transition: {
staggerChildren: 0.1
}
}
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden"> <div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
{/* Background blobs */}
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
{[...Array(20)].map((_, i) => ( {[...Array(20)].map((_, i) => (
<motion.div <motion.div
key={i} key={i}
className="absolute rounded-full bg-amber-500/10" className="absolute rounded-full bg-amber-500/10"
style={{ style={{
left: `${Math.random() * 100}%`, left: `${Math.random() * 100}%`, top: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`, width: Math.random() * 200 + 50, height: Math.random() * 200 + 50,
width: Math.random() * 200 + 50,
height: Math.random() * 200 + 50,
}}
animate={{
x: [0, Math.random() * 100 - 50, 0],
y: [0, Math.random() * 100 - 50, 0],
}}
transition={{
duration: Math.random() * 15 + 15,
repeat: Infinity,
ease: "linear"
}} }}
animate={{ x: [0, Math.random() * 100 - 50, 0], y: [0, Math.random() * 100 - 50, 0] }}
transition={{ duration: Math.random() * 15 + 15, repeat: Infinity, ease: "linear" }}
/> />
))} ))}
</div> </div>
@ -241,35 +275,19 @@ export default function OwnerRegisterPage() {
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="relative z-10 w-full max-w-2xl" className="relative z-10 w-full max-w-2xl"
> >
{/* Progress bar */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<Link <Link href="/auth/choose-role" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group">
href="/auth/choose-role" <motion.div whileHover={{ x: -5 }}><ArrowLeft className="w-4 h-4" /></motion.div>
className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group"
>
<motion.div whileHover={{ x: -5 }}>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>العودة</span> <span>العودة</span>
</Link> </Link>
<div className="flex items-center gap-2"> <span className="text-sm text-gray-400">خطوة {step} من 3</span>
<span className="text-sm text-gray-400">خطوة {step} من 2</span>
</div> </div>
</div>
<div className="flex gap-2"> <div className="flex gap-2">
<motion.div {[1, 2, 3].map((s) => (
className={`h-2 flex-1 rounded-full ${ <motion.div key={s} className={`h-2 flex-1 rounded-full ${step >= s ? 'bg-amber-500' : 'bg-gray-700'}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} />
step >= 1 ? 'bg-amber-500' : 'bg-gray-700' ))}
}`}
animate={{ scaleX: step >= 1 ? 1 : 0.5 }}
/>
<motion.div
className={`h-2 flex-1 rounded-full ${
step >= 2 ? 'bg-amber-500' : 'bg-gray-700'
}`}
animate={{ scaleX: step >= 2 ? 1 : 0.5 }}
/>
</div> </div>
</div> </div>
@ -281,33 +299,23 @@ export default function OwnerRegisterPage() {
transition={{ duration: 0.3 }} transition={{ duration: 0.3 }}
className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden" className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"
> >
{/* Header */}
<div className="bg-gradient-to-r from-amber-500 to-amber-600 p-8 text-center relative overflow-hidden"> <div className="bg-gradient-to-r from-amber-500 to-amber-600 p-8 text-center relative overflow-hidden">
<motion.div <motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }}
initial={{ scale: 0 }} className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
animate={{ scale: 1 }} <motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="relative z-10">
transition={{ delay: 0.2, type: "spring" }}
className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full"
/>
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
className="relative z-10"
>
<motion.div <motion.div
animate={{ rotate: [0, 10, -10, 0] }} animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }} transition={{ duration: 2, repeat: Infinity }}
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm" className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
> >
<Building className="w-10 h-10 text-white" /> {step === 3 ? <KeyRound className="w-10 h-10 text-white" /> : <Building className="w-10 h-10 text-white" />}
</motion.div> </motion.div>
<h1 className="text-3xl font-bold text-white mb-2"> <h1 className="text-3xl font-bold text-white mb-2">
{step === 1 ? 'معلومات المالك' : 'الوثائق الرسمية'} {step === 1 ? 'معلومات المالك' : step === 2 ? 'الوثائق الرسمية' : 'التحقق من البريد'}
</h1> </h1>
<p className="text-amber-100"> <p className="text-amber-100">
{step === 1 {step === 1 ? 'أدخل معلوماتك الأساسية' : step === 2 ? 'يرجى رفع صور الهوية للتحقق' : 'أدخل رمز التحقق المرسل إلى بريدك'}
? 'أدخل معلوماتك الأساسية للتواصل'
: 'يرجى رفع صور الهوية الشخصية للتحقق'}
</p> </p>
</motion.div> </motion.div>
</div> </div>
@ -317,247 +325,138 @@ export default function OwnerRegisterPage() {
variants={staggerContainer} variants={staggerContainer}
initial="initial" initial="initial"
animate="animate" animate="animate"
onSubmit={step === 1 ? handleNextStep : handleSubmit} onSubmit={step === 1 ? handleNextStep : step === 2 ? handleSubmit : (e) => { e.preventDefault(); handleVerifyOTP(); }}
className="space-y-6" className="space-y-6"
> >
{step === 1 ? ( {/* ─── STEP 1: Form fields ─── */}
{step === 1 && (
<> <>
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">الاسم الكامل <span className="text-red-500">*</span></label>
الاسم الكامل <span className="text-red-500">*</span>
</label>
<div className="relative group"> <div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${ <User className={`w-5 h-5 ${errors.name ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
errors.name ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'
}`} />
</div> </div>
<input <input type="text" value={formData.name}
type="text" onChange={(e) => { setFormData({...formData, name: e.target.value}); setErrors({...errors, name: null}); }}
value={formData.name} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.name ? 'border-red-500' : 'border-gray-700'}`}
onChange={(e) => { placeholder="أدخل اسمك الكامل" />
setFormData({...formData, name: e.target.value});
setErrors({...errors, name: null});
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.name ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل اسمك الكامل"
/>
</div> </div>
{errors.name && ( {errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
</motion.div> </motion.div>
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">البريد الإلكتروني <span className="text-red-500">*</span></label>
البريد الإلكتروني <span className="text-red-500">*</span>
</label>
<div className="relative group"> <div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Mail className={`w-5 h-5 ${ <Mail className={`w-5 h-5 ${errors.email ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
errors.email ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'
}`} />
</div> </div>
<input <input type="email" value={formData.email}
type="email" onChange={(e) => { setFormData({...formData, email: e.target.value}); setErrors({...errors, email: null}); }}
value={formData.email} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.email ? 'border-red-500' : 'border-gray-700'}`}
onChange={(e) => { placeholder="أدخل بريدك الإلكتروني" />
setFormData({...formData, email: e.target.value});
setErrors({...errors, email: null});
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.email ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل بريدك الإلكتروني"
/>
</div> </div>
{errors.email && ( {errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
<p className="text-red-500 text-sm mt-1">{errors.email}</p>
)}
</motion.div> </motion.div>
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">رقم الهاتف <span className="text-gray-500">(اختياري)</span></label>
رقم الهاتف <span className="text-gray-500">(اختياري)</span>
</label>
<div className="relative group"> <div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500" /> <Phone className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500" />
</div> </div>
<input <input type="tel" value={formData.phone}
type="tel" onChange={(e) => { setFormData({...formData, phone: e.target.value}); setErrors({...errors, phone: null}); }}
value={formData.phone}
onChange={(e) => {
setFormData({...formData, phone: e.target.value});
setErrors({...errors, phone: null});
}}
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all" className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all"
placeholder="أدخل رقم هاتفك (اختياري)" placeholder="أدخل رقم هاتفك (اختياري)" />
/>
</div> </div>
{errors.phone && ( {errors.phone && <p className="text-red-500 text-sm mt-1">{errors.phone}</p>}
<p className="text-red-500 text-sm mt-1">{errors.phone}</p>
)}
</motion.div> </motion.div>
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">رقم الواتساب <span className="text-red-500">*</span></label>
رقم الواتساب <span className="text-red-500">*</span>
</label>
<div className="relative group"> <div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<MessageCircle className={`w-5 h-5 ${ <MessageCircle className={`w-5 h-5 ${errors.whatsapp ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
errors.whatsapp ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'
}`} />
</div> </div>
<input <input type="tel" value={formData.whatsapp}
type="tel" onChange={(e) => { setFormData({...formData, whatsapp: e.target.value}); setErrors({...errors, whatsapp: null}); }}
value={formData.whatsapp} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.whatsapp ? 'border-red-500' : 'border-gray-700'}`}
onChange={(e) => { placeholder="أدخل رقم الواتساب" />
setFormData({...formData, whatsapp: e.target.value});
setErrors({...errors, whatsapp: null});
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.whatsapp ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل رقم الواتساب"
/>
</div> </div>
{errors.whatsapp && ( {errors.whatsapp && <p className="text-red-500 text-sm mt-1">{errors.whatsapp}</p>}
<p className="text-red-500 text-sm mt-1">{errors.whatsapp}</p>
)}
</motion.div> </motion.div>
{/* Owner Type */}
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">نوع المالك <span className="text-red-500">*</span></label>
كلمة المرور <span className="text-red-500">*</span> <select
</label> value={formData.ownerType}
<div className="relative group"> onChange={(e) => setFormData({...formData, ownerType: e.target.value})}
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white appearance-none cursor-pointer"
<Lock className={`w-5 h-5 ${
errors.password ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'
}`} />
</div>
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => {
setFormData({...formData, password: e.target.value});
setErrors({...errors, password: null});
}}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.password ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل كلمة المرور"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 left-0 pl-3 flex items-center"
> >
{showPassword ? ( {Object.entries(OwnerTypeLabels).map(([value, label]) => (
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300" /> <option key={value} value={value} className="bg-gray-900 text-white">{label}</option>
) : ( ))}
<Eye className="w-5 h-5 text-gray-400 hover:text-gray-300" /> </select>
)} <p className="text-xs text-gray-500 mt-1">المحدد: {OwnerTypeLabels[formData.ownerType]}</p>
<p className="text-xs text-gray-600">[Console] ownerType = {formData.ownerType}</p>
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">كلمة المرور <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${errors.password ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
</div>
<input type={showPassword ? "text" : "password"} value={formData.password}
onChange={(e) => { setFormData({...formData, password: e.target.value}); setErrors({...errors, password: null}); }}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.password ? 'border-red-500' : 'border-gray-700'}`}
placeholder="أدخل كلمة المرور" />
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showPassword ? <EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300" /> : <Eye className="w-5 h-5 text-gray-400 hover:text-gray-300" />}
</button> </button>
</div> </div>
{errors.password && ( {errors.password && <p className="text-red-500 text-sm mt-1">{errors.password}</p>}
<p className="text-red-500 text-sm mt-1">{errors.password}</p>
)}
</motion.div> </motion.div>
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">تأكيد كلمة المرور <span className="text-red-500">*</span></label>
تأكيد كلمة المرور <span className="text-red-500">*</span>
</label>
<div className="relative group"> <div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${ <Lock className={`w-5 h-5 ${errors.confirmPassword ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'}`} />
errors.confirmPassword ? 'text-red-500' : 'text-gray-400 group-focus-within:text-amber-500'
}`} />
</div> </div>
<input <input type={showConfirmPassword ? "text" : "password"} value={formData.confirmPassword}
type={showConfirmPassword ? "text" : "password"} onChange={(e) => { setFormData({...formData, confirmPassword: e.target.value}); setErrors({...errors, confirmPassword: null}); }}
value={formData.confirmPassword} className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.confirmPassword ? 'border-red-500' : 'border-gray-700'}`}
onChange={(e) => { placeholder="أعد إدخال كلمة المرور" />
setFormData({...formData, confirmPassword: e.target.value}); <button type="button" onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
setErrors({...errors, confirmPassword: null}); {showConfirmPassword ? <EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300" /> : <Eye className="w-5 h-5 text-gray-400 hover:text-gray-300" />}
}}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.confirmPassword ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أعد إدخال كلمة المرور"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute inset-y-0 left-0 pl-3 flex items-center"
>
{showConfirmPassword ? (
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300" />
) : (
<Eye className="w-5 h-5 text-gray-400 hover:text-gray-300" />
)}
</button> </button>
{formData.confirmPassword && ( {formData.confirmPassword && (
<div className="absolute inset-y-0 left-12 flex items-center"> <div className="absolute inset-y-0 left-12 flex items-center">
{formData.password === formData.confirmPassword ? ( {formData.password === formData.confirmPassword ? <CheckCircle className="w-5 h-5 text-green-500" /> : <XCircle className="w-5 h-5 text-red-500" />}
<CheckCircle className="w-5 h-5 text-green-500" />
) : (
<XCircle className="w-5 h-5 text-red-500" />
)}
</div> </div>
)} )}
</div> </div>
{errors.confirmPassword && ( {errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>}
<p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>
)}
</motion.div> </motion.div>
</> </>
) : ( )}
{/* ─── STEP 2: ID Images ─── */}
{step === 2 && (
<> <>
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">صورة الهوية - الوجه الأمامي <span className="text-red-500">*</span></label>
صورة الهوية - الوجه الأمامي <span className="text-red-500">*</span> <div onClick={() => fileInputFrontRef.current?.click()}
</label> className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.front ? 'border-green-500 bg-green-500/10' : errors.front ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}>
<div <input ref={fileInputFrontRef} type="file" accept="image/*" onChange={(e) => handleImageUpload('front', e.target.files?.[0])} className="hidden" />
onClick={() => fileInputFrontRef.current?.click()}
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${
idImagePreviews.front
? 'border-green-500 bg-green-500/10'
: errors.front
? 'border-red-500 bg-red-500/10'
: 'border-gray-700 hover:border-amber-500 hover:bg-white/5'
}`}
>
<input
ref={fileInputFrontRef}
type="file"
accept="image/*"
onChange={(e) => handleImageUpload('front', e.target.files?.[0])}
className="hidden"
/>
{idImagePreviews.front ? ( {idImagePreviews.front ? (
<div className="relative"> <div className="relative">
<Image <Image src={idImagePreviews.front} alt="Front ID" width={200} height={120} className="mx-auto rounded-lg object-cover" />
src={idImagePreviews.front} <button onClick={(e) => { e.stopPropagation(); setIdImages(prev => ({...prev, front: null})); setIdImagePreviews(prev => ({...prev, front: ''})); }}
alt="Front ID" className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600">
width={200}
height={120}
className="mx-auto rounded-lg object-cover"
/>
<button
onClick={(e) => {
e.stopPropagation();
setIdImages(prev => ({...prev, front: null}));
setIdImagePreviews(prev => ({...prev, front: ''}));
}}
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600"
>
<X className="w-4 h-4 text-white" /> <X className="w-4 h-4 text-white" />
</button> </button>
</div> </div>
@ -565,56 +464,23 @@ export default function OwnerRegisterPage() {
<> <>
<Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" /> <Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" />
<p className="text-gray-400">اضغط لرفع الصورة</p> <p className="text-gray-400">اضغط لرفع الصورة</p>
<p className="text-xs text-gray-500 mt-2"> <p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG حتى 5MB</p>
JPEG, PNG, JPG حتى 5MB 800x600 بكسل
</p>
</> </>
)} )}
</div> </div>
{errors.front && ( {errors.front && <p className="text-red-500 text-sm mt-1">{errors.front}</p>}
<p className="text-red-500 text-sm mt-1">{errors.front}</p>
)}
</motion.div> </motion.div>
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">صورة الهوية - الوجه الخلفي <span className="text-red-500">*</span></label>
صورة الهوية - الوجه الخلفي <span className="text-red-500">*</span> <div onClick={() => fileInputBackRef.current?.click()}
</label> className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${idImagePreviews.back ? 'border-green-500 bg-green-500/10' : errors.back ? 'border-red-500 bg-red-500/10' : 'border-gray-700 hover:border-amber-500 hover:bg-white/5'}`}>
<div <input ref={fileInputBackRef} type="file" accept="image/*" onChange={(e) => handleImageUpload('back', e.target.files?.[0])} className="hidden" />
onClick={() => fileInputBackRef.current?.click()}
className={`relative border-2 border-dashed rounded-xl p-6 text-center cursor-pointer transition-all ${
idImagePreviews.back
? 'border-green-500 bg-green-500/10'
: errors.back
? 'border-red-500 bg-red-500/10'
: 'border-gray-700 hover:border-amber-500 hover:bg-white/5'
}`}
>
<input
ref={fileInputBackRef}
type="file"
accept="image/*"
onChange={(e) => handleImageUpload('back', e.target.files?.[0])}
className="hidden"
/>
{idImagePreviews.back ? ( {idImagePreviews.back ? (
<div className="relative"> <div className="relative">
<Image <Image src={idImagePreviews.back} alt="Back ID" width={200} height={120} className="mx-auto rounded-lg object-cover" />
src={idImagePreviews.back} <button onClick={(e) => { e.stopPropagation(); setIdImages(prev => ({...prev, back: null})); setIdImagePreviews(prev => ({...prev, back: ''})); }}
alt="Back ID" className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600">
width={200}
height={120}
className="mx-auto rounded-lg object-cover"
/>
<button
onClick={(e) => {
e.stopPropagation();
setIdImages(prev => ({...prev, back: null}));
setIdImagePreviews(prev => ({...prev, back: ''}));
}}
className="absolute -top-2 -right-2 w-6 h-6 bg-red-500 rounded-full flex items-center justify-center hover:bg-red-600"
>
<X className="w-4 h-4 text-white" /> <X className="w-4 h-4 text-white" />
</button> </button>
</div> </div>
@ -622,83 +488,87 @@ export default function OwnerRegisterPage() {
<> <>
<Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" /> <Camera className="w-12 h-12 text-gray-500 mx-auto mb-3" />
<p className="text-gray-400">اضغط لرفع الصورة</p> <p className="text-gray-400">اضغط لرفع الصورة</p>
<p className="text-xs text-gray-500 mt-2"> <p className="text-xs text-gray-500 mt-2">JPEG, PNG, JPG حتى 5MB</p>
JPEG, PNG, JPG حتى 5MB 800x600 بكسل
</p>
</> </>
)} )}
</div> </div>
{errors.back && ( {errors.back && <p className="text-red-500 text-sm mt-1">{errors.back}</p>}
<p className="text-red-500 text-sm mt-1">{errors.back}</p>
)}
</motion.div> </motion.div>
<motion.div variants={fadeInUp} className="flex items-center gap-2"> <motion.div variants={fadeInUp} className="flex items-center gap-2">
<input <input type="checkbox" id="terms" checked={formData.agreeTerms}
type="checkbox"
id="terms"
checked={formData.agreeTerms}
onChange={(e) => setFormData({...formData, agreeTerms: e.target.checked})} onChange={(e) => setFormData({...formData, agreeTerms: e.target.checked})}
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0" className="w-4 h-4 rounded border-gray-600 bg-white/5 text-amber-500 focus:ring-amber-500 focus:ring-offset-0" required />
required
/>
<label htmlFor="terms" className="text-sm text-gray-300"> <label htmlFor="terms" className="text-sm text-gray-300">
أوافق على{' '} أوافق على <Link href="/terms" className="text-amber-400 hover:text-amber-300">شروط الاستخدام</Link> و <Link href="/privacy" className="text-amber-400 hover:text-amber-300">سياسة الخصوصية</Link>
<Link href="/terms" className="text-amber-400 hover:text-amber-300">
شروط الاستخدام
</Link>
{' '}و{' '}
<Link href="/privacy" className="text-amber-400 hover:text-amber-300">
سياسة الخصوصية
</Link>
</label> </label>
</motion.div> </motion.div>
</> </>
)} )}
{/* ─── STEP 3: OTP ─── */}
{step === 3 && (
<motion.div variants={fadeInUp} className="space-y-6">
<div className="bg-amber-500/10 border border-amber-500/20 rounded-xl p-4 text-center">
<Shield className="w-10 h-10 text-amber-500 mx-auto mb-2" />
<p className="text-gray-300 text-sm">تم إرسال رمز التحقق إلى</p>
<p className="text-amber-400 font-medium">{formData.email}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">رمز التحقق <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<KeyRound className="w-5 h-5 text-gray-400 group-focus-within:text-amber-500" />
</div>
<input type="text" value={otpCode} maxLength={6}
onChange={(e) => setOtpCode(e.target.value)}
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white placeholder-gray-500 text-center tracking-[0.5em] text-xl transition-all"
placeholder="------" />
</div>
</div>
<button type="button" onClick={handleResendOTP} disabled={isLoading}
className="w-full text-center text-amber-400 hover:text-amber-300 text-sm transition-colors disabled:opacity-50">
إعادة إرسال الرمز
</button>
</motion.div>
)}
{/* ─── Navigation Buttons ─── */}
<motion.div variants={fadeInUp} className="flex gap-3 pt-4"> <motion.div variants={fadeInUp} className="flex gap-3 pt-4">
{step === 1 ? ( {step === 1 && (
<> <>
<button <button type="button" onClick={() => router.push('/auth/choose-role')}
type="button" className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">إلغاء</button>
onClick={() => router.push('/auth/choose-role')} <button type="button" onClick={handleNextStep}
className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors" className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all">التالي</button>
>
إلغاء
</button>
<button
type="button"
onClick={handleNextStep}
className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all"
>
التالي
</button>
</> </>
) : ( )}
{step === 2 && (
<> <>
<button <button type="button" onClick={() => setStep(1)}
type="button" className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">السابق</button>
onClick={() => setStep(1)} <button type="submit" disabled={isLoading || !formData.agreeTerms}
className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors" className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
>
السابق
</button>
<button
type="submit"
disabled={isLoading || !formData.agreeTerms}
className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" /> <Loader2 className="w-5 h-5 animate-spin" /><span>جاري التسجيل...</span>
<span>جاري التسجيل...</span>
</div> </div>
) : ( ) : 'إنشاء حساب'}
'إنشاء حساب'
)}
</button> </button>
</> </>
)} )}
{step === 3 && (
<button type="submit" disabled={isLoading || !otpCode}
className="flex-1 bg-gradient-to-r from-amber-500 to-amber-600 text-white py-3 px-4 rounded-xl font-medium hover:from-amber-600 hover:to-amber-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" /><span>جاري التحقق...</span>
</div>
) : 'تحقق من الرمز'}
</button>
)}
</motion.div> </motion.div>
</motion.form> </motion.form>
</div> </div>

View File

@ -5,80 +5,59 @@ import { motion } from 'framer-motion';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import { import {
User, User, Mail, Phone, Lock, Eye, EyeOff,
Mail, CheckCircle, XCircle, ArrowLeft, Home, Loader2,
Phone, Shield, KeyRound
Lock,
Eye,
EyeOff,
CheckCircle,
XCircle,
ArrowLeft,
Home,
Loader2
} from 'lucide-react'; } from 'lucide-react';
import toast, { Toaster } from 'react-hot-toast'; import toast, { Toaster } from 'react-hot-toast';
import { addCustomer, loginWithEmail, sendEmailOTP, verifyEmail } from '../../utils/api';
import AuthService from '../../services/AuthService';
import { CustomerType, CustomerTypeLabels } from '../../enums';
export default function TenantRegisterPage() { export default function TenantRegisterPage() {
const router = useRouter(); const router = useRouter();
const [step, setStep] = useState(1); // 1=form, 2=OTP
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
name: '', name: '',
email: '', email: '',
phone: '', phone: '',
password: '', password: '',
confirmPassword: '', confirmPassword: '',
customerType: CustomerType.PERSONAL,
agreeTerms: false agreeTerms: false
}); });
const [otpCode, setOtpCode] = useState('');
const [errors, setErrors] = useState({}); const [errors, setErrors] = useState({});
const validateEmail = (email) => { const validateEmail = (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const validatePhone = (phone) => /^(09|05)[0-9]{8}$/.test(phone);
return re.test(email);
};
const validatePhone = (phone) => {
const re = /^(09|05)[0-9]{8}$/;
return re.test(phone);
};
const validateForm = () => { const validateForm = () => {
const newErrors = {}; const newErrors = {};
if (!formData.name) newErrors.name = 'الاسم الكامل مطلوب';
else if (formData.name.length < 3) newErrors.name = 'الاسم يجب أن يكون 3 أحرف على الأقل';
if (!formData.name) { if (!formData.email) newErrors.email = 'البريد الإلكتروني مطلوب';
newErrors.name = 'الاسم الكامل مطلوب'; else if (!validateEmail(formData.email)) newErrors.email = 'البريد الإلكتروني غير صالح';
} else if (formData.name.length < 3) {
newErrors.name = 'الاسم يجب أن يكون 3 أحرف على الأقل';
}
if (!formData.email) { if (!formData.phone) newErrors.phone = 'رقم الهاتف مطلوب';
newErrors.email = 'البريد الإلكتروني مطلوب'; else if (!validatePhone(formData.phone)) newErrors.phone = 'رقم الهاتف غير صالح (يجب أن يبدأ 09 أو 05)';
} else if (!validateEmail(formData.email)) {
newErrors.email = 'البريد الإلكتروني غير صالح';
}
if (!formData.phone) { if (!formData.password) newErrors.password = 'كلمة المرور مطلوبة';
newErrors.phone = 'رقم الهاتف مطلوب'; else if (formData.password.length < 6) newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل';
} else if (!validatePhone(formData.phone)) {
newErrors.phone = 'رقم الهاتف غير صالح (يجب أن يبدأ 09 أو 05)';
}
if (!formData.password) { if (formData.password !== formData.confirmPassword) newErrors.confirmPassword = 'كلمات المرور غير متطابقة';
newErrors.password = 'كلمة المرور مطلوبة';
} else if (formData.password.length < 6) {
newErrors.password = 'كلمة المرور يجب أن تكون 6 أحرف على الأقل';
}
if (formData.password !== formData.confirmPassword) {
newErrors.confirmPassword = 'كلمات المرور غير متطابقة';
}
setErrors(newErrors); setErrors(newErrors);
return Object.keys(newErrors).length === 0; return Object.keys(newErrors).length === 0;
}; };
// ─── Main signup handler ───
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -86,32 +65,130 @@ export default function TenantRegisterPage() {
toast.error('يرجى تصحيح الأخطاء في النموذج'); toast.error('يرجى تصحيح الأخطاء في النموذج');
return; return;
} }
if (!formData.agreeTerms) { if (!formData.agreeTerms) {
toast.error('يجب الموافقة على الشروط والأحكام'); toast.error('يجب الموافقة على الشروط والأحكام');
return; return;
} }
setIsLoading(true); setIsLoading(true);
console.log('[CustomerRegister] Submitting customer registration...');
setTimeout(() => { const payload = {
setIsLoading(false);
toast.success('تم إنشاء الحساب بنجاح!', {
style: { background: '#dcfce7', color: '#166534' },
duration: 3000
});
localStorage.setItem('user', JSON.stringify({
name: formData.name, name: formData.name,
email: formData.email, email: formData.email,
role: 'tenant', phoneNumber: formData.phone,
avatar: formData.name.charAt(0).toUpperCase() password: formData.password,
})); customerType: formData.customerType,
};
try {
const res = await addCustomer(payload);
console.log('[CustomerRegister] addCustomer response:', res);
if (res.status === 200 || res.ok) {
// ── Store temp token for OTP flow ──
const tempToken = res.data;
if (tempToken) {
AuthService.addToken(tempToken);
console.log('[CustomerRegister] Temp token stored for OTP');
}
const apiMessage = res.message || res.data?.message;
toast.success(apiMessage || 'تم إنشاء الحساب! يرجى التحقق من بريدك الإلكتروني', {
duration: 4000,
});
// ── Auto-login to trigger OTP ──
console.log('[CustomerRegister] Auto-login to send OTP...');
const loginRes = await loginWithEmail(formData.email, formData.password);
console.log('[CustomerRegister] login response:', loginRes);
if (loginRes.status === 206) {
// OTP sent — move to OTP step
const otpToken = loginRes.data;
if (otpToken) {
AuthService.addToken(otpToken);
console.log('[CustomerRegister] OTP token stored');
}
const loginMsg = loginRes.message || loginRes.data?.message;
toast(loginMsg || 'تم إرسال رمز التحقق إلى بريدك الإلكتروني', { icon: '📧' });
setStep(2);
} else if (loginRes.status === 200) {
// Direct login success (no OTP needed)
const loginToken = loginRes.data;
if (loginToken) {
AuthService.addToken(loginToken);
}
toast.success(loginRes.message || 'تم تسجيل الدخول بنجاح!');
router.push('/');
}
} else {
// Registration failed
const errMsg = res.message || res.data?.message || 'فشل في إنشاء الحساب';
console.error('[CustomerRegister] Registration failed:', errMsg);
toast.error(errMsg);
}
} catch (err) {
console.error('[CustomerRegister] Error:', err);
toast.error(err.message || 'حدث خطأ أثناء التسجيل');
} finally {
setIsLoading(false);
}
};
// ─── OTP verification handler ───
const handleVerifyOTP = async () => {
if (!otpCode || otpCode.length < 4) {
toast.error('يرجى إدخال رمز التحقق');
return;
}
setIsLoading(true);
console.log('[CustomerRegister] Verifying OTP:', otpCode);
try {
const res = await verifyEmail(otpCode);
console.log('[CustomerRegister] VerifyEmail response:', res);
if (res.status === 200) {
// ── Verified! Remove temp token, redirect to login ──
AuthService.deleteToken();
console.log('[CustomerRegister] Temp token removed after verification');
toast.success(res.message || 'تم التحقق من البريد الإلكتروني بنجاح!', {
duration: 3000,
});
setTimeout(() => { setTimeout(() => {
router.push('/'); router.push('/login');
}, 1500); }, 1500);
}, 2000); } else {
const errMsg = res.message || res.data?.message || 'رمز التحقق غير صحيح';
console.error('[CustomerRegister] Verification failed:', errMsg);
toast.error(errMsg);
}
} catch (err) {
console.error('[CustomerRegister] Verify error:', err);
toast.error(err.message || 'حدث خطأ أثناء التحقق');
} finally {
setIsLoading(false);
}
};
// ─── Resend OTP ───
const handleResendOTP = async () => {
setIsLoading(true);
console.log('[CustomerRegister] Resending email OTP...');
try {
await sendEmailOTP();
toast.success('تم إرسال رمز تحقق جديد');
} catch (err) {
console.error('[CustomerRegister] Resend OTP error:', err);
toast.error('فشل في إرسال الرمز');
} finally {
setIsLoading(false);
}
}; };
const fadeInUp = { const fadeInUp = {
@ -121,37 +198,25 @@ export default function TenantRegisterPage() {
}; };
const staggerContainer = { const staggerContainer = {
animate: { animate: { transition: { staggerChildren: 0.1 } }
transition: {
staggerChildren: 0.1
}
}
}; };
return ( return (
<div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden"> <div className="min-h-screen bg-gradient-to-br from-gray-950 via-gray-900 to-gray-950 flex items-center justify-center p-4 relative overflow-hidden">
<Toaster position="top-center" reverseOrder={false} /> <Toaster position="top-center" reverseOrder={false} />
{/* Background blobs */}
<div className="absolute inset-0 overflow-hidden"> <div className="absolute inset-0 overflow-hidden">
{[...Array(20)].map((_, i) => ( {[...Array(20)].map((_, i) => (
<motion.div <motion.div
key={i} key={i}
className="absolute rounded-full bg-blue-500/10" className="absolute rounded-full bg-blue-500/10"
style={{ style={{
left: `${Math.random() * 100}%`, left: `${Math.random() * 100}%`, top: `${Math.random() * 100}%`,
top: `${Math.random() * 100}%`, width: Math.random() * 200 + 50, height: Math.random() * 200 + 50,
width: Math.random() * 200 + 50,
height: Math.random() * 200 + 50,
}}
animate={{
x: [0, Math.random() * 100 - 50, 0],
y: [0, Math.random() * 100 - 50, 0],
}}
transition={{
duration: Math.random() * 15 + 15,
repeat: Infinity,
ease: "linear"
}} }}
animate={{ x: [0, Math.random() * 100 - 50, 0], y: [0, Math.random() * 100 - 50, 0] }}
transition={{ duration: Math.random() * 15 + 15, repeat: Infinity, ease: "linear" }}
/> />
))} ))}
</div> </div>
@ -162,45 +227,40 @@ export default function TenantRegisterPage() {
transition={{ duration: 0.5 }} transition={{ duration: 0.5 }}
className="relative z-10 w-full max-w-md" className="relative z-10 w-full max-w-md"
> >
<motion.div {/* Back link */}
initial={{ opacity: 0, x: -20 }} <motion.div initial={{ opacity: 0, x: -20 }} animate={{ opacity: 1, x: 0 }} className="absolute -top-16 left-0">
animate={{ opacity: 1, x: 0 }} <Link href="/auth/choose-role" className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group">
className="absolute -top-16 left-0" <motion.div whileHover={{ x: -5 }}><ArrowLeft className="w-4 h-4" /></motion.div>
>
<Link
href="/auth/choose-role"
className="flex items-center gap-2 text-gray-400 hover:text-white transition-colors group"
>
<motion.div whileHover={{ x: -5 }}>
<ArrowLeft className="w-4 h-4" />
</motion.div>
<span>العودة</span> <span>العودة</span>
</Link> </Link>
</motion.div> </motion.div>
<div className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"> {/* Progress */}
<div className="bg-gradient-to-r from-blue-500 to-blue-600 p-8 text-center relative overflow-hidden"> <div className="mb-6 mt-4 flex gap-2">
<motion.div {[1, 2].map((s) => (
initial={{ scale: 0 }} <motion.div key={s} className={`h-2 flex-1 rounded-full ${step >= s ? 'bg-blue-500' : 'bg-gray-700'}`} animate={{ scaleX: step >= s ? 1 : 0.5 }} />
animate={{ scale: 1 }} ))}
transition={{ delay: 0.2, type: "spring" }} </div>
className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full"
/>
<motion.div <div className="bg-white/5 backdrop-blur-xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden">
initial={{ y: 20, opacity: 0 }} {/* Header */}
animate={{ y: 0, opacity: 1 }} <div className="bg-gradient-to-r from-blue-500 to-blue-600 p-8 text-center relative overflow-hidden">
className="relative z-10" <motion.div initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ delay: 0.2, type: "spring" }}
> className="absolute -top-10 -right-10 w-40 h-40 bg-white/10 rounded-full" />
<motion.div initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} className="relative z-10">
<motion.div <motion.div
animate={{ rotate: [0, 10, -10, 0] }} animate={{ rotate: [0, 10, -10, 0] }}
transition={{ duration: 2, repeat: Infinity }} transition={{ duration: 2, repeat: Infinity }}
className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm" className="w-20 h-20 mx-auto mb-4 bg-white/20 rounded-2xl flex items-center justify-center backdrop-blur-sm"
> >
<Home className="w-10 h-10 text-white" /> {step === 2 ? <KeyRound className="w-10 h-10 text-white" /> : <Home className="w-10 h-10 text-white" />}
</motion.div> </motion.div>
<h1 className="text-3xl font-bold text-white mb-2">إنشاء حساب مستأجر</h1> <h1 className="text-3xl font-bold text-white mb-2">
<p className="text-blue-100">انضم إلينا وابحث عن منزل أحلامك</p> {step === 1 ? 'إنشاء حساب مستأجر' : 'التحقق من البريد'}
</h1>
<p className="text-blue-100">
{step === 1 ? 'انضم إلينا وابحث عن منزل أحلامك' : 'أدخل رمز التحقق المرسل إلى بريدك'}
</p>
</motion.div> </motion.div>
</div> </div>
@ -209,226 +269,183 @@ export default function TenantRegisterPage() {
variants={staggerContainer} variants={staggerContainer}
initial="initial" initial="initial"
animate="animate" animate="animate"
onSubmit={handleSubmit} onSubmit={step === 1 ? handleSubmit : (e) => { e.preventDefault(); handleVerifyOTP(); }}
className="space-y-6" className="space-y-6"
> >
{/* ─── STEP 1: Form fields ─── */}
{step === 1 && (
<>
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">الاسم الكامل <span className="text-red-500">*</span></label>
الاسم الكامل <span className="text-red-500">*</span>
</label>
<div className="relative group"> <div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<User className={`w-5 h-5 ${ <User className={`w-5 h-5 ${errors.name ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
errors.name ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'
}`} />
</div> </div>
<input <input type="text" value={formData.name}
type="text" onChange={(e) => { setFormData({...formData, name: e.target.value}); setErrors({...errors, name: null}); }}
value={formData.name} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.name ? 'border-red-500' : 'border-gray-700'}`}
onChange={(e) => { placeholder="أدخل اسمك الكامل" />
setFormData({...formData, name: e.target.value});
setErrors({...errors, name: null});
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.name ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل اسمك الكامل"
/>
</div> </div>
{errors.name && ( {errors.name && <p className="text-red-500 text-sm mt-1">{errors.name}</p>}
<p className="text-red-500 text-sm mt-1">{errors.name}</p>
)}
</motion.div> </motion.div>
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">البريد الإلكتروني <span className="text-red-500">*</span></label>
البريد الإلكتروني <span className="text-red-500">*</span>
</label>
<div className="relative group"> <div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Mail className={`w-5 h-5 ${ <Mail className={`w-5 h-5 ${errors.email ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
errors.email ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'
}`} />
</div> </div>
<input <input type="email" value={formData.email}
type="email" onChange={(e) => { setFormData({...formData, email: e.target.value}); setErrors({...errors, email: null}); }}
value={formData.email} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.email ? 'border-red-500' : 'border-gray-700'}`}
onChange={(e) => { placeholder="أدخل بريدك الإلكتروني" />
setFormData({...formData, email: e.target.value});
setErrors({...errors, email: null});
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.email ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل بريدك الإلكتروني"
/>
</div> </div>
{errors.email && ( {errors.email && <p className="text-red-500 text-sm mt-1">{errors.email}</p>}
<p className="text-red-500 text-sm mt-1">{errors.email}</p>
)}
</motion.div> </motion.div>
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">رقم الهاتف <span className="text-red-500">*</span></label>
رقم الهاتف <span className="text-red-500">*</span>
</label>
<div className="relative group"> <div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Phone className={`w-5 h-5 ${ <Phone className={`w-5 h-5 ${errors.phone ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
errors.phone ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'
}`} />
</div> </div>
<input <input type="tel" value={formData.phone}
type="tel" onChange={(e) => { setFormData({...formData, phone: e.target.value}); setErrors({...errors, phone: null}); }}
value={formData.phone} className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.phone ? 'border-red-500' : 'border-gray-700'}`}
onChange={(e) => { placeholder="أدخل رقم هاتفك" />
setFormData({...formData, phone: e.target.value});
setErrors({...errors, phone: null});
}}
className={`w-full pr-12 pl-4 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.phone ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل رقم هاتفك"
/>
</div> </div>
{errors.phone && ( {errors.phone && <p className="text-red-500 text-sm mt-1">{errors.phone}</p>}
<p className="text-red-500 text-sm mt-1">{errors.phone}</p>
)}
</motion.div> </motion.div>
{/* Customer Type */}
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">نوع العميل <span className="text-red-500">*</span></label>
كلمة المرور <span className="text-red-500">*</span> <select
</label> value={formData.customerType}
<div className="relative group"> onChange={(e) => setFormData({...formData, customerType: e.target.value})}
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> className="w-full py-3 px-4 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white appearance-none cursor-pointer"
<Lock className={`w-5 h-5 ${
errors.password ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'
}`} />
</div>
<input
type={showPassword ? "text" : "password"}
value={formData.password}
onChange={(e) => {
setFormData({...formData, password: e.target.value});
setErrors({...errors, password: null});
}}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.password ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أدخل كلمة المرور"
/>
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 left-0 pl-3 flex items-center"
> >
{showPassword ? ( {Object.entries(CustomerTypeLabels).map(([value, label]) => (
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300" /> <option key={value} value={value} className="bg-gray-900 text-white">{label}</option>
) : ( ))}
<Eye className="w-5 h-5 text-gray-400 hover:text-gray-300" /> </select>
)} <p className="text-xs text-gray-500 mt-1">المحدد: {CustomerTypeLabels[formData.customerType]}</p>
<p className="text-xs text-gray-600">[Console] customerType = {formData.customerType}</p>
</motion.div>
<motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2">كلمة المرور <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${errors.password ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
</div>
<input type={showPassword ? "text" : "password"} value={formData.password}
onChange={(e) => { setFormData({...formData, password: e.target.value}); setErrors({...errors, password: null}); }}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.password ? 'border-red-500' : 'border-gray-700'}`}
placeholder="أدخل كلمة المرور" />
<button type="button" onClick={() => setShowPassword(!showPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
{showPassword ? <EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300" /> : <Eye className="w-5 h-5 text-gray-400 hover:text-gray-300" />}
</button> </button>
</div> </div>
{errors.password && ( {errors.password && <p className="text-red-500 text-sm mt-1">{errors.password}</p>}
<p className="text-red-500 text-sm mt-1">{errors.password}</p>
)}
</motion.div> </motion.div>
<motion.div variants={fadeInUp}> <motion.div variants={fadeInUp}>
<label className="block text-sm font-medium text-gray-300 mb-2"> <label className="block text-sm font-medium text-gray-300 mb-2">تأكيد كلمة المرور <span className="text-red-500">*</span></label>
تأكيد كلمة المرور <span className="text-red-500">*</span>
</label>
<div className="relative group"> <div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none"> <div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<Lock className={`w-5 h-5 ${ <Lock className={`w-5 h-5 ${errors.confirmPassword ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'}`} />
errors.confirmPassword ? 'text-red-500' : 'text-gray-400 group-focus-within:text-blue-500'
}`} />
</div> </div>
<input <input type={showConfirmPassword ? "text" : "password"} value={formData.confirmPassword}
type={showConfirmPassword ? "text" : "password"} onChange={(e) => { setFormData({...formData, confirmPassword: e.target.value}); setErrors({...errors, confirmPassword: null}); }}
value={formData.confirmPassword} className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${errors.confirmPassword ? 'border-red-500' : 'border-gray-700'}`}
onChange={(e) => { placeholder="أعد إدخال كلمة المرور" />
setFormData({...formData, confirmPassword: e.target.value}); <button type="button" onClick={() => setShowConfirmPassword(!showConfirmPassword)} className="absolute inset-y-0 left-0 pl-3 flex items-center">
setErrors({...errors, confirmPassword: null}); {showConfirmPassword ? <EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300" /> : <Eye className="w-5 h-5 text-gray-400 hover:text-gray-300" />}
}}
className={`w-full pr-12 pl-12 py-3 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 transition-all ${
errors.confirmPassword ? 'border-red-500' : 'border-gray-700'
}`}
placeholder="أعد إدخال كلمة المرور"
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
className="absolute inset-y-0 left-0 pl-3 flex items-center"
>
{showConfirmPassword ? (
<EyeOff className="w-5 h-5 text-gray-400 hover:text-gray-300" />
) : (
<Eye className="w-5 h-5 text-gray-400 hover:text-gray-300" />
)}
</button> </button>
{formData.confirmPassword && ( {formData.confirmPassword && (
<div className="absolute inset-y-0 left-12 flex items-center"> <div className="absolute inset-y-0 left-12 flex items-center">
{formData.password === formData.confirmPassword ? ( {formData.password === formData.confirmPassword ? <CheckCircle className="w-5 h-5 text-green-500" /> : <XCircle className="w-5 h-5 text-red-500" />}
<CheckCircle className="w-5 h-5 text-green-500" />
) : (
<XCircle className="w-5 h-5 text-red-500" />
)}
</div> </div>
)} )}
</div> </div>
{errors.confirmPassword && ( {errors.confirmPassword && <p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>}
<p className="text-red-500 text-sm mt-1">{errors.confirmPassword}</p>
)}
</motion.div> </motion.div>
<motion.div variants={fadeInUp} className="flex items-center gap-2"> <motion.div variants={fadeInUp} className="flex items-center gap-2">
<input <input type="checkbox" id="terms" checked={formData.agreeTerms}
type="checkbox"
id="terms"
checked={formData.agreeTerms}
onChange={(e) => setFormData({...formData, agreeTerms: e.target.checked})} onChange={(e) => setFormData({...formData, agreeTerms: e.target.checked})}
className="w-4 h-4 rounded border-gray-600 bg-white/5 text-blue-500 focus:ring-blue-500 focus:ring-offset-0" className="w-4 h-4 rounded border-gray-600 bg-white/5 text-blue-500 focus:ring-blue-500 focus:ring-offset-0" required />
required
/>
<label htmlFor="terms" className="text-sm text-gray-300"> <label htmlFor="terms" className="text-sm text-gray-300">
أوافق على{' '} أوافق على <Link href="/terms" className="text-blue-400 hover:text-blue-300">شروط الاستخدام</Link> و <Link href="/privacy" className="text-blue-400 hover:text-blue-300">سياسة الخصوصية</Link>
<Link href="/terms" className="text-blue-400 hover:text-blue-300">
شروط الاستخدام
</Link>
{' '}و{' '}
<Link href="/privacy" className="text-blue-400 hover:text-blue-300">
سياسة الخصوصية
</Link>
</label> </label>
</motion.div> </motion.div>
</>
)}
<motion.button {/* ─── STEP 2: OTP ─── */}
variants={fadeInUp} {step === 2 && (
type="submit" <motion.div variants={fadeInUp} className="space-y-6">
disabled={isLoading || !formData.agreeTerms} <div className="bg-blue-500/10 border border-blue-500/20 rounded-xl p-4 text-center">
className="w-full bg-gradient-to-r from-blue-500 to-blue-600 text-white py-4 rounded-xl font-bold text-lg hover:from-blue-600 hover:to-blue-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed shadow-lg shadow-blue-500/25" <Shield className="w-10 h-10 text-blue-500 mx-auto mb-2" />
> <p className="text-gray-300 text-sm">تم إرسال رمز التحقق إلى</p>
<p className="text-blue-400 font-medium">{formData.email}</p>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-2">رمز التحقق <span className="text-red-500">*</span></label>
<div className="relative group">
<div className="absolute inset-y-0 right-0 pr-3 flex items-center pointer-events-none">
<KeyRound className="w-5 h-5 text-gray-400 group-focus-within:text-blue-500" />
</div>
<input type="text" value={otpCode} maxLength={6}
onChange={(e) => setOtpCode(e.target.value)}
className="w-full pr-12 pl-4 py-3 bg-white/5 border border-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white placeholder-gray-500 text-center tracking-[0.5em] text-xl transition-all"
placeholder="------" />
</div>
</div>
<button type="button" onClick={handleResendOTP} disabled={isLoading}
className="w-full text-center text-blue-400 hover:text-blue-300 text-sm transition-colors disabled:opacity-50">
إعادة إرسال الرمز
</button>
</motion.div>
)}
{/* ─── Navigation Buttons ─── */}
<motion.div variants={fadeInUp} className="flex gap-3 pt-4">
{step === 1 && (
<>
<button type="button" onClick={() => router.push('/auth/choose-role')}
className="flex-1 py-3 px-4 bg-white/5 border border-gray-700 rounded-xl text-gray-300 hover:bg-white/10 transition-colors">إلغاء</button>
<button type="submit" disabled={isLoading}
className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 px-4 rounded-xl font-medium hover:from-blue-600 hover:to-blue-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
{isLoading ? ( {isLoading ? (
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" /> <Loader2 className="w-5 h-5 animate-spin" /><span>جاري التسجيل...</span>
<span>جاري إنشاء الحساب...</span>
</div> </div>
) : ( ) : 'إنشاء حساب'}
'إنشاء حساب' </button>
</>
)} )}
</motion.button> {step === 2 && (
<button type="submit" disabled={isLoading || !otpCode}
className="flex-1 bg-gradient-to-r from-blue-500 to-blue-600 text-white py-3 px-4 rounded-xl font-medium hover:from-blue-600 hover:to-blue-700 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
{isLoading ? (
<div className="flex items-center justify-center gap-2">
<Loader2 className="w-5 h-5 animate-spin" /><span>جاري التحقق...</span>
</div>
) : 'تحقق من الرمز'}
</button>
)}
</motion.div>
{step === 1 && (
<motion.p variants={fadeInUp} className="text-center text-gray-400 mt-4"> <motion.p variants={fadeInUp} className="text-center text-gray-400 mt-4">
لديك حساب بالفعل؟{' '} لديك حساب بالفعل؟{' '}
<Link <Link href="/login" className="text-blue-400 hover:text-blue-300 font-medium transition-colors">تسجيل الدخول</Link>
href="/login"
className="text-blue-400 hover:text-blue-300 font-medium transition-colors"
>
تسجيل الدخول
</Link>
</motion.p> </motion.p>
)}
</motion.form> </motion.form>
</div> </div>
</div> </div>

View File

@ -0,0 +1,51 @@
/**
* AuthService
* Manages authentication tokens securely using localStorage.
*
* Methods:
* addToken(token) — store JWT token
* getToken() — retrieve JWT token
* deleteToken() — remove JWT token
*
* Usage:
* import AuthService from '@/app/services/AuthService';
* AuthService.addToken(token);
* const token = AuthService.getToken();
* AuthService.deleteToken();
*/
const TOKEN_KEY = 'auth_token';
const AuthService = Object.freeze({
/**
* Store token in localStorage
* @param {string} token — JWT string
*/
addToken(token) {
if (!token || typeof token !== 'string') {
console.error('[AuthService] addToken: invalid token', token);
return;
}
localStorage.setItem(TOKEN_KEY, token);
console.log('[AuthService] Token stored');
},
/**
* Retrieve token from localStorage
* @returns {string|null}
*/
getToken() {
const token = localStorage.getItem(TOKEN_KEY);
console.log('[AuthService] getToken:', token ? '***exists***' : null);
return token;
},
/**
* Remove token from localStorage
*/
deleteToken() {
localStorage.removeItem(TOKEN_KEY);
console.log('[AuthService] Token deleted');
},
});
export default AuthService;

View File

@ -1,7 +1,12 @@
import AuthService from '../services/AuthService';
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://45.93.137.91/api'; const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://45.93.137.91/api';
/**
* Generic API fetch — attaches auth token, unwraps { data } envelope
*/
async function apiFetch(endpoint, options = {}) { async function apiFetch(endpoint, options = {}) {
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null; const token = AuthService.getToken();
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -9,7 +14,7 @@ async function apiFetch(endpoint, options = {}) {
...options.headers, ...options.headers,
}; };
console.log('[API] Request:', `${API_BASE}${endpoint}`, options.method || 'GET'); console.log('[API] Request:', options.method || 'GET', `${API_BASE}${endpoint}`);
const res = await fetch(`${API_BASE}${endpoint}`, { const res = await fetch(`${API_BASE}${endpoint}`, {
...options, ...options,
@ -38,7 +43,9 @@ async function apiFetch(endpoint, options = {}) {
} }
} }
// Raw fetch for auth (no token, returns full response for status code handling) /**
* Auth fetch — no token attached, returns full { status, data, ok } for status-code handling
*/
async function authFetch(endpoint, body) { async function authFetch(endpoint, body) {
console.log('[Auth] Request:', `${API_BASE}${endpoint}`); console.log('[Auth] Request:', `${API_BASE}${endpoint}`);
@ -61,7 +68,10 @@ async function authFetch(endpoint, body) {
data = text; data = text;
} }
return { status: res.status, data, ok: res.ok || res.status === 206 }; // Build message from response for toast display
const message = (typeof data === 'object' && data?.message) ? data.message : null;
return { status: res.status, data, ok: res.ok || res.status === 206, message };
} }
// ─── Rent Properties ─── // ─── Rent Properties ───
@ -143,9 +153,32 @@ export async function getTerms() {
return apiFetch('/Terms/GetTerms'); return apiFetch('/Terms/GetTerms');
} }
// ─── Auth ─── // ─── Auth: Registration ───
/**
* Register a new owner
* @param {Object} data — { name, email, phoneNumber, whatsAppNumber, password, ownerType }
* @returns {Promise<{status, data, ok, message}>}
*/
export async function addOwner(data) {
console.log('[Auth] Registering owner:', data.email);
return authFetch('/Owner/Add', data);
}
/**
* Register a new customer/tenant
* @param {Object} data — { name, email, phoneNumber, password, customerType }
* @returns {Promise<{status, data, ok, message}>}
*/
export async function addCustomer(data) {
console.log('[Auth] Registering customer:', data.email);
return authFetch('/Customer/Add', data);
}
// ─── Auth: Login ───
export async function loginWithEmail(credential, password) { export async function loginWithEmail(credential, password) {
console.log('[Auth] Login with email:', credential);
return authFetch('/Auth/LogInWithEmail', { return authFetch('/Auth/LogInWithEmail', {
credential, credential,
password, password,
@ -155,6 +188,7 @@ export async function loginWithEmail(credential, password) {
} }
export async function loginWithPhone(credential, password) { export async function loginWithPhone(credential, password) {
console.log('[Auth] Login with phone:', credential);
return authFetch('/Auth/LogInWithPhoneNumber', { return authFetch('/Auth/LogInWithPhoneNumber', {
credential, credential,
password, password,
@ -163,6 +197,8 @@ export async function loginWithPhone(credential, password) {
}); });
} }
// ─── Auth: OTP ───
export async function sendEmailOTP() { export async function sendEmailOTP() {
console.log('[Auth] Sending email OTP...'); console.log('[Auth] Sending email OTP...');
return apiFetch('/Auth/SendEmailOTP', { method: 'POST' }); return apiFetch('/Auth/SendEmailOTP', { method: 'POST' });
@ -183,7 +219,8 @@ export async function verifyPhone(code) {
return authFetch(`/Auth/VerifyPhoneNumber?code=${encodeURIComponent(code)}`, {}); return authFetch(`/Auth/VerifyPhoneNumber?code=${encodeURIComponent(code)}`, {});
} }
// Helpers // ─── Helpers ───
export function isEmail(value) { export function isEmail(value) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value); return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
} }

View File

@ -1,41 +1,71 @@
export const PROPERTY_STATUS = { /**
AVAILABLE: 'available', * Constants — re-exports from enums for backward compatibility
BOOKED: 'booked', *
MAINTENANCE: 'maintenance' * New code should import directly from:
}; * import { BuildingType, BookingStatus, City, ... } from '@/app/enums';
*
* Old imports from '@/app/utils/constants' continue to work.
*/
export const BOOKING_STATUS = { // Re-export all enums
PENDING: 'pending', export {
OWNER_APPROVED: 'owner_approved', BuildingType,
ADMIN_APPROVED: 'admin_approved', BuildingTypeLabels,
REJECTED: 'rejected', BuildingTypeKeys,
ACTIVE: 'active', BuildingTypeByKey,
COMPLETED: 'completed', } from '../enums/BuildingType';
CANCELLED: 'cancelled'
};
export const COMMISSION_TYPE = { export {
FROM_OWNER: 'from_owner', PropertyStatus,
FROM_TENANT: 'from_tenant', PropertyStatusLabels,
FROM_BOTH: 'from_both' PropertyStatusKeys,
}; PropertyStatusByKey,
} from '../enums/PropertyStatus';
export const IDENTITY_TYPE = { export {
SYRIAN: 'syrian', BookingStatus,
PASSPORT: 'passport' BookingStatusLabels,
}; BookingStatusColors,
} from '../enums/BookingStatus';
export const PAYMENT_METHOD = { export {
CommissionType,
CommissionTypeLabels,
} from '../enums/CommissionType';
export {
IdentityType,
IdentityTypeLabels,
IdentityTypeFlags,
} from '../enums/IdentityType';
export {
UserRole,
UserRoleLabels,
UserRoleColors,
} from '../enums/UserRole';
export {
City,
CitiesList,
extractCity,
} from '../enums/City';
export { LoginMethod } from '../enums/LoginMethod';
export { OwnerType, OwnerTypeLabels } from '../enums/OwnerType';
export { CustomerType, CustomerTypeLabels } from '../enums/CustomerType';
// ─── Legacy aliases (keep old imports working) ───
export const PROPERTY_STATUS = PropertyStatusKeys;
export const BOOKING_STATUS = BookingStatus;
export const COMMISSION_TYPE = CommissionType;
export const IDENTITY_TYPE = IdentityType;
export const CITIES = City;
// ─── Misc constants ───
export const PAYMENT_METHOD = Object.freeze({
CASH: 'cash', CASH: 'cash',
ELECTRONIC: 'electronic' ELECTRONIC: 'electronic',
}; });
export const CITIES = {
DAMASCUS: 'damascus',
ALEPPO: 'aleppo',
HOMS: 'homs',
LATTAKIA: 'latakia',
DARAA: 'daraa'
};
export const DEFAULT_COMMISSION_RATE = 5; export const DEFAULT_COMMISSION_RATE = 5;