Merge remote-tracking branch 'upstream/master' into feat/forgot-password

This commit is contained in:
Alex Holliday
2024-06-11 10:38:06 -07:00
8 changed files with 231 additions and 44 deletions
+11 -7
View File
@@ -1014,6 +1014,7 @@
"version": "0.11.14",
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
"integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
"deprecated": "Use @eslint/config-array instead",
"dev": true,
"dependencies": {
"@humanwhocodes/object-schema": "^2.0.2",
@@ -1335,6 +1336,7 @@
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.6.2.tgz",
"integrity": "sha512-oG22NRno1+HSLV/9jVLThnHAKN4g+MXOO6GqaQxN9LM0hjt1tgRsrNAlfcJndmj/dVwqFtynkFB5qWnTEBZs7Q==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.24.6",
"@mui/base": "^5.0.0-beta.40",
@@ -1925,9 +1927,9 @@
"dev": true
},
"node_modules/@vitejs/plugin-react": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.0.tgz",
"integrity": "sha512-KcEbMsn4Dpk+LIbHMj7gDPRKaTMStxxWRkRmxsg/jVdFdJCZWt1SchZcf0M4t8lIKdwwMsEyzhrcOXRrDPtOBw==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.1.tgz",
"integrity": "sha512-m/V2syj5CuVnaxcUJOQRel/Wr31FFXRFlnOoq1TVtkCxsY5veGMTEmpWHndrhB2U8ScHtCQB1e+4hWYExQc6Lg==",
"dev": true,
"dependencies": {
"@babel/core": "^7.24.5",
@@ -2268,9 +2270,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001629",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001629.tgz",
"integrity": "sha512-c3dl911slnQhmxUIT4HhYzT7wnBK/XYpGnYLOj4nJBaRiw52Ibe7YxlDaAeRECvA786zCuExhxIUJ2K7nHMrBw==",
"version": "1.0.30001632",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001632.tgz",
"integrity": "sha512-udx3o7yHJfUxMLkGohMlVHCvFvWmirKh9JAH/d7WOLPetlH+LTL5cocMZ0t7oZx/mdlOWXti97xLZWc8uURRHg==",
"dev": true,
"funding": [
{
@@ -2643,7 +2645,9 @@
"version": "1.4.796",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.796.tgz",
"integrity": "sha512-NglN/xprcM+SHD2XCli4oC6bWe6kHoytcyLKCWXmRL854F0qhPhaYgUswUsglnPxYaNQIg2uMY4BvaomIf3kLA==",
"dev": true
"dev": true,
"license": "ISC"
},
"node_modules/error-ex": {
"version": "1.3.2",
+24 -9
View File
@@ -1,11 +1,17 @@
import { Box, Typography, useTheme } from '@mui/material';
import Button from '../Button';
import PropTypes from 'prop-types';
/**
* Integrations component
* @returns {JSX.Element}
* @param {Object} props - Props for the IntegrationsComponent.
* @param {string} props.url - The URL for the integration image.
* @param {string} props.header - The header for the integration.
* @param {string} props.info - Information about the integration.
* @param {Function} props.onClick - The onClick handler for the integration button.
* @returns {JSX.Element} The JSX representation of the IntegrationsComponent.
*/
const Integrations = () => {
const IntegrationsComponent = ({ url, header, info, onClick }) => {
const theme = useTheme();
return (
@@ -19,8 +25,8 @@ const Integrations = () => {
>
<Box
component="img"
src="https://via.placeholder.com/100"
alt="Placeholder"
src={url}
alt="Integration"
width={100}
height={100}
/>
@@ -32,8 +38,8 @@ const Integrations = () => {
flexDirection="column"
alignItems="flex-start"
>
<Typography variant="h6" component="div">
Header
<Typography variant="h6" component="div" style={{ fontSize: '16px' }}>
{header}
</Typography>
<Typography
variant="body1"
@@ -43,14 +49,23 @@ const Integrations = () => {
wordWrap: 'break-word',
textAlign: 'left'
}}
style={{ fontSize: '13px' }}
>
This is a paragraph that provides more detail about the header.
{info}
</Typography>
</Box>
<Button label="Click Me" level="primary" />
<Button label="Click Me" level="primary" onClick={onClick} />
</Box>
);
};
export default Integrations;
// PropTypes for IntegrationsComponent
IntegrationsComponent.propTypes = {
url: PropTypes.string.isRequired,
header: PropTypes.string.isRequired,
info: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired
};
export default IntegrationsComponent;
+33 -12
View File
@@ -5,6 +5,7 @@ const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
const initialState = {
isLoading: false,
authToken: "",
user: "",
success: null,
msg: null,
@@ -22,28 +23,48 @@ export const register = createAsyncThunk(
}
);
export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
try {
const res = await axios.post(`${BASE_URL}/auth/login`, form);
return res.data;
} catch (error) {
return thunkApi.rejectWithValue(error.response.data);
}
});
const handleAuthFulfilled = (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
state.authToken = action.payload.data;
const decodedToken = jwtDecode(action.payload.data);
const user = { ...decodedToken };
state.user = user;
};
const handleAuthRejected = (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
};
const authSlice = createSlice({
name: "auth",
initialState,
reducers: {},
extraReducers: (builder) => {
builder
// Register thunk
.addCase(register.pending, (state, action) => {
state.isLoading = true;
})
.addCase(register.fulfilled, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
const decodedToken = jwtDecode(action.payload.data);
const user = { ...decodedToken };
state.user = user;
.addCase(register.fulfilled, handleAuthFulfilled)
.addCase(register.rejected, handleAuthRejected)
// Login thunk
.addCase(login.pending, (state, action) => {
state.isLoading = true;
})
.addCase(register.rejected, (state, action) => {
state.isLoading = false;
state.success = action.payload.success;
state.msg = action.payload.msg;
});
.addCase(login.fulfilled, handleAuthFulfilled)
.addCase(login.rejected, handleAuthRejected);
},
});
-1
View File
@@ -10,7 +10,6 @@ import { useSelector } from "react-redux";
const HomeLayout = () => {
const authState = useSelector((state) => state.auth);
const { user, msg } = authState;
console.log(user, msg);
return (
<div className="home-layout">
<DashboardSidebar />
+67 -6
View File
@@ -1,9 +1,70 @@
import React from 'react'
import { Box, Typography, useTheme } from '@mui/material';
import IntegrationsComponent from '../../Components/Integrations';
/**
* Integrations Page Component
* @returns {JSX.Element} The JSX representation of the Integrations page.
*/
const Integrations = () => {
return (
<div>Integrations</div>
)
}
const theme = useTheme();
export default Integrations
const integrations = [
{
url: 'https://via.placeholder.com/100',
header: 'Integration 1',
info: 'Info about Integration 1',
onClick: () => {}
},
{
url: 'https://via.placeholder.com/100',
header: 'Integration 2',
info: 'Info about Integration 2',
onClick: () => {}
},
{
url: 'https://via.placeholder.com/100',
header: 'Integration 2',
info: 'Info about Integration 2',
onClick: () => {}
}
// Add more integrations as needed
];
return (
<Box
display="flex"
flexDirection="column"
alignItems="flex-start"
justifyContent="flex-start"
height="100vh"
p={theme.spacing(4)}
mt={theme.spacing(8)}
>
<Typography variant="h4" component="h1" gutterBottom style={{ fontSize: '16px' }}>
Integrations
</Typography>
<Typography variant="h6" component="h2" gutterBottom style={{ fontSize: '13px' }}>
Connect Uptime Genie to your favorite service
</Typography>
<Box
display="flex"
flexWrap="wrap"
gap={theme.spacing(4)}
>
{integrations.map((integration, index) => (
<IntegrationsComponent
key={index}
url={integration.url}
header={integration.header}
info={integration.info}
onClick={integration.onClick}
/>
))}
</Box>
</Box>
);
};
export default Integrations;
+78 -8
View File
@@ -1,4 +1,6 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import "./index.css";
import BackgroundPattern from "../../Components/BackgroundPattern/BackgroundPattern";
import Logomark from "../../assets/Images/Logomark.png";
@@ -8,23 +10,84 @@ import Button from "../../Components/Button";
import Google from "../../assets/Images/Google.png";
import PasswordTextField from "../../Components/TextFields/Password/PasswordTextField";
import { loginValidation } from "../../Validation/validation";
import { login } from "../../Features/Auth/authSlice";
import { useDispatch } from "react-redux";
const Login = () => {
const dispatch = useDispatch();
const navigate = useNavigate();
const idMap = {
"login-email-input": "email",
"login-password-input": "password",
};
const [errors, setErrors] = useState({});
const [form, setForm] = useState({
email: "",
password: "",
});
useEffect(() => {
const { error } = loginValidation.validate(form, {
abortEarly: false,
});
if (error) {
// Creates an error object in the format { field: message }
const validationErrors = error.details.reduce((acc, err) => {
return { ...acc, [err.path[0]]: err.message };
}, {});
setErrors(validationErrors);
} else {
setErrors({});
}
}, []);
const handleSubmit = async () => {
try {
await loginValidation.validateAsync(form, { abortEarly: false });
const action = await dispatch(login(form));
if (action.meta.requestStatus === "fulfilled") {
const token = action.payload.data;
localStorage.setItem("token", token);
navigate("/");
}
if (action.meta.requestStatus === "rejected") {
const error = new Error("Request rejected");
error.response = action.payload;
throw error;
}
} catch (error) {
if (error.name === "ValidationError") {
// TODO Handle validation errors
console.log(error.details);
alert(error);
} else if (error.response) {
// TODO handle dispatch errors
alert(error.response.msg);
} else {
// TODO handle unknown errors
console.log(error);
alert("Unknown error");
}
}
};
const handleInput = (e) => {
const fieldName = idMap[e.target.id];
setForm({
...form,
[fieldName]: e.target.value,
});
// Extract and validate individual fields as input changes
const fieldSchema = loginValidation.extract(fieldName);
const { error } = fieldSchema.validate(e.target.value);
let errMsg = "";
if (error) {
errMsg = error.message;
}
setErrors({ ...errors, [fieldName]: errMsg });
const newForm = { ...form, [idMap[e.target.id]]: e.target.value };
setForm(newForm);
};
return (
@@ -44,14 +107,16 @@ const Login = () => {
<div className="login-form-inputs">
<EmailTextField
onChange={handleInput}
error={false}
error={errors.email ? true : false}
helperText={errors.email ? errors.email : ""}
placeholder="Enter your email"
id="login-email-input"
/>
<div className="login-form-v2-spacing" />
<PasswordTextField
onChange={handleInput}
error={false}
error={errors.password ? true : false}
helperText={errors.password ? errors.password : ""}
placeholder="Password"
id="login-password-input"
/>
@@ -63,7 +128,12 @@ const Login = () => {
</div>
<div className="login-form-v3-spacing" />
<div className="login-form-actions">
<Button level="primary" label="Sign in" sx={{ width: "100%" }} />
<Button
level="primary"
label="Sign in"
sx={{ width: "100%" }}
onClick={handleSubmit}
/>
<div className="login-form-v-spacing" />
<Button
level="secondary"
+1
View File
@@ -88,6 +88,7 @@ const Register = () => {
} catch (error) {
if (error.name === "ValidationError") {
// TODO Handle validation errors
console.log(error);
alert(error);
} else if (error.response) {
// TODO handle dispatch errors
+17 -1
View File
@@ -24,4 +24,20 @@ const registerValidation = joi.object({
}),
});
export { registerValidation };
const loginValidation = joi.object({
email: joi
.string()
.email({ tlds: { allow: false } })
.required()
.messages({
"string.email": "Email must be a valid email",
"string.empty": "Email is required",
}),
password: joi.string().min(8).required().messages({
"string.min": "Password must be at least 8 characters",
"string.empty": "Password is required",
}),
});
export { registerValidation, loginValidation };