fixed device registration and onboarding flow
This commit is contained in:
parent
c4743896a4
commit
953169d8ad
9 changed files with 128 additions and 468 deletions
|
|
@ -4,9 +4,8 @@ import { encodedRedirect } from "@/utils/utils";
|
||||||
import { createClient } from "@/utils/supabase/server";
|
import { createClient } from "@/utils/supabase/server";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { getMacAddressFromDeviceCode, isValidMacAddress } from "@/lib/utils";
|
|
||||||
import { addUserToDevice, dbCheckUserCode } from "@/db/devices";
|
import { addUserToDevice, dbCheckUserCode } from "@/db/devices";
|
||||||
import { getSimpleUserById, updateUser } from "@/db/users";
|
import { getSimpleUserById } from "@/db/users";
|
||||||
|
|
||||||
export async function deleteUserApiKey(userId: string) {
|
export async function deleteUserApiKey(userId: string) {
|
||||||
const supabase = createClient();
|
const supabase = createClient();
|
||||||
|
|
@ -174,30 +173,3 @@ export const isPremiumUser = async (userId: string) => {
|
||||||
const dbUser = await getSimpleUserById(supabase, userId);
|
const dbUser = await getSimpleUserById(supabase, userId);
|
||||||
return dbUser?.is_premium;
|
return dbUser?.is_premium;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function registerDevice(userId: string, deviceCode: string) {
|
|
||||||
// check if deviceCode is valid mac address
|
|
||||||
if (!isValidMacAddress(deviceCode)) {
|
|
||||||
return { error: "Invalid device code" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const supabase = createClient();
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from("devices")
|
|
||||||
.insert({
|
|
||||||
user_id: userId,
|
|
||||||
user_code: deviceCode, // this is the device code that the user will use to register their device (friendly code preferred)
|
|
||||||
mac_address: deviceCode,
|
|
||||||
}).select();
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
console.log(error);
|
|
||||||
return { error: "Error registering device" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data && data.length > 0) {
|
|
||||||
await updateUser(supabase, { device_id: data[0].device_id }, userId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return { error: null };
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,135 +1,57 @@
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
import React from "react";
|
||||||
import React, { useState } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import UserType from "./UserType";
|
|
||||||
import GeneralUserForm from "../Settings/UserForm";
|
import GeneralUserForm from "../Settings/UserForm";
|
||||||
import DoctorForm from "../Settings/DoctorForm";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Label } from "@/components/ui/label";
|
import { createClient } from "@/utils/supabase/client";
|
||||||
import { Input } from "@/components/ui/input";
|
import { updateUser } from "@/db/users";
|
||||||
import { checkDoctorAction } from "@/app/actions";
|
import { Loader2 } from "lucide-react";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
|
||||||
|
|
||||||
type TUserType = "doctor" | "user" | "business";
|
|
||||||
|
|
||||||
const Steps: React.FC<{
|
const Steps: React.FC<{
|
||||||
selectedUser: IUser;
|
selectedUser?: IUser;
|
||||||
}> = ({ selectedUser }) => {
|
userId: string;
|
||||||
|
}> = ({ selectedUser, userId }) => {
|
||||||
|
const supabase = createClient();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { toast } = useToast();
|
const [progress, setProgress] = React.useState(50);
|
||||||
const [progress, setProgress] = React.useState(40);
|
const [step, setStep] = React.useState(1);
|
||||||
const [step, setStep] = React.useState(0);
|
|
||||||
const [selectedType, setSelectedType] = useState<TUserType | null>(null);
|
|
||||||
const [doctorAuthCode, setDoctorAuthCode] = useState<string>("");
|
|
||||||
|
|
||||||
const onSelectType = (type: TUserType) => {
|
|
||||||
setSelectedType(type);
|
|
||||||
setStep(1);
|
|
||||||
setProgress(progress + 30);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClickBack = () => {
|
|
||||||
setStep(step - 1);
|
|
||||||
setProgress(progress - 30);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onClickFormCallback = async () => {
|
const onClickFormCallback = async () => {
|
||||||
if (selectedType === "doctor") {
|
|
||||||
const res = await checkDoctorAction(doctorAuthCode);
|
|
||||||
if (!res) {
|
|
||||||
toast({
|
|
||||||
description:
|
|
||||||
"Your sign-up code did not match our records. Try again or reach out to us for help.",
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setStep(step + 1);
|
setStep(step + 1);
|
||||||
setProgress(progress + 30);
|
setProgress(progress + 50);
|
||||||
router.push("/home");
|
router.push("/home");
|
||||||
};
|
};
|
||||||
|
|
||||||
const CurrentForm = () => {
|
const CurrentForm = () => {
|
||||||
if (step === 0) {
|
if (step === 1) {
|
||||||
return (
|
|
||||||
<UserType
|
|
||||||
selectedUser={selectedUser}
|
|
||||||
onSelectType={onSelectType}
|
|
||||||
selectedType={selectedType}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (selectedType === "doctor") {
|
|
||||||
return (
|
|
||||||
<DoctorForm
|
|
||||||
selectedUser={selectedUser}
|
|
||||||
heading={<Navigation />}
|
|
||||||
onClickCallback={onClickFormCallback}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
return (
|
||||||
<GeneralUserForm
|
<GeneralUserForm
|
||||||
selectedUser={selectedUser}
|
selectedUser={selectedUser}
|
||||||
heading={<Navigation />}
|
userId={userId}
|
||||||
onClickCallback={onClickFormCallback}
|
onClickCallback={onClickFormCallback}
|
||||||
/>
|
onSave={
|
||||||
);
|
async (values, userType) => {
|
||||||
}
|
await updateUser(
|
||||||
}
|
supabase,
|
||||||
};
|
{
|
||||||
|
supervisee_age: values.supervisee_age,
|
||||||
const Navigation = () => {
|
supervisee_name: values.supervisee_name,
|
||||||
return (
|
supervisee_persona: values.supervisee_persona,
|
||||||
<div className="flex flex-col gap-4">
|
user_info: {
|
||||||
<div className="mt-4 text-center flex flex-row items-center justify-between gap-4">
|
user_type: userType,
|
||||||
{step > 0 && (
|
user_metadata: values,
|
||||||
<Button
|
},
|
||||||
onClick={onClickBack}
|
},
|
||||||
variant="link"
|
userId);
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
className="mr-4 pl-0 flex flex-row items-center gap-2"
|
|
||||||
>
|
|
||||||
<ArrowLeft size={16} /> Back
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{step > 0 && (
|
|
||||||
<Button
|
|
||||||
disabled={!selectedType}
|
|
||||||
className="flex flex-row items-center gap-2"
|
|
||||||
size="sm"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Continue <ArrowRight size={16} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{selectedType === "doctor" && (
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<Label className="flex flex-row gap-4 items-center">
|
|
||||||
{"Your unique sign-up code"}
|
|
||||||
</Label>
|
|
||||||
<Input
|
|
||||||
type="text"
|
|
||||||
autoFocus
|
|
||||||
required
|
|
||||||
value={doctorAuthCode}
|
|
||||||
onChange={(e) => setDoctorAuthCode(e.target.value)}
|
|
||||||
placeholder="Sign-up code"
|
|
||||||
className="max-w-screen-sm h-10 bg-white"
|
|
||||||
autoComplete="on"
|
|
||||||
style={{
|
|
||||||
fontSize: 16,
|
|
||||||
}}
|
}}
|
||||||
|
disabled={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return <Loader2 className="w-4 h-4 animate-spin" />;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let heading = "Let's get your Elato device & account set up";
|
let heading = "Let's get your Elato device & account set up";
|
||||||
|
|
@ -137,11 +59,7 @@ const Steps: React.FC<{
|
||||||
"We want to make sure that your Elato is set up to provide you the best experience possible.";
|
"We want to make sure that your Elato is set up to provide you the best experience possible.";
|
||||||
|
|
||||||
if (step === 1) {
|
if (step === 1) {
|
||||||
if (selectedType === "doctor") {
|
{
|
||||||
heading = "Hello Doctor!";
|
|
||||||
subHeading =
|
|
||||||
"With the following details we will be able to personalize your and your patients' Elato experience.";
|
|
||||||
} else {
|
|
||||||
heading = "Hello there!";
|
heading = "Hello there!";
|
||||||
subHeading =
|
subHeading =
|
||||||
"With the following details we will be able to personalize your Elato experience.";
|
"With the following details we will be able to personalize your Elato experience.";
|
||||||
|
|
|
||||||
|
|
@ -24,12 +24,6 @@ const UserTypes: IUserType[] = [
|
||||||
title: "You are looking to use Elato for personal use",
|
title: "You are looking to use Elato for personal use",
|
||||||
icon: <User />,
|
icon: <User />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: "doctor",
|
|
||||||
name: "Doctor",
|
|
||||||
title: "You are a licensed doctor or physician",
|
|
||||||
icon: <Hospital />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "business",
|
type: "business",
|
||||||
name: "Business",
|
name: "Business",
|
||||||
|
|
@ -40,10 +34,9 @@ const UserTypes: IUserType[] = [
|
||||||
];
|
];
|
||||||
|
|
||||||
const UserType: React.FC<{
|
const UserType: React.FC<{
|
||||||
selectedUser: IUser;
|
|
||||||
selectedType: TUserType | null;
|
selectedType: TUserType | null;
|
||||||
onSelectType: (type: TUserType) => void;
|
onSelectType: (type: TUserType) => void;
|
||||||
}> = ({ selectedType, onSelectType, selectedUser }) => {
|
}> = ({ selectedType, onSelectType }) => {
|
||||||
const onPickType = async (userType: IUserType) => {
|
const onPickType = async (userType: IUserType) => {
|
||||||
if (!userType.disabled) {
|
if (!userType.disabled) {
|
||||||
onSelectType(userType.type);
|
onSelectType(userType.type);
|
||||||
|
|
@ -55,7 +48,7 @@ const UserType: React.FC<{
|
||||||
<h1 className="text-xl font-medium my-4 inline">
|
<h1 className="text-xl font-medium my-4 inline">
|
||||||
You are a <ArrowRight className="inline-block" />{" "}
|
You are a <ArrowRight className="inline-block" />{" "}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 mt-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 mt-2 gap-6">
|
||||||
{UserTypes.map((userType) => (
|
{UserTypes.map((userType) => (
|
||||||
<Card
|
<Card
|
||||||
key={userType.type}
|
key={userType.type}
|
||||||
|
|
@ -99,5 +92,3 @@ const UserType: React.FC<{
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UserType;
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import { registerDevice, signOutAction } from "@/app/actions";
|
|
||||||
|
import { connectUserToDevice, signOutAction } from "@/app/actions";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { LogOut } from "lucide-react";
|
import { LogOut } from "lucide-react";
|
||||||
import DoctorForm from "./DoctorForm";
|
|
||||||
import GeneralUserForm from "./UserForm";
|
import GeneralUserForm from "./UserForm";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { updateUser } from "@/db/users";
|
import { updateUser } from "@/db/users";
|
||||||
|
|
@ -12,12 +13,6 @@ import { createClient } from "@/utils/supabase/client";
|
||||||
import React, { useCallback } from "react";
|
import React, { useCallback } from "react";
|
||||||
import { doesUserHaveADevice, updateDevice } from "@/db/devices";
|
import { doesUserHaveADevice, updateDevice } from "@/db/devices";
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
import { useToast } from "@/components/ui/use-toast";
|
||||||
import {
|
|
||||||
Tooltip,
|
|
||||||
TooltipContent,
|
|
||||||
TooltipTrigger,
|
|
||||||
TooltipProvider,
|
|
||||||
} from "@/components/ui/tooltip";
|
|
||||||
|
|
||||||
interface AppSettingsProps {
|
interface AppSettingsProps {
|
||||||
selectedUser: IUser;
|
selectedUser: IUser;
|
||||||
|
|
@ -50,6 +45,11 @@ const AppSettings: React.FC<AppSettingsProps> = ({
|
||||||
);
|
);
|
||||||
}, [selectedUser.user_id, supabase]);
|
}, [selectedUser.user_id, supabase]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
checkIfUserHasDevice();
|
||||||
|
}, [checkIfUserHasDevice]);
|
||||||
|
|
||||||
|
|
||||||
const [volume, setVolume] = React.useState([
|
const [volume, setVolume] = React.useState([
|
||||||
selectedUser.device?.volume ?? 50,
|
selectedUser.device?.volume ?? 50,
|
||||||
]);
|
]);
|
||||||
|
|
@ -69,7 +69,8 @@ const AppSettings: React.FC<AppSettingsProps> = ({
|
||||||
debouncedUpdateVolume();
|
debouncedUpdateVolume();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSave = async (values: any, userType: "doctor" | "user") => {
|
const onSave = async (values: any, userType: "doctor" | "user", userId: string) => {
|
||||||
|
|
||||||
await updateUser(
|
await updateUser(
|
||||||
supabase,
|
supabase,
|
||||||
{
|
{
|
||||||
|
|
@ -81,7 +82,7 @@ const AppSettings: React.FC<AppSettingsProps> = ({
|
||||||
user_metadata: values,
|
user_metadata: values,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
selectedUser!.user_id);
|
userId);
|
||||||
toast({
|
toast({
|
||||||
description: "Your prefereces have been saved!",
|
description: "Your prefereces have been saved!",
|
||||||
});
|
});
|
||||||
|
|
@ -89,21 +90,14 @@ const AppSettings: React.FC<AppSettingsProps> = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{selectedUser.user_info.user_type === "doctor" ? (
|
|
||||||
<DoctorForm
|
|
||||||
selectedUser={selectedUser}
|
|
||||||
heading={heading}
|
|
||||||
onSave={onSave}
|
|
||||||
onClickCallback={() => handleSave()}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<GeneralUserForm
|
<GeneralUserForm
|
||||||
selectedUser={selectedUser}
|
selectedUser={selectedUser}
|
||||||
|
userId={selectedUser.user_id}
|
||||||
heading={heading}
|
heading={heading}
|
||||||
onSave={onSave}
|
onSave={onSave}
|
||||||
onClickCallback={() => handleSave()}
|
onClickCallback={() => handleSave()}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
<section className="space-y-4 max-w-screen-sm mt-12">
|
<section className="space-y-4 max-w-screen-sm mt-12">
|
||||||
<h2 className="text-lg font-semibold border-b border-gray-200 pb-2">
|
<h2 className="text-lg font-semibold border-b border-gray-200 pb-2">
|
||||||
Device settings
|
Device settings
|
||||||
|
|
@ -114,16 +108,6 @@ const AppSettings: React.FC<AppSettingsProps> = ({
|
||||||
<Label className="text-sm font-medium text-gray-700">
|
<Label className="text-sm font-medium text-gray-700">
|
||||||
Register your device
|
Register your device
|
||||||
</Label>
|
</Label>
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger className="inline-flex items-center justify-center h-4 w-4 rounded-full bg-gray-200 text-gray-600 text-xs hover:bg-gray-300">
|
|
||||||
?
|
|
||||||
</TooltipTrigger>
|
|
||||||
<TooltipContent>
|
|
||||||
<p>For simplicity, you can register your ESP32 MAC address here. <br /> Ideally you want this to be a friendly code for your device.</p>
|
|
||||||
</TooltipContent>
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
<div
|
<div
|
||||||
className={`rounded-full flex-shrink-0 h-2 w-2 ${
|
className={`rounded-full flex-shrink-0 h-2 w-2 ${
|
||||||
isConnected ? 'bg-green-500' : 'bg-amber-500'
|
isConnected ? 'bg-green-500' : 'bg-amber-500'
|
||||||
|
|
@ -137,16 +121,17 @@ const AppSettings: React.FC<AppSettingsProps> = ({
|
||||||
value={deviceCode}
|
value={deviceCode}
|
||||||
disabled={isConnected}
|
disabled={isConnected}
|
||||||
onChange={(e) => setDeviceCode(e.target.value)}
|
onChange={(e) => setDeviceCode(e.target.value)}
|
||||||
placeholder={isConnected ? "**********" : "Enter your ESP32-S3 MAC address"}
|
placeholder={isConnected ? "**********" : "Enter your device code"}
|
||||||
|
maxLength={100}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
disabled={isConnected}
|
disabled={isConnected}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const result = await registerDevice(selectedUser.user_id, deviceCode);
|
const result = await connectUserToDevice(selectedUser.user_id, deviceCode);
|
||||||
if (result.error) {
|
if (!result) {
|
||||||
setError(result.error);
|
setError("Error registering device");
|
||||||
}
|
}
|
||||||
checkIfUserHasDevice();
|
checkIfUserHasDevice();
|
||||||
}}
|
}}
|
||||||
|
|
@ -157,7 +142,7 @@ const AppSettings: React.FC<AppSettingsProps> = ({
|
||||||
<p className="text-xs text-gray-400">
|
<p className="text-xs text-gray-400">
|
||||||
{isConnected ? <span className="font-medium text-gray-800">Registered!</span> :
|
{isConnected ? <span className="font-medium text-gray-800">Registered!</span> :
|
||||||
error ? <span className="text-red-500">{error}.</span> :
|
error ? <span className="text-red-500">{error}.</span> :
|
||||||
"Add your ESP32-S3 MAC address (e.g. 12:34:56:78:9A:BC) to your account to register it."
|
"Enter your device code to register it."
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,218 +0,0 @@
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import * as z from "zod";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import React, { forwardRef } from "react";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
|
|
||||||
interface DoctorFormProps {
|
|
||||||
selectedUser: IUser;
|
|
||||||
heading: React.ReactNode;
|
|
||||||
onSave?: (values: any, userType: "doctor" | "user") => void;
|
|
||||||
onClickCallback: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const doctorSettingsSchema = z.object({
|
|
||||||
doctor_name: z.string().min(1).max(50),
|
|
||||||
specialization: z.string().min(1).max(500),
|
|
||||||
hospital_name: z.string().min(1).max(200),
|
|
||||||
favorite_phrases: z.string().min(1).max(200),
|
|
||||||
hospital_layout: z.string().min(1).max(500),
|
|
||||||
});
|
|
||||||
|
|
||||||
export type DoctorSettingsInput = z.infer<typeof doctorSettingsSchema>;
|
|
||||||
|
|
||||||
const DoctorForm = ({ selectedUser, heading, onSave, onClickCallback }: DoctorFormProps) => {
|
|
||||||
const userMetadata = selectedUser.user_info
|
|
||||||
.user_metadata as IDoctorMetadata;
|
|
||||||
|
|
||||||
const form = useForm<DoctorSettingsInput>({
|
|
||||||
defaultValues: {
|
|
||||||
doctor_name:
|
|
||||||
userMetadata?.doctor_name ?? selectedUser.supervisee_name,
|
|
||||||
specialization: userMetadata?.specialization ?? "",
|
|
||||||
hospital_name: userMetadata?.hospital_name ?? "",
|
|
||||||
favorite_phrases: userMetadata?.favorite_phrases ?? "",
|
|
||||||
hospital_layout: userMetadata?.hospital_layout ?? "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof doctorSettingsSchema>) {
|
|
||||||
onSave && onSave(values, "doctor");
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
onSave && onSave(form.getValues(), "doctor");
|
|
||||||
onClickCallback();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="flex flex-col gap-8 mb-4 max-w-screen-sm"
|
|
||||||
>
|
|
||||||
{heading}
|
|
||||||
<section className="space-y-4">
|
|
||||||
<h2 className="text-lg font-semibold border-b border-gray-200 pb-2">
|
|
||||||
Basic Info
|
|
||||||
</h2>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="doctor_name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="w-full rounded-md">
|
|
||||||
<FormLabel className="text-sm font-medium text-gray-700">
|
|
||||||
{"Your Name"}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
// autoFocus
|
|
||||||
required
|
|
||||||
placeholder="e.g. Dr. John Doe"
|
|
||||||
{...field}
|
|
||||||
className="mt-1"
|
|
||||||
// autoComplete="on"
|
|
||||||
// style={{
|
|
||||||
// fontSize: 16,
|
|
||||||
// }}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<section className="space-y-4 flex flex-col gap-2">
|
|
||||||
<h2 className="text-lg font-semibold border-b border-gray-200 pb-2">
|
|
||||||
Hospital Details
|
|
||||||
</h2>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="hospital_name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="w-full rounded-md">
|
|
||||||
<FormLabel className="text-sm font-medium text-gray-700">
|
|
||||||
{"Hospital or clinic name(s)"}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
// autoFocus
|
|
||||||
required
|
|
||||||
placeholder="e.g. St. Mary's Hospital"
|
|
||||||
{...field}
|
|
||||||
// className="max-w-screen-sm h-10 bg-white"
|
|
||||||
// autoComplete="on"
|
|
||||||
// style={{
|
|
||||||
// fontSize: 16,
|
|
||||||
// }}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="favorite_phrases"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="w-full rounded-md">
|
|
||||||
<FormLabel className="text-sm font-medium text-gray-700">
|
|
||||||
{"Nurse's favorite phrases"}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
rows={3}
|
|
||||||
placeholder={
|
|
||||||
"e.g. 'You're doing great!' or 'Take a deep breath'"
|
|
||||||
}
|
|
||||||
{...field}
|
|
||||||
// className="max-w-screen-sm bg-white"
|
|
||||||
// autoComplete="on"
|
|
||||||
// style={{
|
|
||||||
// fontSize: 16,
|
|
||||||
// }}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="specialization"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="w-full rounded-md">
|
|
||||||
<FormLabel className="text-sm font-medium text-gray-700">
|
|
||||||
{"Specializations and conditions treated"}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
rows={5}
|
|
||||||
placeholder={
|
|
||||||
"e.g. Pediatrician neurologist or Cardiologist treating heart conditions"
|
|
||||||
}
|
|
||||||
{...field}
|
|
||||||
// className="max-w-screen-sm bg-white"
|
|
||||||
// autoComplete="on"
|
|
||||||
// style={{
|
|
||||||
// fontSize: 16,
|
|
||||||
// }}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="hospital_layout"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="w-full rounded-md">
|
|
||||||
<FormLabel className="text-sm font-medium text-gray-700">
|
|
||||||
{"Hospital layout"}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Textarea
|
|
||||||
rows={5}
|
|
||||||
placeholder={
|
|
||||||
"e.g. Our hospital has 2 floors. Main entrance leads to the lobby with reception desk straight ahead. \n\nFloor 1: Outpatient clinics and cafeteria. \nFloor 2: Surgery and recovery rooms. \n\nCafeteria is on the ground floor."
|
|
||||||
}
|
|
||||||
{...field}
|
|
||||||
// className="max-w-screen-sm bg-white"
|
|
||||||
// autoComplete="on"
|
|
||||||
// style={{
|
|
||||||
// fontSize: 16,
|
|
||||||
// }}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</section>
|
|
||||||
<Button
|
|
||||||
variant="default"
|
|
||||||
className="rounded-full w-fit mt-4"
|
|
||||||
size="sm"
|
|
||||||
onClick={handleSave}
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Save settings
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
DoctorForm.displayName = "DoctorForm";
|
|
||||||
|
|
||||||
export default DoctorForm;
|
|
||||||
|
|
@ -19,14 +19,17 @@ import {
|
||||||
userFormPersonaPlaceholder,
|
userFormPersonaPlaceholder,
|
||||||
} from "@/lib/data";
|
} from "@/lib/data";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Loader2 } from "lucide-react";
|
||||||
interface GeneralUserFormProps {
|
interface GeneralUserFormProps {
|
||||||
selectedUser: IUser;
|
selectedUser?: IUser;
|
||||||
heading: React.ReactNode;
|
heading?: React.ReactNode;
|
||||||
onSave?: (values: any, userType: "doctor" | "user") => void;
|
onSave?: (values: any, userType: "doctor" | "user", userId: string) => void;
|
||||||
onClickCallback: () => void;
|
onClickCallback: () => void;
|
||||||
|
userId: string;
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export const UserSettingsSchema = z.object({
|
export const UserSettingsSchema = z.object({
|
||||||
supervisee_name: z.string().min(1).max(50),
|
supervisee_name: z.string().min(1).max(50),
|
||||||
supervisee_age: z.number().min(1).max(18),
|
supervisee_age: z.number().min(1).max(18),
|
||||||
|
|
@ -40,7 +43,7 @@ export const UserSettingsSchema = z.object({
|
||||||
|
|
||||||
export type GeneralUserInput = z.infer<typeof UserSettingsSchema>;
|
export type GeneralUserInput = z.infer<typeof UserSettingsSchema>;
|
||||||
|
|
||||||
const GeneralUserForm = ({ selectedUser, heading, onSave, onClickCallback }: GeneralUserFormProps) => {
|
const GeneralUserForm = ({ selectedUser, onSave, onClickCallback, userId, heading, disabled }: GeneralUserFormProps) => {
|
||||||
const form = useForm<GeneralUserInput>({
|
const form = useForm<GeneralUserInput>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
supervisee_name: selectedUser?.supervisee_name ?? "",
|
supervisee_name: selectedUser?.supervisee_name ?? "",
|
||||||
|
|
@ -50,11 +53,11 @@ const GeneralUserForm = ({ selectedUser, heading, onSave, onClickCallback }: Gen
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSubmit(values: z.infer<typeof UserSettingsSchema>) {
|
async function onSubmit(values: z.infer<typeof UserSettingsSchema>) {
|
||||||
onSave && onSave(values, "user");
|
onSave && onSave(values, "user", userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
onSave && onSave(form.getValues(), "user");
|
onSave && onSave(form.getValues(), "user", userId);
|
||||||
onClickCallback();
|
onClickCallback();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -113,11 +116,11 @@ const GeneralUserForm = ({ selectedUser, heading, onSave, onClickCallback }: Gen
|
||||||
required
|
required
|
||||||
placeholder="e.g. 8"
|
placeholder="e.g. 8"
|
||||||
{...field}
|
{...field}
|
||||||
// className="max-w-screen-sm h-10 bg-white"
|
|
||||||
// autoComplete="on"
|
|
||||||
// style={{
|
|
||||||
// fontSize: 16,
|
|
||||||
// }}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
@ -139,11 +142,11 @@ const GeneralUserForm = ({ selectedUser, heading, onSave, onClickCallback }: Gen
|
||||||
userFormPersonaPlaceholder
|
userFormPersonaPlaceholder
|
||||||
}
|
}
|
||||||
{...field}
|
{...field}
|
||||||
// className="max-w-screen-sm bg-white"
|
|
||||||
// autoComplete="on"
|
|
||||||
// style={{
|
|
||||||
// fontSize: 16,
|
|
||||||
// }}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
|
|
@ -154,12 +157,13 @@ const GeneralUserForm = ({ selectedUser, heading, onSave, onClickCallback }: Gen
|
||||||
</section>
|
</section>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
className="rounded-full w-fit mt-4"
|
className="rounded-full w-fit mt-4 flex flex-row items-center gap-2"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
Save settings
|
{disabled ? <Loader2 className="w-4 h-4 animate-spin" /> : <span>Save settings</span>}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import { createClient } from "@/utils/supabase/server";
|
import { createClient } from "@/utils/supabase/server";
|
||||||
import { getUserById } from "@/db/users";
|
|
||||||
import Steps from "../components/Onboarding/Steps";
|
import Steps from "../components/Onboarding/Steps";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
|
|
@ -9,11 +8,9 @@ export default async function Home() {
|
||||||
data: { user },
|
data: { user },
|
||||||
} = await supabase.auth.getUser();
|
} = await supabase.auth.getUser();
|
||||||
|
|
||||||
const dbUser = user ? await getUserById(supabase, user.id) : undefined;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{dbUser && <Steps selectedUser={dbUser} />}
|
<Steps userId={user?.id ?? ""} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { SupabaseClient } from "@supabase/supabase-js";
|
import { SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
import { updateUser } from "./users";
|
||||||
|
|
||||||
export const dbCheckUserCode = async (
|
export const dbCheckUserCode = async (
|
||||||
supabase: SupabaseClient,
|
supabase: SupabaseClient,
|
||||||
userCode: string
|
userCode: string,
|
||||||
) => {
|
) => {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("devices")
|
.from("devices")
|
||||||
|
|
@ -19,9 +20,12 @@ export const dbCheckUserCode = async (
|
||||||
export const updateDevice = async (
|
export const updateDevice = async (
|
||||||
supabase: SupabaseClient,
|
supabase: SupabaseClient,
|
||||||
device: Partial<IDevice>,
|
device: Partial<IDevice>,
|
||||||
device_id: string
|
device_id: string,
|
||||||
) => {
|
) => {
|
||||||
const { error } = await supabase.from("devices").update(device).eq("device_id", device_id);
|
const { error } = await supabase.from("devices").update(device).eq(
|
||||||
|
"device_id",
|
||||||
|
device_id,
|
||||||
|
);
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
@ -30,23 +34,29 @@ export const updateDevice = async (
|
||||||
export const addUserToDevice = async (
|
export const addUserToDevice = async (
|
||||||
supabase: SupabaseClient,
|
supabase: SupabaseClient,
|
||||||
userCode: string,
|
userCode: string,
|
||||||
userId: string
|
userId: string,
|
||||||
) => {
|
) => {
|
||||||
const { error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("devices")
|
.from("devices")
|
||||||
.update({ user_id: userId })
|
.update({ user_id: userId })
|
||||||
.eq("user_code", userCode);
|
.eq("user_code", userCode)
|
||||||
|
.select("*")
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
await updateUser(supabase, { device_id: data.device_id }, userId);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const doesUserHaveADevice = async (
|
export const doesUserHaveADevice = async (
|
||||||
supabase: SupabaseClient,
|
supabase: SupabaseClient,
|
||||||
userId: string
|
userId: string,
|
||||||
) => {
|
) => {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("devices")
|
.from("devices")
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { type SupabaseClient, type User } from "@supabase/supabase-js";
|
||||||
export const createUser = async (
|
export const createUser = async (
|
||||||
supabase: SupabaseClient,
|
supabase: SupabaseClient,
|
||||||
user: User,
|
user: User,
|
||||||
userProps: Partial<IUser>
|
userProps: Partial<IUser>,
|
||||||
) => {
|
) => {
|
||||||
// console.log("creating user", user, userProps);
|
// console.log("creating user", user, userProps);
|
||||||
|
|
||||||
|
|
@ -18,9 +18,10 @@ export const createUser = async (
|
||||||
supervisee_persona: "",
|
supervisee_persona: "",
|
||||||
personality_id: userProps.personality_id, // selecting default personality
|
personality_id: userProps.personality_id, // selecting default personality
|
||||||
session_time: 0,
|
session_time: 0,
|
||||||
avatar_url:
|
avatar_url: user.user_metadata?.avatar_url ??
|
||||||
user.user_metadata?.avatar_url ??
|
`/user_avatar/user_avatar_${
|
||||||
`/user_avatar/user_avatar_${Math.floor(Math.random() * 10)}.png`,
|
Math.floor(Math.random() * 10)
|
||||||
|
}.png`,
|
||||||
} as IUser,
|
} as IUser,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -31,7 +32,7 @@ export const createUser = async (
|
||||||
|
|
||||||
export const getSimpleUserById = async (
|
export const getSimpleUserById = async (
|
||||||
supabase: SupabaseClient,
|
supabase: SupabaseClient,
|
||||||
id: string
|
id: string,
|
||||||
) => {
|
) => {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("users")
|
.from("users")
|
||||||
|
|
@ -40,7 +41,7 @@ export const getSimpleUserById = async (
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log("error", error);
|
console.log("error in getSimpleUserById", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data as IUser | undefined;
|
return data as IUser | undefined;
|
||||||
|
|
@ -50,13 +51,13 @@ export const getUserById = async (supabase: SupabaseClient, id: string) => {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from("users")
|
.from("users")
|
||||||
.select(
|
.select(
|
||||||
`*, personality:personality_id(*), device:devices!users_device_id_fkey(device_id, volume)`
|
`*, personality:personality_id(*), device:devices!users_device_id_fkey(device_id, volume)`,
|
||||||
)
|
)
|
||||||
.eq("user_id", id)
|
.eq("user_id", id)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log("error", error);
|
console.log("error in getUserById", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return data as IUser | undefined;
|
return data as IUser | undefined;
|
||||||
|
|
@ -64,7 +65,7 @@ export const getUserById = async (supabase: SupabaseClient, id: string) => {
|
||||||
|
|
||||||
export const doesUserExist = async (
|
export const doesUserExist = async (
|
||||||
supabase: SupabaseClient,
|
supabase: SupabaseClient,
|
||||||
authUser: User
|
authUser: User,
|
||||||
) => {
|
) => {
|
||||||
const { data: user, error } = await supabase
|
const { data: user, error } = await supabase
|
||||||
.from("users")
|
.from("users")
|
||||||
|
|
@ -73,7 +74,7 @@ export const doesUserExist = async (
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.log("error", error);
|
console.log("error in doesUserExist", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!user;
|
return !!user;
|
||||||
|
|
@ -82,7 +83,7 @@ export const doesUserExist = async (
|
||||||
export const updateUser = async (
|
export const updateUser = async (
|
||||||
supabase: SupabaseClient,
|
supabase: SupabaseClient,
|
||||||
user: Partial<IUser>,
|
user: Partial<IUser>,
|
||||||
userId: string
|
userId: string,
|
||||||
) => {
|
) => {
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
.from("users")
|
.from("users")
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue