feat(forms & pages & services): add a lot of stuff
improve login & register (integrate that with redux and api) + improve user settings dashboard (divide forms to particular files as form components) + improve formGenerator (catching refs and etc) + morefeat/x_gpu/chat_gpt_new_version
parent
ae781652de
commit
afe0b88808
|
|
@ -13,6 +13,7 @@
|
|||
"axios": "^1.8.1",
|
||||
"gatsby": "^5.13.3",
|
||||
"gatsby-plugin-sass": "^6.14.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-redux": "^9.2.0",
|
||||
|
|
@ -11751,6 +11752,15 @@
|
|||
"@sideway/pinpoint": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
@ -25860,6 +25870,11 @@
|
|||
"@sideway/pinpoint": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"js-cookie": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="
|
||||
},
|
||||
"js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
"axios": "^1.8.1",
|
||||
"gatsby": "^5.13.3",
|
||||
"gatsby-plugin-sass": "^6.14.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-redux": "^9.2.0",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState } from 'react'
|
||||
import React, { useState, useEffect } from 'react'
|
||||
|
||||
import passwordVisibleImg from '../../images/password-visible.png'
|
||||
import passwordHiddenImg from '../../images/password-hidden.png'
|
||||
|
|
@ -6,43 +6,28 @@ import passwordHiddenImg from '../../images/password-hidden.png'
|
|||
/**
|
||||
*
|
||||
* @param { [ {}, {}, ...{} ] } inputList - list of dicts with info about input
|
||||
* @param { [] } refList - react ref objects list for handler validation
|
||||
* @param { } action - fetch method
|
||||
*/
|
||||
export const FormGenerator = ({
|
||||
inputList, refList,
|
||||
inputList,
|
||||
action
|
||||
}) => {
|
||||
|
||||
const handler = async (event) => {
|
||||
event.preventDefault()
|
||||
|
||||
if ( inputList[0].action === 'Async' ) {
|
||||
await action(refList)
|
||||
} else if (
|
||||
inputList[0].action === 'Download'
|
||||
|| inputList[0].action === 'Upload'
|
||||
) {
|
||||
await action()
|
||||
} else {
|
||||
for (let i = 0; i < refList.length; i++) {
|
||||
if (refList[i].current.value === ''
|
||||
&& inputList[0].action !== 'Update'
|
||||
|| i === 0
|
||||
&& refList.length !== 1
|
||||
) {
|
||||
refList[i].current.focus()
|
||||
} else if (i === refList.length - 1) {
|
||||
await action(refList)
|
||||
}
|
||||
}
|
||||
}
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const formData = {};
|
||||
inputList.forEach((singleInput, index) => {
|
||||
if (typeof singleInput.type !== 'undefined' && singleInput.ref?.current) {
|
||||
formData[singleInput.name] = singleInput.ref.current.value;
|
||||
}
|
||||
});
|
||||
action(formData);
|
||||
};
|
||||
|
||||
let info
|
||||
|
||||
return (
|
||||
<form onSubmit={event => handler(event)} className="form">
|
||||
<form onSubmit={handleSubmit} className="form">
|
||||
{
|
||||
inputList.map((input, key) => {
|
||||
|
||||
|
|
@ -104,6 +89,13 @@ export const FormGenerator = ({
|
|||
key={key}
|
||||
/>
|
||||
)
|
||||
} else if (input.type === 'label') {
|
||||
return (
|
||||
<div className="form-field">
|
||||
<label>{input.name}</label>
|
||||
<div className="label-value">{input.value}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -112,10 +104,10 @@ export const FormGenerator = ({
|
|||
? <></>
|
||||
: <button
|
||||
type='submit'
|
||||
disabled={ info.allowButtonAction }
|
||||
className={ info.allowButtonAction === false ? "button-disabled" : "" }
|
||||
disabled={!info.allowButtonAction}
|
||||
className={!info.allowButtonAction ? "button-disabled" : ""}
|
||||
>
|
||||
{ info.button_value }
|
||||
{info.button_value}
|
||||
</button>
|
||||
}
|
||||
|
||||
|
|
@ -156,6 +148,7 @@ const TextInputGenerator = ({
|
|||
} else {
|
||||
setTextInputValidationInfo("Success")
|
||||
}
|
||||
input.validationInfo = textInputValidationInfo
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -167,12 +160,13 @@ const TextInputGenerator = ({
|
|||
id={input.name + info.action + info.endpoint + 'Input'}
|
||||
autoComplete='off'
|
||||
ref={input.ref}
|
||||
onChange={ input.onChange === null ? defaultValidation : input.onChange}
|
||||
onChange={input.onChange === null ? defaultValidation : input.onChange}
|
||||
className={
|
||||
[ "Empty", "Success"].includes(
|
||||
input.validationInfo === null ? textInputValidationInfo : input.validationInfo
|
||||
) ? "" : "input-incorrect"
|
||||
}
|
||||
placeholder={input.placeholder === null ? "" : input.placeholder}
|
||||
/>
|
||||
<div
|
||||
className="popup"
|
||||
|
|
@ -238,7 +232,6 @@ const PasswordInputGenerator = ({
|
|||
<div className="popup-content">
|
||||
{ input.validationInfo }
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
@ -465,7 +458,6 @@ const UploadInputGenerator = ({
|
|||
const onLoadFile = async (event) => {
|
||||
event.preventDefault()
|
||||
let data = event.target.files[0]
|
||||
// input.setFile(await toBase64(data))
|
||||
input.setFile( data )
|
||||
setDropInfos(data.name, data.size)
|
||||
}
|
||||
|
|
@ -474,7 +466,6 @@ const UploadInputGenerator = ({
|
|||
event.preventDefault()
|
||||
event.persist()
|
||||
let data = event.dataTransfer.files[0]
|
||||
// input.setFile(await toBase64(data))
|
||||
input.setFile( data )
|
||||
setDropInfos(data.name, data.size)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,121 +1,106 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, createRef, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { navigate } from 'gatsby';
|
||||
import { loginUser } from '../../../redux/asyncThunks/userAuthAsyncThunk';
|
||||
import { selectAuthError, selectAuthLoading, setError } from '../../../redux/slices/userAuthSlice';
|
||||
import FormGenerator from '../formGenerator';
|
||||
|
||||
// import { useSelector, useDispatch } from 'react-redux'
|
||||
const UserLogin = () => {
|
||||
const dispatch = useDispatch();
|
||||
const error = useSelector(selectAuthError);
|
||||
const loading = useSelector(selectAuthLoading);
|
||||
const [infoMessage, setInfoMessage] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
// import { userAuthSelector } from '../../../redux/slices/userAuthSlice'
|
||||
// import userAuthAsyncThunk from '../../../redux/asyncThunks/userAuthAsyncThunk'
|
||||
const usernameInput = createRef();
|
||||
const passwordInput = createRef();
|
||||
|
||||
import FormGenerator from '../formGenerator'
|
||||
const [usernameValidationInfo, setUsernameValidationInfo] = useState("Empty");
|
||||
const [passwordValidationInfo, setPasswordValidationInfo] = useState("Empty");
|
||||
|
||||
const [allowButtonAction, setAllowButtonAction] = useState(false);
|
||||
|
||||
const UserLoginForm = () => {
|
||||
|
||||
const usernameInput = React.createRef()
|
||||
const passwordInput = React.createRef()
|
||||
|
||||
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
|
||||
|
||||
const [usernameValidationInfo, setUsernameValidationInfo] = useState("Empty")
|
||||
const [passwordValidationInfo, setPasswordValidationInfo] = useState("Empty")
|
||||
|
||||
const [password, setPassword] = useState("")
|
||||
|
||||
const [allowButtonAction, setAllowButtonAction] = useState(false)
|
||||
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,24}$/;
|
||||
|
||||
const usernameValidation = (event) => {
|
||||
|
||||
if (event.target.value === "") {
|
||||
setUsernameValidationInfo("Email is required.")
|
||||
} else if(!emailRegex.test(event.target.value)) {
|
||||
setUsernameValidationInfo("Please provide correct email")
|
||||
setUsernameValidationInfo("Login jest wymagany");
|
||||
} else {
|
||||
setUsernameValidationInfo("Success")
|
||||
}
|
||||
setUsernameValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
const passwordValidation = (event) => {
|
||||
|
||||
setPassword(event.target.value)
|
||||
|
||||
if (event.target.value === "") {
|
||||
setPasswordValidationInfo("Password is required.")
|
||||
} else if(!passwordRegex.test(event.target.value)) {
|
||||
setPasswordValidationInfo("Password require:\n - At least 8 characters,\n - At least one uppercase letter,\n - At least one lowercase letter,\n - At least one digit,\n - At least one special character.")
|
||||
setPasswordValidationInfo("Hasło jest wymagane");
|
||||
} else if (!passwordRegex.test(event.target.value)) {
|
||||
setPasswordValidationInfo("Hasło nie spełnia wymagań tej witryny");
|
||||
} else {
|
||||
setPasswordValidationInfo("Success")
|
||||
}
|
||||
setPasswordValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setAllowButtonAction(
|
||||
usernameValidationInfo === "Success"
|
||||
&& passwordValidationInfo === "Success"
|
||||
)
|
||||
usernameValidationInfo === "Success" &&
|
||||
passwordValidationInfo === "Success"
|
||||
);
|
||||
}, [
|
||||
allowButtonAction,
|
||||
usernameValidationInfo,
|
||||
passwordValidationInfo
|
||||
]
|
||||
)
|
||||
passwordValidationInfo,
|
||||
]);
|
||||
|
||||
// const dispatch = useDispatch()
|
||||
// const { info } = useSelector( userAuthSelector )
|
||||
const info = "" // if redux is integrated - delete this line
|
||||
|
||||
let refList = [
|
||||
usernameInput,
|
||||
passwordInput
|
||||
]
|
||||
|
||||
let inputList = [
|
||||
const inputList = [
|
||||
{
|
||||
type: 'info',
|
||||
action: 'Create',
|
||||
endpint: 'user/auth',
|
||||
button_value: 'SIGN IN'
|
||||
action: 'Login',
|
||||
endpoint: 'auth',
|
||||
button_value: loading ? 'LOGOWANIE...' : 'ZALOGUJ',
|
||||
allowButtonAction: allowButtonAction
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'EMAIL',
|
||||
name: 'LOGIN',
|
||||
ref: usernameInput,
|
||||
onChange: usernameValidation,
|
||||
validationInfo: usernameValidationInfo
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'PASSWORD',
|
||||
name: 'HASŁO',
|
||||
ref: passwordInput,
|
||||
onChange: passwordValidation,
|
||||
validationInfo: passwordValidationInfo
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
const login = async ( refs ) => {
|
||||
let pass = {
|
||||
username: refs[0].current.value,
|
||||
password: refs[1].current.value
|
||||
}
|
||||
// dispatch(
|
||||
// userAuthAsyncThunk.fetchLogin(
|
||||
// pass
|
||||
// )
|
||||
// )
|
||||
const login = async (formData) => {
|
||||
try {
|
||||
const credentials = {
|
||||
username: formData.LOGIN,
|
||||
password: formData.HASŁO
|
||||
};
|
||||
|
||||
await dispatch(loginUser(credentials)).unwrap();
|
||||
setInfoMessage("Logowanie zakończone sukcesem!");
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
setErrorMessage("Wystąpił błąd podczas logowania (" + error.message + ")");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='form-container'>
|
||||
<FormGenerator
|
||||
inputList={ inputList }
|
||||
refList={ refList }
|
||||
action={ login }
|
||||
inputList={inputList}
|
||||
action={login}
|
||||
/>
|
||||
<div className='form_info'>
|
||||
{ info }
|
||||
{infoMessage && <div className="success-message">{infoMessage}</div>}
|
||||
{errorMessage && <div className="error-message">{errorMessage}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export default UserLoginForm
|
||||
export default UserLogin;
|
||||
|
|
|
|||
|
|
@ -1,152 +1,161 @@
|
|||
import React, { useState, useEffect } from 'react'
|
||||
import React, { useState, useEffect, createRef } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { navigate } from 'gatsby';
|
||||
import { registerUser } from '../../../redux/asyncThunks/userAuthAsyncThunk';
|
||||
import { selectAuthError, selectAuthLoading } from '../../../redux/slices/userAuthSlice';
|
||||
import FormGenerator from '../formGenerator';
|
||||
|
||||
// import { useSelector, useDispatch } from 'react-redux'
|
||||
const UserRegister = () => {
|
||||
const dispatch = useDispatch();
|
||||
const error = useSelector(selectAuthError);
|
||||
const loading = useSelector(selectAuthLoading);
|
||||
const [infoMessage, setInfoMessage] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
// import { userCrudSelector } from '../../../redux/slices/userCrudSlice'
|
||||
// import userCrudAsyncThunk from '../../../redux/asyncThunks/userCrudAsyncThunk'
|
||||
const usernameInput = createRef();
|
||||
const emailInput = createRef();
|
||||
const passwordInput = createRef();
|
||||
const confirmPasswordInput = createRef();
|
||||
|
||||
import FormGenerator from '../formGenerator'
|
||||
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,24}$/;
|
||||
|
||||
const [usernameValidationInfo, setUsernameValidationInfo] = useState("Empty");
|
||||
const [emailValidationInfo, setEmailValidationInfo] = useState("Empty");
|
||||
const [passwordValidationInfo, setPasswordValidationInfo] = useState("Empty");
|
||||
const [confirmPasswordValidationInfo, setConfirmPasswordValidationInfo] = useState("Empty");
|
||||
|
||||
const UserRegisterForm = () => {
|
||||
|
||||
const usernameInput = React.createRef()
|
||||
const passwordInput = React.createRef()
|
||||
const confirmPasswordInput = React.createRef()
|
||||
|
||||
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
|
||||
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
|
||||
|
||||
const [usernameValidationInfo, setUsernameValidationInfo] = useState("Empty")
|
||||
const [passwordValidationInfo, setPasswordValidationInfo] = useState("Empty")
|
||||
const [confirmPasswordValidationInfo, setConfirmPasswordValidationInfo] = useState("Empty")
|
||||
|
||||
const [password, setPassword] = useState("")
|
||||
const [confirmPassword, setConfirmPassword] = useState("")
|
||||
|
||||
const [allowButtonAction, setAllowButtonAction] = useState(false)
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [allowButtonAction, setAllowButtonAction] = useState(false);
|
||||
|
||||
const usernameValidation = (event) => {
|
||||
if (event.target.value === "") {
|
||||
setUsernameValidationInfo("Email is required.")
|
||||
} else if(!emailRegex.test(event.target.value)) {
|
||||
setUsernameValidationInfo("Please provide correct email")
|
||||
setUsernameValidationInfo("Login jest wymagany");
|
||||
} else {
|
||||
setUsernameValidationInfo("Success")
|
||||
setUsernameValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
const emailValidation = (event) => {
|
||||
if (event.target.value === "") {
|
||||
setEmailValidationInfo("Email jest wymagany");
|
||||
} else if (!emailRegex.test(event.target.value)) {
|
||||
setEmailValidationInfo("Nieprawidłowy format emaila");
|
||||
} else {
|
||||
setEmailValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
const passwordValidation = (event) => {
|
||||
|
||||
setPassword(event.target.value)
|
||||
setPassword(event.target.value);
|
||||
|
||||
if (event.target.value === "") {
|
||||
setPasswordValidationInfo("Password is required.")
|
||||
} else if(!passwordRegex.test(event.target.value)) {
|
||||
setPasswordValidationInfo("Password require:\n - At least 8 characters,\n - At least one uppercase letter,\n - At least one lowercase letter,\n - At least one digit,\n - At least one special character.")
|
||||
setPasswordValidationInfo("Hasło jest wymagane");
|
||||
} else if (!passwordRegex.test(event.target.value)) {
|
||||
setPasswordValidationInfo("Hasło musi zawierać:\n - Minimum 8 znaków\n - Maksimum 24 znaki\n - Minimum jedną wielką literę\n - Minimum jedną małą literę\n - Minimum jedną cyfrę\n - Minimum jeden znak specjalny");
|
||||
} else {
|
||||
setPasswordValidationInfo("Success")
|
||||
setPasswordValidationInfo("Success");
|
||||
}
|
||||
|
||||
if(event.target.value !== confirmPassword) {
|
||||
setConfirmPasswordValidationInfo("Passwords are different.")
|
||||
if (event.target.value !== confirmPassword) {
|
||||
setConfirmPasswordValidationInfo("Hasła nie są identyczne");
|
||||
} else {
|
||||
setConfirmPasswordValidationInfo("Success")
|
||||
}
|
||||
setConfirmPasswordValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
const confirmPasswordValidation = (event) => {
|
||||
setConfirmPassword(event.target.value);
|
||||
|
||||
setConfirmPassword(event.target.value)
|
||||
|
||||
if(event.target.value !== password) {
|
||||
setConfirmPasswordValidationInfo("Passwords are different.")
|
||||
if (event.target.value !== password) {
|
||||
setConfirmPasswordValidationInfo("Hasła nie są identyczne");
|
||||
} else {
|
||||
setConfirmPasswordValidationInfo("Success")
|
||||
}
|
||||
setConfirmPasswordValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setAllowButtonAction(
|
||||
usernameValidationInfo === "Success"
|
||||
&& passwordValidationInfo === "Success"
|
||||
&& confirmPasswordValidationInfo === "Success"
|
||||
)
|
||||
usernameValidationInfo === "Success" &&
|
||||
emailValidationInfo === "Success" &&
|
||||
passwordValidationInfo === "Success" &&
|
||||
confirmPasswordValidationInfo === "Success"
|
||||
);
|
||||
}, [
|
||||
allowButtonAction,
|
||||
usernameValidationInfo,
|
||||
emailValidationInfo,
|
||||
passwordValidationInfo,
|
||||
confirmPasswordValidationInfo
|
||||
]
|
||||
)
|
||||
]);
|
||||
|
||||
|
||||
// const dispatch = useDispatch()
|
||||
// const { info } = useSelector( userCrudSelector )
|
||||
const info = "" // if redux is integrated - delete this line
|
||||
|
||||
let refList = [
|
||||
usernameInput,
|
||||
passwordInput,
|
||||
confirmPasswordInput
|
||||
]
|
||||
|
||||
let inputList = [
|
||||
const inputList = [
|
||||
{
|
||||
type: 'info',
|
||||
action: 'Create',
|
||||
endpint: 'user/auth/register',
|
||||
button_value: 'SIGN UP',
|
||||
action: 'Register',
|
||||
endpoint: 'auth',
|
||||
button_value: loading ? 'REJESTRACJA...' : 'ZAREJESTRUJ',
|
||||
allowButtonAction: allowButtonAction
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'EMAIL',
|
||||
name: 'LOGIN',
|
||||
ref: usernameInput,
|
||||
onChange: usernameValidation,
|
||||
validationInfo: usernameValidationInfo
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'EMAIL',
|
||||
ref: emailInput,
|
||||
onChange: emailValidation,
|
||||
validationInfo: emailValidationInfo
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'PASSWORD',
|
||||
name: 'HASŁO',
|
||||
ref: passwordInput,
|
||||
onChange: passwordValidation,
|
||||
validationInfo: passwordValidationInfo
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'CONFIRM PASSWORD',
|
||||
name: 'POTWIERDŹ HASŁO',
|
||||
ref: confirmPasswordInput,
|
||||
onChange: confirmPasswordValidation,
|
||||
validationInfo: confirmPasswordValidationInfo
|
||||
}
|
||||
]
|
||||
];
|
||||
|
||||
const register = async ( refs ) => {
|
||||
let pass = {
|
||||
username: refs[0].current.value,
|
||||
password: refs[1].current.value
|
||||
}
|
||||
// dispatch(
|
||||
// userCrudAsyncThunk.fetchRegister(
|
||||
// pass
|
||||
// )
|
||||
// )
|
||||
const register = async (formData) => {
|
||||
try {
|
||||
const userData = {
|
||||
username: formData.LOGIN,
|
||||
email: formData.EMAIL,
|
||||
password: formData.HASŁO
|
||||
};
|
||||
|
||||
await dispatch(registerUser(userData)).unwrap();
|
||||
setInfoMessage("Rejestracja zakończona sukcesem!");
|
||||
navigate('/dashboard');
|
||||
} catch (error) {
|
||||
setErrorMessage("Wystąpił błąd podczas rejestracji (" + error.massage + ")");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='form-container'>
|
||||
<FormGenerator
|
||||
inputList={ inputList }
|
||||
refList={ refList }
|
||||
action={ register }
|
||||
inputList={inputList}
|
||||
action={register}
|
||||
/>
|
||||
<div className='form_info'>
|
||||
{ info }
|
||||
{infoMessage && <div className="success-message">{infoMessage}</div>}
|
||||
{errorMessage && <div className="error-message">{errorMessage}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export default UserRegisterForm
|
||||
export default UserRegister;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,137 @@
|
|||
import React, { useState, createRef, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { changePassword } from '../../../redux/asyncThunks/userAuthAsyncThunk';
|
||||
import { selectAuthError, selectAuthLoading } from '../../../redux/slices/userAuthSlice';
|
||||
import FormGenerator from '../formGenerator';
|
||||
|
||||
const UserChangePasswordForm = () => {
|
||||
const dispatch = useDispatch();
|
||||
const loading = useSelector(selectAuthLoading);
|
||||
const error = useSelector(selectAuthError);
|
||||
const [infoMessage, setInfoMessage] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const currentPasswordInput = createRef();
|
||||
const newPasswordInput = createRef();
|
||||
const confirmPasswordInput = createRef();
|
||||
|
||||
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,24}$/;
|
||||
|
||||
const [currentPasswordValidationInfo, setCurrentPasswordValidationInfo] = useState("Empty");
|
||||
const [newPasswordValidationInfo, setNewPasswordValidationInfo] = useState("Empty");
|
||||
const [confirmPasswordValidationInfo, setConfirmPasswordValidationInfo] = useState("Empty");
|
||||
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [allowButtonAction, setAllowButtonAction] = useState(false);
|
||||
|
||||
const currentPasswordValidation = (event) => {
|
||||
if (event.target.value === "") {
|
||||
setCurrentPasswordValidationInfo("Hasło jest wymagane");
|
||||
} else if (event.target.value === newPassword) {
|
||||
setCurrentPasswordValidationInfo("Nowe hasło nie może być takie samo jak aktualne");
|
||||
} else {
|
||||
setCurrentPasswordValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
const newPasswordValidation = (event) => {
|
||||
setNewPassword(event.target.value);
|
||||
|
||||
if (event.target.value === "") {
|
||||
setNewPasswordValidationInfo("Hasło jest wymagane");
|
||||
} else if (!passwordRegex.test(event.target.value)) {
|
||||
setNewPasswordValidationInfo("Hasło musi zawierać:\n - Minimum 8 znaków\n - Maksimum 24 znaki\n - Minimum jedną wielką literę\n - Minimum jedną małą literę\n - Minimum jedną cyfrę\n - Minimum jeden znak specjalny");
|
||||
} else {
|
||||
setNewPasswordValidationInfo("Success");
|
||||
}
|
||||
|
||||
if (event.target.value !== confirmPassword) {
|
||||
setConfirmPasswordValidationInfo("Hasła nie są identyczne");
|
||||
} else {
|
||||
setConfirmPasswordValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
const confirmPasswordValidation = (event) => {
|
||||
setConfirmPassword(event.target.value);
|
||||
|
||||
if (event.target.value !== newPassword) {
|
||||
setConfirmPasswordValidationInfo("Hasła nie są identyczne");
|
||||
} else {
|
||||
setConfirmPasswordValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setAllowButtonAction(
|
||||
currentPasswordValidationInfo === "Success" &&
|
||||
newPasswordValidationInfo === "Success" &&
|
||||
confirmPasswordValidationInfo === "Success"
|
||||
);
|
||||
}, [
|
||||
currentPasswordValidationInfo,
|
||||
newPasswordValidationInfo,
|
||||
confirmPasswordValidationInfo
|
||||
]);
|
||||
|
||||
const inputList = [
|
||||
{
|
||||
type: 'info',
|
||||
action: 'Update',
|
||||
endpoint: 'User',
|
||||
button_value: loading ? 'AKTUALIZACJA...' : 'AKTUALIZUJ',
|
||||
allowButtonAction: allowButtonAction
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'AKTUALNE HASŁO',
|
||||
ref: currentPasswordInput,
|
||||
onChange: currentPasswordValidation,
|
||||
validationInfo: currentPasswordValidationInfo
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'NOWE HASŁO',
|
||||
ref: newPasswordInput,
|
||||
onChange: newPasswordValidation,
|
||||
validationInfo: newPasswordValidationInfo
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'POTWIERDŹ NOWE HASŁO',
|
||||
ref: confirmPasswordInput,
|
||||
onChange: confirmPasswordValidation,
|
||||
validationInfo: confirmPasswordValidationInfo
|
||||
}
|
||||
];
|
||||
|
||||
const update = async (formData) => {
|
||||
try {
|
||||
const userData = {
|
||||
currentPassword: formData['AKTUALNE HASŁO'],
|
||||
newPassword: formData['NOWE HASŁO']
|
||||
};
|
||||
|
||||
await dispatch(changePassword(userData)).unwrap();
|
||||
setInfoMessage("Hasło zostało zmienione!");
|
||||
} catch (error) {
|
||||
setErrorMessage("Wystąpił błąd podczas zmiany hasła (" + error.message + ")");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='form-container'>
|
||||
<FormGenerator
|
||||
inputList={inputList}
|
||||
action={update}
|
||||
/>
|
||||
<div className='form_info'>
|
||||
{infoMessage && <div className="success-message">{infoMessage}</div>}
|
||||
{errorMessage && <div className="error-message">{errorMessage}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserChangePasswordForm;
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
import React, { useState, createRef, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { navigate } from 'gatsby';
|
||||
import { deleteAccount } from '../../../redux/asyncThunks/userAuthAsyncThunk';
|
||||
import { selectAuthError, selectAuthLoading } from '../../../redux/slices/userAuthSlice';
|
||||
import FormGenerator from '../formGenerator';
|
||||
|
||||
const UserDeleteAccountForm = () => {
|
||||
const dispatch = useDispatch();
|
||||
const loading = useSelector(selectAuthLoading);
|
||||
const error = useSelector(selectAuthError);
|
||||
const [infoMessage, setInfoMessage] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const deleteConfirmationInput = createRef();
|
||||
const [deleteConfirmationValidationInfo, setDeleteConfirmationValidationInfo] = useState("Empty");
|
||||
const [allowButtonAction, setAllowButtonAction] = useState(false);
|
||||
|
||||
const deleteConfirmationValidation = (event) => {
|
||||
if (event.target.value === "") {
|
||||
setDeleteConfirmationValidationInfo("Wpisz 'DELETE' aby potwierdzić usunięcie konta");
|
||||
} else if (event.target.value !== "DELETE") {
|
||||
setDeleteConfirmationValidationInfo("Wpisz dokładnie 'DELETE' aby potwierdzić usunięcie konta");
|
||||
} else {
|
||||
setDeleteConfirmationValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setAllowButtonAction(deleteConfirmationValidationInfo === "Success");
|
||||
}, [deleteConfirmationValidationInfo]);
|
||||
|
||||
const inputList = [
|
||||
{
|
||||
type: 'info',
|
||||
action: 'Delete',
|
||||
endpoint: 'User',
|
||||
button_value: loading ? 'USUWANIE...' : 'USUŃ KONTO',
|
||||
allowButtonAction: allowButtonAction
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'POTWIERDŹ USUNIĘCIE',
|
||||
ref: deleteConfirmationInput,
|
||||
onChange: deleteConfirmationValidation,
|
||||
validationInfo: deleteConfirmationValidationInfo,
|
||||
placeholder: "Wpisz 'DELETE' aby potwierdzić usunięcie konta"
|
||||
}
|
||||
];
|
||||
|
||||
const deleteAccountHandler = async (formData) => {
|
||||
try {
|
||||
if (formData['POTWIERDŹ USUNIĘCIE'] === 'DELETE') {
|
||||
await dispatch(deleteAccount()).unwrap();
|
||||
setInfoMessage("Konto zostało usunięte!");
|
||||
setTimeout(() => {
|
||||
navigate('/');
|
||||
}, 2000);
|
||||
} else {
|
||||
setErrorMessage("Wpisz 'DELETE' aby potwierdzić usunięcie konta");
|
||||
}
|
||||
} catch (error) {
|
||||
setErrorMessage("Wystąpił błąd podczas usuwania konta (" + error.message + ")");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='form-container'>
|
||||
<FormGenerator
|
||||
inputList={inputList}
|
||||
action={deleteAccountHandler}
|
||||
/>
|
||||
<div className='form_info'>
|
||||
{infoMessage && <div className="success-message">{infoMessage}</div>}
|
||||
{errorMessage && <div className="error-message">{errorMessage}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserDeleteAccountForm;
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
import React, { useState, createRef, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { updateProfile } from '../../../redux/asyncThunks/userAuthAsyncThunk';
|
||||
import { selectAuthError, selectAuthLoading, selectCurrentUser } from '../../../redux/slices/userAuthSlice';
|
||||
import FormGenerator from '../formGenerator';
|
||||
|
||||
const UserUpdateProfileForm = () => {
|
||||
const dispatch = useDispatch();
|
||||
const user = useSelector(selectCurrentUser);
|
||||
const loading = useSelector(selectAuthLoading);
|
||||
const error = useSelector(selectAuthError);
|
||||
const [infoMessage, setInfoMessage] = useState('');
|
||||
const [errorMessage, setErrorMessage] = useState('');
|
||||
|
||||
const emailInput = createRef();
|
||||
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
const [emailValidationInfo, setEmailValidationInfo] = useState("Empty");
|
||||
const [allowButtonAction, setAllowButtonAction] = useState(false);
|
||||
|
||||
const emailValidation = (event) => {
|
||||
if (event.target.value === "") {
|
||||
setEmailValidationInfo("Email jest wymagany");
|
||||
} else if (!emailRegex.test(event.target.value)) {
|
||||
setEmailValidationInfo("Nieprawidłowy format emaila");
|
||||
} else {
|
||||
setEmailValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setAllowButtonAction(emailValidationInfo === "Success");
|
||||
}, [emailValidationInfo]);
|
||||
|
||||
const inputList = [
|
||||
{
|
||||
type: 'info',
|
||||
action: 'Update',
|
||||
endpoint: 'User',
|
||||
button_value: loading ? 'AKTUALIZACJA...' : 'AKTUALIZUJ',
|
||||
allowButtonAction: allowButtonAction
|
||||
},
|
||||
{
|
||||
type: 'label',
|
||||
name: 'LOGIN',
|
||||
value: user?.login || ''
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'EMAIL',
|
||||
ref: emailInput,
|
||||
onChange: emailValidation,
|
||||
validationInfo: emailValidationInfo,
|
||||
value: user?.email || ''
|
||||
}
|
||||
];
|
||||
|
||||
const update = async (formData) => {
|
||||
try {
|
||||
const userData = {
|
||||
email: formData.EMAIL
|
||||
};
|
||||
|
||||
await dispatch(updateProfile(userData)).unwrap();
|
||||
setInfoMessage("Profil został zaktualizowany!");
|
||||
} catch (error) {
|
||||
setErrorMessage("Wystąpił błąd podczas aktualizacji (" + error.message + ")");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='form-container'>
|
||||
<FormGenerator
|
||||
inputList={inputList}
|
||||
action={update}
|
||||
/>
|
||||
<div className='form_info'>
|
||||
{infoMessage && <div className="success-message">{infoMessage}</div>}
|
||||
{errorMessage && <div className="error-message">{errorMessage}</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserUpdateProfileForm;
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// Konfiguracja API
|
||||
export const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001/api';
|
||||
|
||||
// Konfiguracja aplikacji
|
||||
export const APP_CONFIG = {
|
||||
name: 'Render App',
|
||||
version: '1.0.0',
|
||||
environment: process.env.NODE_ENV || 'development'
|
||||
};
|
||||
|
||||
// Konfiguracja autentykacji
|
||||
export const AUTH_CONFIG = {
|
||||
tokenExpiry: 7, // dni
|
||||
refreshTokenExpiry: 30, // dni
|
||||
cookieOptions: {
|
||||
secure: true,
|
||||
sameSite: 'strict',
|
||||
path: '/'
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
export const useDashboardList = (initialItems = []) => {
|
||||
const [selectedItem, setSelectedItem] = useState(null);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [items, setItems] = useState(initialItems);
|
||||
const [isFormVisible, setIsFormVisible] = useState(false);
|
||||
const [formMode, setFormMode] = useState('create');
|
||||
const [message, setMessage] = useState({ type: '', text: '' });
|
||||
|
||||
const handleSearch = (query) => {
|
||||
setSearchQuery(query);
|
||||
// Implementacja wyszukiwania w zależności od typu elementów
|
||||
const filteredItems = initialItems.filter(item =>
|
||||
Object.values(item).some(value =>
|
||||
String(value).toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
);
|
||||
setItems(filteredItems);
|
||||
};
|
||||
|
||||
const handleItemSelect = (item) => {
|
||||
setSelectedItem(item);
|
||||
};
|
||||
|
||||
const handleFormToggle = (mode = 'create', item = null) => {
|
||||
setFormMode(mode);
|
||||
setSelectedItem(item);
|
||||
setIsFormVisible(!isFormVisible);
|
||||
};
|
||||
|
||||
const handleMessage = (type, text) => {
|
||||
setMessage({ type, text });
|
||||
setTimeout(() => {
|
||||
setMessage({ type: '', text: '' });
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
return {
|
||||
selectedItem,
|
||||
searchQuery,
|
||||
items,
|
||||
isFormVisible,
|
||||
formMode,
|
||||
message,
|
||||
handleSearch,
|
||||
handleItemSelect,
|
||||
handleFormToggle,
|
||||
handleMessage,
|
||||
setItems
|
||||
};
|
||||
};
|
||||
|
|
@ -1,21 +1,36 @@
|
|||
import React, { useState } from 'react';
|
||||
import '../styles/general.scss';
|
||||
import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { navigate } from 'gatsby';
|
||||
import { logoutUser, checkAuth } from '../redux/asyncThunks/userAuthAsyncThunk';
|
||||
import { selectIsAuthenticated, selectUserPermissions } from '../redux/slices/userAuthSlice';
|
||||
|
||||
import FootComponent from '../components/foot.js';
|
||||
import NavBarComponent from '../components/navbar.js';
|
||||
|
||||
import ThreeDModelsDashboard from './dashboards/3d-models.js';
|
||||
import FootComponent from '../components/foot.js';
|
||||
import AIModelsDashboard from './dashboards/ai.models.js';
|
||||
import AITasksDashboard from './dashboards/ai.tasks.js';
|
||||
import RendersDashboard from './dashboards/renders.js';
|
||||
import ServersDashboard from './dashboards/servers.js';
|
||||
import UserSettings from './dashboards/user.js';
|
||||
import UserSettingsDashboard from './dashboards/user.js';
|
||||
import ThreeDModelsDashboard from './dashboards/3d-models.js';
|
||||
|
||||
const DashboardPage = () => {
|
||||
const dispatch = useDispatch();
|
||||
const isAuthenticated = useSelector(selectIsAuthenticated);
|
||||
const permissions = useSelector(selectUserPermissions);
|
||||
|
||||
const icons_size = "fa-2x";
|
||||
const [activeComponent, setActiveComponent] = useState('3d-models');
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated) {
|
||||
dispatch(checkAuth())
|
||||
.unwrap()
|
||||
.catch(() => {
|
||||
navigate('/auth/login');
|
||||
});
|
||||
}
|
||||
}, [dispatch, isAuthenticated]);
|
||||
|
||||
const handleNavigation = (path) => {
|
||||
setActiveComponent(path);
|
||||
};
|
||||
|
|
@ -24,10 +39,13 @@ const DashboardPage = () => {
|
|||
return activeComponent === path;
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
// TODO: Implement proper logout logic (clear tokens, session, etc.)
|
||||
console.log('Logging out...');
|
||||
window.location.href = '/login';
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await dispatch(logoutUser()).unwrap();
|
||||
navigate('/auth/login');
|
||||
} catch (error) {
|
||||
console.error('Błąd podczas wylogowywania:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderContent = () => {
|
||||
|
|
@ -41,7 +59,7 @@ const DashboardPage = () => {
|
|||
case 'servers':
|
||||
return <ServersDashboard />;
|
||||
case 'settings':
|
||||
return <UserSettings />;
|
||||
return <UserSettingsDashboard />;
|
||||
case '3d-models':
|
||||
default:
|
||||
return <ThreeDModelsDashboard />;
|
||||
|
|
|
|||
|
|
@ -1,193 +1,42 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import FormGenerator from '../../components/forms/formGenerator';
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { selectCurrentUser } from '../../redux/slices/userAuthSlice';
|
||||
import UserUpdateProfileForm from '../../components/forms/user_settings/userUpdateProfileForm';
|
||||
import UserChangePasswordForm from '../../components/forms/user_settings/userChangePasswordForm';
|
||||
import UserDeleteAccountForm from '../../components/forms/user_settings/userDeleteAccountForm';
|
||||
|
||||
const UserSettings = () => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [message, setMessage] = useState({ type: '', text: '' });
|
||||
|
||||
const usernameInput = useRef();
|
||||
const emailInput = useRef();
|
||||
const currentPasswordInput = useRef();
|
||||
const newPasswordInput = useRef();
|
||||
const confirmPasswordInput = useRef();
|
||||
|
||||
const [usernameValidationInfo, setUsernameValidationInfo] = useState("Empty");
|
||||
const [emailValidationInfo, setEmailValidationInfo] = useState("Empty");
|
||||
const [currentPasswordValidationInfo, setCurrentPasswordValidationInfo] = useState("Empty");
|
||||
const [newPasswordValidationInfo, setNewPasswordValidationInfo] = useState("Empty");
|
||||
const [confirmPasswordValidationInfo, setConfirmPasswordValidationInfo] = useState("Empty");
|
||||
|
||||
const emailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
|
||||
|
||||
const usernameValidation = (event) => {
|
||||
if (event.target.value === "") {
|
||||
setUsernameValidationInfo("Username is required.");
|
||||
} else {
|
||||
setUsernameValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
const emailValidation = (event) => {
|
||||
if (event.target.value === "") {
|
||||
setEmailValidationInfo("Email is required.");
|
||||
} else if (!emailRegex.test(event.target.value)) {
|
||||
setEmailValidationInfo("Please provide correct email");
|
||||
} else {
|
||||
setEmailValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
const currentPasswordValidation = (event) => {
|
||||
if (event.target.value === "") {
|
||||
setCurrentPasswordValidationInfo("Current password is required.");
|
||||
} else {
|
||||
setCurrentPasswordValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
const newPasswordValidation = (event) => {
|
||||
if (event.target.value === "") {
|
||||
setNewPasswordValidationInfo("New password is required.");
|
||||
} else if (!passwordRegex.test(event.target.value)) {
|
||||
setNewPasswordValidationInfo("Password require:\n - At least 8 characters,\n - At least one uppercase letter,\n - At least one lowercase letter,\n - At least one digit,\n - At least one special character.");
|
||||
} else {
|
||||
setNewPasswordValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
const confirmPasswordValidation = (event) => {
|
||||
if (event.target.value === "") {
|
||||
setConfirmPasswordValidationInfo("Please confirm your new password.");
|
||||
} else if (event.target.value !== newPasswordInput.current.value) {
|
||||
setConfirmPasswordValidationInfo("Passwords do not match.");
|
||||
} else {
|
||||
setConfirmPasswordValidationInfo("Success");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (refs) => {
|
||||
try {
|
||||
// TODO: Implement API call to update user data
|
||||
console.log('Updating user data:', {
|
||||
username: refs[0].current.value,
|
||||
email: refs[1].current.value,
|
||||
currentPassword: refs[2].current.value,
|
||||
newPassword: refs[3].current.value
|
||||
});
|
||||
|
||||
setMessage({ type: 'success', text: 'Settings updated successfully!' });
|
||||
setIsEditing(false);
|
||||
} catch (error) {
|
||||
setMessage({ type: 'error', text: 'Failed to update settings. Please try again.' });
|
||||
}
|
||||
};
|
||||
|
||||
const getInputList = () => {
|
||||
const baseInputs = [
|
||||
{
|
||||
type: 'info',
|
||||
action: 'Update',
|
||||
endpoint: 'user/settings',
|
||||
button_value: isEditing ? 'SAVE CHANGES' : ''
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'USERNAME',
|
||||
ref: usernameInput,
|
||||
onChange: usernameValidation,
|
||||
validationInfo: usernameValidationInfo,
|
||||
disabled: !isEditing
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
name: 'EMAIL',
|
||||
ref: emailInput,
|
||||
onChange: emailValidation,
|
||||
validationInfo: emailValidationInfo,
|
||||
disabled: !isEditing
|
||||
}
|
||||
];
|
||||
|
||||
if (isEditing) {
|
||||
baseInputs.push(
|
||||
{
|
||||
type: 'password',
|
||||
name: 'CURRENT PASSWORD',
|
||||
ref: currentPasswordInput,
|
||||
onChange: currentPasswordValidation,
|
||||
validationInfo: currentPasswordValidationInfo
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'NEW PASSWORD',
|
||||
ref: newPasswordInput,
|
||||
onChange: newPasswordValidation,
|
||||
validationInfo: newPasswordValidationInfo
|
||||
},
|
||||
{
|
||||
type: 'password',
|
||||
name: 'CONFIRM NEW PASSWORD',
|
||||
ref: confirmPasswordInput,
|
||||
onChange: confirmPasswordValidation,
|
||||
validationInfo: confirmPasswordValidationInfo
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return baseInputs;
|
||||
};
|
||||
|
||||
const formRefs = [usernameInput, emailInput, currentPasswordInput, newPasswordInput, confirmPasswordInput];
|
||||
|
||||
const handleFormAction = (refs) => {
|
||||
const formData = {};
|
||||
formRefs.forEach((ref, index) => {
|
||||
formData[getInputList()[index].name] = ref.current.value;
|
||||
});
|
||||
handleSubmit(formRefs);
|
||||
};
|
||||
const UserSettingsDashboard = () => {
|
||||
const user = useSelector(selectCurrentUser);
|
||||
|
||||
return (
|
||||
<div className="list-container">
|
||||
<div className="dashboard-container">
|
||||
<div className="dashboard-header">
|
||||
<h1>Panel Użytkownika</h1>
|
||||
<div className="user-info">
|
||||
<span>Zalogowany jako: {user?.login}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="dashboard-content">
|
||||
<div className="user-settings">
|
||||
<div className="settings-header">
|
||||
<h2>User Settings</h2>
|
||||
<button
|
||||
className={`edit-button ${isEditing ? 'cancel' : ''}`}
|
||||
onClick={() => setIsEditing(!isEditing)}
|
||||
>
|
||||
{isEditing ? 'Cancel' : 'Edit'}
|
||||
</button>
|
||||
<div className="settings-section">
|
||||
<h2>Aktualizacja profilu</h2>
|
||||
<UserUpdateProfileForm />
|
||||
</div>
|
||||
|
||||
{message.text && (
|
||||
<div className={`message ${message.type}`}>
|
||||
{message.text}
|
||||
<div className="settings-section">
|
||||
<h2>Zmiana hasła</h2>
|
||||
<UserChangePasswordForm />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isEditing && (
|
||||
<div className="form-overlay">
|
||||
<div className="form-container">
|
||||
<FormGenerator
|
||||
inputList={getInputList().map((field, index) => ({
|
||||
...field,
|
||||
ref: formRefs[index],
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
onChange: field.onChange,
|
||||
validationInfo: field.validationInfo
|
||||
}))}
|
||||
refList={formRefs}
|
||||
action={handleFormAction}
|
||||
/>
|
||||
<div className="settings-section">
|
||||
<h2>Usuwanie konta</h2>
|
||||
<UserDeleteAccountForm />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserSettings;
|
||||
export default UserSettingsDashboard;
|
||||
|
|
|
|||
|
|
@ -1,32 +1,179 @@
|
|||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import axios from 'axios';
|
||||
|
||||
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000';
|
||||
import { setCredentials, setLoading, setError, logout } from '../slices/userAuthSlice';
|
||||
import { API_URL, AUTH_CONFIG } from '../../config';
|
||||
import { cookieService } from '../../services/cookieService';
|
||||
|
||||
export const loginUser = createAsyncThunk(
|
||||
'userAuth/login',
|
||||
async (credentials) => {
|
||||
const formData = new FormData();
|
||||
formData.append('username', credentials.username);
|
||||
formData.append('password', credentials.password);
|
||||
async (credentials, { dispatch }) => {
|
||||
try {
|
||||
dispatch(setLoading(true));
|
||||
const response = await fetch(`${API_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
|
||||
const response = await axios.post(`${API_URL}/auth`, formData);
|
||||
return response.data;
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Błąd logowania');
|
||||
}
|
||||
|
||||
dispatch(setCredentials({
|
||||
user: data.user,
|
||||
token: data.token,
|
||||
permissions: data.permissions
|
||||
}));
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
dispatch(setError(error.message));
|
||||
throw error;
|
||||
} finally {
|
||||
dispatch(setLoading(false));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const registerUser = createAsyncThunk(
|
||||
'userAuth/register',
|
||||
async (userData) => {
|
||||
const response = await axios.post(`${API_URL}/register`, userData);
|
||||
return response.data;
|
||||
async (userData, { dispatch }) => {
|
||||
try {
|
||||
dispatch(setLoading(true));
|
||||
const response = await fetch(`${API_URL}/auth/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(userData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || 'Błąd rejestracji');
|
||||
}
|
||||
|
||||
dispatch(setCredentials({
|
||||
user: data.user,
|
||||
token: data.token,
|
||||
permissions: data.permissions
|
||||
}));
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
dispatch(setError(error.message));
|
||||
throw error;
|
||||
} finally {
|
||||
dispatch(setLoading(false));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const logoutUser = createAsyncThunk(
|
||||
'userAuth/logout',
|
||||
async (_, { dispatch }) => {
|
||||
try {
|
||||
dispatch(setLoading(true));
|
||||
const token = cookieService.getToken();
|
||||
|
||||
if (token) {
|
||||
await fetch(`${API_URL}/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
dispatch(logout());
|
||||
} catch (error) {
|
||||
console.error('Błąd podczas wylogowywania:', error);
|
||||
} finally {
|
||||
dispatch(setLoading(false));
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const checkAuth = createAsyncThunk(
|
||||
'userAuth/checkAuth',
|
||||
async (_, { dispatch }) => {
|
||||
try {
|
||||
const token = cookieService.getToken();
|
||||
|
||||
if (!token) {
|
||||
throw new Error('Brak tokenu');
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_URL}/auth/verify`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Token nieprawidłowy');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
dispatch(logout());
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const changePassword = createAsyncThunk(
|
||||
'userAuth/changePassword',
|
||||
async (passwordData) => {
|
||||
const response = await axios.post(`${API_URL}/change-password`, passwordData);
|
||||
const response = await fetch(`${API_URL}/change-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(passwordData),
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
);
|
||||
|
||||
export const updateProfile = createAsyncThunk(
|
||||
'userAuth/updateProfile',
|
||||
async (profileData, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/user/profile`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${cookieService.getToken()}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(profileData),
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const deleteAccount = createAsyncThunk(
|
||||
'userAuth/deleteAccount',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/user/delete`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${cookieService.getToken()}`,
|
||||
},
|
||||
});
|
||||
cookieService.clearAll();
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.response?.data);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
@ -1,72 +1,54 @@
|
|||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { loginUser, registerUser, changePassword } from '../asyncThunks/userAuthAsyncThunk';
|
||||
import { cookieService } from '../../services/cookieService';
|
||||
|
||||
const initialState = {
|
||||
user: null,
|
||||
token: null,
|
||||
isLoading: false,
|
||||
error: null
|
||||
user: cookieService.getUserData() || null,
|
||||
token: cookieService.getToken() || null,
|
||||
isAuthenticated: !!cookieService.getToken(),
|
||||
loading: false,
|
||||
error: null,
|
||||
permissions: []
|
||||
};
|
||||
|
||||
const userAuthSlice = createSlice({
|
||||
name: 'userAuth',
|
||||
initialState,
|
||||
reducers: {
|
||||
setCredentials: (state, { payload }) => {
|
||||
const { user, token, permissions } = payload;
|
||||
state.user = user;
|
||||
state.token = token;
|
||||
state.isAuthenticated = true;
|
||||
state.permissions = permissions;
|
||||
cookieService.setToken(token);
|
||||
cookieService.setUserData({ ...user, permissions });
|
||||
},
|
||||
logout: (state) => {
|
||||
state.user = null;
|
||||
state.token = null;
|
||||
state.error = null;
|
||||
state.isAuthenticated = false;
|
||||
state.permissions = [];
|
||||
cookieService.clearAll();
|
||||
},
|
||||
setLoading: (state, { payload }) => {
|
||||
state.loading = payload;
|
||||
},
|
||||
setError: (state, { payload }) => {
|
||||
state.error = payload;
|
||||
},
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
}
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// Login
|
||||
builder.addCase(loginUser.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(loginUser.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.user = action.payload.user;
|
||||
state.token = action.payload.token;
|
||||
});
|
||||
builder.addCase(loginUser.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Register
|
||||
builder.addCase(registerUser.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(registerUser.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.user = action.payload.user;
|
||||
state.token = action.payload.token;
|
||||
});
|
||||
builder.addCase(registerUser.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
|
||||
// Change Password
|
||||
builder.addCase(changePassword.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
});
|
||||
builder.addCase(changePassword.fulfilled, (state) => {
|
||||
state.isLoading = false;
|
||||
});
|
||||
builder.addCase(changePassword.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = action.error.message;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const { logout, clearError } = userAuthSlice.actions;
|
||||
export const userAuthSelector = (state) => state.userAuth;
|
||||
export const { setCredentials, logout, setLoading, setError, clearError } = userAuthSlice.actions;
|
||||
|
||||
export const selectCurrentUser = (state) => state.userAuth.user;
|
||||
export const selectCurrentToken = (state) => state.userAuth.token;
|
||||
export const selectIsAuthenticated = (state) => state.userAuth.isAuthenticated;
|
||||
export const selectUserPermissions = (state) => state.userAuth.permissions;
|
||||
export const selectAuthLoading = (state) => state.userAuth.loading;
|
||||
export const selectAuthError = (state) => state.userAuth.error;
|
||||
|
||||
export default userAuthSlice.reducer;
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import Cookies from 'js-cookie';
|
||||
|
||||
const TOKEN_COOKIE_NAME = 'auth_token';
|
||||
const USER_COOKIE_NAME = 'user_data';
|
||||
|
||||
export const cookieService = {
|
||||
setToken: (token) => {
|
||||
Cookies.set(TOKEN_COOKIE_NAME, token, {
|
||||
expires: 7, // 7 dni
|
||||
secure: true, // tylko HTTPS
|
||||
sameSite: 'strict', // ochrona przed CSRF
|
||||
path: '/'
|
||||
});
|
||||
},
|
||||
|
||||
getToken: () => {
|
||||
return Cookies.get(TOKEN_COOKIE_NAME);
|
||||
},
|
||||
|
||||
removeToken: () => {
|
||||
Cookies.remove(TOKEN_COOKIE_NAME, { path: '/' });
|
||||
},
|
||||
|
||||
setUserData: (userData) => {
|
||||
Cookies.set(USER_COOKIE_NAME, JSON.stringify(userData), {
|
||||
expires: 7,
|
||||
secure: true,
|
||||
sameSite: 'strict',
|
||||
path: '/'
|
||||
});
|
||||
},
|
||||
|
||||
getUserData: () => {
|
||||
const userData = Cookies.get(USER_COOKIE_NAME);
|
||||
return userData ? JSON.parse(userData) : null;
|
||||
},
|
||||
|
||||
removeUserData: () => {
|
||||
Cookies.remove(USER_COOKIE_NAME, { path: '/' });
|
||||
},
|
||||
|
||||
clearAll: () => {
|
||||
this.removeToken();
|
||||
this.removeUserData();
|
||||
}
|
||||
};
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
@import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
|
||||
// colors
|
||||
|
||||
$first-color: rgba(0, 90, 25, 1);
|
||||
|
|
@ -7,6 +9,7 @@ $error-color: #dc3545;
|
|||
$in-progress-color: #ffa500;
|
||||
$queued-color: #fbff00;
|
||||
$success-color: $first-color;
|
||||
$card-background: rgba(0, 0, 0, 0.2);
|
||||
|
||||
$title-color: white;
|
||||
$subtitle-color: #a6a6a6;
|
||||
|
|
@ -805,6 +808,13 @@ body, html {
|
|||
filter: invert(8%) sepia(63%) saturate(4888%) hue-rotate(8deg) brightness(113%) contrast(112%);
|
||||
}
|
||||
}
|
||||
|
||||
.label-value {
|
||||
width: 90%;
|
||||
padding: 5%;
|
||||
color: $subtitle-color;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.popup {
|
||||
|
|
@ -980,12 +990,6 @@ body, html {
|
|||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.form_info {
|
||||
float: float;
|
||||
position: absolute;
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.float_form_render_sync {
|
||||
|
|
@ -1147,118 +1151,107 @@ body, html {
|
|||
|
||||
.user-settings {
|
||||
padding: 20px;
|
||||
color: $secondary-color;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
|
||||
.settings-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
h2 {
|
||||
color: $title-color;
|
||||
margin: 0;
|
||||
color: $title-color;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.settings-actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
|
||||
.edit-button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: $first-color;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&.cancel {
|
||||
background: #dc3545;
|
||||
}
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
&.cancel {
|
||||
background: darken(#dc3545, 10%);
|
||||
}
|
||||
&:not(.cancel) {
|
||||
background: darken($first-color, 10%);
|
||||
}
|
||||
|
||||
&.cancel {
|
||||
background: $error-color;
|
||||
|
||||
&:hover {
|
||||
background: darken($error-color, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: $error-color;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: darken($error-color, 10%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-sections-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.settings-section {
|
||||
flex: 1;
|
||||
width: 450px;
|
||||
background: $card-background;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 20px 0;
|
||||
color: $title-color;
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settings-section.delete-section {
|
||||
border: 1px solid $error-color;
|
||||
background: rgba($error-color, 0.1);
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 10px;
|
||||
padding: 12px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
|
||||
&.success {
|
||||
background: rgba(0, 120, 0, 0.2);
|
||||
color: #00ff00;
|
||||
background: rgba($success-color, 0.1);
|
||||
color: $success-color;
|
||||
border: 1px solid $success-color;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: rgba(220, 53, 69, 0.2);
|
||||
color: #ff4444;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-form {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: $title-color;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid $border-color;
|
||||
border-radius: 5px;
|
||||
color: $secondary-color;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:disabled {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $first-color;
|
||||
box-shadow: 0 0 0 2px rgba($first-color, 0.2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.save-button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: $first-color;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background: darken($first-color, 10%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
background: rgba($error-color, 0.1);
|
||||
color: $error-color;
|
||||
border: 1px solid $error-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.task-row {
|
||||
display: flex;
|
||||
|
|
@ -1557,3 +1550,63 @@ body, html {
|
|||
.dashboard-content {
|
||||
min-height: 100%; /* Wymusza pojawienie się suwaka */
|
||||
}
|
||||
|
||||
.form-container {
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
|
||||
h2 {
|
||||
color: $title-color;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.form_info {
|
||||
float: float;
|
||||
width: 400px;
|
||||
height: 100px;
|
||||
margin-top: 10px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
|
||||
.success-message {
|
||||
background: rgba($success-color, 0.1);
|
||||
color: $success-color;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
border: 1px solid $success-color;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: rgba($error-color, 0.1);
|
||||
color: $error-color;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
border: 1px solid $error-color;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
background: $error-color;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: darken($error-color, 10%);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue