mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-04-27 20:19:39 -05:00
Merge remote-tracking branch 'upstream/master' into feat/forgot-password
This commit is contained in:
Generated
+11
-7
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user