From cb1e10f0a95fd671ad8fc227dccd754adcd6d655 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 25 Sep 2025 16:06:52 -0700 Subject: [PATCH] add v2 login page --- client/package-lock.json | 65 +++++++++- client/package.json | 6 +- .../Components/Inputs/TextInput/indexV2.tsx | 6 + client/src/Hooks/v2/UseApi.tsx | 52 ++++++++ client/src/Pages/v2/Auth/Login.tsx | 113 ++++++++++++++++++ client/src/Routes/index.jsx | 15 ++- client/src/Utils/ApiClient.ts | 21 ++++ client/src/Utils/Theme/constants.js | 1 - client/src/Utils/Theme/v2/palette.ts | 82 +++++++++++++ client/src/Utils/Theme/v2/theme.ts | 52 ++++++++ client/src/types/env.d.ts | 9 ++ client/src/types/mui.d.ts | 20 ++++ client/vite.config.ts | 6 + server/src/app.js | 2 +- 14 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 client/src/Components/Inputs/TextInput/indexV2.tsx create mode 100644 client/src/Hooks/v2/UseApi.tsx create mode 100644 client/src/Pages/v2/Auth/Login.tsx create mode 100644 client/src/Utils/ApiClient.ts create mode 100644 client/src/Utils/Theme/v2/palette.ts create mode 100644 client/src/Utils/Theme/v2/theme.ts create mode 100644 client/src/types/env.d.ts create mode 100644 client/src/types/mui.d.ts 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, }) );