mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-08-26 14:19:24 +00:00
email signup
This commit is contained in:
parent
a07f2cbe05
commit
24cabb7070
@ -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 */}
|
||||||
|
@ -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">
|
||||||
|
@ -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,7 +314,351 @@ export default function LoginCompact() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Buttons */}
|
{/* Email/Password Form */}
|
||||||
|
{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' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
{/* GitHub */}
|
{/* GitHub */}
|
||||||
<button
|
<button
|
||||||
@ -318,6 +771,7 @@ export default function LoginCompact() {
|
|||||||
LinkedIn
|
LinkedIn
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div style={{
|
<div style={{
|
||||||
|
301
frontend/src/routes/Signup.tsx
Normal file
301
frontend/src/routes/Signup.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user