-
Notifications
You must be signed in to change notification settings - Fork 265
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #652 from crystal4000/feat/Authentication/Login
Feat/authentication/login
- Loading branch information
Showing
11 changed files
with
785 additions
and
63 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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
44 changes: 44 additions & 0 deletions
44
src/app/(auth-routes)/login/magic-link/link-sent/page.test.tsx
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,44 @@ | ||
import { render, screen } from "@testing-library/react"; | ||
import { describe, expect, it, vi } from "vitest"; | ||
|
||
import MagicLinkSuccess from "./page"; | ||
|
||
type ModuleType = typeof import("./page"); | ||
|
||
vi.mock("./page", async () => { | ||
const actualModule: ModuleType = await vi.importActual("./page"); | ||
return { | ||
default: actualModule.default, | ||
handleOpenEmail: vi.fn(), | ||
}; | ||
}); | ||
|
||
describe("magicLinkSuccess", () => { | ||
it("renders the success message", () => { | ||
expect.hasAssertions(); | ||
render(<MagicLinkSuccess />); | ||
expect(screen.getByText("Sent! Check your email.")).toBeInTheDocument(); | ||
}); | ||
|
||
it("displays the check icon", () => { | ||
expect.hasAssertions(); | ||
render(<MagicLinkSuccess />); | ||
const checkIcon = screen.getByTestId("check-icon"); | ||
expect(checkIcon).toBeInTheDocument(); | ||
expect(checkIcon).toHaveClass("h-20 w-20 text-center text-white"); | ||
}); | ||
|
||
it("shows the email address in the instructions", () => { | ||
expect.hasAssertions(); | ||
render(<MagicLinkSuccess />); | ||
expect(screen.getByText(/[email protected]/)).toBeInTheDocument(); | ||
}); | ||
|
||
it('renders the "Open Email" button', () => { | ||
expect.hasAssertions(); | ||
render(<MagicLinkSuccess />); | ||
expect( | ||
screen.getByRole("button", { name: "Open Email" }), | ||
).toBeInTheDocument(); | ||
}); | ||
}); |
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,45 @@ | ||
"use client"; | ||
|
||
import { Check } from "lucide-react"; | ||
|
||
import { Button } from "~/components/ui/button"; | ||
|
||
export const handleOpenEmail = () => { | ||
window.location.href = "mailto:"; | ||
}; | ||
const MagicLinkSuccess = () => { | ||
return ( | ||
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-0 sm:px-6 lg:px-8"> | ||
<div className="w-full max-w-md space-y-8"> | ||
<div className="text-center"> | ||
<h1 className="font-inter text-neutralColor-dark-2 mb-5 text-center text-2xl font-medium leading-tight"> | ||
Sent! Check your email. | ||
</h1> | ||
|
||
<div className="mx-auto mt-5 flex h-24 w-24 items-center justify-center rounded-full bg-success"> | ||
<Check | ||
className="mx-auto h-20 w-20 text-center text-white" | ||
data-testid="check-icon" | ||
/> | ||
</div> | ||
|
||
<p className="font-inter text-neutralColor-dark-2 mt-8 text-center text-base font-normal leading-6"> | ||
We have sent an email to [email protected]. It contains | ||
instructions on how to get started. | ||
</p> | ||
|
||
<Button | ||
variant="outline" | ||
type="button" | ||
onClick={handleOpenEmail} | ||
className="mt-10 h-12 w-full rounded-md border border-primary bg-white px-4 py-3 text-primary hover:bg-orange-600 hover:text-white focus:outline-none" | ||
> | ||
Open Email | ||
</Button> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default MagicLinkSuccess; |
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,75 @@ | ||
import { fireEvent, render, screen } from "@testing-library/react"; | ||
import { useRouter } from "next/navigation"; | ||
import { beforeEach, describe, expect, it, vi } from "vitest"; | ||
|
||
import { useToast } from "~/components/ui/use-toast"; | ||
import LoginMagicLink from "./page"; | ||
|
||
vi.mock("next/navigation", () => ({ | ||
useRouter: vi.fn(), | ||
})); | ||
|
||
vi.mock("~/components/ui/use-toast", () => ({ | ||
useToast: vi.fn(), | ||
})); | ||
|
||
describe("loginMagicLink", () => { | ||
const mockPush = vi.fn(); | ||
const mockToast = vi.fn(); | ||
|
||
beforeEach(() => { | ||
vi.clearAllMocks(); | ||
(useRouter as jest.Mock).mockReturnValue({ push: mockPush }); | ||
(useToast as jest.Mock).mockReturnValue({ toast: mockToast }); | ||
}); | ||
|
||
it("renders the login form", () => { | ||
expect.hasAssertions(); | ||
render(<LoginMagicLink />); | ||
expect(screen.getByText("Login With Magic Link")).toBeInTheDocument(); | ||
expect( | ||
screen.getByPlaceholderText("Enter Email Address"), | ||
).toBeInTheDocument(); | ||
expect( | ||
screen.getByRole("button", { name: "Get Magic Link" }), | ||
).toBeInTheDocument(); | ||
}); | ||
|
||
it("displays an error message for invalid email", async () => { | ||
expect.hasAssertions(); | ||
render(<LoginMagicLink />); | ||
const emailInput = screen.getByPlaceholderText("Enter Email Address"); | ||
const submitButton = screen.getByRole("button", { name: "Get Magic Link" }); | ||
|
||
fireEvent.change(emailInput, { target: { value: "invalid-email" } }); | ||
fireEvent.click(submitButton); | ||
|
||
// eslint-disable-next-line testing-library/await-async-utils | ||
vi.waitFor(() => { | ||
const emailError = screen.queryByTestId("email-error"); | ||
expect(emailError).toBeInTheDocument(); | ||
}); | ||
}); | ||
|
||
it("submits the form with valid email and redirects", async () => { | ||
expect.hasAssertions(); | ||
render(<LoginMagicLink />); | ||
const emailInput = screen.getByPlaceholderText("Enter Email Address"); | ||
const submitButton = screen.getByRole("button", { name: "Get Magic Link" }); | ||
|
||
fireEvent.change(emailInput, { target: { value: "[email protected]" } }); | ||
fireEvent.click(submitButton); | ||
|
||
// eslint-disable-next-line testing-library/await-async-utils | ||
vi.waitFor(() => { | ||
expect(mockPush).toHaveBeenCalledWith("/login/magic-link/link-sent"); | ||
}); | ||
}); | ||
|
||
it("renders the terms and privacy policy links", () => { | ||
expect.hasAssertions(); | ||
render(<LoginMagicLink />); | ||
expect(screen.getByText("Terms of Service")).toBeInTheDocument(); | ||
expect(screen.getByText("Privacy Policy")).toBeInTheDocument(); | ||
}); | ||
}); |
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,142 @@ | ||
"use client"; | ||
|
||
import { zodResolver } from "@hookform/resolvers/zod"; | ||
import { ShieldCheck } from "lucide-react"; | ||
import { useRouter } from "next/navigation"; | ||
import React from "react"; | ||
import { useForm } from "react-hook-form"; | ||
import { z } from "zod"; | ||
|
||
import { Button } from "~/components/ui/button"; | ||
import { | ||
Form, | ||
FormControl, | ||
FormField, | ||
FormItem, | ||
FormMessage, | ||
} from "~/components/ui/form"; | ||
import { Input } from "~/components/ui/input"; | ||
import { Label } from "~/components/ui/label"; | ||
import { useToast } from "~/components/ui/use-toast"; | ||
|
||
const formSchema = z.object({ | ||
email: z.string().email("Please enter a valid email address"), | ||
}); | ||
|
||
type FormValues = z.infer<typeof formSchema>; | ||
|
||
const getInputClassName = (hasError: boolean, isValid: boolean) => { | ||
const baseClasses = | ||
"font-inter w-full rounded-md border px-3 py-3 text-sm font-normal leading-[21.78px] transition duration-150 ease-in-out focus:outline-none focus:ring-1 focus:ring-opacity-50"; | ||
|
||
if (hasError) { | ||
return `${baseClasses} border-red-500 focus:border-red-500 focus:ring-red-500 text-red-900`; | ||
} else if (isValid) { | ||
return `${baseClasses} border-orange-500 focus:border-orange-500 focus:ring-orange-500 text-neutralColor-dark-2`; | ||
} | ||
return `${baseClasses} border-gray-300 focus:border-orange-500 focus:ring-orange-500 text-neutralColor-dark-2`; | ||
}; | ||
|
||
const LoginMagicLink: React.FC = () => { | ||
const router = useRouter(); | ||
const { toast } = useToast(); | ||
const form = useForm<FormValues>({ | ||
resolver: zodResolver(formSchema), | ||
defaultValues: { | ||
email: "", | ||
}, | ||
}); | ||
|
||
const onSubmit = async () => { | ||
try { | ||
await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
router.push("/login/magic-link/link-sent"); | ||
} catch (error: unknown) { | ||
toast({ | ||
title: "Login failed", | ||
description: | ||
error instanceof Error ? error.message : "An unknown error occurred", | ||
variant: "destructive", | ||
}); | ||
} | ||
}; | ||
|
||
return ( | ||
<div className="flex min-h-screen items-center justify-center bg-gray-50 px-0 sm:px-6 lg:px-8"> | ||
<div className="w-full max-w-md space-y-8"> | ||
<div className="text-center"> | ||
<h1 className="font-inter text-neutralColor-dark-2 mb-5 text-center text-2xl font-semibold leading-tight"> | ||
Login With Magic Link | ||
</h1> | ||
</div> | ||
|
||
<Form {...form}> | ||
<form | ||
onSubmit={form.handleSubmit(onSubmit)} | ||
className="mt-8 space-y-6" | ||
> | ||
<FormField | ||
control={form.control} | ||
name="email" | ||
render={({ field }) => ( | ||
<FormItem> | ||
<Label | ||
htmlFor="email" | ||
className="text-neutralColor-dark-2 sr-only" | ||
> | ||
</Label> | ||
<FormControl> | ||
<Input | ||
id="email" | ||
type="email" | ||
placeholder="Enter Email Address" | ||
className={getInputClassName( | ||
!!form.formState.errors.email, | ||
form.formState.isValid, | ||
)} | ||
{...field} | ||
/> | ||
</FormControl> | ||
<FormMessage | ||
className="mt-1 text-sm" | ||
data-testid="email-error" | ||
/> | ||
</FormItem> | ||
)} | ||
/> | ||
|
||
<Button | ||
type="submit" | ||
variant="default" | ||
size="default" | ||
className="h-12 w-full rounded-md bg-primary px-4 py-3 text-white hover:bg-orange-600 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:ring-offset-2" | ||
> | ||
Get Magic Link | ||
</Button> | ||
</form> | ||
</Form> | ||
|
||
<p className="mt-2 text-center text-xs text-gray-500"> | ||
<ShieldCheck className="mr-1 hidden h-4 w-4 text-gray-400 sm:inline-block" /> | ||
By logging in, you agree to the{" "} | ||
<a | ||
href="#" | ||
className="text-sm font-bold text-primary hover:text-orange-500" | ||
> | ||
Terms of Service | ||
</a>{" "} | ||
and{" "} | ||
<a | ||
href="#" | ||
className="text-sm font-bold text-primary hover:text-orange-500" | ||
> | ||
Privacy Policy | ||
</a> | ||
</p> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default LoginMagicLink; |
Oops, something went wrong.