From ec98fc1c48c049b8648a16f2c1c7ff14c55f7d3c Mon Sep 17 00:00:00 2001 From: Strider Wilson Date: Tue, 27 May 2025 17:19:08 -0400 Subject: [PATCH] Update Auth UX --- app/components/auth/ClientAuth.tsx | 243 ------------------ app/components/auth/ClientAuth/ClientAuth.tsx | 161 ++++++++++++ app/components/auth/ClientAuth/SignInForm.tsx | 121 +++++++++ app/components/auth/ClientAuth/SignUpForm.tsx | 142 ++++++++++ app/components/header/Header.tsx | 2 +- app/components/icons/google-icon.tsx | 10 + icons/google-icon.svg | 1 + 7 files changed, 436 insertions(+), 244 deletions(-) delete mode 100644 app/components/auth/ClientAuth.tsx create mode 100644 app/components/auth/ClientAuth/ClientAuth.tsx create mode 100644 app/components/auth/ClientAuth/SignInForm.tsx create mode 100644 app/components/auth/ClientAuth/SignUpForm.tsx create mode 100644 app/components/icons/google-icon.tsx create mode 100644 icons/google-icon.svg diff --git a/app/components/auth/ClientAuth.tsx b/app/components/auth/ClientAuth.tsx deleted file mode 100644 index be8264a6..00000000 --- a/app/components/auth/ClientAuth.tsx +++ /dev/null @@ -1,243 +0,0 @@ -import { useState, useEffect } from 'react'; -import { toast } from 'react-toastify'; -import { getSupabase } from '~/lib/supabase/client'; -import type { AuthError, Session, User, AuthChangeEvent } from '@supabase/supabase-js'; - -export function ClientAuth() { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - const [showSignIn, setShowSignIn] = useState(false); - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [isSigningIn, setIsSigningIn] = useState(false); - const [showDropdown, setShowDropdown] = useState(false); - const [usageData, setUsageData] = useState<{ peanuts_used: number; peanuts_refunded: number } | null>(null); - - useEffect(() => { - async function getUser() { - try { - const { data } = await getSupabase().auth.getUser(); - setUser(data.user); - } catch (error) { - console.error('Error fetching user:', error); - } finally { - setLoading(false); - } - } - - getUser(); - - const { - data: { subscription }, - } = getSupabase().auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => { - setUser(session?.user ?? null); - }); - - return () => { - subscription.unsubscribe(); - }; - }, []); - - useEffect(() => { - async function updateUsageData() { - try { - const { data, error } = await getSupabase() - .from('profiles') - .select('peanuts_used, peanuts_refunded') - .eq('id', user?.id) - .single(); - - if (error) { - throw error; - } - - setUsageData(data); - } catch (error) { - console.error('Error fetching usage data:', error); - } - } - - if (showDropdown) { - updateUsageData(); - } - }, [showDropdown]); - - const handleSignIn = async (e: React.FormEvent) => { - e.preventDefault(); - setIsSigningIn(true); - - try { - const { error } = await getSupabase().auth.signInWithPassword({ email, password }); - - if (error) { - throw error; - } - - setShowSignIn(false); - toast.success('Successfully signed in!'); - } catch (error) { - const authError = error as AuthError; - toast.error(authError.message || 'Failed to sign in'); - } finally { - setIsSigningIn(false); - } - }; - - const handleSignUp = async (e: React.FormEvent) => { - e.preventDefault(); - setIsSigningIn(true); - - try { - const { error } = await getSupabase().auth.signUp({ email, password }); - - if (error) { - throw error; - } - - toast.success('Check your email for the confirmation link!'); - setShowSignIn(false); - } catch (error) { - const authError = error as AuthError; - toast.error(authError.message || 'Failed to sign up'); - } finally { - setIsSigningIn(false); - } - }; - - const handleSignOut = async () => { - await getSupabase().auth.signOut(); - setShowDropdown(false); - toast.success('Signed out successfully'); - }; - - const handleGoogleSignIn = async () => { - const { error } = await getSupabase().auth.signInWithOAuth({ - provider: 'google', - }); - console.log('GoogleSignIn', error); - }; - - if (loading) { - return
; - } - - // Avatar URLs are disabled due to broken links from CORS issues. - const useAvatarURL = false; - - return ( - <> - {user ? ( -
- - - {showDropdown && ( -
-
- {user.email} -
-
- {`Peanuts used: ${usageData?.peanuts_used ?? '...'}`} -
-
- {`Peanuts refunded: ${usageData?.peanuts_refunded ?? '...'}`} -
- -
- )} -
- ) : ( - - )} - - {showSignIn && ( -
setShowSignIn(false)} - > -
e.stopPropagation()} - > -

Sign In

- -
-
- - setEmail(e.target.value)} - className="w-full p-2 border rounded bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor" - required - /> -
-
- - setPassword(e.target.value)} - className="w-full p-2 border rounded bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor" - required - /> -
-
- - -
-
-
-
- )} - - ); -} diff --git a/app/components/auth/ClientAuth/ClientAuth.tsx b/app/components/auth/ClientAuth/ClientAuth.tsx new file mode 100644 index 00000000..142eb839 --- /dev/null +++ b/app/components/auth/ClientAuth/ClientAuth.tsx @@ -0,0 +1,161 @@ +import { useState, useEffect } from 'react'; +import { toast } from 'react-toastify'; +import { getSupabase } from '~/lib/supabase/client'; +import type { Session, User, AuthChangeEvent } from '@supabase/supabase-js'; +import { SignInForm } from './SignInForm'; +import { SignUpForm } from './SignUpForm'; + +export function ClientAuth() { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [showAuthModal, setShowAuthModal] = useState(false); + const [isSignUp, setIsSignUp] = useState(false); + const [showDropdown, setShowDropdown] = useState(false); + const [usageData, setUsageData] = useState<{ peanuts_used: number; peanuts_refunded: number } | null>(null); + + useEffect(() => { + async function getUser() { + try { + const { data } = await getSupabase().auth.getUser(); + setUser(data.user); + } catch (error) { + console.error('Error fetching user:', error); + } finally { + setLoading(false); + } + } + + getUser(); + + const { + data: { subscription }, + } = getSupabase().auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => { + setUser(session?.user ?? null); + if (session?.user) { + setShowAuthModal(false); + } + }); + + return () => { + subscription.unsubscribe(); + }; + }, []); + + useEffect(() => { + async function updateUsageData() { + try { + const { data, error } = await getSupabase() + .from('profiles') + .select('peanuts_used, peanuts_refunded') + .eq('id', user?.id) + .single(); + + if (error) { + throw error; + } + + setUsageData(data); + } catch (error) { + console.error('Error fetching usage data:', error); + } + } + + if (showDropdown) { + updateUsageData(); + } + }, [showDropdown]); + + const handleSignOut = async () => { + await getSupabase().auth.signOut(); + setShowDropdown(false); + toast.success('Signed out successfully'); + }; + + if (loading) { + return
; + } + + // Avatar URLs are disabled due to broken links from CORS issues. + const useAvatarURL = false; + + return ( + <> + {user ? ( +
+ + + {showDropdown && ( +
+
+
Signed in as
+
{user.email}
+
+
+
Usage
+
+
+ Peanuts used + {usageData?.peanuts_used ?? '...'} +
+
+ Peanuts refunded + {usageData?.peanuts_refunded ?? '...'} +
+
+
+
+ +
+
+ )} +
+ ) : ( + + )} + + {showAuthModal && ( +
setShowAuthModal(false)} + > +
e.stopPropagation()} + > + {isSignUp ? ( + setIsSignUp(false)} /> + ) : ( + setIsSignUp(true)} /> + )} +
+
+ )} + + ); +} \ No newline at end of file diff --git a/app/components/auth/ClientAuth/SignInForm.tsx b/app/components/auth/ClientAuth/SignInForm.tsx new file mode 100644 index 00000000..9bfc6e5e --- /dev/null +++ b/app/components/auth/ClientAuth/SignInForm.tsx @@ -0,0 +1,121 @@ +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { getSupabase } from '~/lib/supabase/client'; +import type { AuthError } from '@supabase/supabase-js'; +import { GoogleIcon } from '~/components/icons/google-icon'; + +interface SignInFormProps { + onToggleForm: () => void; +} + +export function SignInForm({ onToggleForm }: SignInFormProps) { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + + const handleSignIn = async (e: React.FormEvent) => { + e.preventDefault(); + setIsProcessing(true); + + try { + const { error } = await getSupabase().auth.signInWithPassword({ email, password }); + + if (error) { + throw error; + } + + toast.success('Successfully signed in!'); + } catch (error) { + const authError = error as AuthError; + toast.error(authError.message || 'Failed to sign in'); + } finally { + setIsProcessing(false); + } + }; + + const handleGoogleSignIn = async () => { + const { error } = await getSupabase().auth.signInWithOAuth({ + provider: 'google', + }); + if (error) { + toast.error(error.message || 'Failed to sign in with Google'); + } + }; + + return ( + <> +

+ Welcome Back +

+ + + +
+
+
+
+
+ + Or continue with email + +
+
+ +
+
+ + setEmail(e.target.value)} + className="w-full p-3 border rounded-lg bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor focus:ring-2 focus:ring-green-500 focus:border-transparent" + required + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full p-3 border rounded-lg bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor focus:ring-2 focus:ring-green-500 focus:border-transparent" + required + /> +
+ + +
+ +

+ Don't have an account?{' '} + +

+ + ); +} \ No newline at end of file diff --git a/app/components/auth/ClientAuth/SignUpForm.tsx b/app/components/auth/ClientAuth/SignUpForm.tsx new file mode 100644 index 00000000..9effebcb --- /dev/null +++ b/app/components/auth/ClientAuth/SignUpForm.tsx @@ -0,0 +1,142 @@ +import { useState } from 'react'; +import { toast } from 'react-toastify'; +import { getSupabase } from '~/lib/supabase/client'; +import type { AuthError } from '@supabase/supabase-js'; +import { GoogleIcon } from '~/components/icons/google-icon'; + +interface SignUpFormProps { + onToggleForm: () => void; +} + +export function SignUpForm({ onToggleForm }: SignUpFormProps) { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [isProcessing, setIsProcessing] = useState(false); + + const handleSignUp = async (e: React.FormEvent) => { + e.preventDefault(); + + if (password !== confirmPassword) { + toast.error('Passwords do not match'); + return; + } + + setIsProcessing(true); + + try { + const { error } = await getSupabase().auth.signUp({ email, password }); + + if (error) { + throw error; + } + + toast.success('Check your email for the confirmation link!'); + } catch (error) { + const authError = error as AuthError; + toast.error(authError.message || 'Failed to sign up'); + } finally { + setIsProcessing(false); + } + }; + + const handleGoogleSignIn = async () => { + const { error } = await getSupabase().auth.signInWithOAuth({ + provider: 'google', + }); + if (error) { + toast.error(error.message || 'Failed to sign in with Google'); + } + }; + + return ( + <> +

+ Create Account +

+ + + +
+
+
+
+
+ + Or continue with email + +
+
+ +
+
+ + setEmail(e.target.value)} + className="w-full p-3 border rounded-lg bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor focus:ring-2 focus:ring-green-500 focus:border-transparent" + required + /> +
+ +
+ + setPassword(e.target.value)} + className="w-full p-3 border rounded-lg bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor focus:ring-2 focus:ring-green-500 focus:border-transparent" + required + /> +
+ +
+ + setConfirmPassword(e.target.value)} + className="w-full p-3 border rounded-lg bg-bolt-elements-background-depth-2 text-bolt-elements-textPrimary border-bolt-elements-borderColor focus:ring-2 focus:ring-green-500 focus:border-transparent" + required + /> +
+ + +
+ +

+ Already have an account?{' '} + +

+ + ); +} \ No newline at end of file diff --git a/app/components/header/Header.tsx b/app/components/header/Header.tsx index e557588c..3538bec5 100644 --- a/app/components/header/Header.tsx +++ b/app/components/header/Header.tsx @@ -6,7 +6,7 @@ import { HeaderActionButtons } from './HeaderActionButtons.client'; import { ChatDescription } from '~/lib/persistence/ChatDescription.client'; import { Feedback } from './Feedback'; import { Suspense } from 'react'; -import { ClientAuth } from '~/components/auth/ClientAuth'; +import { ClientAuth } from '~/components/auth/ClientAuth/ClientAuth'; import { DeployChatButton } from './DeployChatButton'; import { DownloadButton } from './DownloadButton'; diff --git a/app/components/icons/google-icon.tsx b/app/components/icons/google-icon.tsx new file mode 100644 index 00000000..7acdbd9f --- /dev/null +++ b/app/components/icons/google-icon.tsx @@ -0,0 +1,10 @@ +export function GoogleIcon() { + return ( + + + + + + + ); +} \ No newline at end of file diff --git a/icons/google-icon.svg b/icons/google-icon.svg new file mode 100644 index 00000000..c0669b38 --- /dev/null +++ b/icons/google-icon.svg @@ -0,0 +1 @@ + \ No newline at end of file