onboarding logic working

This commit is contained in:
Khalim Conn-Kowlessar 2025-10-15 14:52:39 +00:00
parent ef24c8f773
commit 4014791306
3 changed files with 208 additions and 91 deletions

View file

@ -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 {

View file

@ -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>
&copy; {new Date().getFullYear()} Domna. All rights reserved. Domna
proprietary IP.

View file

@ -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",
"25",
"620",
"21+",
"150",
"51100",
"101300",
"3011000",
"1000+",
],
{
required_error: "Please tell us how many homes youre 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",
"25",
"620",
"21+",
"150",
"51100",
"101300",
"3011000",
"1000+",
],
{
required_error:
"Please tell us how many homes youre 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 youre 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 youre 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">
Id 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>