add v2 login page

This commit is contained in:
Alex Holliday committed 2025-09-25 16:06:52 -07:00
1 parent 659162d60c
commit cb1e10f0a9
14 files changed
+445 -5

No files matched your search

+64 -1
View File
@@ -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"
}
}
}
}
+5 -1
View File
@@ -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",
@@ -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 <TextField {...props} />;
};
+52
View File
@@ -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 <T,>(url: string, config?: AxiosRequestConfig) => {
const res = await get<T>(url, config);
return res.data;
};
export const useGet = <T,>(
url: string,
axiosConfig?: AxiosRequestConfig,
swrConfig?: SWRConfiguration<T, Error>
) => {
const { data, error, isLoading, mutate } = useSWR<T>(
url,
(url) => fetcher<T>(url, axiosConfig),
swrConfig
);
return {
response: data ?? null,
loading: isLoading,
error: error?.message ?? null,
refetch: mutate,
};
};
export const usePost = <B = any, R = any>(endpoint: string) => {
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const postFn = async (body: B, config?: AxiosRequestConfig): Promise<R | null> => {
setLoading(true);
setError(null);
try {
const res = await post<R>(endpoint, body, config);
return res.data;
} catch (err: any) {
setError(err?.message ?? "Unknown error");
return null;
} finally {
setLoading(false);
}
};
return { post: postFn, loading, error };
};
+113
View File
@@ -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<typeof schema>;
type LoginData = {
username: string;
password: string;
};
const Login = () => {
const { t } = useTranslation();
const theme = useTheme();
const { post, loading, error } = usePost<FormData, LoginData>("/auth/login");
const {
handleSubmit,
control,
formState: { errors },
} = useForm<FormData>({
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 (
<Stack
alignItems={"center"}
justifyContent={"center"}
minHeight="100vh"
>
<Stack
component="form"
padding={theme.spacing(8)}
gap={theme.spacing(12)}
onSubmit={handleSubmit(onSubmit)}
maxWidth={400}
sx={{
width: {
sm: "80%",
md: "70%",
lg: "65%",
xl: "65%",
},
}}
>
<Controller
name="email"
control={control}
defaultValue=""
render={({ field }) => (
<TextInput
{...field}
label={t("auth.common.inputs.email.label")}
fullWidth
placeholder={t("auth.common.inputs.email.placeholder")}
error={!!errors.email}
helperText={errors.email ? errors.email.message : ""}
/>
)}
/>
<Controller
name="password"
control={control}
defaultValue=""
render={({ field }) => (
<TextInput
{...field}
type="password"
label={t("auth.common.inputs.password.label")}
fullWidth
placeholder="••••••••••"
error={!!errors.password}
helperText={errors.password ? errors.password.message : ""}
/>
)}
/>
<Button
variant="contained"
color="accent"
type="submit"
sx={{ width: "100%", alignSelf: "center", fontWeight: 700 }}
>
Login
</Button>
</Stack>
</Stack>
);
};
export default Login;
+14 -1
View File
@@ -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 (
<LibRoutes>
@@ -214,6 +219,14 @@ const Routes = () => {
path="/login"
element={<AuthLogin />}
/>
<Route
path="/v2/login"
element={
<ThemeProvider theme={v2Theme}>
<AuthLoginV2 />
</ThemeProvider>
}
/>
<Route
path="/register"
+21
View File
@@ -0,0 +1,21 @@
import axios from "axios";
import type { AxiosRequestConfig, AxiosResponse } from "axios";
const BASE_URL = import.meta.env.VITE_APP_API_V2_BASE_URL;
const api = axios.create({
baseURL: BASE_URL || "http://localhost:55555/api/v2",
withCredentials: true,
});
export const get = <T>(
url: string,
config: AxiosRequestConfig = {}
): Promise<AxiosResponse<T>> => api.get<T>(url, config);
export const post = <T>(
url: string,
data: any,
config: AxiosRequestConfig = {}
): Promise<AxiosResponse<T>> => api.post<T>(url, data, config);
export default api;
-1
View File
@@ -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",
+82
View File
@@ -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,
},
};
+52
View File
@@ -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));
+9
View File
@@ -0,0 +1,9 @@
/// <reference types="vite/client" />
interface ImportMetaEnv {
readonly VITE_APP_API_V2_BASE_URL?: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
+20
View File
@@ -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;
}
}
+6
View File
@@ -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"],
},
+1 -1
View File
@@ -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,
})
);