-
Notifications
You must be signed in to change notification settings - Fork 0
feat: update auth screens UI #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0f1bec2
9403570
a057b10
87b105c
9aac4c7
61192d6
bda2bae
8b474c4
a4cc51f
66137c0
9e97af0
6f6448b
98ecafe
116b00d
441c87c
4c60662
a1a0d49
f5a804c
d0709b0
8758cb7
39fbc85
3f4290f
2cc3e1c
91f17c9
dedee60
e52454b
4aab02a
90572e3
df1e1b3
1eb34c5
92a9c86
e706ff1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| "use client" | ||
|
|
||
| import { Lock } from "lucide-react" | ||
| import Link from "next/link" | ||
| import { Controller, useForm } from "react-hook-form" | ||
| import { zodResolver } from "@hookform/resolvers/zod" | ||
| import * as z from "zod" | ||
|
|
||
| import { cn } from "@/shared/utils" | ||
| import { Button } from "@/shared/ui/button" | ||
| import { | ||
| Field, | ||
| FieldDescription, | ||
| FieldError, | ||
| FieldGroup, | ||
| FieldLabel, | ||
| FieldSeparator, | ||
| } from "@/shared/ui/field" | ||
| import { Input } from "@/shared/ui/input" | ||
|
|
||
| const codeSchema = z.object({ | ||
| code: z | ||
| .string() | ||
| .trim() | ||
| .regex(/^\d{6}$/, { message: "Enter the 6-digit code" }), | ||
| }) | ||
|
|
||
| type AuthCodeFormProps = React.ComponentProps<"div"> & { | ||
| email: string | ||
| mode: "login" | "signup" | ||
| onDifferentAccount?: () => void | ||
| onResendCode?: () => void | ||
| onSubmitCode?: (code: string) => void | Promise<void> | ||
| } | ||
|
|
||
| export function AuthCodeForm({ | ||
| className, | ||
| email, | ||
| mode, | ||
| onDifferentAccount, | ||
| onResendCode, | ||
| onSubmitCode, | ||
| ...props | ||
| }: AuthCodeFormProps) { | ||
| const form = useForm<z.infer<typeof codeSchema>>({ | ||
| resolver: zodResolver(codeSchema), | ||
| defaultValues: { | ||
| code: "", | ||
| }, | ||
| }) | ||
|
|
||
| const actionLabel = mode === "login" ? "Log in" : "Sign up" | ||
| const flowLabel = mode === "login" ? "login" : "sign up" | ||
| const switchLabel = | ||
| mode === "login" | ||
| ? "Log in to a different account" | ||
| : "Sign up with a different account" | ||
|
|
||
| const onSubmit = async (data: z.infer<typeof codeSchema>) => { | ||
| await onSubmitCode?.(data.code) | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| className={cn("flex w-full flex-col items-center gap-[45px]", className)} | ||
| {...props} | ||
| > | ||
| <div className="flex w-full flex-col items-center gap-[30px] text-center"> | ||
| <h1 className="text-4xl leading-normal font-extrabold text-ma-text"> | ||
| Check your inbox | ||
| </h1> | ||
| <p className="max-w-[442px] text-lg leading-normal text-[#6b7280]"> | ||
| Enter the 6-digit code we sent to{" "} | ||
| <span className="font-medium text-ma-text">{email}</span> to finish | ||
| your {flowLabel}. | ||
| </p> | ||
| </div> | ||
|
|
||
| <form className="w-full" onSubmit={form.handleSubmit(onSubmit)}> | ||
| <FieldGroup className="gap-5"> | ||
| <Controller | ||
| control={form.control} | ||
| name="code" | ||
| render={({ fieldState, field }) => ( | ||
| <Field data-invalid={fieldState.invalid}> | ||
| <FieldLabel htmlFor={field.name} className="sr-only"> | ||
| 6-digit code | ||
| </FieldLabel> | ||
| <div className="relative"> | ||
| <Input | ||
| {...field} | ||
| id={field.name} | ||
| inputMode="numeric" | ||
| autoComplete="one-time-code" | ||
| aria-invalid={fieldState.invalid} | ||
| placeholder="6-digit code" | ||
| className="h-11 rounded-md border-[#6b7280] px-5 py-5 pr-14 text-lg placeholder:text-[#6b7280]" | ||
| /> | ||
| <Lock | ||
| className="pointer-events-none absolute top-1/2 right-5 size-6 -translate-y-1/2 text-[#6b7280]" | ||
| aria-hidden="true" | ||
| /> | ||
| </div> | ||
| <FieldError errors={[fieldState.error]} /> | ||
| </Field> | ||
| )} | ||
| /> | ||
|
|
||
| <Button | ||
| type="submit" | ||
| className="group relative h-[53px] w-full overflow-hidden rounded-[60px] bg-ma-text px-5 py-4 text-base font-semibold text-white" | ||
| > | ||
| <span className="relative z-10">{actionLabel}</span> | ||
| <div className="pointer-events-none absolute inset-0 rounded-[60px] bg-gradient-to-r from-ma-glow-blue to-ma-glow-violet opacity-0 transition-opacity duration-500 group-hover:opacity-100" /> | ||
| </Button> | ||
|
|
||
| <FieldSeparator className="my-0 w-full [&_[data-slot=field-separator-content]]:bg-white"> | ||
| <button | ||
| type="button" | ||
| onClick={onResendCode} | ||
| className="text-lg leading-normal font-medium text-ma-text underline underline-offset-2" | ||
| > | ||
| Resend code | ||
| </button> | ||
| </FieldSeparator> | ||
| </FieldGroup> | ||
| </form> | ||
|
|
||
| <div className="flex w-full items-center justify-center rounded-md bg-[#f5f5f5] px-[30px] py-[50px]"> | ||
| {onDifferentAccount ? ( | ||
| <button | ||
| type="button" | ||
| onClick={onDifferentAccount} | ||
| className="text-lg leading-normal font-semibold text-ma-text underline underline-offset-2" | ||
| > | ||
| {switchLabel} | ||
| </button> | ||
| ) : ( | ||
| <Link | ||
| href={mode === "login" ? "/login" : "/signup"} | ||
| className="text-lg leading-normal font-semibold text-ma-text underline underline-offset-2" | ||
| > | ||
| {switchLabel} | ||
| </Link> | ||
| )} | ||
| </div> | ||
|
|
||
| <FieldDescription className="sr-only"> | ||
| Use the code sent to your email address to continue. | ||
| </FieldDescription> | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Button } from "@/shared/ui/button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function GoogleMark() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <svg | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| aria-hidden="true" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| viewBox="0 0 18 18" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="size-4" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| focusable="false" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fill="#4285f4" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 0 1-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62Z" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fill="#34a853" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.8.54-1.84.86-3.04.86-2.34 0-4.32-1.58-5.03-3.7H.96v2.34A9 9 0 0 0 9 18Z" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fill="#fbbc05" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| d="M3.97 10.72A5.41 5.41 0 0 1 3.68 9c0-.6.1-1.18.29-1.72V4.94H.96A9 9 0 0 0 0 9c0 1.45.35 2.82.96 4.06l3.01-2.34Z" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fill="#ea4335" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| d="M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.58A8.65 8.65 0 0 0 9 0 9 9 0 0 0 .96 4.94l3.01 2.34C4.68 5.16 6.66 3.58 9 3.58Z" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </svg> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function AuthGoogleButton({ label }: { label: string }) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Button | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="button" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| variant="outline" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="h-[53px] w-full gap-2.5 rounded-[60px] border-[#d9d9d9] bg-white px-5 py-4 text-base font-medium text-ma-text hover:bg-[#f5f5f5]" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <GoogleMark /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {label} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+31
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Expose a Google auth action. This renders an inert Proposed prop forwarding+import type { ComponentProps } from "react"
import { Button } from "`@/shared/ui/button`"
+import { cn } from "`@/shared/utils`"
@@
-export function AuthGoogleButton({ label }: { label: string }) {
+type AuthGoogleButtonProps = ComponentProps<typeof Button> & {
+ label: string
+}
+
+export function AuthGoogleButton({
+ label,
+ className,
+ type = "button",
+ ...props
+}: AuthGoogleButtonProps) {
return (
<Button
- type="button"
+ type={type}
variant="outline"
- className="h-[53px] w-full gap-2.5 rounded-[60px] border-[`#d9d9d9`] bg-white px-5 py-4 text-base font-medium text-ma-text hover:bg-[`#f5f5f5`]"
+ className={cn(
+ "h-[53px] w-full gap-2.5 rounded-[60px] border-[`#d9d9d9`] bg-white px-5 py-4 text-base font-medium text-ma-text hover:bg-[`#f5f5f5`]",
+ className,
+ )}
+ {...props}
>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Prevent auth actions from silently no-oping.
onSubmitCodeandonResendCodeare optional, and the current login/signup callers omit them. That means submitting a valid code and clicking “Resend code” can complete with no verification or resend request. Make these callbacks required, or render an explicit disabled/error state until they are provided.Proposed contract tightening
type AuthCodeFormProps = React.ComponentProps<"div"> & { email: string mode: "login" | "signup" onDifferentAccount?: () => void - onResendCode?: () => void - onSubmitCode?: (code: string) => void | Promise<void> + onResendCode: () => void | Promise<void> + onSubmitCode: (code: string) => void | Promise<void> } @@ const onSubmit = async (data: z.infer<typeof codeSchema>) => { - await onSubmitCode?.(data.code) + await onSubmitCode(data.code) }Also applies to: 59-60, 117-123
🤖 Prompt for AI Agents