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(); const result = await requireAdmin();
if (!result.authenticated) { if (!result.authenticated) {
redirect("/user/sign-in"); redirect("/");
} }
if (!result.admin) { if (!result.admin) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ interface Props {
export default async function AnnotatePage({ params }: Props) { export default async function AnnotatePage({ params }: Props) {
const session = await auth.api.getSession({ headers: await headers() }); 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: experimentIdStr } = await params;
const experimentId = parseInt(experimentIdStr, 10); const experimentId = parseInt(experimentIdStr, 10);

View File

@@ -2,25 +2,18 @@ import Link from 'next/link';
import { headers } from 'next/headers'; import { headers } from 'next/headers';
import { auth } from '@/lib/auth'; import { auth } from '@/lib/auth';
import { getAllExperiments, getAnnotationProgress } from '@/app/actions/annotations'; import { getAllExperiments, getAnnotationProgress } from '@/app/actions/annotations';
import { AuthForm } from '@/components/AuthForm';
export default async function HomePage() { export default async function HomePage() {
const session = await auth.api.getSession({ headers: await headers() }); const session = await auth.api.getSession({ headers: await headers() });
if (!session) { if (!session) {
return ( return (
<div className="max-w-xl mx-auto text-center py-20"> <div className="max-w-xl mx-auto text-center px-4 mt-4">
<h1 className="text-3xl font-bold text-gray-800 mb-4"> <p className="text-gray-600 mb-4">
Willkommen zur Dialektannotation 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.
</h1>
<p className="text-gray-600 mb-8">
Bitte melden Sie sich an, um mit der Annotation zu beginnen.
</p> </p>
<Link <AuthForm />
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>
</div> </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 ( return (
<div className="flex items-center justify-center py-8 px-4"> <div className="flex items-center justify-center">
<div className="w-full max-w-md"> <div className="w-full">
{/* Card */} {/* Card */}
<div className="bg-white rounded-lg shadow-lg overflow-hidden border border-gray-200"> <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 */} {/* Tabs */}
<div className="flex border-b border-gray-200"> <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"> <p className="text-center text-sm text-gray-600 mt-6">
{mode === 'signin' ? ( {mode === 'signin' ? (
<> <>
Sie haben noch kein Konto?{' '} Du hast noch kein Konto?{' '}
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
@@ -214,7 +211,7 @@ export function AuthForm() {
</> </>
) : ( ) : (
<> <>
Sie haben bereits ein Konto?{' '} Du hast bereits ein Konto?{' '}
<button <button
type="button" type="button"
onClick={() => { onClick={() => {

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import { useEffect, useState } from 'react';
import { getOnboardingAnswers } from '@/app/actions/onboarding'; import { getOnboardingAnswers } from '@/app/actions/onboarding';
import OnboardingInfoView from './OnboardingInfoView'; import OnboardingInfoView from './OnboardingInfoView';
import OnboardingFormView from './OnboardingFormView'; import OnboardingFormView from './OnboardingFormView';
import { authClient } from '@/lib/auth-client';
const scrollToTop = () => { const scrollToTop = () => {
window.scrollTo({ window.scrollTo({
@@ -21,6 +22,9 @@ export default function OnboardingPhase({ experimentId }: OnboardingPhaseProps)
const [hasExistingAnswers, setHasExistingAnswers] = useState(false); const [hasExistingAnswers, setHasExistingAnswers] = useState(false);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [userEmail, setUserEmail] = useState<string>('');
useEffect(() => { useEffect(() => {
const load = async () => { const load = async () => {
try { try {
@@ -32,6 +36,11 @@ export default function OnboardingPhase({ experimentId }: OnboardingPhaseProps)
setHasExistingAnswers(true); 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) { } catch (err) {
console.error(err); console.error(err);
} finally { } 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 }; 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 };
} }