Files
Dramlog-Prod/src/components/AuthForm.tsx
robin 02bd025bce feat: Enhanced registration with username, name, and auto-subscription
Registration form now includes:
- Username field (required, unique, validated)
- Full name field (optional)
- Auto-validates username format and availability
- Auto-creates 'starter' subscription on signup

Login form unchanged (email + password only)
2025-12-26 22:52:47 +01:00

252 lines
12 KiB
TypeScript

'use client';
import React, { useState } from 'react';
import { createClient } from '@/lib/supabase/client';
import { LogIn, UserPlus, Mail, Lock, Loader2, AlertCircle, User, AtSign } from 'lucide-react';
export default function AuthForm() {
const supabase = createClient();
const [isLogin, setIsLogin] = useState(true);
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [username, setUsername] = useState('');
const [fullName, setFullName] = useState('');
const [rememberMe, setRememberMe] = useState(true);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [message, setMessage] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
setMessage(null);
try {
if (isLogin) {
// Set session persistence based on remember-me
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
// If remember-me is checked, session will persist (default Supabase behavior)
if (!rememberMe) {
sessionStorage.setItem('dramlog_session_only', 'true');
} else {
sessionStorage.removeItem('dramlog_session_only');
}
} else {
// Validate username
if (!username.trim()) {
throw new Error('Bitte gib einen Benutzernamen ein.');
}
if (username.length < 3) {
throw new Error('Benutzername muss mindestens 3 Zeichen haben.');
}
if (!/^[a-zA-Z0-9_]+$/.test(username)) {
throw new Error('Benutzername darf nur Buchstaben, Zahlen und Unterstriche enthalten.');
}
// Check if username is already taken
const { data: existingUser } = await supabase
.from('profiles')
.select('id')
.eq('username', username.toLowerCase())
.single();
if (existingUser) {
throw new Error('Dieser Benutzername ist bereits vergeben.');
}
// Create user with metadata
const { data: signUpData, error } = await supabase.auth.signUp({
email,
password,
options: {
emailRedirectTo: `${window.location.origin}/auth/callback`,
data: {
username: username.toLowerCase(),
full_name: fullName.trim() || undefined,
}
}
});
if (error) throw error;
// After successful signup, create subscription with starter plan
if (signUpData.user) {
// Get starter plan ID
const { data: starterPlan } = await supabase
.from('subscription_plans')
.select('id')
.eq('name', 'starter')
.single();
if (starterPlan) {
await supabase
.from('user_subscriptions')
.upsert({
user_id: signUpData.user.id,
plan_id: starterPlan.id,
}, { onConflict: 'user_id' });
}
}
setMessage('Checke deine E-Mails, um dein Konto zu bestätigen!');
}
} catch (err) {
setError(err instanceof Error ? err.message : 'Ein Fehler ist aufgetreten');
} finally {
setLoading(false);
}
};
return (
<div className="w-full max-w-md p-8 bg-zinc-900 rounded-3xl shadow-2xl border border-zinc-800">
<div className="flex flex-col items-center mb-8">
<div className="w-16 h-16 bg-orange-950/30 rounded-2xl flex items-center justify-center mb-4 border border-orange-900/20">
{isLogin ? <LogIn className="text-orange-600" size={32} /> : <UserPlus className="text-orange-600" size={32} />}
</div>
<h2 className="text-3xl font-black text-white tracking-tight">
{isLogin ? 'Willkommen zurück' : 'Vault erstellen'}
</h2>
<p className="text-zinc-400 mt-2 text-center text-sm font-medium">
{isLogin
? 'Logge dich ein, um auf deine Sammlung zuzugreifen.'
: 'Starte heute mit deinem digitalen Whisky-Vault.'}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Registration-only fields */}
{!isLogin && (
<>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-zinc-400 ml-1">Benutzername</label>
<div className="relative">
<AtSign className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={18} />
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, ''))}
placeholder="dein_username"
required
maxLength={20}
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl focus:ring-1 focus:ring-orange-600 focus:border-transparent outline-none transition-all text-white placeholder:text-zinc-600"
/>
</div>
<p className="text-[10px] text-zinc-600 ml-1">Nur Kleinbuchstaben, Zahlen und _</p>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-zinc-400 ml-1">Name <span className="text-zinc-600">(optional)</span></label>
<div className="relative">
<User className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={18} />
<input
type="text"
value={fullName}
onChange={(e) => setFullName(e.target.value)}
placeholder="Max Mustermann"
maxLength={50}
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl focus:ring-1 focus:ring-orange-600 focus:border-transparent outline-none transition-all text-white placeholder:text-zinc-600"
/>
</div>
</div>
</>
)}
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-zinc-400 ml-1">E-Mail</label>
<div className="relative">
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={18} />
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="name@beispiel.de"
required
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl focus:ring-1 focus:ring-orange-600 focus:border-transparent outline-none transition-all text-white placeholder:text-zinc-600"
/>
</div>
</div>
<div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-zinc-400 ml-1">Passwort</label>
<div className="relative">
<Lock className="absolute left-3 top-1/2 -translate-y-1/2 text-zinc-400" size={18} />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
className="w-full pl-10 pr-4 py-3 bg-zinc-950 border border-zinc-800 rounded-xl focus:ring-1 focus:ring-orange-600 focus:border-transparent outline-none transition-all text-white placeholder:text-zinc-600"
/>
</div>
</div>
{/* Remember Me Checkbox - only show for login */}
{isLogin && (
<label className="flex items-center gap-3 cursor-pointer group">
<div className="relative">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => setRememberMe(e.target.checked)}
className="sr-only peer"
/>
<div className="w-5 h-5 bg-zinc-950 border border-zinc-700 rounded-md peer-checked:bg-orange-600 peer-checked:border-orange-600 transition-all" />
<svg
className="absolute inset-0 w-5 h-5 text-white opacity-0 peer-checked:opacity-100 transition-opacity"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</div>
<span className="text-sm text-zinc-400 group-hover:text-zinc-300 transition-colors">
Angemeldet bleiben
</span>
</label>
)}
{error && (
<div className="flex items-center gap-2 p-3 bg-red-900/10 text-red-500 text-xs rounded-lg border border-red-900/20">
<AlertCircle size={16} />
{error}
</div>
)}
{message && (
<div className="p-3 bg-green-900/10 text-green-500 text-xs rounded-lg border border-green-900/20">
{message}
</div>
)}
<button
type="submit"
disabled={loading}
className="w-full py-4 bg-orange-600 hover:bg-orange-700 text-white font-black uppercase tracking-widest text-xs rounded-xl shadow-lg shadow-orange-950/40 transition-all active:scale-[0.98] disabled:opacity-50 flex items-center justify-center gap-2"
>
{loading ? <Loader2 className="animate-spin" size={20} /> : (isLogin ? 'Einloggen' : 'Konto erstellen')}
</button>
</form>
<div className="mt-6 text-center">
<button
type="button"
onClick={() => setIsLogin(!isLogin)}
className="text-xs font-black uppercase tracking-widest text-orange-600 hover:text-orange-500 transition-colors"
>
{isLogin ? 'Noch kein Konto? Registrieren' : 'Bereits ein Konto? Einloggen'}
</button>
</div>
</div>
);
}