From c2cf17388e03852263addf4e638c64685032a974 Mon Sep 17 00:00:00 2001 From: mehedi-hasan Date: Sun, 14 Apr 2024 17:37:04 +0600 Subject: [PATCH] feat: change password page --- package-lock.json | 60 +++++- package.json | 6 +- .../dashboard/change-password/page.tsx | 17 ++ src/components/change-password-form.tsx | 117 ++++++++++++ src/components/ui/form.tsx | 176 ++++++++++++++++++ src/components/ui/label.tsx | 26 +++ src/components/ui/textarea.tsx | 24 +++ 7 files changed, 424 insertions(+), 2 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/change-password/page.tsx create mode 100644 src/components/change-password-form.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/textarea.tsx diff --git a/package-lock.json b/package-lock.json index ad1130c..25d6933 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,14 @@ "name": "skilld-admin", "version": "0.1.0", "dependencies": { + "@hookform/resolvers": "^3.3.4", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", @@ -31,9 +33,11 @@ "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.51.3", "react-resizable-panels": "^2.0.16", "tailwind-merge": "^2.2.2", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20", @@ -168,6 +172,14 @@ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" }, + "node_modules/@hookform/resolvers": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.3.4.tgz", + "integrity": "sha512-o5cgpGOuJYrd+iMKvkttOclgwRW86EsWJZZRC23prf0uU2i48Htq4PuT73AVb9ionFyZrwYEITuOFGF+BydEtQ==", + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.11.14", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", @@ -838,6 +850,29 @@ } } }, + "node_modules/@radix-ui/react-label": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", + "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-menu": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.6.tgz", @@ -4845,6 +4880,21 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.51.3", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.3.tgz", + "integrity": "sha512-cvJ/wbHdhYx8aviSWh28w9ImjmVsb5Y05n1+FW786vEZQJV5STNM0pW6ujS+oiBecb0ARBxJFyAnXj9+GHXACQ==", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6075,6 +6125,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.22.4", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", + "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index c0fad3e..1ccc1f6 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,14 @@ "lint": "next lint" }, "dependencies": { + "@hookform/resolvers": "^3.3.4", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", + "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", @@ -32,9 +34,11 @@ "next-themes": "^0.3.0", "react": "^18", "react-dom": "^18", + "react-hook-form": "^7.51.3", "react-resizable-panels": "^2.0.16", "tailwind-merge": "^2.2.2", - "tailwindcss-animate": "^1.0.7" + "tailwindcss-animate": "^1.0.7", + "zod": "^3.22.4" }, "devDependencies": { "@types/node": "^20", diff --git a/src/app/(dashboard)/dashboard/change-password/page.tsx b/src/app/(dashboard)/dashboard/change-password/page.tsx new file mode 100644 index 0000000..9c32b58 --- /dev/null +++ b/src/app/(dashboard)/dashboard/change-password/page.tsx @@ -0,0 +1,17 @@ +import ChangePasswordForm from "@/components/change-password-form"; +import { Separator } from "@/components/ui/separator"; + +export default function SettingsProfilePage() { + return ( +
+
+

Change Password

+

+ Change your password carefully. +

+
+ + +
+ ); +} diff --git a/src/components/change-password-form.tsx b/src/components/change-password-form.tsx new file mode 100644 index 0000000..953c973 --- /dev/null +++ b/src/components/change-password-form.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +import { Button } from "@/components/ui/button"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { toast } from "@/components/ui/use-toast"; + +const profileFormSchema = z.object({ + currentPassword: z.string().trim().min(6, { + message: "Password must be at least 6 characters.", + }), + newPassword: z.string().trim().min(6, { + message: "Password must be at least 6 characters.", + }), + confirmPassword: z.string().trim().min(6, { + message: "Password must be at least 6 characters.", + }), +}); + +type ProfileFormValues = z.infer; + +const defaultValues: Partial = { + currentPassword: "", + newPassword: "", + confirmPassword: "", +}; + +export default function ChangePasswordForm() { + const form = useForm({ + resolver: zodResolver(profileFormSchema), + defaultValues, + mode: "onChange", + }); + + function onSubmit(data: ProfileFormValues) { + if (data.newPassword !== data.confirmPassword) { + toast({ + variant: "destructive", + title: "New Password and Confirm Password don't match!", + }); + + return; + } + + toast({ + title: "You submitted the following values:", + description: ( +
+          {JSON.stringify(data, null, 2)}
+        
+ ), + }); + + form.reset(); + } + + return ( +
+ + ( + + Current Password + + + + + + )} + /> + ( + + New Password + + + + + + )} + /> + ( + + Confirm New Password + + + + + + )} + /> + + + + ); +} diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..f6afdaf --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,176 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = React.createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +