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

@@ -17,7 +17,7 @@ export default async function DatasetEntryPage({ params }: DatasetEntryPageProps
const result = await requireAdmin();
if (!result.authenticated) {
redirect("/user/sign-in");
redirect("/");
}
if (!result.admin) {

View File

@@ -22,7 +22,7 @@ export default async function DatasetPage({ params }: DatasetPageProps) {
const result = await requireAdmin();
if (!result.authenticated) {
redirect("/user/sign-in");
redirect("/");
}
if (!result.admin) {

View File

@@ -8,7 +8,7 @@ export default async function DatasetsAdminPage() {
const result = await requireAdmin();
if (!result.authenticated) {
redirect("/user/sign-in");
redirect("/");
}
if (!result.admin) {

View File

@@ -22,7 +22,7 @@ export default async function ExperimentPage({ params }: ExperimentPageProps) {
const result = await requireAdmin();
if (!result.authenticated) {
redirect("/user/sign-in");
redirect("/");
}
if (!result.admin) {

View File

@@ -18,7 +18,7 @@ export default async function ParticipantPage({ params }: ParticipantDetailPageP
const result = await requireAdmin();
if (!result.authenticated) {
redirect("/user/sign-in");
redirect("/");
}
if (!result.admin) {

View File

@@ -17,7 +17,7 @@ export default async function ParticipantsPage({ params }: ParticipantsPageProps
const result = await requireAdmin();
if (!result.authenticated) {
redirect("/user/sign-in");
redirect("/");
}
if (!result.admin) {

View File

@@ -8,7 +8,7 @@ export default async function ExperimentsAdminPage() {
const result = await requireAdmin();
if (!result.authenticated) {
redirect("/user/sign-in");
redirect("/");
}
if (!result.admin) {

View File

@@ -10,7 +10,7 @@ export default async function AdminPage() {
const result = await requireAdmin();
if (!result.authenticated) {
redirect("/user/sign-in");
redirect("/");
}
if (!result.admin) {

View File

@@ -8,7 +8,7 @@ export default async function UsersPage() {
const result = await requireAdmin();
if (!result.authenticated) {
redirect("/user/sign-in");
redirect("/");
}
if (!result.admin) {

View File

@@ -16,7 +16,7 @@ interface Props {
export default async function AnnotatePage({ params }: Props) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) redirect('/user/sign-in');
if (!session) redirect("/");
const { experimentId: experimentIdStr } = await params;
const experimentId = parseInt(experimentIdStr, 10);

View File

@@ -2,25 +2,18 @@ import Link from 'next/link';
import { headers } from 'next/headers';
import { auth } from '@/lib/auth';
import { getAllExperiments, getAnnotationProgress } from '@/app/actions/annotations';
import { AuthForm } from '@/components/AuthForm';
export default async function HomePage() {
const session = await auth.api.getSession({ headers: await headers() });
if (!session) {
return (
<div className="max-w-xl mx-auto text-center py-20">
<h1 className="text-3xl font-bold text-gray-800 mb-4">
Willkommen zur Dialektannotation
</h1>
<p className="text-gray-600 mb-8">
Bitte melden Sie sich an, um mit der Annotation zu beginnen.
<div className="max-w-xl mx-auto text-center px-4 mt-4">
<p className="text-gray-600 mb-4">
Für die Teilnahme an Annotationsstudien benötigst du ein Benutzerkonto. Mit diesem Konto speichern wir deinen Fortschritt, damit du jederzeit unterbrechen und später fortsetzen kannst. Deine Daten werden vertraulich behandelt und ausschließlich für die Zwecke dieser Studien verwendet. Wir werden dir niemals E-Mails ohne deine ausdrückliche Zustimmung senden.
</p>
<Link
href="/user/sign-in"
className="px-6 py-3 bg-blue-600 hover:bg-blue-700 text-white font-semibold rounded-lg transition-colors"
>
Anmelden
</Link>
<AuthForm />
</div>
);
}

View File

@@ -1,5 +0,0 @@
import { AuthForm } from '@/components/AuthForm';
export default function SignInPage() {
return <AuthForm />;
}

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); }} />;
}

View File

@@ -41,4 +41,16 @@ export async function requireAdmin() {
}
return { authenticated: true, admin: session.user.admin, session };
}
export async function requireAuthentication() {
const session = await auth.api.getSession({
headers: await headers()
});
if (!session) {
return { authenticated: false, session: null };
}
return { authenticated: true, session };
}