feat: reworked registration/sign-in, auth is now on homescreen and includes an explanation text. Reworked onboarding to only include a participation checkbox for the raffle.

This commit is contained in:
averel10
2026-04-10 08:29:45 +02:00
parent 17af4da939
commit fe279f649c
17 changed files with 109 additions and 108 deletions

View File

@@ -57,13 +57,10 @@ export function AuthForm() {
};
return (
<div className="flex items-center justify-center py-8 px-4">
<div className="w-full max-w-md">
<div className="flex items-center justify-center">
<div className="w-full">
{/* Card */}
<div className="bg-white rounded-lg shadow-lg overflow-hidden border border-gray-200">
{/* Header */}
<div className="bg-gradient-to-r from-blue-600 to-blue-700 px-6 py-8">
</div>
{/* Tabs */}
<div className="flex border-b border-gray-200">
@@ -200,7 +197,7 @@ export function AuthForm() {
<p className="text-center text-sm text-gray-600 mt-6">
{mode === 'signin' ? (
<>
Sie haben noch kein Konto?{' '}
Du hast noch kein Konto?{' '}
<button
type="button"
onClick={() => {
@@ -214,7 +211,7 @@ export function AuthForm() {
</>
) : (
<>
Sie haben bereits ein Konto?{' '}
Du hast bereits ein Konto?{' '}
<button
type="button"
onClick={() => {

View File

@@ -25,68 +25,65 @@ export function Header() {
<div className="flex justify-between items-center py-5">
{/* Logo */}
<Link href="/" className="font-bold hover:text-blue-100 transition-colors text-lg sm:text-2xl">
<span className="hidden sm:inline">TTS Dialektannotations-Plattform</span>
<span className="sm:hidden">Dialektannotation</span>
<span className="hidden sm:inline">TTS Dialektannotation</span>
<span className="sm:hidden">TTS Dialektannotation</span>
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-6">
{loading ? (
<span className="text-sm text-blue-100">Laden...</span>
) : session ? (
<div className="flex items-center gap-6">
<div className="text-right">
<p className="text-xs text-blue-100 uppercase tracking-wide">Willkommen zurück</p>
<p className="text-sm font-semibold">{session.user?.email}</p>
</div>
{(session.user as any)?.admin && (
<Link
href="/admin"
className="px-4 py-2 bg-yellow-500 hover:bg-yellow-600 active:bg-yellow-700 text-white font-semibold rounded-lg transition-all duration-200 transform hover:scale-105"
{(loading || session) && (
<nav className="hidden md:flex items-center gap-6">
{loading ? (
<span className="text-sm text-blue-100">Laden...</span>
) : session && (
<div className="flex items-center gap-6">
<div className="text-right">
<p className="text-xs text-blue-100 uppercase tracking-wide">Willkommen zurück</p>
<p className="text-sm font-semibold">{session.user?.email}</p>
</div>
{(session.user as any)?.admin && (
<Link
href="/admin"
className="px-4 py-2 bg-yellow-500 hover:bg-yellow-600 active:bg-yellow-700 text-white font-semibold rounded-lg transition-all duration-200 transform hover:scale-105"
>
Admin
</Link>
)}
<button
onClick={handleSignOut}
className="px-4 py-2 bg-red-600 hover:bg-red-700 active:bg-red-800 text-white font-semibold rounded-lg transition-all duration-200 transform hover:scale-105"
>
Admin
</Link>
)}
<button
onClick={handleSignOut}
className="px-4 py-2 bg-red-600 hover:bg-red-700 active:bg-red-800 text-white font-semibold rounded-lg transition-all duration-200 transform hover:scale-105"
>
Abmelden
</button>
</div>
) : (
<Link
href="/user/sign-in"
className="px-5 py-2 bg-green-500 hover:bg-green-600 active:bg-green-700 text-white font-semibold rounded-lg transition-all duration-200 transform hover:scale-105"
>
Anmelden
</Link>
)}
</nav>
Abmelden
</button>
</div>
)}
</nav>
)}
{/* Mobile Menu Button */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden p-2"
title={mobileMenuOpen ? 'Menü schließen' : 'Menü öffnen'}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
{(loading || session) && (
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="md:hidden p-2"
title={mobileMenuOpen ? 'Menü schließen' : 'Menü öffnen'}
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{mobileMenuOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
)}
</svg>
</button>
)}
</div>
</div>
{/* Mobile Fullscreen Menu */}
{mobileMenuOpen && (
{mobileMenuOpen && session && (
<div className="md:hidden fixed inset-0 top-16 bg-blue-700 z-40 flex flex-col p-4 space-y-4">
{loading ? (
<span className="text-sm text-blue-100">Laden...</span>
) : session ? (
) : (
<>
<div className="text-center py-4 border-b border-blue-600">
<p className="text-xs text-blue-100 uppercase tracking-wide">Willkommen zurück</p>
@@ -111,14 +108,6 @@ export function Header() {
Abmelden
</button>
</>
) : (
<Link
href="/user/sign-in"
onClick={() => setMobileMenuOpen(false)}
className="px-4 py-3 bg-green-500 hover:bg-green-600 text-white font-semibold rounded-lg transition-colors text-center"
>
Anmelden
</Link>
)}
</div>
)}

View File

@@ -8,6 +8,7 @@ import { DIALECT_LABELS_WITHOUT_DE } from '@/lib/dialects';
interface OnboardingFormViewProps {
experimentId: number;
onBack: () => void;
userEmail: string;
}
interface ResidenceEntry {
@@ -17,7 +18,7 @@ interface ResidenceEntry {
interface FormData {
age?: string;
email?: string;
participateInRaffle?: boolean;
residences?: ResidenceEntry[];
dialectQualities?: Record<string, string>;
listeningExperience?: string;
@@ -26,7 +27,7 @@ interface FormData {
type Step = 1 | 2 | 3;
export default function OnboardingFormView({ experimentId, onBack }: OnboardingFormViewProps) {
export default function OnboardingFormView({ experimentId, onBack, userEmail }: OnboardingFormViewProps) {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const [currentStep, setCurrentStep] = useState<Step>(1);
@@ -37,7 +38,7 @@ export default function OnboardingFormView({ experimentId, onBack }: OnboardingF
const [formData, setFormData] = useState<FormData>({
age: '',
email: '',
participateInRaffle: false,
residences: [{ region: '', years: '' }],
dialectQualities: initializeDialectQualities()
});
@@ -50,7 +51,7 @@ export default function OnboardingFormView({ experimentId, onBack }: OnboardingF
});
}, [currentStep]);
const handleChange = (field: keyof FormData, value: string) => {
const handleChange = (field: keyof FormData, value: string | boolean) => {
setFormData((prev) => ({
...prev,
[field]: value,
@@ -223,18 +224,23 @@ export default function OnboardingFormView({ experimentId, onBack }: OnboardingF
</div>
<div className="border-t border-gray-200 pt-6">
<label htmlFor="email" className="block text-s font-medium text-gray-900 mb-1">
E-Mail-Adresse (optional)
</label>
<p className="text-sm text-gray-500 mb-2">Wird nur für die Gutschein-Verlosung verwendet.</p>
<input
type="email"
id="email"
value={formData.email}
onChange={(e) => handleChange('email', e.target.value)}
placeholder="beispiel@email.ch"
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<div className="flex items-start gap-3">
<input
type="checkbox"
id="participateInRaffle"
checked={formData.participateInRaffle ?? false}
onChange={(e) => handleChange('participateInRaffle', e.target.checked ? true : false)}
className="mt-1 w-5 h-5 cursor-pointer accent-blue-600"
/>
<div className="flex-1">
<label htmlFor="participateInRaffle" className="block text-s font-medium text-gray-900 mb-2 cursor-pointer">
Ich möchte an der Gutscheinverlosung teilnehmen
</label>
<p className="text-sm text-gray-600 mb-2">
Wir werden deine E-Mail-Adresse<strong>{userEmail && ` (${userEmail})`}</strong> verwenden, um dich im Falle eines Gewinns zu kontaktieren.
</p>
</div>
</div>
</div>
</div>
)}

View File

@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import { getOnboardingAnswers } from '@/app/actions/onboarding';
import OnboardingInfoView from './OnboardingInfoView';
import OnboardingFormView from './OnboardingFormView';
import { authClient } from '@/lib/auth-client';
const scrollToTop = () => {
window.scrollTo({
@@ -21,6 +22,9 @@ export default function OnboardingPhase({ experimentId }: OnboardingPhaseProps)
const [hasExistingAnswers, setHasExistingAnswers] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [userEmail, setUserEmail] = useState<string>('');
useEffect(() => {
const load = async () => {
try {
@@ -32,6 +36,11 @@ export default function OnboardingPhase({ experimentId }: OnboardingPhaseProps)
setHasExistingAnswers(true);
}
}
// Get user email for raffle participation
const session = await authClient.getSession();
if (session && session.data && session.data.user && session.data.user.email) {
setUserEmail(session.data.user.email);
}
} catch (err) {
console.error(err);
} finally {
@@ -69,5 +78,5 @@ export default function OnboardingPhase({ experimentId }: OnboardingPhaseProps)
}
return <OnboardingFormView experimentId={experimentId} onBack={() => { scrollToTop(); setShowInfoPage(true); }} />;
return <OnboardingFormView experimentId={experimentId} userEmail={userEmail} onBack={() => { scrollToTop(); setShowInfoPage(true); }} />;
}