mirror of
https://github.com/Hestia-Homes/assessment-model.git
synced 2026-06-30 12:55:02 +00:00
onboarding logic working
This commit is contained in:
parent
ef24c8f773
commit
4014791306
3 changed files with 208 additions and 91 deletions
|
|
@ -3,6 +3,7 @@ import GoogleProvider from "next-auth/providers/google";
|
|||
import AzureADB2CProvider from "next-auth/providers/azure-ad-b2c";
|
||||
import EmailProvider from "next-auth/providers/email";
|
||||
import DrizzleEmailAdapter from "./DrizzleEmailAdapter";
|
||||
import { MagicLinksEmail } from "@/app/email_templates/magic_link";
|
||||
|
||||
import { db } from "@/app/db/db";
|
||||
import {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ const getSession = cache(async () => {
|
|||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="bg-brandblue text-white p-4 text-center border-t border-gray-300 mt-8">
|
||||
<footer className="bg-brandblue text-white p-4 text-center border-t border-gray-300">
|
||||
<p>
|
||||
© {new Date().getFullYear()} Domna. All rights reserved. Domna
|
||||
proprietary IP.
|
||||
|
|
|
|||
|
|
@ -16,62 +16,83 @@ import {
|
|||
} from "@/app/shadcn_components/ui/select";
|
||||
import { Fragment } from "react";
|
||||
|
||||
const OnboardingSchema = z.object({
|
||||
firstName: z.string().min(1, "First name is required"),
|
||||
lastName: z.string().min(1, "Last name is required"),
|
||||
userType: z.enum(
|
||||
[
|
||||
"private_landlord",
|
||||
"private_tenant",
|
||||
"social_landlord",
|
||||
"social_tenant",
|
||||
"homeowner",
|
||||
"other",
|
||||
],
|
||||
{
|
||||
required_error: "Please tell us who you are",
|
||||
}
|
||||
),
|
||||
propertyCount: z
|
||||
.enum(
|
||||
const referralLabels: Record<string, string> = {
|
||||
search: "Search engine (e.g. Google)",
|
||||
social_media: "Social media",
|
||||
NRLA: "NRLA (National Residential Landlords Association)",
|
||||
partner: "Through a partner organisation",
|
||||
word_of_mouth: "Word of mouth",
|
||||
other: "Other",
|
||||
};
|
||||
|
||||
const OnboardingSchema = z
|
||||
.object({
|
||||
firstName: z.string().min(1, "First name is required"),
|
||||
lastName: z.string().min(1, "Last name is required"),
|
||||
userType: z.enum(
|
||||
[
|
||||
"1",
|
||||
"2–5",
|
||||
"6–20",
|
||||
"21+",
|
||||
"1–50",
|
||||
"51–100",
|
||||
"101–300",
|
||||
"301–1000",
|
||||
"1000+",
|
||||
],
|
||||
{
|
||||
required_error: "Please tell us how many homes you’re responsible for",
|
||||
}
|
||||
)
|
||||
.nullable()
|
||||
.optional(),
|
||||
goals: z
|
||||
.array(
|
||||
z.enum([
|
||||
"access_funding",
|
||||
"net_zero",
|
||||
"improve_condition",
|
||||
"save_money",
|
||||
"private_landlord",
|
||||
"private_tenant",
|
||||
"social_landlord",
|
||||
"social_tenant",
|
||||
"homeowner",
|
||||
"other",
|
||||
])
|
||||
)
|
||||
.min(1, "Please select at least one goal"),
|
||||
referralSource: z.enum(
|
||||
["search", "social_media", "NRLA", "partner", "word_of_mouth", "other"],
|
||||
{ required_error: "Please tell us how you heard about Domna" }
|
||||
),
|
||||
nrlaMembershipId: z.string().optional(),
|
||||
acceptedPrivacy: z.boolean().refine((v) => v === true, {
|
||||
message: "You must accept our privacy policy to continue",
|
||||
}),
|
||||
marketingOptIn: z.boolean().optional(),
|
||||
});
|
||||
],
|
||||
{ required_error: "Please tell us who you are" }
|
||||
),
|
||||
propertyCount: z
|
||||
.enum(
|
||||
[
|
||||
"1",
|
||||
"2–5",
|
||||
"6–20",
|
||||
"21+",
|
||||
"1–50",
|
||||
"51–100",
|
||||
"101–300",
|
||||
"301–1000",
|
||||
"1000+",
|
||||
],
|
||||
{
|
||||
required_error:
|
||||
"Please tell us how many homes you’re responsible for",
|
||||
}
|
||||
)
|
||||
.optional(),
|
||||
goals: z
|
||||
.array(
|
||||
z.enum([
|
||||
"access_funding",
|
||||
"net_zero",
|
||||
"improve_condition",
|
||||
"save_money",
|
||||
"other",
|
||||
])
|
||||
)
|
||||
.min(1, "Please select at least one goal"),
|
||||
referralSource: z.enum(
|
||||
["search", "social_media", "NRLA", "partner", "word_of_mouth", "other"],
|
||||
{ required_error: "Please tell us how you heard about Domna" }
|
||||
),
|
||||
nrlaMembershipId: z.string().optional(),
|
||||
acceptedPrivacy: z.boolean().refine((v) => v === true, {
|
||||
message: "You must accept our privacy policy to continue",
|
||||
}),
|
||||
marketingOptIn: z.boolean().optional(), // only optional field
|
||||
})
|
||||
.refine(
|
||||
(data) => {
|
||||
const isLandlord =
|
||||
data.userType === "private_landlord" ||
|
||||
data.userType === "social_landlord";
|
||||
if (isLandlord && !data.propertyCount) return false;
|
||||
return true;
|
||||
},
|
||||
{
|
||||
message: "Please tell us how many homes you’re responsible for",
|
||||
path: ["propertyCount"],
|
||||
}
|
||||
);
|
||||
|
||||
type OnboardingData = z.infer<typeof OnboardingSchema>;
|
||||
|
||||
|
|
@ -103,25 +124,44 @@ export default function OnboardingPage() {
|
|||
const referralSource = watch("referralSource");
|
||||
|
||||
async function onSubmit(data: OnboardingData) {
|
||||
console.log("Onboarding data submitted:");
|
||||
await fetch("/api/user/onboard", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
router.push("/home");
|
||||
router.push("/");
|
||||
}
|
||||
|
||||
async function nextStep() {
|
||||
const valid = await trigger(
|
||||
step === 1
|
||||
? [
|
||||
"userType",
|
||||
...(userType?.includes("landlord") ? ["propertyCount"] : []),
|
||||
]
|
||||
: step === 2
|
||||
? ["goals", "referralSource"]
|
||||
: []
|
||||
);
|
||||
let valid = false;
|
||||
|
||||
if (step === 1) {
|
||||
// Validate step 1 fields only
|
||||
const ok = await trigger(["userType", "propertyCount"], {
|
||||
shouldFocus: true,
|
||||
});
|
||||
const values = form.getValues();
|
||||
|
||||
const isLandlord =
|
||||
values.userType === "private_landlord" ||
|
||||
values.userType === "social_landlord";
|
||||
|
||||
if (isLandlord && !values.propertyCount) {
|
||||
form.setError("propertyCount", {
|
||||
type: "manual",
|
||||
message: "Please tell us how many homes you’re responsible for",
|
||||
});
|
||||
valid = false;
|
||||
} else {
|
||||
valid = ok;
|
||||
}
|
||||
} else if (step === 2) {
|
||||
valid = await trigger(["goals", "referralSource"], { shouldFocus: true });
|
||||
} else {
|
||||
valid = true;
|
||||
}
|
||||
|
||||
if (valid) setStep((s) => Math.min(s + 1, 3));
|
||||
}
|
||||
|
||||
|
|
@ -130,7 +170,7 @@ export default function OnboardingPage() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex bg-gray-50 min-h-screen">
|
||||
<div className="flex bg-gray-50">
|
||||
{/* Left image panel */}
|
||||
<div className="hidden md:flex w-1/2 relative overflow-hidden">
|
||||
<div
|
||||
|
|
@ -149,7 +189,7 @@ export default function OnboardingPage() {
|
|||
</div>
|
||||
|
||||
{/* Right section with journey and form */}
|
||||
<div className="flex flex-col flex-1 items-center justify-start p-10 mt-8 relative">
|
||||
<div className="flex flex-col flex-1 items-center justify-start p-10 relative">
|
||||
{/* Domna Journey Progress */}
|
||||
<div className="w-full max-w-3xl mt-8 mb-8">
|
||||
<h3 className="text-center text-brandblue text-xl font-semibold mb-5">
|
||||
|
|
@ -238,8 +278,10 @@ export default function OnboardingPage() {
|
|||
|
||||
{/* Onboarding Form */}
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="w-full max-w-lg bg-white rounded-xl shadow-md p-8 flex flex-col min-h-[400px]"
|
||||
onSubmit={handleSubmit(onSubmit, (errors) => {
|
||||
console.log("Validation errors:", errors);
|
||||
})}
|
||||
className="w-full max-w-lg bg-white rounded-xl shadow-md p-8 flex flex-col min-h-[450px]"
|
||||
>
|
||||
{/* Progress indicator */}
|
||||
<div className="flex items-center pb-4">
|
||||
|
|
@ -256,14 +298,24 @@ export default function OnboardingPage() {
|
|||
<div className="flex-grow space-y-6">
|
||||
{step === 1 && (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-semibold text-brandblue">
|
||||
<h1 className="text-xl font-semibold text-brandblue">
|
||||
Step 1 of 3
|
||||
</h1>
|
||||
<p className="text-sm font-medium text-gray-700">
|
||||
By telling us a bit about yourself, we'll know how to best
|
||||
support your journey.
|
||||
</p>
|
||||
<Controller
|
||||
control={control}
|
||||
name="userType"
|
||||
render={({ field }) => (
|
||||
<Select value={field.value} onValueChange={field.onChange}>
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
setValue("propertyCount", undefined); // ✅ clear previous selection
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
{field.value
|
||||
? field.value
|
||||
|
|
@ -308,7 +360,6 @@ export default function OnboardingPage() {
|
|||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
placeholder="How many properties do you manage?"
|
||||
>
|
||||
<SelectTrigger>
|
||||
{field.value ??
|
||||
|
|
@ -339,11 +390,19 @@ export default function OnboardingPage() {
|
|||
</div>
|
||||
)}
|
||||
|
||||
{errors.propertyCount && (
|
||||
<p className="text-sm text-red-500">
|
||||
{errors.propertyCount.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{step === 2 && (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-semibold text-brandblue">
|
||||
Step 2 of 3
|
||||
</h1>
|
||||
|
||||
{/* Goals section */}
|
||||
<p className="font-medium text-gray-700">
|
||||
What would you like to achieve with Domna?
|
||||
<span className="text-red-500">*</span>
|
||||
|
|
@ -368,55 +427,84 @@ export default function OnboardingPage() {
|
|||
"goals",
|
||||
checked
|
||||
? [...goals, g]
|
||||
: goals.filter((gg) => gg !== g)
|
||||
: goals.filter((gg) => gg !== g),
|
||||
{ shouldValidate: true } // ✅ triggers live validation feedback
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<span className="capitalize">{g.replace("_", " ")}</span>
|
||||
<span className="capitalize text-sm">
|
||||
{g.replace("_", " ")}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
{errors.goals && (
|
||||
<p className="text-sm text-red-500">{errors.goals.message}</p>
|
||||
)}
|
||||
|
||||
{/* Referral source section */}
|
||||
<Controller
|
||||
control={control}
|
||||
name="referralSource"
|
||||
render={({ field }) => (
|
||||
<Select
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
onValueChange={(value) => {
|
||||
field.onChange(value);
|
||||
trigger("referralSource"); // ✅ update validation instantly
|
||||
}}
|
||||
placeholder="How did you hear about us?"
|
||||
>
|
||||
<SelectTrigger>
|
||||
{field.value ?? "Referral source"}
|
||||
{field.value
|
||||
? referralLabels[field.value]
|
||||
: "How did you hear about us?"}
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="search">Search</SelectItem>
|
||||
<SelectItem value="social_media">
|
||||
Social Media
|
||||
<SelectItem value="search">
|
||||
Search engine (e.g. Google)
|
||||
</SelectItem>
|
||||
<SelectItem value="social_media">
|
||||
Social media
|
||||
</SelectItem>
|
||||
<SelectItem value="NRLA">
|
||||
NRLA (National Residential Landlords Association)
|
||||
</SelectItem>
|
||||
<SelectItem value="partner">
|
||||
Through a partner organisation
|
||||
</SelectItem>
|
||||
<SelectItem value="NRLA">NRLA</SelectItem>
|
||||
<SelectItem value="partner">Partner</SelectItem>
|
||||
<SelectItem value="word_of_mouth">
|
||||
Word of Mouth
|
||||
Word of mouth
|
||||
</SelectItem>
|
||||
<SelectItem value="other">Other</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
{errors.referralSource && (
|
||||
<p className="text-sm text-red-500">
|
||||
{errors.referralSource.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{referralSource === "NRLA" && (
|
||||
<Input
|
||||
{...register("nrlaMembershipId")}
|
||||
placeholder="Enter your NRLA membership ID"
|
||||
/>
|
||||
<div>
|
||||
<label className="flex items-center space-x-2 text-xs text-gray-700 mb-1">
|
||||
You don't need to enter your NRLA membership ID but you
|
||||
may be eligible for additional discounts
|
||||
</label>
|
||||
<Input
|
||||
{...register("nrlaMembershipId")}
|
||||
placeholder="Enter your NRLA membership ID"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 3 && (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-2xl font-semibold text-brandblue">
|
||||
<h1 className="text-xl font-semibold text-brandblue">
|
||||
Step 3 of 3
|
||||
</h1>
|
||||
<Input
|
||||
|
|
@ -431,7 +519,18 @@ export default function OnboardingPage() {
|
|||
/>
|
||||
|
||||
<label className="flex items-center space-x-2">
|
||||
<Checkbox {...register("acceptedPrivacy")} />
|
||||
<Controller
|
||||
name="acceptedPrivacy"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) =>
|
||||
field.onChange(checked === true)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<span className="text-sm">
|
||||
I agree to the{" "}
|
||||
<a
|
||||
|
|
@ -445,8 +544,26 @@ export default function OnboardingPage() {
|
|||
</span>
|
||||
</label>
|
||||
|
||||
{errors.acceptedPrivacy && (
|
||||
<p className="text-sm text-red-500">
|
||||
{errors.acceptedPrivacy.message}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<label className="flex items-center space-x-2">
|
||||
<Checkbox {...register("marketingOptIn")} />
|
||||
<Controller
|
||||
name="marketingOptIn"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
checked={field.value}
|
||||
onCheckedChange={(checked) =>
|
||||
field.onChange(checked === true)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<span className="text-sm text-gray-700">
|
||||
I’d like to hear more about funding opportunities, retrofit
|
||||
schemes, and energy upgrades.
|
||||
|
|
@ -480,7 +597,6 @@ export default function OnboardingPage() {
|
|||
<Button
|
||||
type="submit"
|
||||
className="bg-brandbrown hover:bg-hoverblue"
|
||||
disabled={!isValid}
|
||||
>
|
||||
Finish
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue