email signup

This commit is contained in:
Anthony Stirling 2025-08-18 10:22:21 +01:00
parent a07f2cbe05
commit 24cabb7070
4 changed files with 992 additions and 3 deletions

View File

@ -6,6 +6,7 @@ import { FilesModalProvider } from './contexts/FilesModalContext';
import { AuthProvider } from './lib/useSession'; import { AuthProvider } from './lib/useSession';
import HomePage from './pages/HomePage'; import HomePage from './pages/HomePage';
import LoginCompact from './routes/LoginCompact'; import LoginCompact from './routes/LoginCompact';
import Signup from './routes/Signup';
import AuthCallback from './routes/AuthCallback'; import AuthCallback from './routes/AuthCallback';
import AuthDebug from './routes/AuthDebug'; import AuthDebug from './routes/AuthDebug';
@ -22,6 +23,7 @@ export default function App() {
<Routes> <Routes>
<Route path="/" element={<HomePage />} /> <Route path="/" element={<HomePage />} />
<Route path="/login" element={<LoginCompact />} /> <Route path="/login" element={<LoginCompact />} />
<Route path="/signup" element={<Signup />} />
<Route path="/auth/callback" element={<AuthCallback />} /> <Route path="/auth/callback" element={<AuthCallback />} />
<Route path="/debug" element={<AuthDebug />} /> <Route path="/debug" element={<AuthDebug />} />
{/* Catch-all route - redirects unknown paths to home */} {/* Catch-all route - redirects unknown paths to home */}

View File

@ -14,6 +14,11 @@ export default function AuthDebug() {
const [apiResponse, setApiResponse] = useState<any>(null) const [apiResponse, setApiResponse] = useState<any>(null)
const [isTestingApi, setIsTestingApi] = useState(false) const [isTestingApi, setIsTestingApi] = useState(false)
// Admin functions state
const [inviteEmail, setInviteEmail] = useState('')
const [newEmail, setNewEmail] = useState('')
const [isProcessingAdmin, setIsProcessingAdmin] = useState(false)
const runAuthTests = async () => { const runAuthTests = async () => {
setIsTestingAuth(true) setIsTestingAuth(true)
setTestResults(null) setTestResults(null)
@ -203,6 +208,94 @@ export default function AuthDebug() {
} }
} }
const inviteUser = async () => {
if (!inviteEmail || !session) {
alert('Please enter an email and ensure you are signed in')
return
}
// Show information about service role requirement
const proceed = confirm(
`⚠️ Admin Invite requires SERVICE ROLE permissions.\n\n` +
`This will likely fail unless you:\n` +
`1. Have a service role key configured\n` +
`2. Are using RLS bypass\n` +
`3. Have admin privileges\n\n` +
`Alternative: Use the Magic Link feature instead.\n\n` +
`Continue anyway?`
)
if (!proceed) return
try {
setIsProcessingAdmin(true)
console.log('[Admin Debug] Inviting user:', inviteEmail)
// Note: This requires admin/service role permissions
const { data, error } = await supabase.auth.admin.inviteUserByEmail(
inviteEmail.trim(),
{ redirectTo: `${window.location.origin}/welcome` }
)
if (error) {
console.error('[Admin Debug] Invite error:', error)
// Provide helpful error message
let errorMsg = error.message
if (error.message.includes('Bearer token') || error.message.includes('service role')) {
errorMsg = `❌ Service Role Required\n\n` +
`The invite function requires a service role key, not a user JWT.\n\n` +
`Solutions:\n` +
`• Use Magic Link instead (works with user permissions)\n` +
`• Configure service role in backend\n` +
`• Use Supabase Dashboard → Authentication → Users → Invite\n\n` +
`Original error: ${error.message}`
}
alert(errorMsg)
} else {
console.log('[Admin Debug] Invite successful:', data)
alert(`✅ Invitation sent to ${inviteEmail}!`)
setInviteEmail('')
}
} catch (err) {
console.error('[Admin Debug] Invite unexpected error:', err)
alert(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`)
} finally {
setIsProcessingAdmin(false)
}
}
const changeEmail = async () => {
if (!newEmail || !session) {
alert('Please enter a new email and ensure you are signed in')
return
}
try {
setIsProcessingAdmin(true)
console.log('[Admin Debug] Changing email to:', newEmail)
const { data, error } = await supabase.auth.updateUser({
email: newEmail.trim()
})
if (error) {
console.error('[Admin Debug] Email change error:', error)
alert(`Failed to change email: ${error.message}`)
} else {
console.log('[Admin Debug] Email change initiated:', data)
alert(`Email change confirmation sent to both ${user?.email} and ${newEmail}. Check both inboxes for confirmation links.`)
setNewEmail('')
}
} catch (err) {
console.error('[Admin Debug] Email change unexpected error:', err)
alert(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`)
} finally {
setIsProcessingAdmin(false)
}
}
return ( return (
<div className="min-h-screen bg-gray-50 py-8"> <div className="min-h-screen bg-gray-50 py-8">
<div className="max-w-4xl mx-auto px-4 space-y-8"> <div className="max-w-4xl mx-auto px-4 space-y-8">
@ -539,6 +632,145 @@ export default function AuthDebug() {
</div> </div>
)} )}
{/* Admin Functions */}
{session && (
<div className="bg-white rounded-lg shadow-md p-6">
<h2 className="text-xl font-semibold text-gray-900 mb-4 flex items-center">
👑 Admin Functions
</h2>
<p className="text-gray-600 mb-6 text-sm">
Test admin-level authentication features (requires appropriate permissions)
</p>
<div className="space-y-6">
{/* Invite User */}
<div className="p-4 bg-blue-50 border border-blue-200 rounded-md">
<h3 className="text-lg font-medium text-blue-900 mb-3">📨 Invite User</h3>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-blue-800 mb-1">
Email Address to Invite:
</label>
<input
type="email"
value={inviteEmail}
onChange={(e) => setInviteEmail(e.target.value)}
placeholder="user@example.com"
className="w-full px-3 py-2 border border-blue-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex gap-2">
<button
onClick={inviteUser}
disabled={isProcessingAdmin || !inviteEmail}
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isProcessingAdmin ? 'Sending Invite...' : 'Try Admin Invite'}
</button>
<button
onClick={() => {
if (inviteEmail) {
supabase.auth.signInWithOtp({
email: inviteEmail.trim(),
options: { emailRedirectTo: `${window.location.origin}/auth/callback` }
}).then(({ error }) => {
if (error) alert(`Error: ${error.message}`)
else {
alert(`✅ Magic link sent to ${inviteEmail}!\n\nThey can use this to create an account and sign in.`)
setInviteEmail('')
}
})
}
}}
disabled={!inviteEmail}
className="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Send Magic Link Instead
</button>
</div>
<div className="text-xs text-blue-700 space-y-1">
<div><strong> Admin Invite:</strong> Requires service role key (will likely fail from frontend)</div>
<div><strong> Magic Link:</strong> Works with user permissions, allows account creation</div>
<div><strong>Alternative:</strong> Use Supabase Dashboard Authentication Users Invite</div>
</div>
</div>
</div>
{/* Change Email */}
<div className="p-4 bg-orange-50 border border-orange-200 rounded-md">
<h3 className="text-lg font-medium text-orange-900 mb-3">📧 Change Email Address</h3>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-orange-800 mb-1">
New Email Address:
</label>
<input
type="email"
value={newEmail}
onChange={(e) => setNewEmail(e.target.value)}
placeholder="new-email@example.com"
className="w-full px-3 py-2 border border-orange-300 rounded-md focus:outline-none focus:ring-2 focus:ring-orange-500"
/>
</div>
<button
onClick={changeEmail}
disabled={isProcessingAdmin || !newEmail}
className="px-4 py-2 bg-orange-600 text-white rounded hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isProcessingAdmin ? 'Processing...' : 'Change Email'}
</button>
<div className="text-xs text-orange-700 space-y-1">
<div><strong>Current Email:</strong> {user?.email}</div>
<div><strong>Note:</strong> Sends confirmation emails to both old and new addresses.</div>
</div>
</div>
</div>
{/* Quick Admin Actions */}
<div className="p-4 bg-purple-50 border border-purple-200 rounded-md">
<h3 className="text-lg font-medium text-purple-900 mb-3"> Quick Actions</h3>
<div className="flex flex-wrap gap-3">
<button
onClick={() => {
const email = prompt('Enter email address for magic link:')
if (email) {
supabase.auth.signInWithOtp({
email: email.trim(),
options: { emailRedirectTo: `${window.location.origin}/auth/callback` }
}).then(({ error }) => {
if (error) alert(`Error: ${error.message}`)
else alert(`Magic link sent to ${email}!`)
})
}
}}
className="px-3 py-2 bg-purple-600 text-white text-sm rounded hover:bg-purple-700"
>
🪄 Send Magic Link
</button>
<button
onClick={() => {
const email = prompt('Enter email address for password reset:')
if (email) {
supabase.auth.resetPasswordForEmail(
email.trim(),
{ redirectTo: `${window.location.origin}/auth/reset` }
).then(({ error }) => {
if (error) alert(`Error: ${error.message}`)
else alert(`Password reset sent to ${email}!`)
})
}
}}
className="px-3 py-2 bg-purple-600 text-white text-sm rounded hover:bg-purple-700"
>
🔑 Reset Password
</button>
</div>
</div>
</div>
</div>
)}
{/* Test Results */} {/* Test Results */}
{testResults && ( {testResults && (
<div className="bg-white rounded-lg shadow-md p-6"> <div className="bg-white rounded-lg shadow-md p-6">

View File

@ -8,6 +8,13 @@ export default function LoginCompact() {
const { session, user, loading, signOut } = useAuth() const { session, user, loading, signOut } = useAuth()
const [isSigningIn, setIsSigningIn] = useState(false) const [isSigningIn, setIsSigningIn] = useState(false)
const [error, setError] = useState<string | null>(null) const [error, setError] = useState<string | null>(null)
const [showEmailForm, setShowEmailForm] = useState(false)
const [showMagicLink, setShowMagicLink] = useState(false)
const [showPasswordReset, setShowPasswordReset] = useState(false)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [magicLinkEmail, setMagicLinkEmail] = useState('')
const [resetEmail, setResetEmail] = useState('')
// Show logged in state if authenticated // Show logged in state if authenticated
if (session && !loading) { if (session && !loading) {
@ -130,6 +137,108 @@ export default function LoginCompact() {
} }
} }
const signInWithEmail = async () => {
if (!email || !password) {
setError('Please enter both email and password')
return
}
try {
setIsSigningIn(true)
setError(null)
console.log('[LoginCompact] Signing in with email:', email)
const { data, error } = await supabase.auth.signInWithPassword({
email: email.trim(),
password: password
})
if (error) {
console.error('[LoginCompact] Email sign in error:', error)
setError(error.message)
} else if (data.user) {
console.log('[LoginCompact] Email sign in successful')
// User will be redirected by the auth state change
}
} catch (err) {
console.error('[LoginCompact] Unexpected error:', err)
setError(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`)
} finally {
setIsSigningIn(false)
}
}
const signInWithMagicLink = async () => {
if (!magicLinkEmail) {
setError('Please enter your email address')
return
}
try {
setIsSigningIn(true)
setError(null)
console.log('[LoginCompact] Sending magic link to:', magicLinkEmail)
const { error } = await supabase.auth.signInWithOtp({
email: magicLinkEmail.trim(),
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`
}
})
if (error) {
console.error('[LoginCompact] Magic link error:', error)
setError(error.message)
} else {
setError(null)
alert(`Magic link sent to ${magicLinkEmail}! Check your email and click the link to sign in.`)
setMagicLinkEmail('')
setShowMagicLink(false)
}
} catch (err) {
console.error('[LoginCompact] Unexpected error:', err)
setError(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`)
} finally {
setIsSigningIn(false)
}
}
const resetPassword = async () => {
if (!resetEmail) {
setError('Please enter your email address')
return
}
try {
setIsSigningIn(true)
setError(null)
console.log('[LoginCompact] Sending password reset to:', resetEmail)
const { error } = await supabase.auth.resetPasswordForEmail(
resetEmail.trim(),
{ redirectTo: `${window.location.origin}/auth/reset` }
)
if (error) {
console.error('[LoginCompact] Password reset error:', error)
setError(error.message)
} else {
setError(null)
alert(`Password reset link sent to ${resetEmail}! Check your email and follow the instructions.`)
setResetEmail('')
setShowPasswordReset(false)
}
} catch (err) {
console.error('[LoginCompact] Unexpected error:', err)
setError(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`)
} finally {
setIsSigningIn(false)
}
}
if (loading) { if (loading) {
return ( return (
<div style={{ <div style={{
@ -205,8 +314,352 @@ export default function LoginCompact() {
</div> </div>
)} )}
{/* Buttons */} {/* Email/Password Form */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}> {showEmailForm ? (
<div style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<input
type="email"
placeholder="Email address"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !isSigningIn && signInWithEmail()}
style={{
width: '100%',
padding: '12px 16px',
border: '1px solid #d1d5db',
borderRadius: '8px',
fontSize: '14px',
backgroundColor: '#ffffff',
boxSizing: 'border-box'
}}
/>
<input
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !isSigningIn && signInWithEmail()}
style={{
width: '100%',
padding: '12px 16px',
border: '1px solid #d1d5db',
borderRadius: '8px',
fontSize: '14px',
backgroundColor: '#ffffff',
boxSizing: 'border-box'
}}
/>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={signInWithEmail}
disabled={isSigningIn || !email || !password}
style={{
flex: '1',
padding: '12px 16px',
border: 'none',
borderRadius: '8px',
backgroundColor: '#059669',
color: '#ffffff',
fontSize: '14px',
fontWeight: '600',
cursor: isSigningIn || !email || !password ? 'not-allowed' : 'pointer',
opacity: isSigningIn || !email || !password ? 0.6 : 1,
}}
>
{isSigningIn ? 'Signing In...' : 'Sign In'}
</button>
<button
onClick={() => {
setShowEmailForm(false)
setEmail('')
setPassword('')
setError(null)
}}
disabled={isSigningIn}
style={{
padding: '12px 16px',
border: '1px solid #d1d5db',
borderRadius: '8px',
backgroundColor: '#ffffff',
color: '#374151',
fontSize: '14px',
cursor: isSigningIn ? 'not-allowed' : 'pointer',
opacity: isSigningIn ? 0.6 : 1,
}}
>
Cancel
</button>
</div>
<div style={{ textAlign: 'center' }}>
<button
onClick={() => navigate('/signup')}
disabled={isSigningIn}
style={{
background: 'none',
border: 'none',
color: '#3b82f6',
fontSize: '13px',
cursor: isSigningIn ? 'not-allowed' : 'pointer',
textDecoration: 'underline',
opacity: isSigningIn ? 0.6 : 1,
}}
>
Don't have an account? Sign up
</button>
</div>
</div>
</div>
) : showMagicLink ? (
/* Magic Link Form */
<div style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<input
type="email"
placeholder="Enter your email address"
value={magicLinkEmail}
onChange={(e) => setMagicLinkEmail(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !isSigningIn && signInWithMagicLink()}
style={{
width: '100%',
padding: '12px 16px',
border: '1px solid #d1d5db',
borderRadius: '8px',
fontSize: '14px',
backgroundColor: '#ffffff',
boxSizing: 'border-box'
}}
/>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={signInWithMagicLink}
disabled={isSigningIn || !magicLinkEmail}
style={{
flex: '1',
padding: '12px 16px',
border: 'none',
borderRadius: '8px',
backgroundColor: '#7c3aed',
color: '#ffffff',
fontSize: '14px',
fontWeight: '600',
cursor: isSigningIn || !magicLinkEmail ? 'not-allowed' : 'pointer',
opacity: isSigningIn || !magicLinkEmail ? 0.6 : 1,
}}
>
{isSigningIn ? 'Sending...' : 'Send Magic Link'}
</button>
<button
onClick={() => {
setShowMagicLink(false)
setMagicLinkEmail('')
setError(null)
}}
disabled={isSigningIn}
style={{
padding: '12px 16px',
border: '1px solid #d1d5db',
borderRadius: '8px',
backgroundColor: '#ffffff',
color: '#374151',
fontSize: '14px',
cursor: isSigningIn ? 'not-allowed' : 'pointer',
opacity: isSigningIn ? 0.6 : 1,
}}
>
Cancel
</button>
</div>
<div style={{ textAlign: 'center', fontSize: '12px', color: '#6b7280' }}>
We'll send you a secure link to sign in without a password
</div>
</div>
</div>
) : showPasswordReset ? (
/* Password Reset Form */
<div style={{ marginBottom: '20px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<input
type="email"
placeholder="Enter your email address"
value={resetEmail}
onChange={(e) => setResetEmail(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !isSigningIn && resetPassword()}
style={{
width: '100%',
padding: '12px 16px',
border: '1px solid #d1d5db',
borderRadius: '8px',
fontSize: '14px',
backgroundColor: '#ffffff',
boxSizing: 'border-box'
}}
/>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={resetPassword}
disabled={isSigningIn || !resetEmail}
style={{
flex: '1',
padding: '12px 16px',
border: 'none',
borderRadius: '8px',
backgroundColor: '#dc2626',
color: '#ffffff',
fontSize: '14px',
fontWeight: '600',
cursor: isSigningIn || !resetEmail ? 'not-allowed' : 'pointer',
opacity: isSigningIn || !resetEmail ? 0.6 : 1,
}}
>
{isSigningIn ? 'Sending...' : 'Reset Password'}
</button>
<button
onClick={() => {
setShowPasswordReset(false)
setResetEmail('')
setError(null)
}}
disabled={isSigningIn}
style={{
padding: '12px 16px',
border: '1px solid #d1d5db',
borderRadius: '8px',
backgroundColor: '#ffffff',
color: '#374151',
fontSize: '14px',
cursor: isSigningIn ? 'not-allowed' : 'pointer',
opacity: isSigningIn ? 0.6 : 1,
}}
>
Cancel
</button>
</div>
<div style={{ textAlign: 'center', fontSize: '12px', color: '#6b7280' }}>
We'll send you instructions to reset your password
</div>
</div>
</div>
) : (
<>
{/* Auth Method Toggles */}
<div style={{ marginBottom: '16px', display: 'flex', flexDirection: 'column', gap: '8px' }}>
<button
onClick={() => {
setShowEmailForm(true)
setShowMagicLink(false)
setShowPasswordReset(false)
setError(null)
}}
disabled={isSigningIn}
style={{
width: '100%',
padding: '10px 16px',
border: '2px solid #059669',
borderRadius: '8px',
backgroundColor: '#f0fdf4',
fontSize: '13px',
fontWeight: '600',
color: '#059669',
cursor: isSigningIn ? 'not-allowed' : 'pointer',
opacity: isSigningIn ? 0.6 : 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px'
}}
>
📧 Email & Password
</button>
<div style={{ display: 'flex', gap: '8px' }}>
<button
onClick={() => {
setShowMagicLink(true)
setShowEmailForm(false)
setShowPasswordReset(false)
setError(null)
}}
disabled={isSigningIn}
style={{
flex: '1',
padding: '10px 16px',
border: '2px solid #7c3aed',
borderRadius: '8px',
backgroundColor: '#faf5ff',
fontSize: '13px',
fontWeight: '600',
color: '#7c3aed',
cursor: isSigningIn ? 'not-allowed' : 'pointer',
opacity: isSigningIn ? 0.6 : 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px'
}}
>
🪄 Magic Link
</button>
<button
onClick={() => {
setShowPasswordReset(true)
setShowEmailForm(false)
setShowMagicLink(false)
setError(null)
}}
disabled={isSigningIn}
style={{
flex: '1',
padding: '10px 16px',
border: '2px solid #dc2626',
borderRadius: '8px',
backgroundColor: '#fef2f2',
fontSize: '13px',
fontWeight: '600',
color: '#dc2626',
cursor: isSigningIn ? 'not-allowed' : 'pointer',
opacity: isSigningIn ? 0.6 : 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '6px'
}}
>
🔑 Reset
</button>
</div>
</div>
{/* Separator */}
<div style={{
position: 'relative',
margin: '16px 0',
textAlign: 'center'
}}>
<div style={{
position: 'absolute',
top: '50%',
left: '0',
right: '0',
height: '1px',
backgroundColor: '#e5e7eb'
}} />
<span style={{
backgroundColor: '#ffffff',
color: '#6b7280',
fontSize: '12px',
padding: '0 12px'
}}>
or continue with
</span>
</div>
</>
)}
{/* OAuth Buttons Container */}
{!showEmailForm && (
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
{/* GitHub */} {/* GitHub */}
<button <button
onClick={() => signInWithProvider('github')} onClick={() => signInWithProvider('github')}
@ -317,7 +770,8 @@ export default function LoginCompact() {
</svg> </svg>
LinkedIn LinkedIn
</button> </button>
</div> </div>
)}
{/* Footer */} {/* Footer */}
<div style={{ <div style={{

View File

@ -0,0 +1,301 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { supabase } from '../lib/supabase'
export default function Signup() {
const navigate = useNavigate()
const [isSigningUp, setIsSigningUp] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [confirmPassword, setConfirmPassword] = useState('')
const validateForm = () => {
if (!email || !password || !confirmPassword) {
setError('Please fill in all fields')
return false
}
if (password !== confirmPassword) {
setError('Passwords do not match')
return false
}
if (password.length < 6) {
setError('Password must be at least 6 characters long')
return false
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(email)) {
setError('Please enter a valid email address')
return false
}
return true
}
const signUp = async () => {
if (!validateForm()) return
try {
setIsSigningUp(true)
setError(null)
setSuccess(null)
console.log('[Signup] Creating account for:', email)
const { data, error } = await supabase.auth.signUp({
email: email.trim(),
password: password,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`
}
})
if (error) {
console.error('[Signup] Sign up error:', error)
setError(error.message)
} else if (data.user) {
console.log('[Signup] Sign up successful:', data.user)
// Check if email confirmation is required
if (data.user && !data.session) {
setSuccess('Check your email for a confirmation link to complete your registration.')
} else {
setSuccess('Account created successfully! You can now sign in.')
setTimeout(() => navigate('/login'), 2000)
}
}
} catch (err) {
console.error('[Signup] Unexpected error:', err)
setError(`Unexpected error: ${err instanceof Error ? err.message : 'Unknown error'}`)
} finally {
setIsSigningUp(false)
}
}
return (
<div style={{
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f3f4f6',
padding: '16px',
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'
}}>
<div style={{
maxWidth: '400px',
width: '100%',
backgroundColor: '#ffffff',
borderRadius: '16px',
boxShadow: '0 10px 25px rgba(0, 0, 0, 0.1)',
padding: '32px'
}}>
{/* Header */}
<div style={{ textAlign: 'center', marginBottom: '32px' }}>
<div style={{ fontSize: '40px', marginBottom: '12px' }}>🚀</div>
<h1 style={{
fontSize: '24px',
fontWeight: '600',
color: '#1f2937',
marginBottom: '8px',
margin: '0'
}}>
Create Account
</h1>
<p style={{
color: '#6b7280',
fontSize: '14px',
margin: '0'
}}>
Join Stirling PDF to get started
</p>
</div>
{/* Success Message */}
{success && (
<div style={{
padding: '16px',
backgroundColor: '#f0fdf4',
border: '1px solid #bbf7d0',
borderRadius: '8px',
marginBottom: '24px'
}}>
<p style={{
color: '#059669',
fontSize: '14px',
margin: '0'
}}>
{success}
</p>
</div>
)}
{/* Error */}
{error && (
<div style={{
padding: '16px',
backgroundColor: '#fef2f2',
border: '1px solid #fecaca',
borderRadius: '8px',
marginBottom: '24px'
}}>
<p style={{
color: '#dc2626',
fontSize: '14px',
margin: '0'
}}>
{error}
</p>
</div>
)}
{/* Form */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '16px', marginBottom: '24px' }}>
<div>
<label style={{
display: 'block',
fontSize: '14px',
fontWeight: '500',
color: '#374151',
marginBottom: '6px'
}}>
Email Address
</label>
<input
type="email"
placeholder="your@email.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !isSigningUp && signUp()}
style={{
width: '100%',
padding: '12px 16px',
border: '1px solid #d1d5db',
borderRadius: '8px',
fontSize: '14px',
backgroundColor: '#ffffff',
boxSizing: 'border-box'
}}
/>
</div>
<div>
<label style={{
display: 'block',
fontSize: '14px',
fontWeight: '500',
color: '#374151',
marginBottom: '6px'
}}>
Password
</label>
<input
type="password"
placeholder="Minimum 6 characters"
value={password}
onChange={(e) => setPassword(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !isSigningUp && signUp()}
style={{
width: '100%',
padding: '12px 16px',
border: '1px solid #d1d5db',
borderRadius: '8px',
fontSize: '14px',
backgroundColor: '#ffffff',
boxSizing: 'border-box'
}}
/>
</div>
<div>
<label style={{
display: 'block',
fontSize: '14px',
fontWeight: '500',
color: '#374151',
marginBottom: '6px'
}}>
Confirm Password
</label>
<input
type="password"
placeholder="Re-enter your password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && !isSigningUp && signUp()}
style={{
width: '100%',
padding: '12px 16px',
border: '1px solid #d1d5db',
borderRadius: '8px',
fontSize: '14px',
backgroundColor: '#ffffff',
boxSizing: 'border-box'
}}
/>
</div>
</div>
{/* Sign Up Button */}
<button
onClick={signUp}
disabled={isSigningUp || !email || !password || !confirmPassword}
style={{
width: '100%',
padding: '14px 16px',
border: 'none',
borderRadius: '8px',
backgroundColor: '#059669',
color: '#ffffff',
fontSize: '16px',
fontWeight: '600',
cursor: isSigningUp || !email || !password || !confirmPassword ? 'not-allowed' : 'pointer',
opacity: isSigningUp || !email || !password || !confirmPassword ? 0.6 : 1,
marginBottom: '20px'
}}
>
{isSigningUp ? 'Creating Account...' : 'Create Account'}
</button>
{/* Sign In Link */}
<div style={{ textAlign: 'center' }}>
<button
onClick={() => navigate('/login')}
disabled={isSigningUp}
style={{
background: 'none',
border: 'none',
color: '#3b82f6',
fontSize: '14px',
cursor: isSigningUp ? 'not-allowed' : 'pointer',
textDecoration: 'underline',
opacity: isSigningUp ? 0.6 : 1,
}}
>
Already have an account? Sign in
</button>
</div>
{/* Footer */}
<div style={{
textAlign: 'center',
marginTop: '24px',
paddingTop: '20px',
borderTop: '1px solid #e5e7eb'
}}>
<p style={{
color: '#9ca3af',
fontSize: '12px',
margin: '0'
}}>
By creating an account, you agree to our terms of service
</p>
</div>
</div>
</div>
)
}