From 934859156478d5344e1b4b621dea00bf07e21476 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 19 Jan 2024 13:19:24 +0100 Subject: [PATCH 1/4] IPK-117 Initial error functionality given --- src/app/login/page.tsx | 138 ++++++++++++++++---------------- src/lib/actions.ts | 175 +++++++++++++++++++++++++++++++---------- 2 files changed, 204 insertions(+), 109 deletions(-) diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 9b5ae89..7a16374 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,5 +1,5 @@ "use client"; -import { login } from "@/services/authService"; +import { loginUser } from "@/lib/actions"; import { useDispatch } from "react-redux"; import Link from "next/link"; import { Separator } from "@/components/ui/separator"; @@ -13,28 +13,54 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useForm } from "react-hook-form"; import * as z from "zod"; import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form"; -import { useState } from "react"; -import { FetchEventResult } from "next/dist/server/web/types"; +import { useEffect, useState } from "react"; +import { useFormStatus, useFormState } from "react-dom"; -const formSchema = z.object({ +export const LoginFormSchema = z.object({ email: z.string().email({ message: "Invalid email" }).min(5), password: z.string().min(8, { message: "Password must be at least 8 characters long" }) }); +export function SubmitButton() { + const { pending } = useFormStatus(); + + return ( + + ); +} + const LoginForm = () => { - const dispatch = useDispatch(); + const form = useForm>({ + resolver: zodResolver(LoginFormSchema), + defaultValues: { + email: "", + password: "" + } + }); const [loading, setLoading] = useState(false); const [error, setError] = useState(""); - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { + const [formState, formAction] = useFormState(loginUser, { + message: "", + errors: undefined, + fieldValues: { email: "", password: "" } }); - async function onSubmit(values: z.infer) { + useEffect(() => { + if (formState.message === "success") { + setLoading(false); + setError(""); + } + }, [formState]); + + /* + async function onSubmit(values: z.infer) { setLoading(true); try { const result = (await login(dispatch, values)) as PromiseFulfilledResult; @@ -46,7 +72,7 @@ const LoginForm = () => { setLoading(false); throw error; } - } + } */ return (
@@ -83,65 +109,43 @@ const LoginForm = () => {
-
- - ( - - - - - - - )} - /> - ( - - - - - - - )} - /> - {error &&

{error}

} -
-
- -
- -
+ + + + + + {error &&

{error}

} +
+
+ +
+
- - Forgot password? -
- - - + + Forgot password? + +
+ + +

{formState?.message}

+

{formState?.errors?.email}

+

{formState?.errors?.password}

diff --git a/src/lib/actions.ts b/src/lib/actions.ts index ca896de..b1dddfe 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -1,6 +1,8 @@ "use server"; +import { LoginFormSchema } from "@/app/login/page"; import { cookies } from "next/headers"; +import { ZodError, z } from "zod"; interface StoreTokenRequest { token: string; @@ -34,59 +36,148 @@ export async function deleteToken() { } export async function refreshAccessToken() { - console.log("refreshing access token"); - if (!cookies().get("accessToken")) { - return { status: 401, message: "No access token" }; - } - await fetch("http://localhost:8080/api/test/refresh", { - method: "POST", - headers: { - "Content-Type": "application/json" - /* Authorization: cookies().get("accessToken")?.value */ + try { + if (!cookies().get("accessToken")) { + return { status: 401, message: "No access token" }; } - /* body: JSON.stringify({ refreshToken: cookies().get("refreshToken") }) */ - }) - .then((response) => { - if (!response.ok) { - throw new Error("Network error"); - } - return response.json(); - }) - .then((data) => { - if (data.accessToken) { - storeToken({ token: data.accessToken, refresh_token: data.refreshToken }); - return data; + await fetch("http://localhost:8080/api/test/refresh", { + method: "POST", + headers: { + "Content-Type": "application/json" + /* Authorization: cookies().get("accessToken")?.value */ } + /* body: JSON.stringify({ refreshToken: cookies().get("refreshToken") }) */ }) - .catch((error) => { - console.error("There was a problem with the Fetch operation:", error); - }); + .then((response) => { + if (!response.ok) { + throw new Error("Network error"); + } + return response.json(); + }) + .then((data) => { + if (data.accessToken) { + storeToken({ token: data.accessToken, refresh_token: data.refreshToken }); + return data; + } + }); + } catch (error) { + console.error("There was a problem with the Fetch operation: ", error); + } } export async function getUser() { - if (!cookies().get("accessToken")) { - return { status: 401, message: "No access token" }; - } - await fetch("http://localhost:8080/api/test/user", { - method: "GET", - headers: { - "Content-Type": "application/json" + try { + if (!cookies().get("accessToken")) { + return { status: 401, message: "No access token" }; } - }) - .then((response) => { - if (!response.ok) { - throw new Error("Network error"); + await fetch("http://localhost:8080/api/test/user", { + method: "GET", + headers: { + "Content-Type": "application/json" } - return response.json(); - }) - .then((data) => { - return data; }) - .catch((error) => { - console.error("There was a problem with the Fetch operation:", error); - }); + .then((response) => { + if (!response.ok) { + throw new Error("Network error"); + } + return response.json(); + }) + .then((data) => { + return data; + }); + } catch (error) { + console.error("There was a problem with the Fetch operation: ", error); + } } export async function getAccessToken() { return cookies().get("accessToken")?.value; } + +export type Fields = { + email: string; + password: string; +}; + +export type FormState = { + message: string; + errors: Record | undefined; + fieldValues: Fields; +}; + +export async function loginUser(prevState: FormState, formData: FormData): Promise { + const email = formData.get("email") as string; + const password = formData.get("password") as string; + const schema = z.object({ + email: z.string().email(), + password: z.string().min(8) + }); + const parse = schema.safeParse({ + email: formData.get("email"), + password: formData.get("password") + }); + + if (!parse.success) { + return { + message: "error", + errors: { + email: parse.error.flatten().fieldErrors["email"]?.[0] ?? "", + password: parse.error.flatten().fieldErrors["password"]?.[0] ?? "" + }, + fieldValues: { email, password } + }; + } + const data = parse.data; + + try { + return { + message: "Success", + errors: undefined, + fieldValues: { + email: "", + password: "" + } + }; + } catch (error) { + const zodError = error as ZodError; + const errorMap = zodError.flatten().fieldErrors; + return { + message: "error", + errors: { email: errorMap["email"]?.[0] ?? "", password: errorMap["password"]?.[0] ?? "" }, + fieldValues: { email, password } + }; + } + /* try { + return { + message: "Success", + errors: undefined, + fieldValues: { + email: "", + password: "" + } + }; + await fetch("http://localhost:8080/api/auth/signin", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + email: formData.get("email"), + password: formData.get("password") + }) + }).then((response) => { + if (!response.ok) { + return { status: response.status, message: response.statusText }; + } + return response.json(); + }); + } catch (error) { + const zodError = error as ZodError; + const errorMap = zodError.flatten().fieldErrors; + return { + message: "error", + errors: { email: errorMap["email"]?.[0] ?? "", password: errorMap["password"]?.[0] ?? "" }, + fieldValues: { email, password } + }; + } */ +} From ca29e7befc06b7b4449c43a0fd6527a64ed5aa16 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 19 Jan 2024 22:21:58 +0100 Subject: [PATCH 2/4] IPK-117 Implemented full error handling system from server actions; backend integration has been removed though --- bun.lockb | Bin 216354 -> 218233 bytes package.json | 2 + src/app/layout.tsx | 2 + src/app/login/page.tsx | 78 +++++-------- src/app/signup/page.tsx | 193 +++++++++++---------------------- src/components/ui/toast.tsx | 127 ++++++++++++++++++++++ src/components/ui/toaster.tsx | 35 ++++++ src/components/ui/use-toast.ts | 192 ++++++++++++++++++++++++++++++++ src/lib/actions.ts | 100 +++++++++++++---- 9 files changed, 532 insertions(+), 197 deletions(-) create mode 100644 src/components/ui/toast.tsx create mode 100644 src/components/ui/toaster.tsx create mode 100644 src/components/ui/use-toast.ts diff --git a/bun.lockb b/bun.lockb index f5945cd552bf59285e08df923629f025c516e22b..fcc706e0856799a79c7500fe25f338989e08b042 100755 GIT binary patch delta 39622 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 z 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 J-jSvL{|}2C5x4*V delta 38881 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 diff --git a/package.json b/package.json index b338346..421bd67 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.5", "@radix-ui/react-tooltip": "^1.0.7", "@react-three/drei": "^9.93.0", "@react-three/fiber": "^8.15.14", @@ -23,6 +24,7 @@ "@splinetool/runtime": "^1.0.26", "@tweenjs/tween.js": "^21.0.0", "class-variance-authority": "^0.7.0", + "classnames": "^2.5.1", "clsx": "^2.1.0", "framer-motion": "^10.18.0", "jsonwebtoken": "^9.0.2", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6e820f8..aaf7abf 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,6 +6,7 @@ import React from "react"; import { cn } from "@/lib/utils"; import TokenRefresh from "@/components/auth/TokenRefresh"; +import { Toaster } from "@/components/ui/toaster"; const fontSans = FontSans({ subsets: ["latin"], @@ -22,6 +23,7 @@ export default function RootLayout({ children }: { children: React.ReactNode }) {/* */} {children} + ); diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 7a16374..e1f9d84 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,6 +1,4 @@ "use client"; -import { loginUser } from "@/lib/actions"; -import { useDispatch } from "react-redux"; import Link from "next/link"; import { Separator } from "@/components/ui/separator"; import { Input } from "@/components/ui/input"; @@ -9,17 +7,11 @@ import { Button } from "@/components/ui/button"; import { FaGithub, FaGoogle } from "react-icons/fa6"; import { IoLogInOutline } from "react-icons/io5"; import Image from "next/image"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import * as z from "zod"; -import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form"; -import { useEffect, useState } from "react"; import { useFormStatus, useFormState } from "react-dom"; - -export const LoginFormSchema = z.object({ - email: z.string().email({ message: "Invalid email" }).min(5), - password: z.string().min(8, { message: "Password must be at least 8 characters long" }) -}); +import { loginUser } from "@/lib/actions"; +import cx from "classnames"; +import { useEffect } from "react"; +import { useToast } from "@/components/ui/use-toast"; export function SubmitButton() { const { pending } = useFormStatus(); @@ -33,16 +25,7 @@ export function SubmitButton() { } const LoginForm = () => { - const form = useForm>({ - resolver: zodResolver(LoginFormSchema), - defaultValues: { - email: "", - password: "" - } - }); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); - + const { toast } = useToast(); const [formState, formAction] = useFormState(loginUser, { message: "", errors: undefined, @@ -54,25 +37,14 @@ const LoginForm = () => { useEffect(() => { if (formState.message === "success") { - setLoading(false); - setError(""); + toast({ + title: "Logged In!", + description: "Welcome back! You will be redirected any moment", + variant: "default", + className: "border-emerald-300" + }); } - }, [formState]); - - /* - async function onSubmit(values: z.infer) { - setLoading(true); - try { - const result = (await login(dispatch, values)) as PromiseFulfilledResult; - setError(result?.status === undefined ? "Invalid email or password" : ""); - setLoading(false); - } catch (error) { - console.error(error); - setError("Invalid email or password"); - setLoading(false); - throw error; - } - } */ + }, [formState, toast]); return (
@@ -112,20 +84,33 @@ const LoginForm = () => {
- {error &&

{error}

} + {formState?.message === "error" ? ( +
+

{formState?.errors?.email}

+

{formState?.errors?.password}

+
+ ) : ( + "" + )}
@@ -143,9 +128,6 @@ const LoginForm = () => {
-

{formState?.message}

-

{formState?.errors?.email}

-

{formState?.errors?.password}

diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 41039aa..5f6fac2 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -1,6 +1,4 @@ "use client"; -import { signup } from "@/services/authService"; -import { useDispatch } from "react-redux"; import Link from "next/link"; import { Separator } from "@/components/ui/separator"; import { Input } from "@/components/ui/input"; @@ -9,47 +7,36 @@ import { Button } from "@/components/ui/button"; import { FaGithub, FaGoogle } from "react-icons/fa6"; import { IoLogInOutline } from "react-icons/io5"; import Image from "next/image"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { useForm } from "react-hook-form"; -import * as z from "zod"; -import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form"; import { useState } from "react"; -import { FetchEventResult } from "next/dist/server/web/types"; -import { RegisterInputs } from "@/types"; +import { useFormState, useFormStatus } from "react-dom"; +import { registerUser } from "@/lib/actions"; -const formSchema = z.object({ - name: z.string().min(3, { message: "Name must be at least 3 characters long" }), - email: z.string().email({ message: "Invalid email" }).min(5), - password: z.string().min(8, { message: "Password must be at least 8 characters long" }) -}); +export function SubmitButton() { + const { pending } = useFormStatus(); + + return ( + + ); +} const SignupForm = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(""); - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { + const [formState, formAction] = useFormState(registerUser, { + message: "", + errors: undefined, + fieldValues: { name: "", email: "", - password: "" + password: "", + confirmPassword: "" } }); - async function onSubmit(values: z.infer) { - setLoading(true); - try { - const result = (await signup(values as RegisterInputs)) as PromiseFulfilledResult; - setError(result?.status === undefined ? "Invalid email or password" : ""); - setLoading(false); - } catch (error) { - console.error(error); - setError("Invalid email or password"); - setLoading(false); - throw error; - } - } - return (
@@ -85,105 +72,55 @@ const SignupForm = () => {
-
- - ( - - - - - - - )} - /> - ( - - - - - - - )} - /> - ( - - - - - - - )} - /> - ( - - - - - - - )} - /> - {error &&

{error}

} -
-
- -
- -
+ + + + + + + {error &&

{error}

} +
+
+ +
+
- - - +
+ +
diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx new file mode 100644 index 0000000..a822477 --- /dev/null +++ b/src/components/ui/toast.tsx @@ -0,0 +1,127 @@ +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx new file mode 100644 index 0000000..e223385 --- /dev/null +++ b/src/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" +import { useToast } from "@/components/ui/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts new file mode 100644 index 0000000..1671307 --- /dev/null +++ b/src/components/ui/use-toast.ts @@ -0,0 +1,192 @@ +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/src/lib/actions.ts b/src/lib/actions.ts index b1dddfe..fed8fde 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -1,6 +1,5 @@ "use server"; -import { LoginFormSchema } from "@/app/login/page"; import { cookies } from "next/headers"; import { ZodError, z } from "zod"; @@ -94,23 +93,18 @@ export async function getAccessToken() { return cookies().get("accessToken")?.value; } -export type Fields = { - email: string; - password: string; -}; - -export type FormState = { +type LoginFormState = { message: string; - errors: Record | undefined; - fieldValues: Fields; + errors: Record | undefined; + fieldValues: { email: string; password: string }; }; -export async function loginUser(prevState: FormState, formData: FormData): Promise { +export async function loginUser(prevState: LoginFormState, formData: FormData): Promise { const email = formData.get("email") as string; const password = formData.get("password") as string; const schema = z.object({ - email: z.string().email(), - password: z.string().min(8) + email: z.string().email({ message: "Please enter your email in format: yourname@example.com" }).min(5), + password: z.string().min(8, { message: "Your Password must be at least 8 characters long" }) }); const parse = schema.safeParse({ email: formData.get("email"), @@ -129,9 +123,10 @@ export async function loginUser(prevState: FormState, formData: FormData): Promi } const data = parse.data; + await new Promise((resolve) => setTimeout(resolve, 1000)); try { return { - message: "Success", + message: "success", errors: undefined, fieldValues: { email: "", @@ -148,14 +143,6 @@ export async function loginUser(prevState: FormState, formData: FormData): Promi }; } /* try { - return { - message: "Success", - errors: undefined, - fieldValues: { - email: "", - password: "" - } - }; await fetch("http://localhost:8080/api/auth/signin", { method: "POST", headers: { @@ -181,3 +168,74 @@ export async function loginUser(prevState: FormState, formData: FormData): Promi }; } */ } + +export type SignupFormState = { + message: string; + errors: Record | undefined; + fieldValues: { name: string; email: string; password: string; confirmPassword: string }; +}; + +export async function registerUser(prevState: SignupFormState, formData: FormData): Promise { + const name = formData.get("name") as string; + const email = formData.get("email") as string; + const password = formData.get("password") as string; + const confirmPassword = formData.get("confirmPassword") as string; + const schema = z + .object({ + name: z.string().min(3), + email: z.string().email(), + password: z.string().min(8), + confirmPassword: z.string().min(8) + }) + .refine((data) => data.password === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"] + }); + const parse = schema.safeParse({ + name: formData.get("name"), + email: formData.get("email"), + password: formData.get("password"), + confirmPassword: formData.get("confirmPassword") + }); + + if (!parse.success) { + return { + message: "error", + errors: { + name: parse.error.flatten().fieldErrors["name"]?.[0] ?? "", + email: parse.error.flatten().fieldErrors["email"]?.[0] ?? "", + password: parse.error.flatten().fieldErrors["password"]?.[0] ?? "", + confirmPassword: parse.error.flatten().fieldErrors["confirmPassword"]?.[0] ?? "" + }, + fieldValues: { name, email, password, confirmPassword } + }; + } + const data = parse.data; + console.log(data); + + try { + return { + message: "Success", + errors: undefined, + fieldValues: { + name: "", + email: "", + password: "", + confirmPassword: "" + } + }; + } catch (error) { + const zodError = error as ZodError; + const errorMap = zodError.flatten().fieldErrors; + return { + message: "error", + errors: { + name: errorMap["name"]?.[0] ?? "", + email: errorMap["email"]?.[0] ?? "", + password: errorMap["password"]?.[0] ?? "", + confirmPassword: errorMap["confirmPassword"]?.[0] ?? "" + }, + fieldValues: { name, email, password, confirmPassword } + }; + } +} From a3555d925c3d4484f424d391d8b25688bfbefed4 Mon Sep 17 00:00:00 2001 From: Ben Date: Fri, 19 Jan 2024 22:25:28 +0100 Subject: [PATCH 3/4] IPK-117 refactor --- src/app/login/page.tsx | 4 ++-- src/app/signup/page.tsx | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index e1f9d84..52d8eb0 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -13,7 +13,7 @@ import cx from "classnames"; import { useEffect } from "react"; import { useToast } from "@/components/ui/use-toast"; -export function SubmitButton() { +const SubmitButton = () => { const { pending } = useFormStatus(); return ( @@ -22,7 +22,7 @@ export function SubmitButton() { Log In ); -} +}; const LoginForm = () => { const { toast } = useToast(); diff --git a/src/app/signup/page.tsx b/src/app/signup/page.tsx index 5f6fac2..45dce82 100644 --- a/src/app/signup/page.tsx +++ b/src/app/signup/page.tsx @@ -11,7 +11,7 @@ import { useState } from "react"; import { useFormState, useFormStatus } from "react-dom"; import { registerUser } from "@/lib/actions"; -export function SubmitButton() { +const SubmitButton = () => { const { pending } = useFormStatus(); return ( @@ -20,7 +20,7 @@ export function SubmitButton() { Create Account ); -} +}; const SignupForm = () => { const [loading, setLoading] = useState(false); @@ -120,6 +120,7 @@ const SignupForm = () => { Create Account +
From 5782074d7992a7e5544bf6c53128c99709ad6135 Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Mon, 22 Jan 2024 12:06:45 +0000 Subject: [PATCH 4/4] style: format code with Prettier This commit fixes the style issues introduced in a1d5e55 according to the output from Prettier. Details: https://github.com/Informatik-Projekt-Kurs/frontend/pull/25 --- src/components/ui/toast.tsx | 81 +++++++---------- src/components/ui/toaster.tsx | 23 ++--- src/components/ui/use-toast.ts | 153 ++++++++++++++++----------------- 3 files changed, 113 insertions(+), 144 deletions(-) diff --git a/src/components/ui/toast.tsx b/src/components/ui/toast.tsx index a822477..28a233b 100644 --- a/src/components/ui/toast.tsx +++ b/src/components/ui/toast.tsx @@ -1,11 +1,11 @@ -import * as React from "react" -import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" +import * as React from "react"; +import * as ToastPrimitives from "@radix-ui/react-toast"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const ToastProvider = ToastPrimitives.Provider +const ToastProvider = ToastPrimitives.Provider; const ToastViewport = React.forwardRef< React.ElementRef, @@ -19,8 +19,8 @@ const ToastViewport = React.forwardRef< )} {...props} /> -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; const toastVariants = cva( "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", @@ -28,30 +28,22 @@ const toastVariants = cva( variants: { variant: { default: "border bg-background text-foreground", - destructive: - "destructive group border-destructive bg-destructive text-destructive-foreground", - }, + destructive: "destructive group border-destructive bg-destructive text-destructive-foreground" + } }, defaultVariants: { - variant: "default", - }, + variant: "default" + } } -) +); const Toast = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps + React.ComponentPropsWithoutRef & VariantProps >(({ className, variant, ...props }, ref) => { - return ( - - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName + return ; +}); +Toast.displayName = ToastPrimitives.Root.displayName; const ToastAction = React.forwardRef< React.ElementRef, @@ -65,8 +57,8 @@ const ToastAction = React.forwardRef< )} {...props} /> -)) -ToastAction.displayName = ToastPrimitives.Action.displayName +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; const ToastClose = React.forwardRef< React.ElementRef, @@ -79,40 +71,31 @@ const ToastClose = React.forwardRef< className )} toast-close="" - {...props} - > + {...props}> -)) -ToastClose.displayName = ToastPrimitives.Close.displayName +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; const ToastTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName + +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; const ToastDescription = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName + +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; -type ToastProps = React.ComponentPropsWithoutRef +type ToastProps = React.ComponentPropsWithoutRef; -type ToastActionElement = React.ReactElement +type ToastActionElement = React.ReactElement; export { type ToastProps, @@ -123,5 +106,5 @@ export { ToastTitle, ToastDescription, ToastClose, - ToastAction, -} + ToastAction +}; diff --git a/src/components/ui/toaster.tsx b/src/components/ui/toaster.tsx index e223385..9f42138 100644 --- a/src/components/ui/toaster.tsx +++ b/src/components/ui/toaster.tsx @@ -1,17 +1,10 @@ -"use client" +"use client"; -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "@/components/ui/toast" -import { useToast } from "@/components/ui/use-toast" +import { Toast, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from "@/components/ui/toast"; +import { useToast } from "@/components/ui/use-toast"; export function Toaster() { - const { toasts } = useToast() + const { toasts } = useToast(); return ( @@ -20,16 +13,14 @@ export function Toaster() {
{title && {title}} - {description && ( - {description} - )} + {description && {description}}
{action}
- ) + ); })}
- ) + ); } diff --git a/src/components/ui/use-toast.ts b/src/components/ui/use-toast.ts index 1671307..c62a8eb 100644 --- a/src/components/ui/use-toast.ts +++ b/src/components/ui/use-toast.ts @@ -1,104 +1,99 @@ // Inspired by react-hot-toast library -import * as React from "react" +import * as React from "react"; -import type { - ToastActionElement, - ToastProps, -} from "@/components/ui/toast" +import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 +const TOAST_LIMIT = 1; +const TOAST_REMOVE_DELAY = 1000000; type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: ToastActionElement; +}; const actionTypes = { ADD_TOAST: "ADD_TOAST", UPDATE_TOAST: "UPDATE_TOAST", DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const + REMOVE_TOAST: "REMOVE_TOAST" +} as const; -let count = 0 +let count = 0; function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER - return count.toString() + count = (count + 1) % Number.MAX_SAFE_INTEGER; + return count.toString(); } -type ActionType = typeof actionTypes +type ActionType = typeof actionTypes; type Action = | { - type: ActionType["ADD_TOAST"] - toast: ToasterToast + type: ActionType["ADD_TOAST"]; + toast: ToasterToast; } | { - type: ActionType["UPDATE_TOAST"] - toast: Partial + type: ActionType["UPDATE_TOAST"]; + toast: Partial; } | { - type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] + type: ActionType["DISMISS_TOAST"]; + toastId?: ToasterToast["id"]; } | { - type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] - } + type: ActionType["REMOVE_TOAST"]; + toastId?: ToasterToast["id"]; + }; interface State { - toasts: ToasterToast[] + toasts: ToasterToast[]; } -const toastTimeouts = new Map>() +const toastTimeouts = new Map>(); const addToRemoveQueue = (toastId: string) => { if (toastTimeouts.has(toastId)) { - return + return; } const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) + toastTimeouts.delete(toastId); dispatch({ type: "REMOVE_TOAST", - toastId: toastId, - }) - }, TOAST_REMOVE_DELAY) + toastId: toastId + }); + }, TOAST_REMOVE_DELAY); - toastTimeouts.set(toastId, timeout) -} + toastTimeouts.set(toastId, timeout); +}; export const reducer = (state: State, action: Action): State => { switch (action.type) { case "ADD_TOAST": return { ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT) + }; case "UPDATE_TOAST": return { ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - } + toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)) + }; case "DISMISS_TOAST": { - const { toastId } = action + const { toastId } = action; // ! Side effects ! - This could be extracted into a dismissToast() action, // but I'll keep it here for simplicity if (toastId) { - addToRemoveQueue(toastId) + addToRemoveQueue(toastId); } else { state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) + addToRemoveQueue(toast.id); + }); } return { @@ -107,48 +102,48 @@ export const reducer = (state: State, action: Action): State => { t.id === toastId || toastId === undefined ? { ...t, - open: false, + open: false } : t - ), - } + ) + }; } case "REMOVE_TOAST": if (action.toastId === undefined) { return { ...state, - toasts: [], - } + toasts: [] + }; } return { ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - } + toasts: state.toasts.filter((t) => t.id !== action.toastId) + }; } -} +}; -const listeners: Array<(state: State) => void> = [] +const listeners: Array<(state: State) => void> = []; -let memoryState: State = { toasts: [] } +let memoryState: State = { toasts: [] }; function dispatch(action: Action) { - memoryState = reducer(memoryState, action) + memoryState = reducer(memoryState, action); listeners.forEach((listener) => { - listener(memoryState) - }) + listener(memoryState); + }); } -type Toast = Omit +type Toast = Omit; function toast({ ...props }: Toast) { - const id = genId() + const id = genId(); const update = (props: ToasterToast) => dispatch({ type: "UPDATE_TOAST", - toast: { ...props, id }, - }) - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + toast: { ...props, id } + }); + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); dispatch({ type: "ADD_TOAST", @@ -157,36 +152,36 @@ function toast({ ...props }: Toast) { id, open: true, onOpenChange: (open) => { - if (!open) dismiss() - }, - }, - }) + if (!open) dismiss(); + } + } + }); return { id: id, dismiss, - update, - } + update + }; } function useToast() { - const [state, setState] = React.useState(memoryState) + const [state, setState] = React.useState(memoryState); React.useEffect(() => { - listeners.push(setState) + listeners.push(setState); return () => { - const index = listeners.indexOf(setState) + const index = listeners.indexOf(setState); if (index > -1) { - listeners.splice(index, 1) + listeners.splice(index, 1); } - } - }, [state]) + }; + }, [state]); return { ...state, toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }) + }; } -export { useToast, toast } +export { useToast, toast };