-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
646 additions
and
4,367 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,52 +1,27 @@ | ||
"use client"; | ||
|
||
import { Icons } from "@/components/icons"; | ||
import { UserAuthFormUI, type FormData } from "@/components/email-password-form"; | ||
import { Button } from "@/components/ui/button"; | ||
import { Input } from "@/components/ui/input"; | ||
import { Label } from "@/components/ui/label"; | ||
import { toast } from "@/components/ui/use-toast"; | ||
import { type Database } from "@/lib/schema"; | ||
import { cn } from "@/lib/utils"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"; | ||
import { useState, type BaseSyntheticEvent } from "react"; | ||
import { useForm } from "react-hook-form"; | ||
import { z } from "zod"; | ||
|
||
// Template: https://github.com/shadcn/taxonomy/blob/main/components/user-auth-form.tsx | ||
|
||
// Create Zod object schema with validations | ||
const userAuthSchema = z.object({ | ||
email: z.string().email(), | ||
}); | ||
|
||
// Use Zod to extract inferred type from schema | ||
type FormData = z.infer<typeof userAuthSchema>; | ||
|
||
export default function UserAuthForm({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) { | ||
// Create form with react-hook-form and use Zod schema to validate the form submission (with resolver) | ||
const { | ||
register, | ||
handleSubmit, | ||
formState: { errors }, | ||
} = useForm<FormData>({ | ||
resolver: zodResolver(userAuthSchema), | ||
}); | ||
import { useRouter } from "next/navigation"; | ||
import { useState } from "react"; | ||
|
||
export default function UserAuthForm() { | ||
const [isLoading, setIsLoading] = useState<boolean>(false); | ||
|
||
// Obtain supabase client from context provider | ||
const supabaseClient = createClientComponentClient<Database>(); | ||
const router = useRouter(); | ||
|
||
const onSubmit = async (input: FormData) => { | ||
setIsLoading(true); | ||
|
||
// Supabase magic link sign-in | ||
const { error } = await supabaseClient.auth.signInWithOtp({ | ||
const { error } = await supabaseClient.auth.signInWithPassword({ | ||
email: input.email.toLowerCase(), | ||
options: { | ||
emailRedirectTo: `${location.origin}/auth/callback`, | ||
}, | ||
password: input.password, | ||
}); | ||
|
||
setIsLoading(false); | ||
|
@@ -59,38 +34,29 @@ export default function UserAuthForm({ className, ...props }: React.HTMLAttribut | |
}); | ||
} | ||
|
||
return toast({ | ||
title: "Check your email", | ||
description: "We sent you a login link. Be sure to check your spam too.", | ||
}); | ||
router.refresh(); | ||
}; | ||
|
||
const OAuthSubmit = async () => { | ||
setIsLoading(true); | ||
|
||
const { error } = await supabaseClient.auth.signInWithOAuth({ provider: "google" }); | ||
|
||
setIsLoading(false); | ||
|
||
if (error) { | ||
return toast({ | ||
title: "Something went wrong.", | ||
description: error.message, | ||
variant: "destructive", | ||
}); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className={cn("grid gap-6", className)} {...props}> | ||
<form onSubmit={(e: BaseSyntheticEvent) => void handleSubmit(onSubmit)(e)}> | ||
<div className="grid gap-2"> | ||
<div className="grid gap-1"> | ||
<Label className="sr-only" htmlFor="email"> | ||
</Label> | ||
<Input | ||
id="email" | ||
placeholder="[email protected]" | ||
type="email" | ||
autoCapitalize="none" | ||
autoComplete="email" | ||
autoCorrect="off" | ||
disabled={isLoading} | ||
{...register("email")} | ||
/> | ||
{errors?.email && <p className="px-1 text-xs text-red-600">{errors.email.message}</p>} | ||
</div> | ||
<Button disabled={isLoading}> | ||
{isLoading && <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />} | ||
Sign In with Email | ||
</Button> | ||
</div> | ||
</form> | ||
<div className="grid gap-2"> | ||
<UserAuthFormUI _onSubmit={onSubmit} isLoading={isLoading} buttonDisplay="Sign In" /> | ||
<Button onClick={() => void OAuthSubmit()}>Sign in with Google</Button> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { LoginCard } from "@/components/login-card"; | ||
import { createServerSupabaseClient } from "@/lib/server-utils"; | ||
import { redirect } from "next/navigation"; | ||
import UserAuthForm from "./user-auth-form"; | ||
|
||
export default async function LoginPage() { | ||
// Create supabase server component client and obtain user session from stored cookie | ||
const supabase = createServerSupabaseClient(); | ||
const { | ||
data: { session }, | ||
} = await supabase.auth.getSession(); | ||
|
||
if (session) { | ||
// Users who are already signed in should be redirected to dashboard | ||
redirect("/dashboard"); | ||
} | ||
|
||
return ( | ||
<LoginCard className="sm:w-[550px]"> | ||
<div className="flex flex-col space-y-2 text-center"> | ||
<h1 className="text-2xl font-semibold tracking-tight">Sign Up</h1> | ||
<p className="text-sm text-muted-foreground">Enter your email and password below to sign up</p> | ||
</div> | ||
<UserAuthForm /> | ||
</LoginCard> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
"use client"; | ||
|
||
import { UserAuthFormUI, type FormData } from "@/components/signup-form"; | ||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; | ||
import { toast } from "@/components/ui/use-toast"; | ||
import { type Database } from "@/lib/schema"; | ||
import { createClientComponentClient } from "@supabase/auth-helpers-nextjs"; | ||
import { useState } from "react"; | ||
|
||
export default function UserAuthForm() { | ||
const [isLoading, setIsLoading] = useState<boolean>(false); | ||
|
||
// Obtain supabase client from context provider | ||
const supabaseClient = createClientComponentClient<Database>(); | ||
|
||
const onSubmit = async (input: FormData) => { | ||
setIsLoading(true); | ||
let data; | ||
switch (input.type) { | ||
case "patient": | ||
data = { | ||
first_name: input.first_name, | ||
last_name: input.last_name, | ||
age: input.age, | ||
state: input.last_name, | ||
city: input.city, | ||
zipcode: input.zipcode, | ||
type: input.type, | ||
}; | ||
break; | ||
case "clinician": | ||
data = { | ||
first_name: input.first_name, | ||
last_name: input.last_name, | ||
employer: input.employer, | ||
state: input.last_name, | ||
city: input.city, | ||
zipcode: input.zipcode, | ||
type: input.type, | ||
}; | ||
break; | ||
} | ||
const { error } = await supabaseClient.auth.signUp({ | ||
email: input.email.toLowerCase(), | ||
password: input.password, | ||
options: { | ||
data: data, | ||
emailRedirectTo: `${location.origin}/auth/callback`, | ||
}, | ||
}); | ||
|
||
setIsLoading(false); | ||
|
||
if (error) { | ||
return toast({ | ||
title: "Something went wrong.", | ||
description: error.message, | ||
variant: "destructive", | ||
}); | ||
} | ||
|
||
return toast({ | ||
title: "Check your email", | ||
description: "We sent you a confirmation email.", | ||
}); | ||
}; | ||
|
||
return ( | ||
<Tabs defaultValue="patient" className="grid gap-6"> | ||
<TabsList className="grid w-full grid-cols-2"> | ||
<TabsTrigger disabled={isLoading} value="patient"> | ||
Patient | ||
</TabsTrigger> | ||
<TabsTrigger disabled={isLoading} value="clinician"> | ||
Clinician | ||
</TabsTrigger> | ||
</TabsList> | ||
<TabsContent value="patient"> | ||
<UserAuthFormUI _onSubmit={onSubmit} isLoading={isLoading} patient /> | ||
</TabsContent> | ||
<TabsContent value="clinician"> | ||
<UserAuthFormUI _onSubmit={onSubmit} isLoading={isLoading} patient={false} /> | ||
</TabsContent> | ||
</Tabs> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
import { Icons } from "@/components/icons"; | ||
import { Button } from "@/components/ui/button"; | ||
import { Input } from "@/components/ui/input"; | ||
import { Label } from "@/components/ui/label"; | ||
import { cn } from "@/lib/utils"; | ||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { type BaseSyntheticEvent } from "react"; | ||
import { useForm, type SubmitHandler } from "react-hook-form"; | ||
import { z } from "zod"; | ||
|
||
// Template: https://github.com/shadcn/taxonomy/blob/main/components/user-auth-form.tsx | ||
|
||
// Create Zod object schema with validations | ||
const userAuthSchema = z.object({ | ||
email: z.string().email(), | ||
password: z.string(), | ||
}); | ||
|
||
const patientAuthSchema = z.object({ | ||
email: z.string().email(), | ||
password: z.string(), | ||
first_name: z.string(), | ||
last_name: z.string(), | ||
age: z.number().gte(0).optional(), | ||
state: z.string().optional(), | ||
city: z.string().optional(), | ||
zipcode: z.string().optional(), | ||
}); | ||
|
||
const clinicianAuthSchema = z.object({ | ||
email: z.string().email(), | ||
password: z.string(), | ||
first_name: z.string(), | ||
last_name: z.string(), | ||
employer: z.string().optional(), | ||
state: z.string().optional(), | ||
city: z.string().optional(), | ||
zipcode: z.string().optional(), | ||
}); | ||
|
||
// Use Zod to extract inferred type from schema | ||
// export type FormData = z.infer<typeof userAuthSchema>; | ||
|
||
export type FormData = | ||
| z.infer<typeof userAuthSchema> | ||
| z.infer<typeof patientAuthSchema> | ||
| z.infer<typeof clinicianAuthSchema>; | ||
|
||
interface UserAuthForm extends React.HTMLAttributes<HTMLDivElement> { | ||
_onSubmit: SubmitHandler<FormData>; | ||
isLoading: boolean; | ||
buttonDisplay: string; | ||
} | ||
|
||
export function UserAuthFormUI({ _onSubmit, isLoading, buttonDisplay, className, ...props }: UserAuthForm) { | ||
// Create form with react-hook-form and use Zod schema to validate the form submission (with resolver) | ||
const { | ||
register, | ||
handleSubmit, | ||
formState: { errors }, | ||
} = useForm<FormData>({ | ||
resolver: zodResolver(userAuthSchema), | ||
}); | ||
|
||
return ( | ||
<div className={cn("grid gap-6", className)} {...props}> | ||
<form onSubmit={(e: BaseSyntheticEvent) => void handleSubmit(_onSubmit)(e)}> | ||
<div className="grid gap-2"> | ||
<div className="grid gap-1"> | ||
<Label className="sr-only" htmlFor="email"> | ||
</Label> | ||
<Input | ||
id="email" | ||
className="border-black" | ||
placeholder="Username" | ||
type="email" | ||
autoCapitalize="none" | ||
autoComplete="email" | ||
autoCorrect="off" | ||
disabled={isLoading} | ||
{...register("email")} | ||
/> | ||
{errors?.email && <p className="px-1 text-xs text-red-600">{errors.email.message}</p>} | ||
</div> | ||
<div className="grid gap-1"> | ||
<Label className="sr-only" htmlFor="password"> | ||
Password | ||
</Label> | ||
<Input | ||
id="password" | ||
className="border-black" | ||
placeholder="Password" | ||
type="password" | ||
autoCapitalize="none" | ||
autoComplete="none" | ||
autoCorrect="off" | ||
disabled={isLoading} | ||
{...register("password")} | ||
/> | ||
{errors?.password && <p className="px-1 text-xs text-red-600">{errors.password.message}</p>} | ||
</div> | ||
<Button className="border-black" variant="outline" disabled={isLoading}> | ||
{isLoading && <Icons.spinner className="mr-2 h-4 w-4 animate-spin" />} | ||
{buttonDisplay} | ||
</Button> | ||
</div> | ||
</form> | ||
</div> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import React from "react"; | ||
|
||
interface CardProps { | ||
children: React.ReactNode; | ||
className?: string; | ||
} | ||
|
||
export const LoginCard: React.FC<CardProps> = ({ className, children }) => { | ||
const cardClasses = `mx-auto flex w-full flex-col justify-center space-y-6 rounded border-2 border-black px-12 py-8 sm:w-[350px] ${ | ||
className ?? "" | ||
}`; | ||
return <div className={cardClasses}>{children}</div>; | ||
}; |
Oops, something went wrong.