Skip to content

Commit

Permalink
Merge branch 'main' into background
Browse files Browse the repository at this point in the history
  • Loading branch information
AEst2002 authored Oct 29, 2023
2 parents 61726bc + 7303eaa commit 678b160
Show file tree
Hide file tree
Showing 14 changed files with 646 additions and 4,367 deletions.
16 changes: 12 additions & 4 deletions app/login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { LoginCard } from "@/components/login-card";
import { createServerSupabaseClient } from "@/lib/server-utils";
import Link from "next/link";
import { redirect } from "next/navigation";
import UserAuthForm from "./user-auth-form";

Expand All @@ -15,12 +17,18 @@ export default async function LoginPage() {
}

return (
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<LoginCard className="sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<h1 className="text-2xl font-semibold tracking-tight">Sign up/log in</h1>
<p className="text-sm text-muted-foreground">Enter your email below to sign in or create a new account</p>
<h1 className="text-2xl font-semibold tracking-tight">Kinnect</h1>
</div>
<UserAuthForm />
</div>
<div className="flex flex-col space-y-1 text-center">
<p className="text-xs text-muted-foreground">
<Link className="text-base underline" href="/signup">
New user? Create an account.
</Link>{" "}
</p>
</div>
</LoginCard>
);
}
88 changes: 27 additions & 61 deletions app/login/user-auth-form.tsx
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);
Expand All @@ -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">
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>
);
}
27 changes: 27 additions & 0 deletions app/signup/page.tsx
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>
);
}
86 changes: 86 additions & 0 deletions app/signup/user-auth-form.tsx
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>
);
}
111 changes: 111 additions & 0 deletions components/email-password-form.tsx
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">
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>
);
}
13 changes: 13 additions & 0 deletions components/login-card.tsx
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>;
};
Loading

0 comments on commit 678b160

Please sign in to comment.