diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bbb3c7e..165c390 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'; import { LandingPage } from './pages/LandingPage'; import { ExploreMap } from './pages/ExploreMap'; import { TourDetailPage } from './pages/TourDetailPage'; -import { SignupPage } from './pages/SignupPage'; +import SignupPage from './pages/SignupPage'; import { MyPhotosPage } from './pages/MyPhotosPage'; import { MyNotePage } from './pages/MyNotePage'; import { useTourStore } from './store/useTourStore'; diff --git a/frontend/src/pages/SignupPage.tsx b/frontend/src/pages/SignupPage.tsx index c30c965..ec6cf92 100644 --- a/frontend/src/pages/SignupPage.tsx +++ b/frontend/src/pages/SignupPage.tsx @@ -1,12 +1,12 @@ import React, { useState } from 'react'; -import { User, Mail, Lock, ArrowRight, ChevronLeft, Compass, Phone, MapPin } from 'lucide-react'; +import { User, Mail, Lock, ArrowRight, ChevronLeft, Compass, Phone, MapPin, ShieldCheck } from 'lucide-react'; interface SignupPageProps { onBack: () => void; onSuccess: () => void; } -export const SignupPage: React.FC = ({ onBack, onSuccess }) => { +const SignupPage: React.FC = ({ onBack, onSuccess }) => { const [formData, setFormData] = useState({ name: '', email: '', @@ -15,39 +15,58 @@ export const SignupPage: React.FC = ({ onBack, onSuccess }) => phone: '', address: '' }); + const [step, setStep] = useState<'form' | 'otp'>('form'); + const [otp, setOtp] = useState(''); const [error, setError] = useState(''); const [isLoading, setIsLoading] = useState(false); + const handleChange = (field: string, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setError(''); - if (formData.password !== formData.confirmPassword) { - setError('Mật khẩu xác nhận không khớp'); - return; - } - setIsLoading(true); try { - const response = await fetch(`/api/v1/auth/signup`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - email: formData.email, - password: formData.password, - name: formData.name, - phone: formData.phone || undefined, - address: formData.address || undefined - }), - }); + if (step === 'form') { + if (formData.password !== formData.confirmPassword) { + setError('Mật khẩu xác nhận không khớp'); + setIsLoading(false); + return; + } - const data = await response.json(); - - if (!response.ok) { - throw new Error(data.message || 'Đăng ký thất bại'); + const response = await fetch(`/api/v1/auth/signup/request`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: formData.email, + password: formData.password, + name: formData.name, + phone: formData.phone || undefined, + address: formData.address || undefined + }), + }); + const data = await response.json(); + if (!response.ok) throw new Error(data.message || 'Yêu cầu đăng ký thất bại'); + + setStep('otp'); + } else { + // Xác thực mã OTP bước hoàn tất + const response = await fetch(`/api/v1/auth/signup/verify`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: formData.email, + otp: otp + }), + }); + const data = await response.json(); + if (!response.ok) throw new Error(data.message || 'Mã OTP không chính xác'); + + onSuccess(); } - - onSuccess(); } catch (err: any) { setError(err.message); } finally { @@ -56,139 +75,155 @@ export const SignupPage: React.FC = ({ onBack, onSuccess }) => }; return ( -
- {/* Nửa bên trái: Hình ảnh decor (Đồng bộ với LandingPage) */} -
- Travel background -
-
- - Travel Planner -
-
-

Bắt đầu hành trình của riêng bạn.

-

Tạo tài khoản để lưu lại những kế hoạch du lịch tuyệt vời nhất cùng bạn bè.

-
-
+
+
+ - {/* Nửa bên phải: Form đăng ký */} -
-
- +
+

+ {step === 'form' ? 'Tạo tài khoản mới' : 'Xác thực tài khoản'} +

+

+ {step === 'form' + ? 'Khám phá các tính năng lập kế hoạch chuyên nghiệp.' + : `Vui lòng nhập mã OTP đã được gửi tới ${formData.email}`} +

+
-
-

Tạo tài khoản mới

-

Khám phá các tính năng lập kế hoạch chuyên nghiệp.

+ {error && ( +
+ {error}
+ )} - {error && ( -
- {error} -
- )} +
+ {step === 'form' ? ( + <> +
+ +
+ + handleChange('name', e.target.value)} + className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" + /> +
+
- -
- +
+ +
+ + handleChange('email', e.target.value)} + className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" + /> +
+
+ +
+
+ +
+ + handleChange('password', e.target.value)} + className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" + /> +
+
+
+ +
+ + handleChange('confirmPassword', e.target.value)} + className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" + /> +
+
+
+ +
+
+ +
+ + handleChange('phone', e.target.value)} + className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" + /> +
+
+
+ +
+ + handleChange('address', e.target.value)} + className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" + /> +
+
+
+ + ) : ( +
+
- + setFormData({ ...formData, name: e.target.value })} - className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" + maxLength={6} + placeholder="Nhập 6 số mã OTP" + value={otp} + onChange={(e) => setOtp(e.target.value.replace(/\D/g, ''))} + className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl text-center font-bold tracking-[0.5em] text-lg focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" />
+ )} -
- -
- - setFormData({ ...formData, email: e.target.value })} - className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" - /> -
-
- -
-
- -
- - setFormData({ ...formData, phone: e.target.value })} - className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" - /> -
-
-
- -
- - setFormData({ ...formData, address: e.target.value })} - className="w-full pl-12 pr-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" - /> -
-
-
- -
-
- - setFormData({ ...formData, password: e.target.value })} - className="w-full px-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" - /> -
-
- - setFormData({ ...formData, confirmPassword: e.target.value })} - className="w-full px-4 py-4 bg-gray-50 border border-gray-100 rounded-2xl focus:ring-2 focus:ring-blue-500 focus:bg-white outline-none transition-all" - /> -
-
- - - -
+ +
); -}; \ No newline at end of file +}; + +export default SignupPage; diff --git a/package-lock.json b/package-lock.json index 5abe669..4a73edd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "cache-manager": "^7.2.8", "cache-manager-redis-yet": "^5.1.5", "dotenv": "^17.4.2", + "nodemailer": "^9.0.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "pg": "^8.12.0", @@ -6471,6 +6472,15 @@ "node": ">=18" } }, + "node_modules/nodemailer": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-9.0.1.tgz", + "integrity": "sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==", + "license": "MIT-0", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",