Compare commits

...

2 Commits

Author SHA1 Message Date
TBS093A 0c08a4d942 fix(dashboards): remove useless files
-
2025-03-06 14:32:57 +01:00
TBS093A afe0b88808 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) + more
2025-03-06 14:30:33 +01:00
15 changed files with 979 additions and 561 deletions

15
package-lock.json generated
View File

@ -13,6 +13,7 @@
"axios": "^1.8.1", "axios": "^1.8.1",
"gatsby": "^5.13.3", "gatsby": "^5.13.3",
"gatsby-plugin-sass": "^6.14.0", "gatsby-plugin-sass": "^6.14.0",
"js-cookie": "^3.0.5",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",
@ -11751,6 +11752,15 @@
"@sideway/pinpoint": "^2.0.0" "@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": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@ -25860,6 +25870,11 @@
"@sideway/pinpoint": "^2.0.0" "@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": { "js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@ -20,6 +20,7 @@
"axios": "^1.8.1", "axios": "^1.8.1",
"gatsby": "^5.13.3", "gatsby": "^5.13.3",
"gatsby-plugin-sass": "^6.14.0", "gatsby-plugin-sass": "^6.14.0",
"js-cookie": "^3.0.5",
"react": "^18", "react": "^18",
"react-dom": "^18", "react-dom": "^18",
"react-redux": "^9.2.0", "react-redux": "^9.2.0",

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react' import React, { useState, useEffect } from 'react'
import passwordVisibleImg from '../../images/password-visible.png' import passwordVisibleImg from '../../images/password-visible.png'
import passwordHiddenImg from '../../images/password-hidden.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 { [ {}, {}, ...{} ] } inputList - list of dicts with info about input
* @param { [] } refList - react ref objects list for handler validation
* @param { } action - fetch method * @param { } action - fetch method
*/ */
export const FormGenerator = ({ export const FormGenerator = ({
inputList, refList, inputList,
action action
}) => { }) => {
const handler = async (event) => { const handleSubmit = (e) => {
event.preventDefault() e.preventDefault();
const formData = {};
if ( inputList[0].action === 'Async' ) { inputList.forEach((singleInput, index) => {
await action(refList) if (typeof singleInput.type !== 'undefined' && singleInput.ref?.current) {
} else if ( formData[singleInput.name] = singleInput.ref.current.value;
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)
}
}
}
} }
});
action(formData);
};
let info let info
return ( return (
<form onSubmit={event => handler(event)} className="form"> <form onSubmit={handleSubmit} className="form">
{ {
inputList.map((input, key) => { inputList.map((input, key) => {
@ -104,6 +89,13 @@ export const FormGenerator = ({
key={key} 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,8 +104,8 @@ export const FormGenerator = ({
? <></> ? <></>
: <button : <button
type='submit' type='submit'
disabled={ info.allowButtonAction } disabled={!info.allowButtonAction}
className={ info.allowButtonAction === false ? "button-disabled" : "" } className={!info.allowButtonAction ? "button-disabled" : ""}
> >
{info.button_value} {info.button_value}
</button> </button>
@ -156,6 +148,7 @@ const TextInputGenerator = ({
} else { } else {
setTextInputValidationInfo("Success") setTextInputValidationInfo("Success")
} }
input.validationInfo = textInputValidationInfo
} }
return ( return (
@ -173,6 +166,7 @@ const TextInputGenerator = ({
input.validationInfo === null ? textInputValidationInfo : input.validationInfo input.validationInfo === null ? textInputValidationInfo : input.validationInfo
) ? "" : "input-incorrect" ) ? "" : "input-incorrect"
} }
placeholder={input.placeholder === null ? "" : input.placeholder}
/> />
<div <div
className="popup" className="popup"
@ -238,7 +232,6 @@ const PasswordInputGenerator = ({
<div className="popup-content"> <div className="popup-content">
{ input.validationInfo } { input.validationInfo }
</div> </div>
</div> </div>
</div> </div>
) )
@ -465,7 +458,6 @@ const UploadInputGenerator = ({
const onLoadFile = async (event) => { const onLoadFile = async (event) => {
event.preventDefault() event.preventDefault()
let data = event.target.files[0] let data = event.target.files[0]
// input.setFile(await toBase64(data))
input.setFile( data ) input.setFile( data )
setDropInfos(data.name, data.size) setDropInfos(data.name, data.size)
} }
@ -474,7 +466,6 @@ const UploadInputGenerator = ({
event.preventDefault() event.preventDefault()
event.persist() event.persist()
let data = event.dataTransfer.files[0] let data = event.dataTransfer.files[0]
// input.setFile(await toBase64(data))
input.setFile( data ) input.setFile( data )
setDropInfos(data.name, data.size) setDropInfos(data.name, data.size)
} }

View File

@ -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' const usernameInput = createRef();
// import userAuthAsyncThunk from '../../../redux/asyncThunks/userAuthAsyncThunk' 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 passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,24}$/;
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 usernameValidation = (event) => { const usernameValidation = (event) => {
if (event.target.value === "") { if (event.target.value === "") {
setUsernameValidationInfo("Email is required.") setUsernameValidationInfo("Login jest wymagany");
} else if(!emailRegex.test(event.target.value)) {
setUsernameValidationInfo("Please provide correct email")
} else { } else {
setUsernameValidationInfo("Success") setUsernameValidationInfo("Success");
}
} }
};
const passwordValidation = (event) => { const passwordValidation = (event) => {
setPassword(event.target.value)
if (event.target.value === "") { if (event.target.value === "") {
setPasswordValidationInfo("Password is required.") setPasswordValidationInfo("Hasło jest wymagane");
} else if (!passwordRegex.test(event.target.value)) { } 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 nie spełnia wymagań tej witryny");
} else { } else {
setPasswordValidationInfo("Success") setPasswordValidationInfo("Success");
}
} }
};
useEffect(() => { useEffect(() => {
setAllowButtonAction( setAllowButtonAction(
usernameValidationInfo === "Success" usernameValidationInfo === "Success" &&
&& passwordValidationInfo === "Success" passwordValidationInfo === "Success"
) );
}, [ }, [
allowButtonAction,
usernameValidationInfo, usernameValidationInfo,
passwordValidationInfo passwordValidationInfo,
] ]);
)
// const dispatch = useDispatch() const inputList = [
// const { info } = useSelector( userAuthSelector )
const info = "" // if redux is integrated - delete this line
let refList = [
usernameInput,
passwordInput
]
let inputList = [
{ {
type: 'info', type: 'info',
action: 'Create', action: 'Login',
endpint: 'user/auth', endpoint: 'auth',
button_value: 'SIGN IN' button_value: loading ? 'LOGOWANIE...' : 'ZALOGUJ',
allowButtonAction: allowButtonAction
}, },
{ {
type: 'text', type: 'text',
name: 'EMAIL', name: 'LOGIN',
ref: usernameInput, ref: usernameInput,
onChange: usernameValidation, onChange: usernameValidation,
validationInfo: usernameValidationInfo validationInfo: usernameValidationInfo
}, },
{ {
type: 'password', type: 'password',
name: 'PASSWORD', name: 'HASŁO',
ref: passwordInput, ref: passwordInput,
onChange: passwordValidation, onChange: passwordValidation,
validationInfo: passwordValidationInfo validationInfo: passwordValidationInfo
} }
] ];
const login = async ( refs ) => { const login = async (formData) => {
let pass = { try {
username: refs[0].current.value, const credentials = {
password: refs[1].current.value username: formData.LOGIN,
} password: formData.HASŁO
// dispatch( };
// userAuthAsyncThunk.fetchLogin(
// pass await dispatch(loginUser(credentials)).unwrap();
// ) setInfoMessage("Logowanie zakończone sukcesem!");
// ) navigate('/dashboard');
} catch (error) {
setErrorMessage("Wystąpił błąd podczas logowania (" + error.message + ")");
} }
};
return ( return (
<div> <div className='form-container'>
<FormGenerator <FormGenerator
inputList={inputList} inputList={inputList}
refList={ refList }
action={login} action={login}
/> />
<div className='form_info'> <div className='form_info'>
{ info } {infoMessage && <div className="success-message">{infoMessage}</div>}
{errorMessage && <div className="error-message">{errorMessage}</div>}
</div> </div>
</div> </div>
) );
};
} export default UserLogin;
export default UserLoginForm

View File

@ -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' const usernameInput = createRef();
// import userCrudAsyncThunk from '../../../redux/asyncThunks/userCrudAsyncThunk' 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 [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const usernameInput = React.createRef() const [allowButtonAction, setAllowButtonAction] = useState(false);
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 usernameValidation = (event) => { const usernameValidation = (event) => {
if (event.target.value === "") { if (event.target.value === "") {
setUsernameValidationInfo("Email is required.") setUsernameValidationInfo("Login jest wymagany");
} else if(!emailRegex.test(event.target.value)) {
setUsernameValidationInfo("Please provide correct email")
} else { } 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) => { const passwordValidation = (event) => {
setPassword(event.target.value);
setPassword(event.target.value)
if (event.target.value === "") { if (event.target.value === "") {
setPasswordValidationInfo("Password is required.") setPasswordValidationInfo("Hasło jest wymagane");
} else if (!passwordRegex.test(event.target.value)) { } 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 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 { } else {
setPasswordValidationInfo("Success") setPasswordValidationInfo("Success");
} }
if (event.target.value !== confirmPassword) { if (event.target.value !== confirmPassword) {
setConfirmPasswordValidationInfo("Passwords are different.") setConfirmPasswordValidationInfo("Hasła nie są identyczne");
} else { } else {
setConfirmPasswordValidationInfo("Success") setConfirmPasswordValidationInfo("Success");
}
} }
};
const confirmPasswordValidation = (event) => { const confirmPasswordValidation = (event) => {
setConfirmPassword(event.target.value);
setConfirmPassword(event.target.value)
if (event.target.value !== password) { if (event.target.value !== password) {
setConfirmPasswordValidationInfo("Passwords are different.") setConfirmPasswordValidationInfo("Hasła nie są identyczne");
} else { } else {
setConfirmPasswordValidationInfo("Success") setConfirmPasswordValidationInfo("Success");
}
} }
};
useEffect(() => { useEffect(() => {
setAllowButtonAction( setAllowButtonAction(
usernameValidationInfo === "Success" usernameValidationInfo === "Success" &&
&& passwordValidationInfo === "Success" emailValidationInfo === "Success" &&
&& confirmPasswordValidationInfo === "Success" passwordValidationInfo === "Success" &&
) confirmPasswordValidationInfo === "Success"
);
}, [ }, [
allowButtonAction,
usernameValidationInfo, usernameValidationInfo,
emailValidationInfo,
passwordValidationInfo, passwordValidationInfo,
confirmPasswordValidationInfo confirmPasswordValidationInfo
] ]);
)
// const dispatch = useDispatch() const inputList = [
// const { info } = useSelector( userCrudSelector )
const info = "" // if redux is integrated - delete this line
let refList = [
usernameInput,
passwordInput,
confirmPasswordInput
]
let inputList = [
{ {
type: 'info', type: 'info',
action: 'Create', action: 'Register',
endpint: 'user/auth/register', endpoint: 'auth',
button_value: 'SIGN UP', button_value: loading ? 'REJESTRACJA...' : 'ZAREJESTRUJ',
allowButtonAction: allowButtonAction allowButtonAction: allowButtonAction
}, },
{ {
type: 'text', type: 'text',
name: 'EMAIL', name: 'LOGIN',
ref: usernameInput, ref: usernameInput,
onChange: usernameValidation, onChange: usernameValidation,
validationInfo: usernameValidationInfo validationInfo: usernameValidationInfo
}, },
{
type: 'text',
name: 'EMAIL',
ref: emailInput,
onChange: emailValidation,
validationInfo: emailValidationInfo
},
{ {
type: 'password', type: 'password',
name: 'PASSWORD', name: 'HASŁO',
ref: passwordInput, ref: passwordInput,
onChange: passwordValidation, onChange: passwordValidation,
validationInfo: passwordValidationInfo validationInfo: passwordValidationInfo
}, },
{ {
type: 'password', type: 'password',
name: 'CONFIRM PASSWORD', name: 'POTWIERDŹ HASŁO',
ref: confirmPasswordInput, ref: confirmPasswordInput,
onChange: confirmPasswordValidation, onChange: confirmPasswordValidation,
validationInfo: confirmPasswordValidationInfo validationInfo: confirmPasswordValidationInfo
} }
] ];
const register = async ( refs ) => { const register = async (formData) => {
let pass = { try {
username: refs[0].current.value, const userData = {
password: refs[1].current.value username: formData.LOGIN,
} email: formData.EMAIL,
// dispatch( password: formData.HASŁO
// userCrudAsyncThunk.fetchRegister( };
// pass
// ) await dispatch(registerUser(userData)).unwrap();
// ) setInfoMessage("Rejestracja zakończona sukcesem!");
navigate('/dashboard');
} catch (error) {
setErrorMessage("Wystąpił błąd podczas rejestracji (" + error.massage + ")");
} }
};
return ( return (
<div> <div className='form-container'>
<FormGenerator <FormGenerator
inputList={inputList} inputList={inputList}
refList={ refList }
action={register} action={register}
/> />
<div className='form_info'> <div className='form_info'>
{ info } {infoMessage && <div className="success-message">{infoMessage}</div>}
{errorMessage && <div className="error-message">{errorMessage}</div>}
</div> </div>
</div> </div>
) );
};
} export default UserRegister;
export default UserRegisterForm

View File

@ -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;

View File

@ -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;

View File

@ -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;

20
src/config.js 100644
View File

@ -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: '/'
}
};

View File

@ -1,21 +1,36 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import '../styles/general.scss'; import { useDispatch, useSelector } from 'react-redux';
import '@fortawesome/fontawesome-free/css/all.min.css'; 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 NavBarComponent from '../components/navbar.js';
import FootComponent from '../components/foot.js';
import ThreeDModelsDashboard from './dashboards/3d-models.js';
import AIModelsDashboard from './dashboards/ai.models.js'; import AIModelsDashboard from './dashboards/ai.models.js';
import AITasksDashboard from './dashboards/ai.tasks.js'; import AITasksDashboard from './dashboards/ai.tasks.js';
import RendersDashboard from './dashboards/renders.js'; import RendersDashboard from './dashboards/renders.js';
import ServersDashboard from './dashboards/servers.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 DashboardPage = () => {
const dispatch = useDispatch();
const isAuthenticated = useSelector(selectIsAuthenticated);
const permissions = useSelector(selectUserPermissions);
const icons_size = "fa-2x"; const icons_size = "fa-2x";
const [activeComponent, setActiveComponent] = useState('3d-models'); const [activeComponent, setActiveComponent] = useState('3d-models');
useEffect(() => {
if (!isAuthenticated) {
dispatch(checkAuth())
.unwrap()
.catch(() => {
navigate('/auth/login');
});
}
}, [dispatch, isAuthenticated]);
const handleNavigation = (path) => { const handleNavigation = (path) => {
setActiveComponent(path); setActiveComponent(path);
}; };
@ -24,10 +39,13 @@ const DashboardPage = () => {
return activeComponent === path; return activeComponent === path;
}; };
const handleLogout = () => { const handleLogout = async () => {
// TODO: Implement proper logout logic (clear tokens, session, etc.) try {
console.log('Logging out...'); await dispatch(logoutUser()).unwrap();
window.location.href = '/login'; navigate('/auth/login');
} catch (error) {
console.error('Błąd podczas wylogowywania:', error);
}
}; };
const renderContent = () => { const renderContent = () => {
@ -41,7 +59,7 @@ const DashboardPage = () => {
case 'servers': case 'servers':
return <ServersDashboard />; return <ServersDashboard />;
case 'settings': case 'settings':
return <UserSettings />; return <UserSettingsDashboard />;
case '3d-models': case '3d-models':
default: default:
return <ThreeDModelsDashboard />; return <ThreeDModelsDashboard />;

View File

@ -1,193 +1,42 @@
import React, { useState, useRef } from 'react'; import React from 'react';
import FormGenerator from '../../components/forms/formGenerator'; 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 UserSettingsDashboard = () => {
const [isEditing, setIsEditing] = useState(false); const user = useSelector(selectCurrentUser);
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);
};
return ( 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="user-settings">
<div className="settings-header"> <div className="settings-section">
<h2>User Settings</h2> <h2>Aktualizacja profilu</h2>
<button <UserUpdateProfileForm />
className={`edit-button ${isEditing ? 'cancel' : ''}`}
onClick={() => setIsEditing(!isEditing)}
>
{isEditing ? 'Cancel' : 'Edit'}
</button>
</div> </div>
{message.text && ( <div className="settings-section">
<div className={`message ${message.type}`}> <h2>Zmiana hasła</h2>
{message.text} <UserChangePasswordForm />
</div> </div>
)}
{isEditing && ( <div className="settings-section">
<div className="form-overlay"> <h2>Usuwanie konta</h2>
<div className="form-container"> <UserDeleteAccountForm />
<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> </div>
</div> </div>
)}
</div> </div>
</div> </div>
); );
}; };
export default UserSettings; export default UserSettingsDashboard;

View File

@ -1,32 +1,179 @@
import { createAsyncThunk } from '@reduxjs/toolkit'; import { createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios'; import { setCredentials, setLoading, setError, logout } from '../slices/userAuthSlice';
import { API_URL, AUTH_CONFIG } from '../../config';
const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000'; import { cookieService } from '../../services/cookieService';
export const loginUser = createAsyncThunk( export const loginUser = createAsyncThunk(
'userAuth/login', 'userAuth/login',
async (credentials) => { async (credentials, { dispatch }) => {
const formData = new FormData(); try {
formData.append('username', credentials.username); dispatch(setLoading(true));
formData.append('password', credentials.password); 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); const data = await response.json();
return response.data;
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( export const registerUser = createAsyncThunk(
'userAuth/register', 'userAuth/register',
async (userData) => { async (userData, { dispatch }) => {
const response = await axios.post(`${API_URL}/register`, userData); try {
return response.data; 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( export const changePassword = createAsyncThunk(
'userAuth/changePassword', 'userAuth/changePassword',
async (passwordData) => { 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; 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);
}
}
);

View File

@ -1,72 +1,54 @@
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import { loginUser, registerUser, changePassword } from '../asyncThunks/userAuthAsyncThunk'; import { cookieService } from '../../services/cookieService';
const initialState = { const initialState = {
user: null, user: cookieService.getUserData() || null,
token: null, token: cookieService.getToken() || null,
isLoading: false, isAuthenticated: !!cookieService.getToken(),
error: null loading: false,
error: null,
permissions: []
}; };
const userAuthSlice = createSlice({ const userAuthSlice = createSlice({
name: 'userAuth', name: 'userAuth',
initialState, initialState,
reducers: { 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) => { logout: (state) => {
state.user = null; state.user = null;
state.token = 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) => { clearError: (state) => {
state.error = null; 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 { setCredentials, logout, setLoading, setError, clearError } = userAuthSlice.actions;
export const userAuthSelector = (state) => state.userAuth;
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; export default userAuthSlice.reducer;

View File

@ -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();
}
};

View File

@ -1,3 +1,5 @@
@import '@fortawesome/fontawesome-free/css/all.min.css';
// colors // colors
$first-color: rgba(0, 90, 25, 1); $first-color: rgba(0, 90, 25, 1);
@ -7,6 +9,7 @@ $error-color: #dc3545;
$in-progress-color: #ffa500; $in-progress-color: #ffa500;
$queued-color: #fbff00; $queued-color: #fbff00;
$success-color: $first-color; $success-color: $first-color;
$card-background: rgba(0, 0, 0, 0.2);
$title-color: white; $title-color: white;
$subtitle-color: #a6a6a6; $subtitle-color: #a6a6a6;
@ -805,6 +808,13 @@ body, html {
filter: invert(8%) sepia(63%) saturate(4888%) hue-rotate(8deg) brightness(113%) contrast(112%); 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 { .popup {
@ -980,12 +990,6 @@ body, html {
margin-top: 30px; margin-top: 30px;
} }
.form_info {
float: float;
position: absolute;
margin-top: 0px;
}
} }
.float_form_render_sync { .float_form_render_sync {
@ -1147,115 +1151,104 @@ body, html {
.user-settings { .user-settings {
padding: 20px; padding: 20px;
color: $secondary-color; max-width: 100%;
margin: 0 auto;
.settings-header { .settings-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20px; margin-bottom: 30px;
h2 { h2 {
color: $title-color;
margin: 0; margin: 0;
color: $title-color;
font-size: 24px; font-size: 24px;
} }
.settings-actions {
display: flex;
gap: 16px;
.edit-button { .edit-button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: $first-color; background: $first-color;
color: white; color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer; cursor: pointer;
font-weight: 600; transition: background-color 0.3s;
transition: all 0.3s ease;
&.cancel {
background: #dc3545;
}
&:hover { &:hover {
transform: translateY(-2px);
&.cancel {
background: darken(#dc3545, 10%);
}
&:not(.cancel) {
background: darken($first-color, 10%); 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 { .message {
padding: 10px; padding: 12px;
border-radius: 4px;
margin-bottom: 20px; margin-bottom: 20px;
border-radius: 5px;
text-align: center;
font-weight: 500;
&.success { &.success {
background: rgba(0, 120, 0, 0.2); background: rgba($success-color, 0.1);
color: #00ff00; color: $success-color;
border: 1px solid $success-color;
} }
&.error { &.error {
background: rgba(220, 53, 69, 0.2); background: rgba($error-color, 0.1);
color: #ff4444; color: $error-color;
} border: 1px solid $error-color;
}
.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);
}
} }
} }
} }
@ -1557,3 +1550,63 @@ body, html {
.dashboard-content { .dashboard-content {
min-height: 100%; /* Wymusza pojawienie się suwaka */ 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%);
}
}