diff --git a/client/src/Hooks/usePasswordForm.ts b/client/src/Hooks/usePasswordForm.ts
new file mode 100644
index 000000000..d38f73abf
--- /dev/null
+++ b/client/src/Hooks/usePasswordForm.ts
@@ -0,0 +1,13 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { passwordSchema, type PasswordFormData } from "@/Validation/password";
+
+export const usePasswordForm = () => {
+ return {
+ resolver: zodResolver(passwordSchema),
+ defaults: {
+ currentPassword: "",
+ newPassword: "",
+ confirm: "",
+ } as PasswordFormData,
+ };
+};
diff --git a/client/src/Pages/Account/TabPassword.tsx b/client/src/Pages/Account/TabPassword.tsx
index 0acecd35a..cd07410ac 100644
--- a/client/src/Pages/Account/TabPassword.tsx
+++ b/client/src/Pages/Account/TabPassword.tsx
@@ -1,5 +1,47 @@
-import { Box } from "@mui/material";
+import { Stack } from "@mui/material";
+import { ConfigBox } from "@/Components/v2/design-elements";
+import { TextField } from "@/Components/v2/inputs";
+
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@mui/material";
export const TabPassword = () => {
- return {/* TODO: Implement TabPassword */};
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ return (
+
+
+ }
+ />
+
+
+
+
+ }
+ />
+
+ );
};
diff --git a/client/src/Pages/Account/TabProfile.tsx b/client/src/Pages/Account/TabProfile.tsx
index 6b05e0082..8ea8a50a0 100644
--- a/client/src/Pages/Account/TabProfile.tsx
+++ b/client/src/Pages/Account/TabProfile.tsx
@@ -1,4 +1,4 @@
-import { Stack, Box } from "@mui/material";
+import { Stack } from "@mui/material";
import { ConfigBox } from "@/Components/v2/design-elements";
import { TextField, Button } from "@/Components/v2/inputs";
import { ImageUpload } from "@/Components/v2/inputs";
diff --git a/client/src/Validation/password.ts b/client/src/Validation/password.ts
new file mode 100644
index 000000000..c287fcb25
--- /dev/null
+++ b/client/src/Validation/password.ts
@@ -0,0 +1,25 @@
+import { z } from "zod";
+import { specialCharPattern } from "@/Validation/patterns";
+
+export const passwordSchema = z
+ .object({
+ currentPassword: z.string().min(1, "Current password is required"),
+ newPassword: z
+ .string()
+ .min(1, "New password is required")
+ .min(8, "Password must be at least 8 characters")
+ .refine((val) => /[A-Z]/.test(val), "Password must contain an uppercase letter")
+ .refine((val) => /[a-z]/.test(val), "Password must contain a lowercase letter")
+ .refine((val) => /\d/.test(val), "Password must contain a number")
+ .refine(
+ (val) => specialCharPattern.test(val),
+ "Password must contain a special character"
+ ),
+ confirm: z.string().min(1, "Please confirm your new password"),
+ })
+ .refine((data) => data.newPassword === data.confirm, {
+ message: "Passwords do not match",
+ path: ["confirm"],
+ });
+
+export type PasswordFormData = z.infer;
diff --git a/client/src/Validation/patterns.ts b/client/src/Validation/patterns.ts
new file mode 100644
index 000000000..d1e20fa73
--- /dev/null
+++ b/client/src/Validation/patterns.ts
@@ -0,0 +1,4 @@
+export const specialCharPattern = /[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]/;
+
+// Unicode-aware name pattern (allows letters, marks, apostrophes, parentheses, hyphens, periods, spaces)
+export const namePattern = /^[\p{L}\p{M}''()\-\. ]+$/u;
diff --git a/client/src/Validation/profile.ts b/client/src/Validation/profile.ts
index 56301c31f..eca0ddf5c 100644
--- a/client/src/Validation/profile.ts
+++ b/client/src/Validation/profile.ts
@@ -1,18 +1,17 @@
import { z } from "zod";
-
-const NAME_PATTERN = /^[A-Za-z'-]+$/;
+import { namePattern } from "@/Validation/patterns";
export const profileSchema = z.object({
firstName: z
.string()
.min(1, "First name is required")
.max(50, "First name must be 50 characters or less")
- .regex(NAME_PATTERN, "First name can only contain letters, hyphens, and apostrophes"),
+ .regex(namePattern, "First name contains invalid characters"),
lastName: z
.string()
.min(1, "Last name is required")
.max(50, "Last name must be 50 characters or less")
- .regex(NAME_PATTERN, "Last name can only contain letters, hyphens, and apostrophes"),
+ .regex(namePattern, "Last name contains invalid characters"),
profileImage: z.instanceof(File).optional().nullable(),
deleteProfileImage: z.boolean().optional(),
});
diff --git a/client/src/Validation/register.ts b/client/src/Validation/register.ts
index 001dfa56a..d76c5f37e 100644
--- a/client/src/Validation/register.ts
+++ b/client/src/Validation/register.ts
@@ -1,7 +1,5 @@
import { z } from "zod";
-
-const namePattern = /^[\p{L}\p{M}''()\-\. ]+$/u;
-const specialCharPattern = /[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]/;
+import { specialCharPattern, namePattern } from "@/Validation/patterns";
export const registerSchema = z
.object({
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 21a8e8e0a..451c59e17 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -523,6 +523,28 @@
"title": "Profile photo",
"description": "Upload a profile picture"
},
+ "currentPassword": {
+ "title": "Current password",
+ "description": "Enter your current password to verify your identity",
+ "option": {
+ "label": "Current password",
+ "placeholder": "Enter current password"
+ }
+ },
+ "newPassword": {
+ "title": "New password",
+ "description": "Choose a strong password with at least 8 characters",
+ "option": {
+ "newPassword": {
+ "label": "New password",
+ "placeholder": "Enter new password"
+ },
+ "confirm": {
+ "label": "Confirm password",
+ "placeholder": "Confirm new password"
+ }
+ }
+ },
"deleteAccount": {
"title": "Delete account",
"description": "This action is permanent and cannot be undone"