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)
This commit is contained in:
2025-12-26 22:52:47 +01:00
parent 73a057b1e3
commit 02bd025bce
2 changed files with 89 additions and 4 deletions

View File

@@ -2,13 +2,15 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { createClient } from '@/lib/supabase/client'; import { createClient } from '@/lib/supabase/client';
import { LogIn, UserPlus, Mail, Lock, Loader2, AlertCircle } from 'lucide-react'; import { LogIn, UserPlus, Mail, Lock, Loader2, AlertCircle, User, AtSign } from 'lucide-react';
export default function AuthForm() { export default function AuthForm() {
const supabase = createClient(); const supabase = createClient();
const [isLogin, setIsLogin] = useState(true); const [isLogin, setIsLogin] = useState(true);
const [email, setEmail] = useState(''); const [email, setEmail] = useState('');
const [password, setPassword] = useState(''); const [password, setPassword] = useState('');
const [username, setUsername] = useState('');
const [fullName, setFullName] = useState('');
const [rememberMe, setRememberMe] = useState(true); const [rememberMe, setRememberMe] = useState(true);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@@ -30,21 +32,67 @@ export default function AuthForm() {
if (error) throw error; if (error) throw error;
// If remember-me is checked, session will persist (default Supabase behavior) // If remember-me is checked, session will persist (default Supabase behavior)
// If not checked, clear session on browser close via localStorage flag
if (!rememberMe) { if (!rememberMe) {
sessionStorage.setItem('dramlog_session_only', 'true'); sessionStorage.setItem('dramlog_session_only', 'true');
} else { } else {
sessionStorage.removeItem('dramlog_session_only'); sessionStorage.removeItem('dramlog_session_only');
} }
} else { } else {
const { error } = await supabase.auth.signUp({ // 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, email,
password, password,
options: { options: {
emailRedirectTo: `${window.location.origin}/auth/callback`, emailRedirectTo: `${window.location.origin}/auth/callback`,
data: {
username: username.toLowerCase(),
full_name: fullName.trim() || undefined,
}
} }
}); });
if (error) throw error; 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!'); setMessage('Checke deine E-Mails, um dein Konto zu bestätigen!');
} }
} catch (err) { } catch (err) {
@@ -71,6 +119,43 @@ export default function AuthForm() {
</div> </div>
<form onSubmit={handleSubmit} className="space-y-4"> <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"> <div className="space-y-2">
<label className="text-[10px] font-black uppercase tracking-widest text-zinc-400 ml-1">E-Mail</label> <label className="text-[10px] font-black uppercase tracking-widest text-zinc-400 ml-1">E-Mail</label>
<div className="relative"> <div className="relative">

File diff suppressed because one or more lines are too long