Seth0330 commited on
Commit
8a87422
·
verified ·
1 Parent(s): 7f7aa89

Create auth/LoginForm.jsx

Browse files
frontend/src/components/auth/LoginForm.jsx ADDED
@@ -0,0 +1,308 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import { motion } from "framer-motion";
3
+ import { Mail, Lock, AlertCircle, Loader2 } from "lucide-react";
4
+ import { useAuth } from "@/contexts/AuthContext";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Button } from "@/components/ui/button";
7
+
8
+ export default function LoginForm() {
9
+ const { firebaseLogin, requestOTP, verifyOTP } = useAuth();
10
+ const [activeTab, setActiveTab] = useState("google"); // "google" or "otp"
11
+ const [otpStep, setOtpStep] = useState("email"); // "email" or "otp"
12
+ const [email, setEmail] = useState("");
13
+ const [otp, setOtp] = useState("");
14
+ const [loading, setLoading] = useState(false);
15
+ const [error, setError] = useState("");
16
+ const [success, setSuccess] = useState("");
17
+
18
+ // Business email validation (frontend check)
19
+ const PERSONAL_EMAIL_DOMAINS = [
20
+ 'gmail.com', 'yahoo.com', 'hotmail.com', 'outlook.com',
21
+ 'aol.com', 'icloud.com', 'mail.com', 'protonmail.com'
22
+ ];
23
+
24
+ const isBusinessEmail = (email) => {
25
+ if (!email || !email.includes('@')) return false;
26
+ const domain = email.split('@')[1].toLowerCase();
27
+ return !PERSONAL_EMAIL_DOMAINS.includes(domain);
28
+ };
29
+
30
+ const handleFirebaseLogin = async () => {
31
+ setLoading(true);
32
+ setError("");
33
+ try {
34
+ await firebaseLogin();
35
+ } catch (err) {
36
+ setError(err.message || "Failed to sign in with Google");
37
+ } finally {
38
+ setLoading(false);
39
+ }
40
+ };
41
+
42
+ const handleOTPRequest = async (e) => {
43
+ e.preventDefault();
44
+ setLoading(true);
45
+ setError("");
46
+ setSuccess("");
47
+
48
+ if (!email) {
49
+ setError("Please enter your email address");
50
+ setLoading(false);
51
+ return;
52
+ }
53
+
54
+ if (!isBusinessEmail(email)) {
55
+ setError("Only business email addresses are allowed. Personal email accounts (Gmail, Yahoo, etc.) are not permitted.");
56
+ setLoading(false);
57
+ return;
58
+ }
59
+
60
+ try {
61
+ await requestOTP(email);
62
+ setOtpStep("otp");
63
+ setSuccess("OTP sent to your email. Please check your inbox.");
64
+ } catch (err) {
65
+ setError(err.message || "Failed to send OTP");
66
+ } finally {
67
+ setLoading(false);
68
+ }
69
+ };
70
+
71
+ const handleOTPVerify = async (e) => {
72
+ e.preventDefault();
73
+ setLoading(true);
74
+ setError("");
75
+ setSuccess("");
76
+
77
+ if (!otp || otp.length !== 6) {
78
+ setError("Please enter a valid 6-digit OTP");
79
+ setLoading(false);
80
+ return;
81
+ }
82
+
83
+ try {
84
+ await verifyOTP(email, otp);
85
+ // Success - user will be redirected by AuthContext
86
+ } catch (err) {
87
+ setError(err.message || "Invalid OTP. Please try again.");
88
+ setOtp("");
89
+ } finally {
90
+ setLoading(false);
91
+ }
92
+ };
93
+
94
+ const handleBackToEmail = () => {
95
+ setOtpStep("email");
96
+ setOtp("");
97
+ setError("");
98
+ setSuccess("");
99
+ };
100
+
101
+ return (
102
+ <div className="min-h-screen flex items-center justify-center bg-[#FAFAFA] p-4">
103
+ <motion.div
104
+ initial={{ opacity: 0, y: 20 }}
105
+ animate={{ opacity: 1, y: 0 }}
106
+ className="w-full max-w-md bg-white rounded-2xl shadow-xl border border-slate-200 overflow-hidden"
107
+ >
108
+ {/* Header */}
109
+ <div className="bg-gradient-to-r from-indigo-600 to-violet-600 p-8 text-center text-white">
110
+ <div className="h-16 w-16 mx-auto rounded-2xl bg-white/20 backdrop-blur-sm flex items-center justify-center mb-4">
111
+ <Lock className="h-8 w-8" />
112
+ </div>
113
+ <h1 className="text-2xl font-bold mb-2">Welcome to EZOFIS AI</h1>
114
+ <p className="text-indigo-100 text-sm">Sign in to access your account</p>
115
+ </div>
116
+
117
+ {/* Tabs */}
118
+ <div className="flex border-b border-slate-200">
119
+ <button
120
+ onClick={() => {
121
+ setActiveTab("google");
122
+ setOtpStep("email");
123
+ setError("");
124
+ setSuccess("");
125
+ }}
126
+ className={`flex-1 py-4 text-center font-medium transition-colors ${
127
+ activeTab === "google"
128
+ ? "text-indigo-600 border-b-2 border-indigo-600"
129
+ : "text-slate-500 hover:text-slate-700"
130
+ }`}
131
+ >
132
+ Google Sign In
133
+ </button>
134
+ <button
135
+ onClick={() => {
136
+ setActiveTab("otp");
137
+ setOtpStep("email");
138
+ setError("");
139
+ setSuccess("");
140
+ }}
141
+ className={`flex-1 py-4 text-center font-medium transition-colors ${
142
+ activeTab === "otp"
143
+ ? "text-indigo-600 border-b-2 border-indigo-600"
144
+ : "text-slate-500 hover:text-slate-700"
145
+ }`}
146
+ >
147
+ Email / OTP
148
+ </button>
149
+ </div>
150
+
151
+ {/* Content */}
152
+ <div className="p-8">
153
+ {activeTab === "google" ? (
154
+ <div className="space-y-4">
155
+ <p className="text-sm text-slate-600 text-center mb-6">
156
+ Sign in with your business Google account
157
+ </p>
158
+ <Button
159
+ onClick={handleFirebaseLogin}
160
+ disabled={loading}
161
+ className="w-full h-12 bg-white border-2 border-slate-300 hover:border-indigo-400 hover:bg-indigo-50 transition-all duration-200 flex items-center justify-center gap-3"
162
+ >
163
+ {loading ? (
164
+ <Loader2 className="h-5 w-5 animate-spin" />
165
+ ) : (
166
+ <>
167
+ <svg className="h-5 w-5" viewBox="0 0 24 24">
168
+ <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
169
+ <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
170
+ <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
171
+ <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
172
+ </svg>
173
+ <span className="font-semibold text-slate-700">Sign in with Google</span>
174
+ </>
175
+ )}
176
+ </Button>
177
+ <p className="text-xs text-slate-500 text-center mt-4">
178
+ Only business email addresses are allowed
179
+ </p>
180
+ </div>
181
+ ) : (
182
+ <div className="space-y-4">
183
+ {otpStep === "email" ? (
184
+ <form onSubmit={handleOTPRequest} className="space-y-4">
185
+ <div>
186
+ <label className="block text-sm font-medium text-slate-700 mb-2">
187
+ Business Email Address
188
+ </label>
189
+ <div className="relative">
190
+ <Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" />
191
+ <Input
192
+ type="email"
193
+ value={email}
194
+ onChange={(e) => {
195
+ setEmail(e.target.value);
196
+ setError("");
197
+ }}
198
+ placeholder="yourname@company.com"
199
+ className="pl-10 h-12"
200
+ required
201
+ />
202
+ </div>
203
+ <p className="text-xs text-slate-500 mt-1">
204
+ We'll send a 6-digit OTP to your email
205
+ </p>
206
+ </div>
207
+ <Button
208
+ type="submit"
209
+ disabled={loading}
210
+ className="w-full h-12 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700"
211
+ >
212
+ {loading ? (
213
+ <>
214
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
215
+ Sending OTP...
216
+ </>
217
+ ) : (
218
+ "Send OTP"
219
+ )}
220
+ </Button>
221
+ </form>
222
+ ) : (
223
+ <form onSubmit={handleOTPVerify} className="space-y-4">
224
+ <div>
225
+ <label className="block text-sm font-medium text-slate-700 mb-2">
226
+ Enter OTP Code
227
+ </label>
228
+ <div className="relative">
229
+ <Lock className="absolute left-3 top-1/2 -translate-y-1/2 h-5 w-5 text-slate-400" />
230
+ <Input
231
+ type="text"
232
+ value={otp}
233
+ onChange={(e) => {
234
+ const value = e.target.value.replace(/\D/g, '').slice(0, 6);
235
+ setOtp(value);
236
+ setError("");
237
+ }}
238
+ placeholder="000000"
239
+ className="pl-10 h-12 text-center text-2xl tracking-widest font-mono"
240
+ maxLength={6}
241
+ required
242
+ />
243
+ </div>
244
+ <p className="text-xs text-slate-500 mt-1">
245
+ OTP sent to {email}</p>
246
+ </div>
247
+ <div className="flex gap-3">
248
+ <Button
249
+ type="button"
250
+ variant="outline"
251
+ onClick={handleBackToEmail}
252
+ className="flex-1 h-12"
253
+ >
254
+ Back
255
+ </Button>
256
+ <Button
257
+ type="submit"
258
+ disabled={loading || otp.length !== 6}
259
+ className="flex-1 h-12 bg-gradient-to-r from-indigo-600 to-violet-600 hover:from-indigo-700 hover:to-violet-700"
260
+ >
261
+ {loading ? (
262
+ <>
263
+ <Loader2 className="h-4 w-4 mr-2 animate-spin" />
264
+ Verifying...
265
+ </>
266
+ ) : (
267
+ "Verify OTP"
268
+ )}
269
+ </Button>
270
+ </div>
271
+ </form>
272
+ )}
273
+ </div>
274
+ )}
275
+
276
+ {/* Error Message */}
277
+ {error && (
278
+ <motion.div
279
+ initial={{ opacity: 0, y: -10 }}
280
+ animate={{ opacity: 1, y: 0 }}
281
+ className="mt-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2"
282
+ >
283
+ <AlertCircle className="h-4 w-4 text-red-600 flex-shrink-0 mt-0.5" />
284
+ <p className="text-sm text-red-700 flex-1">{error}</p>
285
+ </motion.div>
286
+ )}
287
+
288
+ {/* Success Message */}
289
+ {success && (
290
+ <motion.div
291
+ initial={{ opacity: 0, y: -10 }}
292
+ animate={{ opacity: 1, y: 0 }}
293
+ className="mt-4 p-3 bg-emerald-50 border border-emerald-200 rounded-lg flex items-start gap-2"
294
+ >
295
+ <AlertCircle className="h-4 w-4 text-emerald-600 flex-shrink-0 mt-0.5" />
296
+ <p className="text-sm text-emerald-700 flex-1">{success}</p>
297
+ </motion.div>
298
+ )}
299
+
300
+ <p className="text-xs text-slate-500 text-center mt-6">
301
+ By signing in, you agree to our Terms of Service and Privacy Policy
302
+ </p>
303
+ </div>
304
+ </motion.div>
305
+ </div>
306
+ );
307
+ }
308
+