diff --git a/app/login/page.js b/app/login/page.js
index f46c96c..097d01e 100644
--- a/app/login/page.js
+++ b/app/login/page.js
@@ -1,10 +1,9 @@
'use client';
import { useState } from 'react';
-import { motion } from 'framer-motion';
+import { motion, AnimatePresence } from 'framer-motion';
import toast, { Toaster } from 'react-hot-toast';
import Link from 'next/link';
-import Image from 'next/image';
import { useRouter } from 'next/navigation';
import {
Mail,
@@ -16,106 +15,251 @@ import {
CheckCircle,
Loader2,
Home,
- Shield
+ Shield,
+ Phone,
+ KeyRound,
} from 'lucide-react';
+import {
+ loginWithEmail,
+ loginWithPhone,
+ sendEmailOTP,
+ sendPhoneOTP,
+ verifyEmail,
+ verifyPhone,
+ isEmail,
+ isPhoneNumber,
+} from '../utils/api';
export default function LoginPage() {
const router = useRouter();
+
+ // Step: 'login' | 'otp'
+ const [step, setStep] = useState('login');
+ const [loginMethod, setLoginMethod] = useState('email'); // 'email' | 'phone'
const [showPassword, setShowPassword] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
+
const [formData, setFormData] = useState({
- email: '',
+ credential: '',
password: '',
- rememberMe: false
+ rememberMe: false,
});
+
+ const [otpCode, setOtpCode] = useState('');
+ const [otpError, setOtpError] = useState('');
const [errors, setErrors] = useState({});
- const ADMIN_EMAIL = 'admin@gmail.com';
- const ADMIN_PASSWORD = '123';
-
- const validateEmail = (email) => {
- const re = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
- return re.test(email);
- };
-
const validateForm = () => {
const newErrors = {};
-
- if (!formData.email) {
- newErrors.email = 'البريد الإلكتروني مطلوب';
- } else if (!validateEmail(formData.email)) {
- newErrors.email = 'البريد الإلكتروني غير صالح';
+
+ if (!formData.credential) {
+ newErrors.credential = loginMethod === 'email'
+ ? 'البريد الإلكتروني مطلوب'
+ : 'رقم الهاتف مطلوب';
+ } else if (loginMethod === 'email' && !isEmail(formData.credential)) {
+ newErrors.credential = 'البريد الإلكتروني غير صالح';
+ } else if (loginMethod === 'phone' && !isPhoneNumber(formData.credential)) {
+ newErrors.credential = 'رقم الهاتف غير صالح';
}
-
+
if (!formData.password) {
newErrors.password = 'كلمة المرور مطلوبة';
}
-
+
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
- const handleSubmit = async (e) => {
+ const handleLogin = async (e) => {
e.preventDefault();
-
- if (!validateForm()) {
- toast.error('يرجى تصحيح الأخطاء في النموذج', {
- style: { background: '#fee2e2', color: '#991b1b' }
- });
- return;
- }
-
+ if (!validateForm()) return;
+
setIsLoading(true);
-
- setTimeout(() => {
- if (formData.email.toLowerCase() === ADMIN_EMAIL && formData.password === ADMIN_PASSWORD) {
- setIsLoading(false);
+ setErrors({});
+
+ try {
+ const loginFn = loginMethod === 'email' ? loginWithEmail : loginWithPhone;
+ console.log('[Login] Attempting login via', loginMethod, ':', formData.credential);
+
+ const result = await loginFn(formData.credential, formData.password);
+
+ console.log('[Login] Response:', result);
+
+ if (result.status === 200) {
+ // Login success — store token
+ const token = typeof result.data === 'string' ? result.data : result.data?.token || result.data;
+ localStorage.setItem('token', token);
+ console.log('[Login] Token stored successfully');
+
+ // Decode token to get user info (basic JWT decode)
+ try {
+ const payload = JSON.parse(atob(token.split('.')[1]));
+ const user = {
+ name: payload.name || payload.unique_name || formData.credential,
+ email: payload.email || (loginMethod === 'email' ? formData.credential : ''),
+ phone: payload.phone || (loginMethod === 'phone' ? formData.credential : ''),
+ role: payload.role || payload.Role || 'customer',
+ };
+ localStorage.setItem('user', JSON.stringify(user));
+ console.log('[Login] User stored:', user);
+ } catch (decodeErr) {
+ console.warn('[Login] Could not decode JWT, storing credential as user');
+ localStorage.setItem('user', JSON.stringify({
+ name: formData.credential,
+ role: 'customer',
+ }));
+ }
+
setIsSuccess(true);
-
- toast.success('تم تسجيل الدخول كأدمن!', {
+ toast.success('تم تسجيل الدخول بنجاح!', {
style: { background: '#dcfce7', color: '#166534' },
- duration: 3000
});
-
- localStorage.setItem('user', JSON.stringify({
- name: 'مدير النظام',
- email: ADMIN_EMAIL,
- role: 'admin',
- avatar: 'أ'
- }));
-
+
setTimeout(() => {
- router.push('/admin');
+ const user = JSON.parse(localStorage.getItem('user') || '{}');
+ console.log('[Login] Redirecting user:', user);
+ if (user.role === 'admin') {
+ router.push('/admin');
+ } else {
+ router.push('/');
+ }
}, 1500);
+
+ } else if (result.status === 206) {
+ // Needs OTP verification
+ console.log('[Login] 206 — OTP required, sending OTP...');
+ toast('يرجى إدخال رمز التحقق', {
+ icon: '🔐',
+ style: { background: '#fef3c7', color: '#92400e' },
+ });
+
+ // Send OTP
+ try {
+ if (loginMethod === 'email') {
+ await sendEmailOTP();
+ } else {
+ await sendPhoneOTP();
+ }
+ console.log('[Login] OTP sent successfully');
+ } catch (otpErr) {
+ console.warn('[Login] OTP send failed, proceeding anyway:', otpErr);
+ }
+
+ setStep('otp');
} else {
- setIsLoading(false);
- toast.error('بيانات الدخول غير صحيحة. حاول مع admin@gmail.com / 123', {
+ // Other error
+ console.error('[Login] Unexpected status:', result.status, result.data);
+ toast.error(result.data?.message || result.data || 'بيانات الدخول غير صحيحة', {
style: { background: '#fee2e2', color: '#991b1b' },
- duration: 4000
});
}
- }, 1500);
+ } catch (err) {
+ console.error('[Login] Error:', err);
+ toast.error(err.message || 'حدث خطأ في الاتصال', {
+ style: { background: '#fee2e2', color: '#991b1b' },
+ });
+ } finally {
+ setIsLoading(false);
+ }
};
- const particles = Array.from({ length: 30 }, (_, i) => ({
+ const handleVerifyOTP = async (e) => {
+ e.preventDefault();
+ if (!otpCode || otpCode.length < 4) {
+ setOtpError('يرجى إدخال رمز التحقق');
+ return;
+ }
+
+ setIsLoading(true);
+ setOtpError('');
+
+ try {
+ const verifyFn = loginMethod === 'email' ? verifyEmail : verifyPhone;
+ console.log('[OTP] Verifying code:', otpCode);
+
+ const result = await verifyFn(otpCode);
+ console.log('[OTP] Verify response:', result);
+
+ if (result.ok) {
+ // Verified — store token if returned
+ const token = typeof result.data === 'string' ? result.data : result.data?.token || result.data;
+ if (token && typeof token === 'string' && token.includes('.')) {
+ localStorage.setItem('token', token);
+ console.log('[OTP] Token stored');
+ }
+
+ localStorage.setItem('user', JSON.stringify({
+ name: formData.credential,
+ role: 'customer',
+ }));
+
+ setIsSuccess(true);
+ toast.success('تم التحقق بنجاح!', {
+ style: { background: '#dcfce7', color: '#166534' },
+ });
+
+ setTimeout(() => {
+ console.log('[OTP] Redirecting to home');
+ router.push('/');
+ }, 1500);
+ } else {
+ console.error('[OTP] Verification failed:', result.data);
+ setOtpError(result.data?.message || 'رمز التحقق غير صحيح');
+ }
+ } catch (err) {
+ console.error('[OTP] Error:', err);
+ setOtpError(err.message || 'حدث خطأ في التحقق');
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const resendOTP = async () => {
+ try {
+ console.log('[OTP] Resending OTP via', loginMethod);
+ if (loginMethod === 'email') {
+ await sendEmailOTP();
+ } else {
+ await sendPhoneOTP();
+ }
+ toast.success('تم إرسال رمز التحقق مجدداً', {
+ style: { background: '#dcfce7', color: '#166534' },
+ });
+ } catch (err) {
+ console.error('[OTP] Resend failed:', err);
+ toast.error('فشل إرسال الرمز');
+ }
+ };
+
+ // Auto-detect login method from input
+ const handleCredentialChange = (value) => {
+ setFormData({ ...formData, credential: value });
+ if (errors.credential) setErrors({ ...errors, credential: null });
+
+ // Auto-switch method
+ if (isEmail(value)) {
+ setLoginMethod('email');
+ } else if (isPhoneNumber(value)) {
+ setLoginMethod('phone');
+ }
+ };
+
+ const particles = Array.from({ length: 20 }, (_, i) => ({
id: i,
x: Math.random() * 100,
y: Math.random() * 100,
size: Math.random() * 3 + 1,
duration: Math.random() * 15 + 10,
- delay: Math.random() * 5
+ delay: Math.random() * 5,
}));
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
- transition: {
- staggerChildren: 0.1,
- delayChildren: 0.2
- }
- }
+ transition: { staggerChildren: 0.1, delayChildren: 0.2 },
+ },
};
const itemVariants = {
@@ -123,56 +267,36 @@ export default function LoginPage() {
visible: {
y: 0,
opacity: 1,
- transition: { type: 'spring', stiffness: 100 }
- }
+ transition: { type: 'spring', stiffness: 100 },
+ },
};
return (
-
+
+ {/* Particles */}
- {particles.map((particle) => (
+ {particles.map((p) => (
))}
+ {/* Glow orbs */}
@@ -182,18 +306,10 @@ export default function LoginPage() {
animate="visible"
className="relative w-full max-w-md z-10"
>
-
-
-
+ {/* Back link */}
+
+
+
العودة للرئيسية
@@ -204,221 +320,305 @@ export default function LoginPage() {
variants={itemVariants}
className="bg-white/10 backdrop-blur-2xl rounded-3xl shadow-2xl border border-white/10 overflow-hidden"
>
+ {/* Header */}
-
-
+
+
-
+ {step === 'otp' ? (
+
+ ) : (
+
+ )}
SweetHome
- مرحباً بعودتك!
+
+ {step === 'otp' ? 'أدخل رمز التحقق' : 'مرحباً بعودتك!'}
+
-
-
-
-
-
-
-
-
{
- setFormData({...formData, email: e.target.value});
- if (errors.email) setErrors({...errors, email: null});
- }}
- className={`w-full pr-12 pl-4 py-4 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="أدخل بريدك الإلكتروني"
- />
- {formData.email && validateEmail(formData.email) && (
-
-
-
- )}
-
- {errors.email && (
-
- {errors.email}
-
- )}
-
-
-
-
-
-
-
-
-
{
- setFormData({...formData, password: e.target.value});
- if (errors.password) setErrors({...errors, password: null});
- }}
- className={`w-full pr-12 pl-12 py-4 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="أدخل كلمة المرور"
- />
-
-
- {errors.password && (
-
- {errors.password}
-
- )}
-
-
-
-
-
-
-
+ {step === 'login' ? (
+
- نسيت كلمة المرور؟
-
-
+ {/* Login method tabs */}
+
+
+
+
-
-
-
- {isLoading ? (
- <>
-
- جاري تسجيل الدخول...
- >
- ) : isSuccess ? (
- <>
-
- تم بنجاح!
- >
- ) : (
- <>
-
- تسجيل الدخول
- >
- )}
-
-
-
+ {/* Credential input */}
+
+
+
+
+ {loginMethod === 'email' ? (
+
+ ) : (
+
+ )}
+
+
handleCredentialChange(e.target.value)}
+ className={`w-full pr-12 pl-4 py-4 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.credential ? 'border-red-500' : 'border-gray-700'
+ }`}
+ placeholder={loginMethod === 'email' ? 'example@email.com' : '+963XXXXXXXXX'}
+ dir="ltr"
+ />
+
+ {errors.credential && (
+
+ {errors.credential}
+
+ )}
+
-
+ {/* Password */}
+
+
+
+
+
+
+
{
+ setFormData({ ...formData, password: e.target.value });
+ if (errors.password) setErrors({ ...errors, password: null });
+ }}
+ className={`w-full pr-12 pl-12 py-4 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="أدخل كلمة المرور"
+ />
+
+
+ {errors.password && (
+
+ {errors.password}
+
+ )}
+
+
+ {/* Remember + Forgot */}
+
+
+
+ نسيت كلمة المرور؟
+
+
+
+ {/* Submit */}
+
+
+ {isLoading ? (
+ <>
+
+ جاري تسجيل الدخول...
+ >
+ ) : isSuccess ? (
+ <>
+
+ تم بنجاح!
+ >
+ ) : (
+ <>
+
+ تسجيل الدخول
+ >
+ )}
+
+
+
+ ) : (
+ /* OTP Verification Step */
+
+
+
+
+ تم إرسال رمز التحقق إلى{' '}
+
+ {formData.credential}
+
+
+
+
+
+
+ {
+ setOtpCode(e.target.value);
+ if (otpError) setOtpError('');
+ }}
+ className={`w-full px-4 py-4 bg-white/5 border rounded-xl focus:outline-none focus:ring-2 focus:ring-amber-500 focus:border-transparent text-white text-center text-2xl tracking-[0.5em] placeholder-gray-500 transition-all ${
+ otpError ? 'border-red-500' : 'border-gray-700'
+ }`}
+ placeholder="______"
+ maxLength={6}
+ dir="ltr"
+ />
+ {otpError && (
+
+ {otpError}
+
+ )}
+
+
+
+
+ {isLoading ? (
+ <>
+
+ جاري التحقق...
+ >
+ ) : isSuccess ? (
+ <>
+
+ تم بنجاح!
+ >
+ ) : (
+ <>
+
+ تحقق
+ >
+ )}
+
+
+
+
+
+
+
+
+ )}
+
+
+
ليس لديك حساب؟{' '}
-
+
إنشاء حساب جديد
-
+
بتسجيل الدخول، أنت توافق على{' '}
-
- شروط الاستخدام
-
+ شروط الاستخدام
{' '}و{' '}
-
- سياسة الخصوصية
-
+ سياسة الخصوصية
);
-}
\ No newline at end of file
+}
diff --git a/app/utils/api.js b/app/utils/api.js
index 94cad88..c5d8f8c 100644
--- a/app/utils/api.js
+++ b/app/utils/api.js
@@ -9,13 +9,18 @@ async function apiFetch(endpoint, options = {}) {
...options.headers,
};
+ console.log('[API] Request:', `${API_BASE}${endpoint}`, options.method || 'GET');
+
const res = await fetch(`${API_BASE}${endpoint}`, {
...options,
headers,
});
- if (!res.ok) {
+ console.log('[API] Response:', res.status, endpoint);
+
+ if (!res.ok && res.status !== 206) {
const text = await res.text().catch(() => '');
+ console.error('[API] Error:', res.status, text);
throw new Error(`API ${res.status}: ${text || res.statusText}`);
}
@@ -24,7 +29,6 @@ async function apiFetch(endpoint, options = {}) {
try {
const json = JSON.parse(text);
- // API wraps responses in { data, errors, isSuccess, isFailure, statusCode }
if (json && typeof json === 'object' && 'data' in json) {
return json.data;
}
@@ -34,6 +38,32 @@ async function apiFetch(endpoint, options = {}) {
}
}
+// Raw fetch for auth (no token, returns full response for status code handling)
+async function authFetch(endpoint, body) {
+ console.log('[Auth] Request:', `${API_BASE}${endpoint}`);
+
+ const res = await fetch(`${API_BASE}${endpoint}`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(body),
+ });
+
+ console.log('[Auth] Response status:', res.status, endpoint);
+
+ const text = await res.text();
+ let data = null;
+ try {
+ data = text ? JSON.parse(text) : null;
+ if (data && typeof data === 'object' && 'data' in data) {
+ data = data.data;
+ }
+ } catch {
+ data = text;
+ }
+
+ return { status: res.status, data, ok: res.ok || res.status === 206 };
+}
+
// ─── Rent Properties ───
export async function getRentProperties() {
@@ -112,3 +142,52 @@ export async function bookReservation(data) {
export async function getTerms() {
return apiFetch('/Terms/GetTerms');
}
+
+// ─── Auth ───
+
+export async function loginWithEmail(credential, password) {
+ return authFetch('/Auth/LogInWithEmail', {
+ credential,
+ password,
+ device: 0,
+ appVersion: '1.0',
+ });
+}
+
+export async function loginWithPhone(credential, password) {
+ return authFetch('/Auth/LogInWithPhoneNumber', {
+ credential,
+ password,
+ device: 0,
+ appVersion: '1.0',
+ });
+}
+
+export async function sendEmailOTP() {
+ console.log('[Auth] Sending email OTP...');
+ return apiFetch('/Auth/SendEmailOTP', { method: 'POST' });
+}
+
+export async function sendPhoneOTP() {
+ console.log('[Auth] Sending phone OTP...');
+ return apiFetch('/Auth/SendPhoneNumberOTP', { method: 'POST' });
+}
+
+export async function verifyEmail(code) {
+ console.log('[Auth] Verifying email with code:', code);
+ return authFetch(`/Auth/VerifyEmail?code=${encodeURIComponent(code)}`, {});
+}
+
+export async function verifyPhone(code) {
+ console.log('[Auth] Verifying phone with code:', code);
+ return authFetch(`/Auth/VerifyPhoneNumber?code=${encodeURIComponent(code)}`, {});
+}
+
+// Helpers
+export function isEmail(value) {
+ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
+}
+
+export function isPhoneNumber(value) {
+ return /^\+?\d{7,15}$/.test(value.replace(/[\s\-()]/g, ''));
+}