Spaces:
Paused
Paused
| 'use client' | |
| import { useState } from 'react' | |
| import { useRouter } from 'next/navigation' | |
| import Link from 'next/link' | |
| import { createClient } from '@/lib/supabase/client' | |
| import { Mail, Lock, User, Code2, Loader2 } from 'lucide-react' | |
| interface AuthFormProps { | |
| mode: 'login' | 'signup' | |
| } | |
| export function AuthForm({ mode }: AuthFormProps) { | |
| const router = useRouter() | |
| const supabase = createClient() | |
| const [loading, setLoading] = useState(false) | |
| const [error, setError] = useState<string | null>(null) | |
| const [formData, setFormData] = useState({ | |
| email: '', | |
| password: '', | |
| fullName: '', | |
| }) | |
| const isSignup = mode === 'signup' | |
| const handleSubmit = async (e: React.FormEvent) => { | |
| e.preventDefault() | |
| setLoading(true) | |
| setError(null) | |
| try { | |
| if (isSignup) { | |
| const { error } = await supabase.auth.signUp({ | |
| email: formData.email, | |
| password: formData.password, | |
| options: { | |
| data: { | |
| full_name: formData.fullName, | |
| }, | |
| emailRedirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback`, | |
| }, | |
| }) | |
| if (error) throw error | |
| } else { | |
| const { error } = await supabase.auth.signInWithPassword({ | |
| email: formData.email, | |
| password: formData.password, | |
| }) | |
| if (error) throw error | |
| router.push('/dashboard') | |
| router.refresh() | |
| } | |
| } catch (err: any) { | |
| setError(err.message || 'An error occurred') | |
| } finally { | |
| setLoading(false) | |
| } | |
| } | |
| const handleOAuthLogin = async (provider: 'google' | 'github') => { | |
| try { | |
| const { error } = await supabase.auth.signInWithOAuth({ | |
| provider, | |
| options: { | |
| redirectTo: `${process.env.NEXT_PUBLIC_APP_URL}/auth/callback`, | |
| }, | |
| }) | |
| if (error) throw error | |
| } catch (err: any) { | |
| setError(err.message) | |
| } | |
| } | |
| return ( | |
| <div className="bg-dark-900/50 p-6 rounded-2xl border border-dark-700"> | |
| <form onSubmit={handleSubmit} className="space-y-4"> | |
| {error && ( | |
| <div className="p-3 rounded-lg bg-red-500/10 border border-red-500/20 text-red-400 text-sm"> | |
| {error} | |
| </div> | |
| )} | |
| {isSignup && ( | |
| <div className="relative"> | |
| <User className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-dark-500" /> | |
| <input | |
| type="text" | |
| placeholder="Full name" | |
| value={formData.fullName} | |
| onChange={(e) => setFormData({ ...formData, fullName: e.target.value })} | |
| className="w-full pl-10 pr-4 py-3 rounded-xl bg-dark-800 border border-dark-600 focus:border-primary-500 focus:outline-none transition-colors" | |
| required | |
| /> | |
| </div> | |
| )} | |
| <div className="relative"> | |
| <Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-dark-500" /> | |
| <input | |
| type="email" | |
| placeholder="Email address" | |
| value={formData.email} | |
| onChange={(e) => setFormData({ ...formData, email: e.target.value })} | |
| className="w-full pl-10 pr-4 py-3 rounded-xl bg-dark-800 border border-dark-600 focus:border-primary-500 focus:outline-none transition-colors" | |
| required | |
| /> | |
| </div> | |
| <div className="relative"> | |
| <Lock className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-dark-500" /> | |
| <input | |
| type="password" | |
| placeholder="Password" | |
| value={formData.password} | |
| onChange={(e) => setFormData({ ...formData, password: e.target.value })} | |
| className="w-full pl-10 pr-4 py-3 rounded-xl bg-dark-800 border border-dark-600 focus:border-primary-500 focus:outline-none transition-colors" | |
| required | |
| minLength={6} | |
| /> | |
| </div> | |
| <button | |
| type="submit" | |
| disabled={loading} | |
| className="w-full py-3 rounded-xl bg-primary-600 hover:bg-primary-500 disabled:opacity-50 disabled:cursor-not-allowed font-semibold flex items-center justify-center gap-2 transition-colors" | |
| > | |
| {loading ? ( | |
| <Loader2 className="w-5 h-5 animate-spin" /> | |
| ) : isSignup ? ( | |
| 'Create Account' | |
| ) : ( | |
| 'Sign In' | |
| )} | |
| </button> | |
| </form> | |
| <div className="mt-6"> | |
| <div className="relative"> | |
| <div className="absolute inset-0 flex items-center"> | |
| <div className="w-full border-t border-dark-600" /> | |
| </div> | |
| <div className="relative flex justify-center text-sm"> | |
| <span className="px-4 bg-dark-900/50 text-dark-400">Or continue with</span> | |
| </div> | |
| </div> | |
| <div className="mt-6 grid grid-cols-2 gap-3"> | |
| <button | |
| onClick={() => handleOAuthLogin('google')} | |
| className="flex items-center justify-center gap-2 py-2.5 rounded-xl bg-dark-800 hover:bg-dark-700 border border-dark-600 transition-colors" | |
| > | |
| <svg className="w-5 h-5" viewBox="0 0 24 24"> | |
| <path | |
| fill="currentColor" | |
| d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" | |
| /> | |
| <path | |
| fill="currentColor" | |
| d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" | |
| /> | |
| <path | |
| fill="currentColor" | |
| d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" | |
| /> | |
| <path | |
| fill="currentColor" | |
| d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" | |
| /> | |
| </svg> | |
| </button> | |
| <button | |
| onClick={() => handleOAuthLogin('github')} | |
| className="flex items-center justify-center gap-2 py-2.5 rounded-xl bg-dark-800 hover:bg-dark-700 border border-dark-600 transition-colors" | |
| > | |
| <Code2 className="w-5 h-5" /> | |
| GitHub | |
| </button> | |
| </div> | |
| </div> | |
| {isSignup && ( | |
| <p className="mt-4 text-xs text-dark-500 text-center"> | |
| By signing up, you agree to our{' '} | |
| <Link href="/terms" className="text-primary-400 hover:underline">Terms</Link> | |
| {' '}and{' '} | |
| <Link href="/privacy" className="text-primary-400 hover:underline">Privacy Policy</Link> | |
| </p> | |
| )} | |
| {/* Dev Mode: Skip Auth */} | |
| {process.env.NODE_ENV === 'development' && ( | |
| <> | |
| <div className="mt-6 relative"> | |
| <div className="absolute inset-0 flex items-center"> | |
| <div className="w-full border-t border-yellow-500/20" /> | |
| </div> | |
| <div className="relative flex justify-center text-sm"> | |
| <span className="px-4 bg-dark-900/50 text-yellow-500/70">⚡ Dev Mode</span> | |
| </div> | |
| </div> | |
| <button | |
| type="button" | |
| onClick={async () => { | |
| setLoading(true) | |
| setError(null) | |
| try { | |
| const devUser = JSON.stringify({ | |
| id: 'dev-user-' + Date.now(), | |
| email: 'dev@codeview.local', | |
| full_name: 'Dev User', | |
| }) | |
| // Set cookie so middleware can see it | |
| document.cookie = `dev_user=${encodeURIComponent(devUser)}; path=/; max-age=86400; SameSite=Lax` | |
| localStorage.setItem('dev_user', devUser) | |
| router.push('/dashboard') | |
| router.refresh() | |
| } catch (err: any) { | |
| setError(err.message || 'Failed to skip auth') | |
| } finally { | |
| setLoading(false) | |
| } | |
| }} | |
| disabled={loading} | |
| className="mt-4 w-full py-3 rounded-xl bg-yellow-500/10 hover:bg-yellow-500/20 border border-yellow-500/30 text-yellow-400 font-semibold flex items-center justify-center gap-2 transition-colors disabled:opacity-50 disabled:cursor-not-allowed" | |
| > | |
| {loading ? ( | |
| <Loader2 className="w-5 h-5 animate-spin" /> | |
| ) : ( | |
| <> | |
| <span>🚀</span> Continue as Guest (Dev) | |
| </> | |
| )} | |
| </button> | |
| </> | |
| )} | |
| </div> | |
| ) | |
| } | |