feat: added new onboarding step (Dialektzugehörigkeit)
This commit is contained in:
@@ -12,7 +12,7 @@ interface OnboardingFormViewProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ResidenceEntry {
|
interface ResidenceEntry {
|
||||||
region: string;
|
canton: string;
|
||||||
years: string;
|
years: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,12 +20,37 @@ interface FormData {
|
|||||||
age?: string;
|
age?: string;
|
||||||
participateInRaffle?: boolean;
|
participateInRaffle?: boolean;
|
||||||
residences?: ResidenceEntry[];
|
residences?: ResidenceEntry[];
|
||||||
|
ownDialectRegion?: string;
|
||||||
dialectQualities?: Record<string, string>;
|
dialectQualities?: Record<string, string>;
|
||||||
listeningExperience?: string;
|
listeningExperience?: string;
|
||||||
additionalNotes?: string;
|
additionalNotes?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Step = 1 | 2 | 3;
|
type Step = 1 | 2 | 3 | 4;
|
||||||
|
|
||||||
|
const AVAILABLE_CANTONS = {
|
||||||
|
ag: 'Aargau',
|
||||||
|
ar: 'Appenzell Ausserrhoden',
|
||||||
|
ai: 'Appenzell Innerrhoden',
|
||||||
|
bl: 'Basel-Landschaft',
|
||||||
|
bs: 'Basel-Stadt',
|
||||||
|
be: 'Bern',
|
||||||
|
gl: 'Glarus',
|
||||||
|
gr: 'Graubünden',
|
||||||
|
lu: 'Luzern',
|
||||||
|
nw: 'Nidwalden',
|
||||||
|
ow: 'Obwalden',
|
||||||
|
sh: 'Schaffhausen',
|
||||||
|
sz: 'Schwyz',
|
||||||
|
so: 'Solothurn',
|
||||||
|
sg: 'St. Gallen',
|
||||||
|
tg: 'Thurgau',
|
||||||
|
ur: 'Uri',
|
||||||
|
vs: 'Wallis',
|
||||||
|
zg: 'Zug',
|
||||||
|
zh: 'Zürich',
|
||||||
|
other: 'Anderer Kanton/Ausland'
|
||||||
|
};
|
||||||
|
|
||||||
export default function OnboardingFormView({ experimentId, onBack, userEmail }: OnboardingFormViewProps) {
|
export default function OnboardingFormView({ experimentId, onBack, userEmail }: OnboardingFormViewProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -39,7 +64,8 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
const [formData, setFormData] = useState<FormData>({
|
const [formData, setFormData] = useState<FormData>({
|
||||||
age: '',
|
age: '',
|
||||||
participateInRaffle: false,
|
participateInRaffle: false,
|
||||||
residences: [{ region: '', years: '' }],
|
residences: [{ canton: '', years: '' }],
|
||||||
|
ownDialectRegion: '',
|
||||||
dialectQualities: initializeDialectQualities()
|
dialectQualities: initializeDialectQualities()
|
||||||
});
|
});
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
@@ -84,7 +110,7 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
const addResidence = () => {
|
const addResidence = () => {
|
||||||
setFormData((prev) => ({
|
setFormData((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
residences: [...(prev.residences || []), { region: '', years: '' }],
|
residences: [...(prev.residences || []), { canton: '', years: '' }],
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,20 +125,27 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
switch (step) {
|
switch (step) {
|
||||||
case 1:
|
case 1:
|
||||||
if (!formData.age) {
|
if (!formData.age) {
|
||||||
setError('Bitte wählen Sie eine Altersgruppe aus.');
|
setError('Bitte wählen deine Altersgruppe aus.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 2:
|
case 2:
|
||||||
const hasValidResidence = formData.residences?.some(
|
const allResidencesValid = formData.residences?.every((r) => {
|
||||||
(r) => r.region && r.years
|
const years = parseInt(r.years);
|
||||||
);
|
return r.canton && r.years && years >= 1 && years <= 100;
|
||||||
if (!hasValidResidence) {
|
});
|
||||||
setError('Bitte fügen Sie mindestens einen Wohnort mit Jahresangabe hinzu.');
|
if (!allResidencesValid) {
|
||||||
|
setError('Bitte füllen alle deine Aufenthaltsorte vollständig aus und stelle sicher, dass die Jahres-Angaben zwischen 1 und 100 liegen.');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
|
if (!formData.ownDialectRegion) {
|
||||||
|
setError('Bitte wählen deine Region deines eigenen Dialekts aus.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
const allDialects = Object.keys(DIALECT_LABELS_WITHOUT_DE);
|
const allDialects = Object.keys(DIALECT_LABELS_WITHOUT_DE);
|
||||||
const hasAllDialectQualities = allDialects.every(
|
const hasAllDialectQualities = allDialects.every(
|
||||||
(region) => !!formData.dialectQualities?.[region]
|
(region) => !!formData.dialectQualities?.[region]
|
||||||
@@ -129,7 +162,7 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
setError(null);
|
setError(null);
|
||||||
if (validateStep(currentStep)) {
|
if (validateStep(currentStep)) {
|
||||||
if (currentStep < 3) {
|
if (currentStep < 4) {
|
||||||
setCurrentStep((currentStep + 1) as Step);
|
setCurrentStep((currentStep + 1) as Step);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -145,6 +178,9 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
|
if(!validateStep(4)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
startTransition(async () => {
|
startTransition(async () => {
|
||||||
@@ -161,8 +197,9 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
|
|
||||||
const stepTitles: Record<Step, string> = {
|
const stepTitles: Record<Step, string> = {
|
||||||
1: 'Persönliche Informationen',
|
1: 'Persönliche Informationen',
|
||||||
2: 'Aufenthaltsorte',
|
2: 'Aufenthaltsorte in der Deutschschweiz',
|
||||||
3: 'Selbsteinschätzung',
|
3: 'Dialektzuordnung',
|
||||||
|
4: 'Selbsteinschätzung',
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -173,14 +210,14 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex justify-between items-center mb-2">
|
<div className="flex justify-between items-center mb-2">
|
||||||
<h2 className="text-sm font-semibold text-gray-600">
|
<h2 className="text-sm font-semibold text-gray-600">
|
||||||
Schritt {currentStep} von 3
|
Schritt {currentStep} von 4
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-sm text-gray-500">{Math.round((currentStep / 3) * 100)}%</div>
|
<div className="text-sm text-gray-500">{Math.round((currentStep / 4) * 100)}%</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-blue-600 transition-all duration-300"
|
className="h-full bg-blue-600 transition-all duration-300"
|
||||||
style={{ width: `${(currentStep / 3) * 100}%` }}
|
style={{ width: `${(currentStep / 4) * 100}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,7 +241,7 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="age" className="block text-s font-medium text-gray-900 mb-2">
|
<label htmlFor="age" className="block text-s font-medium text-gray-900 mb-2">
|
||||||
Altersgruppe <span className="text-red-600">*</span>
|
Meine Altersgruppe <span className="text-red-600">*</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="age"
|
id="age"
|
||||||
@@ -249,21 +286,17 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
{currentStep === 2 && (
|
{currentStep === 2 && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<p className="text-s text-gray-600 mb-8">
|
<p className="text-s text-gray-600 mb-8">
|
||||||
Bitte gib die Regionen an, in denen du länger als 1 Jahr gelebt hast, sowie die Anzahl der Jahre. So können wir deine Dialektkenntnisse besser einschätzen.
|
Bitte gib an, in welchen deutschschweizer Kantonen du länger als 1 Jahr gelebt hast und wie viele Jahre insgesamt.
|
||||||
|
<br />
|
||||||
|
Wenn du mehrmals im gleichen Kanton gewohnt hast, zähle die Zeit zusammen und mache nur einen Eintrag pro Kanton.
|
||||||
|
<br />
|
||||||
|
Falls ein Kanton nicht in der Liste ist oder du im Ausland gelebt hast, wähle „Anderer Kanton/Ausland“
|
||||||
</p>
|
</p>
|
||||||
{/* Regions Map */}
|
|
||||||
<div className="rounded-lg overflow-hidden border border-gray-200">
|
|
||||||
<img
|
|
||||||
src="/swiss_regions_map.png"
|
|
||||||
alt="Schweizer Dialektregionen"
|
|
||||||
className="w-full h-auto"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Residence Entries */}
|
{/* Residence Entries */}
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-s font-medium text-gray-900 mb-4">
|
<label className="block text-s font-medium text-gray-900 mb-4">
|
||||||
Aufenthaltsorte (mind. 1 Jahr) <span className="text-red-600">*</span>
|
Meine Aufenthaltsorte (mind. 1 Jahr) <span className="text-red-600">*</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -271,22 +304,33 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
<div key={index} className="flex gap-3 items-end">
|
<div key={index} className="flex gap-3 items-end">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<label className="block text-xs font-medium text-gray-700 mb-1">
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
||||||
Region
|
Kanton
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
value={residence.region}
|
value={residence.canton}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
handleResidenceChange(index, 'region', e.target.value)
|
handleResidenceChange(index, 'canton', e.target.value)
|
||||||
}
|
}
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||||
>
|
>
|
||||||
<option value="">-- Wählen --</option>
|
<option value="">-- Wählen --</option>
|
||||||
{Object.entries(DIALECT_LABELS_WITHOUT_DE).map(
|
{Object.entries(AVAILABLE_CANTONS).map(
|
||||||
([key, label]) => (
|
([key, label]) => {
|
||||||
|
// Hide cantons already selected in other residence entries
|
||||||
|
const isSelectedElsewhere = formData.residences?.some(
|
||||||
|
(r, i) => i !== index && r.canton === key
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isSelectedElsewhere) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<option key={key} value={key}>
|
<option key={key} value={key}>
|
||||||
{label}
|
{label}
|
||||||
</option>
|
</option>
|
||||||
)
|
);
|
||||||
|
}
|
||||||
)}
|
)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -312,7 +356,7 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => removeResidence(index)}
|
onClick={() => removeResidence(index)}
|
||||||
disabled={(formData.residences?.length || 0) <= 1}
|
disabled={(formData.residences?.length || 0) <= 1}
|
||||||
className="px-3 py-2 bg-red-100 hover:bg-red-200 disabled:bg-gray-100 text-red-600 disabled:text-gray-400 font-medium rounded-lg transition-colors text-sm"
|
className="px-3 py-2 bg-blue-100 hover:bg-blue-200 disabled:bg-gray-100 text-blue-600 disabled:text-gray-400 font-medium rounded-lg transition-colors text-sm"
|
||||||
>
|
>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
@@ -331,11 +375,52 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Step 3: Selbsteinschätzung */}
|
{/* Step 3: Dialektzuordnung */}
|
||||||
{currentStep === 3 && (
|
{currentStep === 3 && (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<p className="text-s text-gray-600 mb-8">
|
<p className="text-s text-gray-600 mb-8">
|
||||||
Bewerte deine Fähigkeit, die folgenden Dialekte zu erkennen. Diese Selbsteinschätzung hilft uns, deine Kalibrierungsergebnisse besser zu interpretieren.
|
Zu welcher Region würdest du deinen eigenen Dialekt am ehesten zuordnen?
|
||||||
|
<br />
|
||||||
|
Orientiere dich an der folgenden Karte der deutschschweizer Dialektregionen.
|
||||||
|
<br />
|
||||||
|
Wenn du keinen deutschschweizer Dialekt sprichst, wähle "Ich spreche keinen deutschschweizer Dialekt" aus.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex justify-center mb-8">
|
||||||
|
<img
|
||||||
|
src="/swiss_regions_map.png"
|
||||||
|
alt="Karte der deutschschweizer Dialektregionen"
|
||||||
|
className="w-full max-w-md h-auto"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="ownDialect" className="block text-s font-medium text-gray-900 mb-4">
|
||||||
|
Meine Dialektregion <span className="text-red-600">*</span>
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
id="ownDialect"
|
||||||
|
value={formData.ownDialectRegion || ''}
|
||||||
|
onChange={(e) => handleChange('ownDialectRegion', e.target.value)}
|
||||||
|
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
>
|
||||||
|
<option value="">-- Bitte wählen --</option>
|
||||||
|
{Object.entries(DIALECT_LABELS_WITHOUT_DE).map(([key, label]) => (
|
||||||
|
<option key={key} value={key}>
|
||||||
|
{label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
<option value="none">Ich spreche keinen deutschschweizer Dialekt</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Step 4: Selbsteinschätzung */}
|
||||||
|
{currentStep === 4 && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<p className="text-s text-gray-600 mb-8">
|
||||||
|
Bewerte deine Fähigkeit, die folgenden deutschschweizer Dialekte zu erkennen. Diese Selbsteinschätzung hilft uns, deine Kalibrierungsergebnisse besser zu interpretieren.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<ul className="text-s text-gray-600 mb-8 space-y-1">
|
<ul className="text-s text-gray-600 mb-8 space-y-1">
|
||||||
@@ -404,7 +489,7 @@ export default function OnboardingFormView({ experimentId, onBack, userEmail }:
|
|||||||
← Zurück
|
← Zurück
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{currentStep < 3 ? (
|
{currentStep < 4 ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleNext}
|
onClick={handleNext}
|
||||||
|
|||||||
Reference in New Issue
Block a user