Revert "Revert "Pro 1364 ux and codebase improvements""

This commit is contained in:
Strider
2025-05-30 11:56:06 -04:00
committed by GitHub
parent 776e4802bd
commit 4331b476b9
34 changed files with 1402 additions and 926 deletions

View File

@@ -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<User | null>(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 <div className="w-8 h-8 rounded-full bg-gray-300 animate-pulse" />;
}
// Avatar URLs are disabled due to broken links from CORS issues.
const useAvatarURL = false;
return (
<>
{user ? (
<div className="relative">
<button
className="flex items-center justify-center w-8 h-8 rounded-full bg-green-500 text-white"
onClick={() => setShowDropdown(!showDropdown)}
>
{useAvatarURL && user.user_metadata?.avatar_url ? (
<img
src={user.user_metadata.avatar_url}
alt="User avatar"
className="w-full h-full rounded-full object-cover"
/>
) : (
<span>{user.email?.substring(0, 2).toUpperCase()}</span>
)}
</button>
{showDropdown && (
<div className="absolute right-0 mt-2 py-2 w-64 bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded-lg shadow-lg z-10">
<div className="px-4 py-3 text-bolt-elements-textPrimary border-b border-bolt-elements-borderColor">
<div className="text-sm text-bolt-elements-textSecondary">Signed in as</div>
<div className="font-medium truncate">{user.email}</div>
</div>
<div className="px-4 py-3 text-bolt-elements-textPrimary border-b border-bolt-elements-borderColor">
<div className="text-sm text-bolt-elements-textSecondary">Usage</div>
<div className="space-y-1">
<div className="flex justify-between items-center">
<span>Peanuts used</span>
<span className="font-medium">{usageData?.peanuts_used ?? '...'}</span>
</div>
<div className="flex justify-between items-center">
<span>Peanuts refunded</span>
<span className="font-medium">{usageData?.peanuts_refunded ?? '...'}</span>
</div>
</div>
</div>
<div className="px-2 pt-2">
<button
onClick={handleSignOut}
className="w-full px-4 py-2 text-left bg-green-500 text-white hover:bg-green-600 rounded-md transition-colors flex items-center justify-center"
>
Sign Out
</button>
</div>
</div>
)}
</div>
) : (
<button
onClick={() => {
setShowAuthModal(true);
setIsSignUp(false);
}}
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 font-bold"
>
Sign In
</button>
)}
{showAuthModal && (
<div
className="fixed inset-0 bg-black bg-opacity-50 backdrop-blur-sm flex justify-center items-center z-50"
onClick={() => setShowAuthModal(false)}
>
<div
className="bg-bolt-elements-background-depth-1 p-8 rounded-lg w-full max-w-md mx-auto border border-bolt-elements-borderColor"
onClick={(e) => e.stopPropagation()}
>
{isSignUp ? (
<SignUpForm onToggleForm={() => setIsSignUp(false)} />
) : (
<SignInForm onToggleForm={() => setIsSignUp(true)} />
)}
</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,116 @@
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 (
<>
<h2 className="text-2xl font-bold mb-6 text-bolt-elements-textPrimary text-center">Welcome Back</h2>
<button
type="button"
onClick={handleGoogleSignIn}
disabled={isProcessing}
className="w-full mb-6 p-3 flex items-center justify-center gap-2 bg-white text-gray-800 rounded-lg hover:bg-gray-100 disabled:opacity-50 border border-gray-300"
>
<GoogleIcon />
<span>{isProcessing ? 'Processing...' : 'Continue with Google'}</span>
</button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-bolt-elements-borderColor"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-bolt-elements-background-depth-1 text-bolt-elements-textSecondary">
Or continue with email
</span>
</div>
</div>
<form onSubmit={handleSignIn}>
<div className="mb-4">
<label htmlFor="email" className="block mb-2 text-sm font-medium text-bolt-elements-textPrimary">
Email Address
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => 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
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="block mb-2 text-sm font-medium text-bolt-elements-textPrimary">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => 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
/>
</div>
<button
type="submit"
disabled={isProcessing}
className="w-full py-3 bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:opacity-50 font-medium"
>
{isProcessing ? 'Processing...' : 'Sign In'}
</button>
</form>
<p className="mt-6 text-center text-bolt-elements-textSecondary">
Don't have an account?{' '}
<button onClick={onToggleForm} className="text-green-500 hover:text-green-600 font-medium bg-transparent">
Sign Up
</button>
</p>
</>
);
}

View File

@@ -0,0 +1,137 @@
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 (
<>
<h2 className="text-2xl font-bold mb-6 text-bolt-elements-textPrimary text-center">Create Account</h2>
<button
type="button"
onClick={handleGoogleSignIn}
disabled={isProcessing}
className="w-full mb-6 p-3 flex items-center justify-center gap-2 bg-white text-gray-800 rounded-lg hover:bg-gray-100 disabled:opacity-50 border border-gray-300"
>
<GoogleIcon />
<span>{isProcessing ? 'Processing...' : 'Continue with Google'}</span>
</button>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-bolt-elements-borderColor"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-2 bg-bolt-elements-background-depth-1 text-bolt-elements-textSecondary">
Or continue with email
</span>
</div>
</div>
<form onSubmit={handleSignUp}>
<div className="mb-4">
<label htmlFor="email" className="block mb-2 text-sm font-medium text-bolt-elements-textPrimary">
Email Address
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => 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
/>
</div>
<div className="mb-4">
<label htmlFor="password" className="block mb-2 text-sm font-medium text-bolt-elements-textPrimary">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => 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
/>
</div>
<div className="mb-6">
<label htmlFor="confirmPassword" className="block mb-2 text-sm font-medium text-bolt-elements-textPrimary">
Confirm Password
</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => 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
/>
</div>
<button
type="submit"
disabled={isProcessing}
className="w-full py-3 bg-green-500 text-white rounded-lg hover:bg-green-600 disabled:opacity-50 font-medium"
>
{isProcessing ? 'Processing...' : 'Create Account'}
</button>
</form>
<p className="mt-6 text-center text-bolt-elements-textSecondary">
Already have an account?{' '}
<button onClick={onToggleForm} className="text-green-500 hover:text-green-600 font-medium bg-transparent">
Sign In
</button>
</p>
</>
);
}