diff --git a/client/package-lock.json b/client/package-lock.json
index ee1ab1bc1..de297943a 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -11,6 +11,7 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@hello-pangea/dnd": "^18.0.0",
+ "@hookform/resolvers": "5.2.2",
"@mui/icons-material": "6.4.11",
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
@@ -26,6 +27,7 @@
"mui-color-input": "^6.0.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
+ "react-hook-form": "7.63.0",
"react-i18next": "^15.4.0",
"react-icons": "5.5.0",
"react-redux": "9.2.0",
@@ -34,7 +36,9 @@
"react-toastify": "^10.0.5",
"recharts": "2.15.2",
"redux-persist": "6.0.0",
- "vite-plugin-svgr": "^4.2.0"
+ "swr": "2.3.6",
+ "vite-plugin-svgr": "^4.2.0",
+ "zod": "4.1.11"
},
"devDependencies": {
"@types/react": "^18.2.66",
@@ -653,6 +657,18 @@
"react-dom": "^18.0.0 || ^19.0.0"
}
},
+ "node_modules/@hookform/resolvers": {
+ "version": "5.2.2",
+ "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
+ "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/utils": "^0.3.0"
+ },
+ "peerDependencies": {
+ "react-hook-form": "^7.55.0"
+ }
+ },
"node_modules/@humanwhocodes/config-array": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
@@ -2991,6 +3007,15 @@
"node": ">=0.4.0"
}
},
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -5343,6 +5368,22 @@
"react": "^18.3.1"
}
},
+ "node_modules/react-hook-form": {
+ "version": "7.63.0",
+ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
+ "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/react-hook-form"
+ },
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17 || ^18 || ^19"
+ }
+ },
"node_modules/react-i18next": {
"version": "15.5.1",
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz",
@@ -6141,6 +6182,19 @@
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
"license": "MIT"
},
+ "node_modules/swr": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz",
+ "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
@@ -6639,6 +6693,15 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/zod": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz",
+ "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
}
}
}
diff --git a/client/package.json b/client/package.json
index 9ebc99647..03d0cf6bc 100644
--- a/client/package.json
+++ b/client/package.json
@@ -16,6 +16,7 @@
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@hello-pangea/dnd": "^18.0.0",
+ "@hookform/resolvers": "5.2.2",
"@mui/icons-material": "6.4.11",
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
@@ -31,6 +32,7 @@
"mui-color-input": "^6.0.0",
"react": "18.3.1",
"react-dom": "^18.2.0",
+ "react-hook-form": "7.63.0",
"react-i18next": "^15.4.0",
"react-icons": "5.5.0",
"react-redux": "9.2.0",
@@ -39,7 +41,9 @@
"react-toastify": "^10.0.5",
"recharts": "2.15.2",
"redux-persist": "6.0.0",
- "vite-plugin-svgr": "^4.2.0"
+ "swr": "2.3.6",
+ "vite-plugin-svgr": "^4.2.0",
+ "zod": "4.1.11"
},
"unusedDepencies": {
"@solana/wallet-adapter-base": "0.9.25",
diff --git a/client/src/Components/Inputs/TextInput/indexV2.tsx b/client/src/Components/Inputs/TextInput/indexV2.tsx
new file mode 100644
index 000000000..bc1f144b6
--- /dev/null
+++ b/client/src/Components/Inputs/TextInput/indexV2.tsx
@@ -0,0 +1,6 @@
+import TextField from "@mui/material/TextField";
+import Box from "@mui/material/Box";
+import type { TextFieldProps } from "@mui/material";
+export const TextInput = (props: TextFieldProps) => {
+ return ;
+};
diff --git a/client/src/Hooks/v2/UseApi.tsx b/client/src/Hooks/v2/UseApi.tsx
new file mode 100644
index 000000000..fdb4e8a24
--- /dev/null
+++ b/client/src/Hooks/v2/UseApi.tsx
@@ -0,0 +1,52 @@
+import { useState } from "react";
+import useSWR from "swr";
+import type { SWRConfiguration } from "swr";
+import type { AxiosRequestConfig } from "axios";
+import { get, post } from "@/Utils/ApiClient"; // your axios wrapper
+
+// Generic fetcher for GET requests
+const fetcher = async (url: string, config?: AxiosRequestConfig) => {
+ const res = await get(url, config);
+ return res.data;
+};
+
+export const useGet = (
+ url: string,
+ axiosConfig?: AxiosRequestConfig,
+ swrConfig?: SWRConfiguration
+) => {
+ const { data, error, isLoading, mutate } = useSWR(
+ url,
+ (url) => fetcher(url, axiosConfig),
+ swrConfig
+ );
+
+ return {
+ response: data ?? null,
+ loading: isLoading,
+ error: error?.message ?? null,
+ refetch: mutate,
+ };
+};
+
+export const usePost = (endpoint: string) => {
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const postFn = async (body: B, config?: AxiosRequestConfig): Promise => {
+ setLoading(true);
+ setError(null);
+
+ try {
+ const res = await post(endpoint, body, config);
+ return res.data;
+ } catch (err: any) {
+ setError(err?.message ?? "Unknown error");
+ return null;
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return { post: postFn, loading, error };
+};
diff --git a/client/src/Pages/v2/Auth/Login.tsx b/client/src/Pages/v2/Auth/Login.tsx
new file mode 100644
index 000000000..d1d4ec014
--- /dev/null
+++ b/client/src/Pages/v2/Auth/Login.tsx
@@ -0,0 +1,113 @@
+import { zodResolver } from "@hookform/resolvers/zod";
+import { z } from "zod";
+import { useTranslation } from "react-i18next";
+import { useForm, Controller } from "react-hook-form";
+import { useTheme } from "@mui/material/styles";
+
+import { TextInput } from "@/Components/Inputs/TextInput/indexV2.tsx";
+import Button from "@mui/material/Button";
+import Stack from "@mui/material/Stack";
+import { usePost } from "@/Hooks/v2/UseApi";
+const schema = z.object({
+ email: z.email("Invalid email address"),
+ password: z.string().min(6, "Password must be at least 6 characters"),
+});
+
+type FormData = z.infer;
+type LoginData = {
+ username: string;
+ password: string;
+};
+
+const Login = () => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const { post, loading, error } = usePost("/auth/login");
+
+ const {
+ handleSubmit,
+ control,
+ formState: { errors },
+ } = useForm({
+ resolver: zodResolver(schema), // ⬅️ connect Zod
+ defaultValues: {
+ email: "",
+ password: "",
+ },
+ });
+
+ const onSubmit = async (data: FormData) => {
+ const result = await post(data);
+ if (result) {
+ console.log("Login successful:", result);
+ } else {
+ console.error("Login failed:", error);
+ }
+ };
+
+ return (
+
+
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+
+
+
+ );
+};
+
+export default Login;
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 0de847cb7..06a4b1920 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -1,9 +1,12 @@
+import { useSelector } from "react-redux";
+import { lightTheme, darkTheme } from "@/Utils/Theme/v2/theme";
import { Navigate, Route, Routes as LibRoutes } from "react-router";
+import { ThemeProvider } from "@emotion/react";
import HomeLayout from "../Components/Layouts/HomeLayout";
import NotFound from "../Pages/v1/NotFound";
-
// Auth
import AuthLogin from "../Pages/v1/Auth/Login";
+import AuthLoginV2 from "@/Pages/v2/Auth/Login";
import AuthRegister from "../Pages/v1/Auth/Register/";
import AuthForgotPassword from "../Pages/v1/Auth/ForgotPassword";
import AuthCheckEmail from "../Pages/v1/Auth/CheckEmail";
@@ -54,6 +57,8 @@ import BulkImport from "../Pages/v1/Uptime/BulkImport";
import Logs from "../Pages/v1/Logs";
const Routes = () => {
+ const mode = useSelector((state) => state.ui.mode);
+ const v2Theme = mode === "light" ? lightTheme : darkTheme;
const AdminCheckedRegister = withAdminCheck(AuthRegister);
return (
@@ -214,6 +219,14 @@ const Routes = () => {
path="/login"
element={}
/>
+
+
+
+ }
+ />
(
+ url: string,
+ config: AxiosRequestConfig = {}
+): Promise> => api.get(url, config);
+
+export const post = (
+ url: string,
+ data: any,
+ config: AxiosRequestConfig = {}
+): Promise> => api.post(url, data, config);
+
+export default api;
diff --git a/client/src/Utils/Theme/constants.js b/client/src/Utils/Theme/constants.js
index 9df6665d0..45217fdeb 100644
--- a/client/src/Utils/Theme/constants.js
+++ b/client/src/Utils/Theme/constants.js
@@ -100,7 +100,6 @@ const newColors = {
blueGray900: "#515151",
blueBlueWave: "#1570EF",
lightBlueWave: "#CDE2FF",
- /* I changed green 100 and green 700. Need to change red and warning as well, and refactor the object following the structure */
green100: "#67cd78",
green200: "#4B9B77",
green400: "#079455",
diff --git a/client/src/Utils/Theme/v2/palette.ts b/client/src/Utils/Theme/v2/palette.ts
new file mode 100644
index 000000000..2875f750d
--- /dev/null
+++ b/client/src/Utils/Theme/v2/palette.ts
@@ -0,0 +1,82 @@
+import { lighten, darken } from "@mui/material/styles";
+
+const typographyBase = 13;
+
+export const typographyLevels = {
+ base: typographyBase,
+ xs: `${(typographyBase - 4) / 16}rem`,
+ s: `${(typographyBase - 2) / 16}rem`,
+ m: `${typographyBase / 16}rem`,
+ l: `${(typographyBase + 2) / 16}rem`,
+ xl: `${(typographyBase + 10) / 16}rem`,
+};
+
+const colors = {
+ offWhite: "#FEFEFE",
+ offBlack: "#131315",
+ gray0: "#FDFDFD",
+ gray10: "#F4F4FF",
+ gray50: "#F9F9F9",
+ gray100: "#F3F3F3",
+ gray200: "#EFEFEF",
+ gray250: "#DADADA",
+ gray500: "#A2A3A3",
+ gray900: "#1c1c1c",
+ blueGray50: "#E8F0FE",
+ blueGray500: "#475467",
+ blueGray600: "#344054",
+ blueGray800: "#1C2130",
+ blueGray900: "#515151",
+ blueBlueWave: "#1570EF",
+ lightBlueWave: "#CDE2FF",
+ green100: "#67cd78",
+ green200: "#4B9B77",
+ green400: "#079455",
+ green700: "#026513",
+ orange100: "#FD8F22",
+ orange200: "#D69A5D",
+ orange600: "#9B734B",
+ orange700: "#884605",
+ red100: "#F27C7C",
+ red400: "#D92020",
+ red600: "#9B4B4B",
+ red700: "#980303",
+};
+
+export const lightPalette = {
+ accent: {
+ main: colors.blueBlueWave,
+ light: lighten(colors.blueBlueWave, 0.2),
+ dark: darken(colors.blueBlueWave, 0.2),
+ contrastText: colors.offWhite,
+ },
+ primary: {
+ main: colors.offWhite,
+ contrastText: colors.blueGray800,
+ contrastTextSecondary: colors.blueGray600,
+ },
+ secondary: {
+ main: colors.gray200,
+ light: colors.lightBlueWave,
+ contrastText: colors.blueGray600,
+ },
+};
+
+export const darkPalette = {
+ accent: {
+ main: colors.blueBlueWave,
+ light: lighten(colors.blueBlueWave, 0.2),
+ dark: darken(colors.blueBlueWave, 0.2),
+ contrastText: colors.offWhite,
+ },
+ primary: {
+ main: colors.offBlack,
+ contrastText: colors.blueGray50,
+ contrastTextSecondary: colors.gray200,
+ },
+ secondary: {
+ main: "#313131",
+ light: colors.lightBlueWave,
+ contrastText: colors.gray200,
+ },
+};
diff --git a/client/src/Utils/Theme/v2/theme.ts b/client/src/Utils/Theme/v2/theme.ts
new file mode 100644
index 000000000..ce81ea2fd
--- /dev/null
+++ b/client/src/Utils/Theme/v2/theme.ts
@@ -0,0 +1,52 @@
+import { createTheme } from "@mui/material";
+import { lightPalette, darkPalette, typographyLevels } from "./palette";
+const fontFamilyPrimary = '"Inter" , sans-serif';
+
+export const theme = (mode: string, palette: any) =>
+ createTheme({
+ spacing: 2,
+ palette: {
+ mode: mode,
+ ...palette,
+ },
+ typography: {
+ fontFamily: fontFamilyPrimary,
+ fontSize: typographyLevels.base,
+ },
+
+ components: {
+ MuiFormLabel: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ fontSize: typographyLevels.base,
+ "&.Mui-focused": {
+ color: theme.palette.secondary.contrastText,
+ },
+ }),
+ },
+ },
+ MuiInputLabel: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ top: `-${theme.spacing(4)}`,
+ "&.MuiInputLabel-shrink": {
+ top: 0,
+ },
+ }),
+ },
+ },
+ MuiTextField: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ "& .MuiOutlinedInput-root": {
+ height: 34,
+ fontSize: typographyLevels.base,
+ },
+ }),
+ },
+ },
+ },
+ });
+
+export const lightTheme = createTheme(theme("light", lightPalette));
+export const darkTheme = createTheme(theme("dark", darkPalette));
diff --git a/client/src/types/env.d.ts b/client/src/types/env.d.ts
new file mode 100644
index 000000000..e2bce23dd
--- /dev/null
+++ b/client/src/types/env.d.ts
@@ -0,0 +1,9 @@
+///
+
+interface ImportMetaEnv {
+ readonly VITE_APP_API_V2_BASE_URL?: string;
+}
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
diff --git a/client/src/types/mui.d.ts b/client/src/types/mui.d.ts
new file mode 100644
index 000000000..851ffea70
--- /dev/null
+++ b/client/src/types/mui.d.ts
@@ -0,0 +1,20 @@
+import "@mui/material/Button";
+
+declare module "@mui/material/styles" {
+ interface Palette {
+ accent: Palette["primary"];
+ }
+ interface PaletteOptions {
+ accent?: PaletteOptions["primary"];
+ }
+
+ interface PaletteColor {
+ contrastTextSecondary?: string;
+ }
+}
+
+declare module "@mui/material/Button" {
+ interface ButtonPropsColorOverrides {
+ accent: true;
+ }
+}
diff --git a/client/vite.config.ts b/client/vite.config.ts
index 66630e88c..a99c1a254 100644
--- a/client/vite.config.ts
+++ b/client/vite.config.ts
@@ -1,6 +1,7 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
+import path from "path";
export default defineConfig(({}) => {
let version = "3.2.0";
@@ -8,6 +9,11 @@ export default defineConfig(({}) => {
return {
base: "/",
plugins: [svgr(), react()],
+ resolve: {
+ alias: {
+ "@": path.resolve(__dirname, "src"),
+ },
+ },
optimizeDeps: {
include: ["@mui/material/Tooltip", "@emotion/styled"],
},
diff --git a/server/src/app.js b/server/src/app.js
index 253baee88..4578de896 100644
--- a/server/src/app.js
+++ b/server/src/app.js
@@ -27,7 +27,7 @@ export const createApp = ({ services, controllers, envSettings, frontendPath, op
cors({
origin: allowedOrigin,
methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS",
- allowedHeaders: "*",
+ allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
})
);