From 7378f2e33ba6fa7824a99a9c2ea72832c0dca592 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Oct 2025 11:06:34 -0700 Subject: [PATCH] add missing auth elements --- .../src/Components/v2/Auth/AuthBasePage.tsx | 48 ++++ client/src/Components/v2/Auth/HeaderAuth.jsx | 26 ++ client/src/Components/v2/Auth/index.jsx | 2 + .../Components/v2/Inputs/LanguageSelector.tsx | 64 +++++ client/src/Components/v2/Inputs/Select.tsx | 18 +- client/src/Components/v2/Inputs/TextLink.tsx | 37 +++ .../src/Components/v2/Inputs/ThemeSwitch.tsx | 135 ++++++++++ client/src/Components/v2/Inputs/index.tsx | 5 + client/src/Hooks/v2/UseApi.tsx | 8 +- client/src/Pages/v2/Auth/Login.tsx | 155 +++++++----- client/src/Pages/v2/Auth/Register.tsx | 234 +++++++++--------- client/src/Pages/v2/Uptime/Create.tsx | 4 +- 12 files changed, 550 insertions(+), 186 deletions(-) create mode 100644 client/src/Components/v2/Auth/AuthBasePage.tsx create mode 100644 client/src/Components/v2/Auth/HeaderAuth.jsx create mode 100644 client/src/Components/v2/Auth/index.jsx create mode 100644 client/src/Components/v2/Inputs/LanguageSelector.tsx create mode 100644 client/src/Components/v2/Inputs/TextLink.tsx create mode 100644 client/src/Components/v2/Inputs/ThemeSwitch.tsx diff --git a/client/src/Components/v2/Auth/AuthBasePage.tsx b/client/src/Components/v2/Auth/AuthBasePage.tsx new file mode 100644 index 000000000..ba4230a6d --- /dev/null +++ b/client/src/Components/v2/Auth/AuthBasePage.tsx @@ -0,0 +1,48 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import { HeaderAuth } from "@/Components/v2/Auth"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; + +import type { StackProps } from "@mui/material/Stack"; +import { useTheme } from "@mui/material/styles"; +import { Typography } from "@mui/material"; + +interface AuthBasePageProps extends StackProps { + title?: string; + subtitle?: string; + children: React.ReactNode; +} + +export const AuthBasePage: React.FC = ({ + children, + title, + subtitle, + ...props +}) => { + const theme = useTheme(); + return ( + + + + + + + {title} + {subtitle} + {children} + + + ); +}; diff --git a/client/src/Components/v2/Auth/HeaderAuth.jsx b/client/src/Components/v2/Auth/HeaderAuth.jsx new file mode 100644 index 000000000..4e437cd7e --- /dev/null +++ b/client/src/Components/v2/Auth/HeaderAuth.jsx @@ -0,0 +1,26 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; + +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import { LanguageSelector, ThemeSwitch } from "@/Components/v2/Inputs"; + +export const HeaderAuth = () => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + + + + ); +}; diff --git a/client/src/Components/v2/Auth/index.jsx b/client/src/Components/v2/Auth/index.jsx new file mode 100644 index 000000000..84b742113 --- /dev/null +++ b/client/src/Components/v2/Auth/index.jsx @@ -0,0 +1,2 @@ +export { HeaderAuth } from "./HeaderAuth"; +export { AuthBasePage } from "./AuthBasePage"; diff --git a/client/src/Components/v2/Inputs/LanguageSelector.tsx b/client/src/Components/v2/Inputs/LanguageSelector.tsx new file mode 100644 index 000000000..f7953c873 --- /dev/null +++ b/client/src/Components/v2/Inputs/LanguageSelector.tsx @@ -0,0 +1,64 @@ +import "flag-icons/css/flag-icons.min.css"; +import { Select } from "@/Components/v2/Inputs"; +import MenuItem from "@mui/material/MenuItem"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useDispatch } from "react-redux"; +import { useSelector } from "react-redux"; +import { setLanguage } from "@/Features/UI/uiSlice"; +import type { SelectChangeEvent } from "@mui/material/Select"; + +export const LanguageSelector = () => { + const { i18n } = useTranslation(); + const theme = useTheme(); + const dispatch = useDispatch(); + const language = useSelector((state: any) => state.ui.language); + const languages = Object.keys(i18n.options.resources || {}); + const languageMap: Record = { + cs: "cz", + ja: "jp", + uk: "ua", + vi: "vn", + }; + + const handleChange = (event: SelectChangeEvent) => { + const newLang = event.target.value; + dispatch(setLanguage(newLang)); + }; + + const languagesForDisplay = languages.map((l) => { + let formattedLanguage = l === "en" ? "gb" : l; + formattedLanguage = formattedLanguage.includes("-") + ? formattedLanguage.split("-")[1].toLowerCase() + : formattedLanguage; + formattedLanguage = languageMap[formattedLanguage] || formattedLanguage; + const flag = formattedLanguage ? `fi fi-${formattedLanguage}` : null; + + return ( + + + {flag && } + {l} + + + ); + }); + + return ( + + ); +}; diff --git a/client/src/Components/v2/Inputs/Select.tsx b/client/src/Components/v2/Inputs/Select.tsx index fb388b334..7d47286da 100644 --- a/client/src/Components/v2/Inputs/Select.tsx +++ b/client/src/Components/v2/Inputs/Select.tsx @@ -1,6 +1,22 @@ import Select from "@mui/material/Select"; import type { SelectProps } from "@mui/material/Select"; +import { useTheme } from "@mui/material/styles"; export const SelectInput: React.FC = ({ ...props }) => { - return + ); }; diff --git a/client/src/Components/v2/Inputs/TextLink.tsx b/client/src/Components/v2/Inputs/TextLink.tsx new file mode 100644 index 000000000..78525f837 --- /dev/null +++ b/client/src/Components/v2/Inputs/TextLink.tsx @@ -0,0 +1,37 @@ +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import Link from "@mui/material/Link"; +import { Link as RouterLink } from "react-router-dom"; + +import { useTheme } from "@mui/material/styles"; + +export const TextLink = ({ + text, + linkText, + href, + target = "_self", +}: { + text: string; + linkText: string; + href: string; + target?: string; +}) => { + const theme = useTheme(); + + return ( + + {text} + + {linkText} + + + ); +}; diff --git a/client/src/Components/v2/Inputs/ThemeSwitch.tsx b/client/src/Components/v2/Inputs/ThemeSwitch.tsx new file mode 100644 index 000000000..fb4bb6d81 --- /dev/null +++ b/client/src/Components/v2/Inputs/ThemeSwitch.tsx @@ -0,0 +1,135 @@ +import { useTheme } from "@mui/material/styles"; +import { useDispatch, useSelector } from "react-redux"; +import { setMode } from "@/Features/UI/uiSlice.js"; +import { useTranslation } from "react-i18next"; +import IconButton from "@mui/material/IconButton"; + +const SunAndMoonIcon = () => { + const theme = useTheme(); + + return ( + + ); +}; + +export const ThemeSwitch = ({ + width = 48, + height = 48, +}: { + width?: number; + height?: number; +}) => { + const mode = useSelector((state: any) => state.ui.mode); + const dispatch = useDispatch(); + const { t } = useTranslation(); + + const handleChange = () => { + dispatch(setMode(mode === "light" ? "dark" : "light")); + }; + + return ( + + + + ); +}; diff --git a/client/src/Components/v2/Inputs/index.tsx b/client/src/Components/v2/Inputs/index.tsx index 5f3eb2810..ccf17f630 100644 --- a/client/src/Components/v2/Inputs/index.tsx +++ b/client/src/Components/v2/Inputs/index.tsx @@ -1,2 +1,7 @@ export { ButtonInput as Button } from "./Button"; export { ButtonGroupInput as ButtonGroup } from "./ButtonGroup"; +export { TextInput } from "./TextInput"; +export { SelectInput as Select } from "./Select"; +export { LanguageSelector } from "./LanguageSelector"; +export { ThemeSwitch } from "./ThemeSwitch"; +export { TextLink } from "./TextLink"; diff --git a/client/src/Hooks/v2/UseApi.tsx b/client/src/Hooks/v2/UseApi.tsx index 5324f6343..d3001c87b 100644 --- a/client/src/Hooks/v2/UseApi.tsx +++ b/client/src/Hooks/v2/UseApi.tsx @@ -35,11 +35,15 @@ export const useGet = ( }; }; -export const usePost = (endpoint: string) => { +export const usePost = () => { const [loading, setLoading] = useState(false); const [error, setError] = useState(null); - const postFn = async (body: B, config?: AxiosRequestConfig): Promise => { + const postFn = async ( + endpoint: string, + body: B, + config?: AxiosRequestConfig + ): Promise => { setLoading(true); setError(null); diff --git a/client/src/Pages/v2/Auth/Login.tsx b/client/src/Pages/v2/Auth/Login.tsx index a41750c27..79ed2329f 100644 --- a/client/src/Pages/v2/Auth/Login.tsx +++ b/client/src/Pages/v2/Auth/Login.tsx @@ -1,16 +1,19 @@ +import { AuthBasePage } from "@/Components/v2/Auth"; +import { Button } from "@/Components/v2/Inputs"; +import Stack from "@mui/material/Stack"; +import { TextInput, TextLink } from "@/Components/v2/Inputs"; + +import type { ApiResponse } from "@/Hooks/v2/UseApi"; + import { zodResolver } from "@hookform/resolvers/zod"; -import { z } from "zod"; -import { useTranslation } from "react-i18next"; -import { useForm, Controller } from "react-hook-form"; +import { usePost } from "@/Hooks/v2/UseApi"; +import { useNavigate } from "react-router"; import { useTheme } from "@mui/material/styles"; import { useDispatch } from "react-redux"; import { setIsAuthenticated } from "@/Features/Auth/v2AuthSlice"; -import { useNavigate } from "react-router"; -import { TextInput } from "@/Components/v2/Inputs/TextInput"; -import Button from "@mui/material/Button"; -import Stack from "@mui/material/Stack"; -import { usePost } from "@/Hooks/v2/UseApi"; -import type { ApiResponse } from "@/Hooks/v2/UseApi"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { useForm, Controller } from "react-hook-form"; const schema = z.object({ email: z.email("Invalid email address"), @@ -23,7 +26,7 @@ const Login = () => { const { t } = useTranslation(); const dispatch = useDispatch(); const theme = useTheme(); - const { post, loading } = usePost("/auth/login"); + const { post, loading } = usePost(); const navigate = useNavigate(); const { @@ -39,7 +42,7 @@ const Login = () => { }); const onSubmit = async (data: FormData) => { - const result = await post(data); + const result = await post("/auth/login", data); if (result) { dispatch(setIsAuthenticated({ authenticated: true })); navigate("/v2/uptime"); @@ -49,68 +52,84 @@ const Login = () => { }; return ( - - ( - - )} - /> - ( - - )} - /> - + ( + + )} + /> + ( + + )} + /> + + + + - + ); }; diff --git a/client/src/Pages/v2/Auth/Register.tsx b/client/src/Pages/v2/Auth/Register.tsx index 2a02c401f..d4565c66c 100644 --- a/client/src/Pages/v2/Auth/Register.tsx +++ b/client/src/Pages/v2/Auth/Register.tsx @@ -1,14 +1,17 @@ +import { AuthBasePage } from "@/Components/v2/Auth"; +import { TextInput } from "@/Components/v2/Inputs"; +import { Button } from "@/Components/v2/Inputs"; +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; + +import type { ApiResponse } from "@/Hooks/v2/UseApi"; 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 Typography from "@mui/material/Typography"; - -import { TextInput } from "@/Components/v2/Inputs/TextInput"; -import Button from "@mui/material/Button"; -import Stack from "@mui/material/Stack"; import { usePost } from "@/Hooks/v2/UseApi"; +import { useNavigate } from "react-router"; const schema = z .object({ @@ -30,14 +33,15 @@ type FormData = z.infer; const Register = () => { const { t } = useTranslation(); const theme = useTheme(); - const { post, loading, error } = usePost("/auth/register"); + const navigate = useNavigate(); + const { post, loading, error } = usePost(); const { handleSubmit, control, formState: { errors }, } = useForm({ - resolver: zodResolver(schema), // ⬅️ connect Zod + resolver: zodResolver(schema), defaultValues: { email: "", password: "", @@ -45,124 +49,128 @@ const Register = () => { }); const onSubmit = async (data: FormData) => { - const result = await post(data); + const result = await post("/auth/register", data); if (result) { - console.log(result); + navigate("/v2/uptime"); } else { console.error("Login failed:", error); } }; return ( - - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - - {error && {error}} + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + {error && {error}} + - + ); }; diff --git a/client/src/Pages/v2/Uptime/Create.tsx b/client/src/Pages/v2/Uptime/Create.tsx index a135ec84c..38b1511f4 100644 --- a/client/src/Pages/v2/Uptime/Create.tsx +++ b/client/src/Pages/v2/Uptime/Create.tsx @@ -44,7 +44,7 @@ const UptimeCreatePage = () => { mode: "onChange", }); const { response } = useGet("/notification-channels"); - const { post, loading, error } = usePost("/monitors"); + const { post, loading, error } = usePost(); const selectedType = useWatch({ control, name: "type", @@ -58,7 +58,7 @@ const UptimeCreatePage = () => { let interval = humanInterval(data.interval); if (!interval) interval = 60000; const submitData = { ...data, interval }; - const result = await post(submitData); + const result = await post("/monitors", submitData); if (result) { console.log(result); } else {