mirror of
https://github.com/stackblitz-labs/bolt.diy
synced 2025-06-26 18:26:38 +00:00
Update Auth UX
This commit is contained in:
parent
250d5b0161
commit
ec98fc1c48
@ -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<User | null>(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 <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 whitespace-nowrap bg-bolt-elements-background-depth-1 border border-bolt-elements-borderColor rounded shadow-lg z-10">
|
||||
<div className="px-4 py-2 text-bolt-elements-textPrimary border-b border-bolt-elements-borderColor">
|
||||
{user.email}
|
||||
</div>
|
||||
<div className="px-4 py-2 text-bolt-elements-textPrimary border-b border-bolt-elements-borderColor">
|
||||
{`Peanuts used: ${usageData?.peanuts_used ?? '...'}`}
|
||||
</div>
|
||||
<div className="px-4 py-2 text-bolt-elements-textPrimary border-b border-bolt-elements-borderColor">
|
||||
{`Peanuts refunded: ${usageData?.peanuts_refunded ?? '...'}`}
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSignOut}
|
||||
className="block w-full text-left px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 font-bold"
|
||||
>
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setShowSignIn(true)}
|
||||
className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 font-bold"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
)}
|
||||
|
||||
{showSignIn && (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50"
|
||||
onClick={() => setShowSignIn(false)}
|
||||
>
|
||||
<div
|
||||
className="bg-bolt-elements-background-depth-1 p-6 rounded-lg max-w-md mx-auto"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 className="text-2xl font-bold mb-4 text-bolt-elements-textPrimary">Sign In</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGoogleSignIn}
|
||||
disabled={isSigningIn}
|
||||
className="w-full mb-4 p-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50"
|
||||
>
|
||||
{isSigningIn ? 'Processing...' : 'Use Google'}
|
||||
</button>
|
||||
<form onSubmit={handleSignIn}>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="email" className="block mb-1 text-bolt-elements-textPrimary">
|
||||
Email
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="password" className="block mb-1 text-bolt-elements-textPrimary">
|
||||
Password
|
||||
</label>
|
||||
<input
|
||||
id="password"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSigningIn}
|
||||
className="flex-1 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50"
|
||||
>
|
||||
{isSigningIn ? 'Processing...' : 'Sign In'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSignUp}
|
||||
disabled={isSigningIn}
|
||||
className="flex-1 py-2 bg-green-500 text-white rounded hover:bg-green-600 disabled:opacity-50"
|
||||
>
|
||||
{isSigningIn ? 'Processing...' : 'Sign Up'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
161
app/components/auth/ClientAuth/ClientAuth.tsx
Normal file
161
app/components/auth/ClientAuth/ClientAuth.tsx
Normal 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-red-50/10 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
121
app/components/auth/ClientAuth/SignInForm.tsx
Normal file
121
app/components/auth/ClientAuth/SignInForm.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
142
app/components/auth/ClientAuth/SignUpForm.tsx
Normal file
142
app/components/auth/ClientAuth/SignUpForm.tsx
Normal file
@ -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 (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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';
|
||||
|
||||
|
10
app/components/icons/google-icon.tsx
Normal file
10
app/components/icons/google-icon.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
export function GoogleIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18.171 8.368h-.67v-.035H10v3.333h4.709A4.998 4.998 0 0 1 5 10a5 5 0 0 1 5-5c1.275 0 2.434.48 3.317 1.266l2.357-2.357A8.295 8.295 0 0 0 10 1.667a8.334 8.334 0 1 0 8.171 6.7z" fill="#FFC107"/>
|
||||
<path d="M2.628 6.121l2.734 2.008a4.998 4.998 0 0 1 4.638-3.13c1.275 0 2.434.482 3.317 1.267l2.357-2.357A8.295 8.295 0 0 0 10 1.667 8.329 8.329 0 0 0 2.628 6.12z" fill="#FF3D00"/>
|
||||
<path d="M10 18.333a8.294 8.294 0 0 0 5.587-2.163l-2.579-2.183A4.963 4.963 0 0 1 10 15a4.998 4.998 0 0 1-4.701-3.311L2.58 13.783A8.327 8.327 0 0 0 10 18.333z" fill="#4CAF50"/>
|
||||
<path d="M18.171 8.368H17.5v-.034H10v3.333h4.71a5.017 5.017 0 0 1-1.703 2.321l2.58 2.182c-.182.166 2.746-2.003 2.746-6.17 0-.559-.057-1.104-.162-1.632z" fill="#1976D2"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
1
icons/google-icon.svg
Normal file
1
icons/google-icon.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><path fill="#FFC107" d="M43.611,20.083H42V20H24v8h11.303c-1.649,4.657-6.08,8-11.303,8c-6.627,0-12-5.373-12-12c0-6.627,5.373-12,12-12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C12.955,4,4,12.955,4,24c0,11.045,8.955,20,20,20c11.045,0,20-8.955,20-20C44,22.659,43.862,21.35,43.611,20.083z"/><path fill="#FF3D00" d="M6.306,14.691l6.571,4.819C14.655,15.108,18.961,12,24,12c3.059,0,5.842,1.154,7.961,3.039l5.657-5.657C34.046,6.053,29.268,4,24,4C16.318,4,9.656,8.337,6.306,14.691z"/><path fill="#4CAF50" d="M24,44c5.166,0,9.86-1.977,13.409-5.192l-6.19-5.238C29.211,35.091,26.715,36,24,36c-5.202,0-9.619-3.317-11.283-7.946l-6.522,5.025C9.505,39.556,16.227,44,24,44z"/><path fill="#1976D2" d="M43.611,20.083H42V20H24v8h11.303c-0.792,2.237-2.231,4.166-4.087,5.571c0.001-0.001,0.002-0.001,0.003-0.002l6.19,5.238C36.971,39.205,44,34,44,24C44,22.659,43.862,21.35,43.611,20.083z"/></svg>
|
After Width: | Height: | Size: 988 B |
Loading…
Reference in New Issue
Block a user