Added onboarding questions and worked on style (#3)

This commit is contained in:
emilyokeefe
2025-08-07 07:52:19 -04:00
committed by GitHub
parent 520d157a0f
commit b79f8d05be

View File

@@ -1,97 +1,550 @@
import React, {useState} from "react";
import {useForm, SubmitHandler} from "react-hook-form";
// Updated Question type to support more field types and conditions
type Question = {
name: string;
label: string;
type?: string;
options?: string[];
name: string;
label: string;
type?: string;
options?: string[];
optional?: boolean;
group?: boolean;
fields?: Question[];
condition?: (values: any) => boolean;
};
// Onboarding questions with conditional logic and grouping
const questions: Question[] = [
{
name: "firstName",
label: "First Name"
},
{
name: "lastName",
label: "Last Name"
},
{
name: "age",
label: "Age",
type: "number"
},
{
name: "gender",
label: "Gender",
type: "select",
options: ["Male", "Female", "Other"],
},
{
name: "firstName",
label: "Hi! What's your first name?",
},
{
name: "location",
label: "Where are you located?",
group: true,
fields: [
{ name: "country", label: "Country" },
{ name: "zipCode", label: "Zip Code" },
],
},
{
name: "gender",
label: "What's your gender?",
type: "select",
options: ["Man", "Woman", "See all"],
},
{
name: "genderOther",
label: "How do you identify?",
type: "select",
options: [
"Agender",
"Androgynous",
"Bigender",
"Cis Man",
"Cis Woman",
"Genderfluid",
"Genderqueer",
"Gender Nonconforming",
"Hijra",
"Intersex",
"Non-binary",
"Other gender",
"Pangender",
"Transfeminine",
"Transgender",
"Trans Man",
"Transmasculine",
"Transsexual",
"Trans Woman",
"Two Spirit"
],
optional: true,
condition: (values) => values.gender === "See all",
},
{
name: "birthday",
label: "When's your birthday?",
type: "date",
},
{
name: "relationshipType",
label: "What kind of relationship are you looking for?",
type: "multiselect",
options: [
"New friend",
"Short-term relationship",
"Long-term relationship",
"Other",
],
},
{
name: "kids",
label: "What are your ideal plans for children? (optional)",
type: "select",
options: [
"Skip",
"Want someday",
"Don't want",
"Have and want more",
"Have and don't want more",
"Not sure yet",
"Have kids",
"Open to kids",
],
optional: true,
condition: (values) =>
["Short-term dating", "Hookups", "Long-term dating"].includes(
values.relationshipType
),
},
{
name: "nonMonogamy",
label: "Non-Monogamy Options",
type: "select",
options: ["Monogamous", "Non-monogamous", "Open to either"],
condition: (values) =>
["Short-term dating", "Hookups", "Long-term dating"].includes(
values.relationshipType
),
},
{
name: "photos",
label: "Add photos (optional)",
type: "file",
optional: true,
},
{
name: "about",
label: "Tell us about yourself",
type: "textarea",
},
// Personality questions
{
name: "intenseOrCarefree",
label: "Which word describes you better?",
type: "select",
options: ["Intense", "Carefree"],
},
{
name: "religion",
label: "How important is religion/God in your life?",
type: "select",
options: [
"Not at all important",
"Slightly important",
"Moderately important",
"Very important",
"Extremely important"
]
},
{
name: "politics",
label: "Which best describes your political beliefs?",
type: "select",
options: [
"Very liberal",
"Liberal",
"Moderate",
"Conservative",
"Very conservative",
"Other"
]
},
{
name: "introversion",
label: "How would you describe your social style?",
type: "select",
options: [
"Very introverted",
"Somewhat introverted",
"In the middle",
"Somewhat extroverted",
"Very extroverted"
]
},
];
// List of valid countries (shortened for brevity, add more as needed)
const countryOptions = [
"United States", "Canada", "United Kingdom", "Australia", "Germany", "France", "India", "China", "Japan", "Brazil", "Mexico", "Italy", "Spain", "Netherlands", "Sweden", "Norway", "Denmark", "Finland", "Ireland", "New Zealand"
// ...add more countries as needed
];
type FormValues = {
[key: string]: any;
[key: string]: any;
};
// Helper to get visible questions based on current form values
const getVisibleQuestions = (values: FormValues) =>
questions.filter((q) => !q.condition || q.condition(values));
const MultiStepForm: React.FC = () => {
const [step, setStep] = useState(0);
const {register, handleSubmit, getValues, formState: {errors}} = useForm<FormValues>();
const isLastStep = step === questions.length - 1;
const [step, setStep] = useState(0);
const {register, handleSubmit, getValues, formState: {errors}} = useForm<FormValues>();
const [formValues, setFormValues] = useState<FormValues>({});
const [showGenderDefs, setShowGenderDefs] = useState(false);
const onSubmit: SubmitHandler<FormValues> = (data) => {
// handle final form submission
alert(JSON.stringify(data, null, 2));
};
const nextStep = () => setStep((s) => Math.min(s + 1, questions.length - 1));
const prevStep = () => setStep((s) => Math.max(s - 1, 0));
// Helper to calculate age
const getAge = (month: string, day: string, year: string) => {
if (!month || !day || !year) return null;
const mm = parseInt(month, 10);
const dd = parseInt(day, 10);
const yyyy = parseInt(year, 10);
if (isNaN(mm) || isNaN(dd) || isNaN(yyyy)) return null;
const today = new Date();
const birthDate = new Date(yyyy, mm - 1, dd);
let age = today.getFullYear() - yyyy;
const m = today.getMonth() - (mm - 1);
if (m < 0 || (m === 0 && today.getDate() < dd)) {
age--;
}
return age;
};
const currentQuestion = questions[step];
return (
<form onSubmit={handleSubmit(onSubmit)} className="mt-8 space-y-6">
<div>
<label className="px-4">{currentQuestion.label}</label>
{currentQuestion.type === "select" ? (
<select {...register(currentQuestion.name, {required: true})} className="px-4">
<option value="">Select...</option>
{currentQuestion.options?.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
) : (
<input
type={currentQuestion.type || "text"}
{...register(currentQuestion.name, {required: true})}
defaultValue={getValues(currentQuestion.name) || ""}
/>
)}
{errors[currentQuestion.name] && <span>This field is required</span>}
</div>
<div style={{margin: 16,}}>
{step > 0 && (
<button type="button" onClick={prevStep} style={{margin: 16,}}>
Back
</button>
)}
{!isLastStep ? (
<button
type="button"
onClick={() => {
// Validate current step before proceeding
handleSubmit(() => nextStep())();
}}
>
Next
</button>
) : (
<button type="submit">Submit</button>
)}
</div>
</form>
);
const visibleQuestions = getVisibleQuestions(formValues);
const isLastStep = step === visibleQuestions.length - 1;
const currentQuestion = visibleQuestions[step];
const onSubmit: SubmitHandler<FormValues> = (data) => {
// Merge the latest form values with the current step's data
const allValues = { ...formValues, ...getValues() };
alert(JSON.stringify(allValues, null, 2));
};
const nextStep = () => {
const values = getValues();
setFormValues((prev) => ({ ...prev, ...values }));
setStep((s) => Math.min(s + 1, visibleQuestions.length - 1));
};
const prevStep = () => setStep((s) => Math.max(s - 1, 0));
// Questions where skip is allowed (from 'relationshipDuration' onward)
const skipFrom = visibleQuestions.findIndex(q => q.name === 'relationshipDuration');
const canSkip = step >= skipFrom && skipFrom !== -1;
return (
<form onSubmit={handleSubmit(onSubmit)} className="mt-0 space-y-6 flex flex-col items-center">
<div className="w-full flex flex-col items-center">
<label className="px-4 text-center block w-full text-3xl font-bold mb-8 mt-0" style={{marginTop: '0rem'}}>{currentQuestion.label}</label>
{currentQuestion.group && currentQuestion.fields ? (
currentQuestion.fields.map((field) => {
let fieldInput;
if (field.name === "country") {
fieldInput = (
<>
<input
list="country-list"
{...register(field.name, {
required: !field.optional,
validate: value => countryOptions.includes(value) || "Please select a valid country."
})}
defaultValue={getValues(field.name) || ""}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
nextStep();
}
}}
className="text-center text-lg py-3 px-5 h-14 rounded-full border w-72"
/>
<datalist id="country-list">
{countryOptions.map((country) => (
<option key={country} value={country} />
))}
</datalist>
</>
);
} else if (field.name === "zipCode") {
fieldInput = (
<input
type="text"
{...register(field.name, {
required: !field.optional,
pattern: { value: /^\d{5}(-\d{4})?$/, message: "Please enter a valid US zip code." }
})}
defaultValue={getValues(field.name) || ""}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
nextStep();
}
}}
className="text-center text-lg py-3 px-5 h-14 rounded-full border w-48"
/>
);
} else {
fieldInput = (
<input
type={field.type || "text"}
{...register(field.name, { required: !field.optional })}
defaultValue={getValues(field.name) || ""}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
nextStep();
}
}}
className="text-center text-lg py-3 px-5 h-14 rounded-full border"
/>
);
}
return (
<div key={field.name} className="w-full flex flex-col items-center">
<label className="text-center w-full">{field.label}</label>
{fieldInput}
{errors[field.name] && (
<span className="block text-center mt-4">
{(errors[field.name] as any)?.message || "This field is required"}
</span>
)}
</div>
);
})
) : currentQuestion.name === "birthday" ? (
<div className="flex flex-row gap-2 justify-center items-center mt-2">
<input
type="text"
maxLength={2}
placeholder="MM"
{...register("birthday_month", { required: true, pattern: /^(0?[1-9]|1[0-2])$/ })}
className="rounded-full border px-4 py-2 text-lg text-center w-20"
/>
<input
type="text"
maxLength={2}
placeholder="DD"
{...register("birthday_day", { required: true, pattern: /^(0?[1-9]|[12][0-9]|3[01])$/ })}
className="rounded-full border px-4 py-2 text-lg text-center w-20"
/>
<input
type="text"
maxLength={4}
placeholder="YYYY"
{...register("birthday_year", { required: true, pattern: /^[0-9]{4}$/ })}
className="rounded-full border px-4 py-2 text-lg text-center w-28"
/>
{(() => {
const month = getValues("birthday_month");
const day = getValues("birthday_day");
const year = getValues("birthday_year");
const age = getAge(month, day, year);
if (age && age > 0 && age < 120) {
return <span className="ml-4 text-lg font-semibold text-blue-700">You are {age}!</span>;
}
return null;
})()}
</div>
) : (currentQuestion.type === "select" && currentQuestion.options && currentQuestion.options.length <= 6) ? (
<div className="flex flex-wrap gap-2 mt-2 justify-center">
{currentQuestion.options.map((opt) => (
<button
key={opt}
type="button"
className={`px-4 py-2 rounded-full border ${formValues[currentQuestion.name] === opt ? 'bg-red-700 text-white' : 'bg-gray-100 dark:bg-gray-700'} hover:bg-red-100 dark:hover:bg-red-800 transition-colors`}
onClick={() => {
setFormValues({ ...formValues, [currentQuestion.name]: opt });
setStep((s) => Math.min(s + 1, visibleQuestions.length - 1));
}}
>
{opt}
</button>
))}
</div>
) : (currentQuestion.type === "multiselect" && currentQuestion.options && currentQuestion.options.length <= 6) ? (
<div className="flex flex-wrap gap-2 mt-2 justify-center">
{currentQuestion.options.map((opt) => {
const selected = (formValues[currentQuestion.name] || []).includes(opt);
return (
<button
key={opt}
type="button"
className={`px-4 py-2 rounded-full border ${selected ? 'bg-red-700 text-white' : 'bg-gray-100 dark:bg-gray-700'} hover:bg-red-100 dark:hover:bg-red-800 transition-colors`}
onClick={() => {
const prev = formValues[currentQuestion.name] || [];
const next = selected ? prev.filter((v: string) => v !== opt) : [...prev, opt];
setFormValues({ ...formValues, [currentQuestion.name]: next });
}}
>
{opt}
</button>
);
})}
</div>
) : currentQuestion.type === "select" ? (
<select
{...register(currentQuestion.name, { required: !currentQuestion.optional })}
className="px-4 text-center text-lg py-3 h-14 rounded-full border"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
nextStep();
}
}}
>
<option value="">Select...</option>
{currentQuestion.options?.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
) : currentQuestion.type === "multiselect" ? (
<select
multiple
{...register(currentQuestion.name, { required: !currentQuestion.optional })}
className="px-4 border rounded-full w-full min-h-[44px] focus:outline-none focus:ring-2 focus:ring-blue-400 text-center text-lg py-3 h-14"
style={{ minHeight: 44, boxSizing: 'border-box', cursor: 'pointer' }}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
nextStep();
}
}}
>
{currentQuestion.options?.map((opt) => (
<option key={opt} value={opt}>
{opt}
</option>
))}
</select>
// For a more advanced UI, consider using react-select's MultiSelect component
) : currentQuestion.type === "textarea" ? (
<textarea
{...register(currentQuestion.name, { required: !currentQuestion.optional })}
defaultValue={getValues(currentQuestion.name) || ""}
className="text-center text-lg py-3 px-5 h-28 rounded-lg border"
/>
) : currentQuestion.type === "file" ? (
<input
type="file"
{...register(currentQuestion.name)}
className="text-center text-lg py-3 px-5 h-14 rounded-full border"
/>
) : (currentQuestion.name === "genderOther") ? (
<div className="flex flex-col items-center w-full">
<select
{...register(currentQuestion.name, { required: !currentQuestion.optional })}
className="px-4 text-center text-lg py-3 h-14 rounded-full border w-full max-w-xl"
>
<option value="">Select...</option>
{questions.find(q => q.name === "genderOther")?.options?.map((opt) => (
<option key={opt} value={opt}>{opt}</option>
))}
</select>
<button
type="button"
className="mt-4 mb-2 px-4 py-2 rounded-full border bg-gray-100 hover:bg-gray-200 text-gray-700 transition-colors"
onClick={() => setShowGenderDefs((v) => !v)}
>
{showGenderDefs ? "Hide definitions" : "Show definitions"}
</button>
{showGenderDefs && (
<div className="mt-2 p-4 bg-white border rounded-lg shadow max-w-xl text-left text-sm overflow-y-auto max-h-96">
<ul className="list-disc pl-5">
<li><b>Agender</b>: Individuals with no gender identity or a neutral gender identity.</li>
<li><b>Androgynous</b>: Individuals with both male & female presentation or nature.</li>
<li><b>Bigender</b>: Individuals who identify as multiple genders/identities, either simultaneously or at different times.</li>
<li><b>Cis Man</b>: Individuals whose gender identity matches the male sex they were assigned at birth.</li>
<li><b>Cis Woman</b>: Individuals whose gender identity matches the female sex they were assigned at birth.</li>
<li><b>Genderfluid</b>: Individuals who dont have a fixed gender identity.</li>
<li><b>Genderqueer</b>: Individuals who dont identify with binary gender identity norms.</li>
<li><b>Gender Nonconforming</b>: Individuals whose gender expressions dont match masculine & feminine gender norms.</li>
<li><b>Hijra</b>: A third gender identity, largely used in the Indian subcontinent, which typically reflects people who were assigned male at birth, with feminine gender expression, who identify as neither male nor female.</li>
<li><b>Intersex</b>: Individuals born with a reproductive or sexual anatomy that doesnt fit the typical definitions of female or male.</li>
<li><b>Non-binary</b>: A term covering any gender identity or expression that doesnt fit within the gender binary.</li>
<li><b>Other gender</b>: Individuals who identify with any other gender expressions.</li>
<li><b>Pangender</b>: Individuals who identify with a wide multiplicity of gender identities.</li>
<li><b>Transfeminine</b>: Transgender individuals whose gender expression is more feminine presenting.</li>
<li><b>Transgender</b>: Individuals whose gender identity differs from the sex they were assigned at birth.</li>
<li><b>Trans Man</b>: Individuals who were assigned female at birth (AFAB) but have a male gender identity.</li>
<li><b>Transmasculine</b>: Transgender individuals whose gender expression is more masculine presenting.</li>
<li><b>Transsexual</b>: This term is sometimes used to describe trans individuals (who do not identify with the sex they were assigned at birth) who wish to align their gender identity & sex through medical intervention.</li>
<li><b>Trans Woman</b>: Individuals who were assigned male at birth (AMAB) but have a female gender identity.</li>
<li><b>Two Spirit</b>: Term largely used in Indigenous, Native American, and First Nation cultures, reflecting individuals who identify with multiple genders/gender identities that are neither male nor female.</li>
</ul>
</div>
)}
</div>
) : (
<input
type={currentQuestion.type || "text"}
{...register(currentQuestion.name, { required: !currentQuestion.optional })}
defaultValue={getValues(currentQuestion.name) || ""}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
nextStep();
}
}}
className="text-center text-lg py-3 px-5 h-14 rounded-full border"
/>
)}
{errors[currentQuestion.name] && <span className="block text-center mt-4">This field is required</span>}
</div>
<div className="flex flex-row justify-center items-center w-full mt-16 gap-4">
{step > 0 && (
<button
type="button"
onClick={prevStep}
className="rounded-full px-6 py-2 font-semibold bg-gray-200 dark:bg-gray-700 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors"
>
Back
</button>
)}
{canSkip && (
<button
type="button"
onClick={() => {
setFormValues((prev) => ({ ...prev, [currentQuestion.name]: "" }));
setStep((s) => Math.min(s + 1, visibleQuestions.length - 1));
}}
className="rounded-full px-6 py-2 font-semibold bg-gray-100 text-gray-700 hover:bg-gray-200 transition-colors"
>
Skip
</button>
)}
{!isLastStep ? (
<button
type="button"
onClick={() => {
handleSubmit(() => nextStep())();
}}
className="rounded-full px-6 py-2 font-semibold bg-red-700 text-white hover:bg-red-800 transition-colors"
>
Next
</button>
) : (
<button
type="submit"
className="rounded-full px-6 py-2 font-semibold bg-red-700 text-white hover:bg-red-800 transition-colors"
>
Submit
</button>
)}
</div>
</form>
);
};
export default MultiStepForm;