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)
252 lines
12 KiB
TypeScript
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>
|
|
);
|
|
}
|