Skip to content

Commit 20aeed0

Browse files
committed
FEAT: Implemented Google authentication with firebase
1 parent 851fa96 commit 20aeed0

4 files changed

Lines changed: 181 additions & 88 deletions

File tree

src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ function AppContent() {
484484
: "0 min"}
485485
</p>
486486
</div>
487-
<Headphones className="h-8 w-8 text-green-500" />
487+
<Headphones className="h-8 w-8 text-cyan-500" />
488488
</div>
489489
</div>
490490

src/components/AuthModal.tsx

Lines changed: 141 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ export default function AuthModal({
2222
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
2323
const [error, setError] = useState("");
2424
const [loading, setLoading] = useState(false);
25+
const [googleLoading, setGoogleLoading] = useState(false);
2526

26-
const { login, signup } = useAuth();
27+
const { login, signup, signInWithGoogle } = useAuth();
2728

2829
const resetForm = () => {
2930
setEmail("");
@@ -40,6 +41,20 @@ export default function AuthModal({
4041
onClose();
4142
};
4243

44+
const handleGoogleSignIn = async () => {
45+
setError("");
46+
setGoogleLoading(true);
47+
48+
try {
49+
await signInWithGoogle();
50+
handleClose();
51+
} catch (err: any) {
52+
setError(err.message || "Google sign-in failed");
53+
} finally {
54+
setGoogleLoading(false);
55+
}
56+
};
57+
4358
const handleSubmit = async (e: React.FormEvent) => {
4459
e.preventDefault();
4560
setError("");
@@ -87,132 +102,172 @@ export default function AuthModal({
87102
</button>
88103
</div>
89104

90-
<form onSubmit={handleSubmit} className="p-6 space-y-4">
105+
<div className="p-6 space-y-4">
91106
{error && (
92107
<div className="flex items-center space-x-2 p-3 bg-red-900/20 border border-red-500/30 rounded-lg">
93108
<AlertCircle className="h-4 w-4 text-red-400" />
94109
<span className="text-red-400 text-sm">{error}</span>
95110
</div>
96111
)}
97112

98-
{mode === "signup" && (
113+
<button
114+
onClick={handleGoogleSignIn}
115+
disabled={googleLoading || loading}
116+
className="w-full flex items-center justify-center space-x-3 py-3 px-4 bg-white hover:bg-gray-100 disabled:bg-gray-300 disabled:cursor-not-allowed text-gray-700 font-medium rounded-lg transition-colors border"
117+
>
118+
{googleLoading ? (
119+
<div className="w-5 h-5 border-2 border-gray-400 border-t-transparent rounded-full animate-spin"></div>
120+
) : (
121+
<svg className="w-5 h-5" viewBox="0 0 24 24">
122+
<path
123+
fill="#4285F4"
124+
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"
125+
/>
126+
<path
127+
fill="#34A853"
128+
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"
129+
/>
130+
<path
131+
fill="#FBBC05"
132+
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"
133+
/>
134+
<path
135+
fill="#EA4335"
136+
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"
137+
/>
138+
</svg>
139+
)}
140+
<span>
141+
{googleLoading ? "Signing in..." : `Continue with Google`}
142+
</span>
143+
</button>
144+
145+
<div className="flex items-center my-4">
146+
<div className="flex-1 h-px bg-gray-600"></div>
147+
<span className="px-3 text-gray-400 text-sm">or</span>
148+
<div className="flex-1 h-px bg-gray-600"></div>
149+
</div>
150+
151+
<form onSubmit={handleSubmit} className="space-y-4">
152+
{mode === "signup" && (
153+
<div>
154+
<label className="block text-sm font-medium text-gray-300 mb-2">
155+
Full Name
156+
</label>
157+
<div className="relative">
158+
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
159+
<input
160+
type="text"
161+
value={displayName}
162+
onChange={(e) => setDisplayName(e.target.value)}
163+
className="w-full pl-10 pr-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
164+
placeholder="Enter your full name"
165+
required={mode === "signup"}
166+
/>
167+
</div>
168+
</div>
169+
)}
170+
99171
<div>
100172
<label className="block text-sm font-medium text-gray-300 mb-2">
101-
Full Name
173+
Email Address
102174
</label>
103175
<div className="relative">
104-
<User className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
176+
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
105177
<input
106-
type="text"
107-
value={displayName}
108-
onChange={(e) => setDisplayName(e.target.value)}
178+
type="email"
179+
value={email}
180+
onChange={(e) => setEmail(e.target.value)}
109181
className="w-full pl-10 pr-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
110-
placeholder="Enter your full name"
111-
required={mode === "signup"}
182+
placeholder="Enter your email"
183+
required
112184
/>
113185
</div>
114186
</div>
115-
)}
116187

117-
<div>
118-
<label className="block text-sm font-medium text-gray-300 mb-2">
119-
Email Address
120-
</label>
121-
<div className="relative">
122-
<Mail className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
123-
<input
124-
type="email"
125-
value={email}
126-
onChange={(e) => setEmail(e.target.value)}
127-
className="w-full pl-10 pr-4 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
128-
placeholder="Enter your email"
129-
required
130-
/>
131-
</div>
132-
</div>
133-
134-
<div>
135-
<label className="block text-sm font-medium text-gray-300 mb-2">
136-
Password
137-
</label>
138-
<div className="relative">
139-
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
140-
<input
141-
type={showPassword ? "text" : "password"}
142-
value={password}
143-
onChange={(e) => setPassword(e.target.value)}
144-
className="w-full pl-10 pr-10 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
145-
placeholder="Enter your password"
146-
required
147-
/>
148-
<button
149-
type="button"
150-
onClick={() => setShowPassword(!showPassword)}
151-
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-300"
152-
>
153-
{showPassword ? (
154-
<EyeOff className="h-4 w-4" />
155-
) : (
156-
<Eye className="h-4 w-4" />
157-
)}
158-
</button>
159-
</div>
160-
</div>
161-
162-
{mode === "signup" && (
163188
<div>
164189
<label className="block text-sm font-medium text-gray-300 mb-2">
165-
Confirm Password
190+
Password
166191
</label>
167192
<div className="relative">
168193
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
169194
<input
170-
type={showConfirmPassword ? "text" : "password"}
171-
value={confirmPassword}
172-
onChange={(e) => setConfirmPassword(e.target.value)}
195+
type={showPassword ? "text" : "password"}
196+
value={password}
197+
onChange={(e) => setPassword(e.target.value)}
173198
className="w-full pl-10 pr-10 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
174-
placeholder="Confirm your password"
175-
required={mode === "signup"}
199+
placeholder="Enter your password"
200+
required
176201
/>
177202
<button
178203
type="button"
179-
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
204+
onClick={() => setShowPassword(!showPassword)}
180205
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-300"
181206
>
182-
{showConfirmPassword ? (
207+
{showPassword ? (
183208
<EyeOff className="h-4 w-4" />
184209
) : (
185210
<Eye className="h-4 w-4" />
186211
)}
187212
</button>
188213
</div>
189214
</div>
190-
)}
191215

192-
<button
193-
type="submit"
194-
disabled={loading}
195-
className="w-full py-3 bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-600 hover:to-blue-600 disabled:from-gray-600 disabled:to-gray-600 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-all duration-200"
196-
>
197-
{loading
198-
? "Processing..."
199-
: mode === "login"
200-
? "Sign In"
201-
: "Create Account"}
202-
</button>
216+
{mode === "signup" && (
217+
<div>
218+
<label className="block text-sm font-medium text-gray-300 mb-2">
219+
Confirm Password
220+
</label>
221+
<div className="relative">
222+
<Lock className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-gray-400" />
223+
<input
224+
type={showConfirmPassword ? "text" : "password"}
225+
value={confirmPassword}
226+
onChange={(e) => setConfirmPassword(e.target.value)}
227+
className="w-full pl-10 pr-10 py-3 bg-gray-700 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-cyan-500 focus:border-transparent"
228+
placeholder="Confirm your password"
229+
required={mode === "signup"}
230+
/>
231+
<button
232+
type="button"
233+
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
234+
className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-300"
235+
>
236+
{showConfirmPassword ? (
237+
<EyeOff className="h-4 w-4" />
238+
) : (
239+
<Eye className="h-4 w-4" />
240+
)}
241+
</button>
242+
</div>
243+
</div>
244+
)}
203245

204-
<div className="text-center">
205246
<button
206-
type="button"
207-
onClick={switchMode}
208-
className="text-cyan-400 hover:text-cyan-300 text-sm font-medium"
247+
type="submit"
248+
disabled={loading || googleLoading}
249+
className="w-full py-3 bg-gradient-to-r from-cyan-500 to-blue-500 hover:from-cyan-600 hover:to-blue-600 disabled:from-gray-600 disabled:to-gray-600 disabled:cursor-not-allowed text-white font-medium rounded-lg transition-all duration-200"
209250
>
210-
{mode === "login"
211-
? "Don't have an account? Sign up"
212-
: "Already have an account? Sign in"}
251+
{loading
252+
? "Processing..."
253+
: mode === "login"
254+
? "Sign In"
255+
: "Create Account"}
213256
</button>
214-
</div>
215-
</form>
257+
258+
<div className="text-center">
259+
<button
260+
type="button"
261+
onClick={switchMode}
262+
className="text-cyan-400 hover:text-cyan-300 text-sm font-medium"
263+
>
264+
{mode === "login"
265+
? "Don't have an account? Sign up"
266+
: "Already have an account? Sign in"}
267+
</button>
268+
</div>
269+
</form>
270+
</div>
216271
</div>
217272
</div>
218273
);

src/contexts/AuthContext.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import React, {
88
import {
99
createUserWithEmailAndPassword,
1010
signInWithEmailAndPassword,
11+
signInWithPopup,
1112
signOut,
1213
onAuthStateChanged,
1314
User,
@@ -20,7 +21,7 @@ import {
2021
updateDoc,
2122
serverTimestamp,
2223
} from "firebase/firestore";
23-
import { auth, db } from "../firebase";
24+
import { auth, db, googleProvider } from "../firebase";
2425

2526
interface UserProfile {
2627
uid: string;
@@ -46,6 +47,7 @@ interface AuthContextType {
4647
displayName: string
4748
) => Promise<void>;
4849
login: (email: string, password: string) => Promise<void>;
50+
signInWithGoogle: () => Promise<void>;
4951
logout: () => Promise<void>;
5052
loading: boolean;
5153
updateUserStats: (
@@ -111,6 +113,34 @@ export function AuthProvider({ children }: AuthProviderProps) {
111113
});
112114
};
113115

116+
const signInWithGoogle = async () => {
117+
const { user } = await signInWithPopup(auth, googleProvider);
118+
119+
const userDoc = await getDoc(doc(db, "users", user.uid));
120+
121+
if (!userDoc.exists()) {
122+
const userProfileData: UserProfile = {
123+
uid: user.uid,
124+
email: user.email!,
125+
displayName: user.displayName || user.email!.split("@")[0],
126+
createdAt: serverTimestamp(),
127+
lastLogin: serverTimestamp(),
128+
plan: "free",
129+
stats: {
130+
filesProcessed: 0,
131+
audioGenerated: 0,
132+
videosCreated: 0,
133+
transcriptionsGenerated: 0,
134+
},
135+
};
136+
await setDoc(doc(db, "users", user.uid), userProfileData);
137+
} else {
138+
await updateDoc(doc(db, "users", user.uid), {
139+
lastLogin: serverTimestamp(),
140+
});
141+
}
142+
};
143+
114144
const logout = () => {
115145
return signOut(auth);
116146
};
@@ -164,6 +194,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
164194
userProfile,
165195
signup,
166196
login,
197+
signInWithGoogle,
167198
logout,
168199
loading,
169200
updateUserStats,

src/firebase.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { initializeApp } from "firebase/app";
22
import { getAuth } from "firebase/auth";
33
import { getFirestore } from "firebase/firestore";
4+
import { GoogleAuthProvider } from "firebase/auth";
45

56
const firebaseConfig = {
67
apiKey: import.meta.env.VITE_FIREBASE_API_KEY || "",
@@ -14,4 +15,10 @@ const firebaseConfig = {
1415
const app = initializeApp(firebaseConfig);
1516
export const auth = getAuth(app);
1617
export const db = getFirestore(app);
18+
19+
export const googleProvider = new GoogleAuthProvider();
20+
googleProvider.setCustomParameters({
21+
prompt: "select_account",
22+
});
23+
1724
export default app;

0 commit comments

Comments
 (0)