- )
+ );
+};
-}
-
-export default UserLoginForm
+export default UserLogin;
diff --git a/src/components/forms/user_auth/userRegister.js b/src/components/forms/user_auth/userRegister.js
index f204d7d..ac0ec7d 100644
--- a/src/components/forms/user_auth/userRegister.js
+++ b/src/components/forms/user_auth/userRegister.js
@@ -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"
- )
- }, [
- allowButtonAction,
- usernameValidationInfo,
- passwordValidationInfo,
- confirmPasswordValidationInfo
- ]
- )
+ setAllowButtonAction(
+ usernameValidationInfo === "Success" &&
+ emailValidationInfo === "Success" &&
+ passwordValidationInfo === "Success" &&
+ confirmPasswordValidationInfo === "Success"
+ );
+ }, [
+ 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
+ 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 + ")");
}
- // dispatch(
- // userCrudAsyncThunk.fetchRegister(
- // pass
- // )
- // )
- }
+ };
return (
-
+
- { info }
+ {infoMessage &&
{infoMessage}
}
+ {errorMessage &&
{errorMessage}
}
- )
+ );
+};
-}
-
-export default UserRegisterForm
+export default UserRegister;
diff --git a/src/components/forms/user_settings/userChangePasswordForm.js b/src/components/forms/user_settings/userChangePasswordForm.js
new file mode 100644
index 0000000..6640cfa
--- /dev/null
+++ b/src/components/forms/user_settings/userChangePasswordForm.js
@@ -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 (
+
+
+
+ {infoMessage &&
{infoMessage}
}
+ {errorMessage &&
{errorMessage}
}
+
+
+ );
+};
+
+export default UserChangePasswordForm;
\ No newline at end of file
diff --git a/src/components/forms/user_settings/userDeleteAccountForm.js b/src/components/forms/user_settings/userDeleteAccountForm.js
new file mode 100644
index 0000000..41294d6
--- /dev/null
+++ b/src/components/forms/user_settings/userDeleteAccountForm.js
@@ -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 (
+
+
+
+ {infoMessage &&
{infoMessage}
}
+ {errorMessage &&
{errorMessage}
}
+
+
+ );
+};
+
+export default UserDeleteAccountForm;
\ No newline at end of file
diff --git a/src/components/forms/user_settings/userUpdateProfileForm.js b/src/components/forms/user_settings/userUpdateProfileForm.js
new file mode 100644
index 0000000..c0441bc
--- /dev/null
+++ b/src/components/forms/user_settings/userUpdateProfileForm.js
@@ -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 (
+
+
+
+ {infoMessage &&
{infoMessage}
}
+ {errorMessage &&
{errorMessage}
}
+
+
+ );
+};
+
+export default UserUpdateProfileForm;
\ No newline at end of file
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000..7e5d2a8
--- /dev/null
+++ b/src/config.js
@@ -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: '/'
+ }
+};
\ No newline at end of file
diff --git a/src/hooks/useDashboardList.js b/src/hooks/useDashboardList.js
new file mode 100644
index 0000000..32ee413
--- /dev/null
+++ b/src/hooks/useDashboardList.js
@@ -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
+ };
+};
\ No newline at end of file
diff --git a/src/pages/dashboard.js b/src/pages/dashboard.js
index da19c62..3f02113 100644
--- a/src/pages/dashboard.js
+++ b/src/pages/dashboard.js
@@ -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
;
case 'settings':
- return
;
+ return
;
case '3d-models':
default:
return
;
diff --git a/src/pages/dashboards/user.js b/src/pages/dashboards/user.js
index f7c6a42..6d9a3dd 100644
--- a/src/pages/dashboards/user.js
+++ b/src/pages/dashboards/user.js
@@ -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 (
-
-
-
-
User Settings
-
+
+
+
Panel Użytkownika
+
+ Zalogowany jako: {user?.login}
+
- {message.text && (
-
- {message.text}
+
+
+
+
Aktualizacja profilu
+
- )}
- {isEditing && (
-
-
- ({
- ...field,
- ref: formRefs[index],
- type: field.type,
- name: field.name,
- onChange: field.onChange,
- validationInfo: field.validationInfo
- }))}
- refList={formRefs}
- action={handleFormAction}
- />
-
+
+
Zmiana hasła
+
- )}
+
+
+
Usuwanie konta
+
+
+
);
};
-export default UserSettings;
+export default UserSettingsDashboard;
diff --git a/src/redux/asyncThunks/userAuthAsyncThunk.js b/src/redux/asyncThunks/userAuthAsyncThunk.js
index 55264dd..28d033e 100644
--- a/src/redux/asyncThunks/userAuthAsyncThunk.js
+++ b/src/redux/asyncThunks/userAuthAsyncThunk.js
@@ -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;
}
-);
\ No newline at end of file
+);
+
+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);
+ }
+ }
+);
\ No newline at end of file
diff --git a/src/redux/slices/userAuthSlice.js b/src/redux/slices/userAuthSlice.js
index a6efb00..3a25f11 100644
--- a/src/redux/slices/userAuthSlice.js
+++ b/src/redux/slices/userAuthSlice.js
@@ -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;
\ No newline at end of file
diff --git a/src/services/cookieService.js b/src/services/cookieService.js
new file mode 100644
index 0000000..a96229a
--- /dev/null
+++ b/src/services/cookieService.js
@@ -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();
+ }
+};
\ No newline at end of file
diff --git a/src/styles/general.scss b/src/styles/general.scss
index d8eb7d7..8e92d84 100644
--- a/src/styles/general.scss
+++ b/src/styles/general.scss
@@ -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 {
@@ -1146,119 +1150,108 @@ body, html {
}
.user-settings {
- padding: 20px;
- color: $secondary-color;
+ padding: 20px;
+ max-width: 100%;
+ margin: 0 auto;
- .settings-header {
+ .settings-header {
display: flex;
justify-content: space-between;
align-items: center;
- margin-bottom: 20px;
+ margin-bottom: 30px;
h2 {
- color: $title-color;
- margin: 0;
- font-size: 24px;
+ margin: 0;
+ color: $title-color;
+ font-size: 24px;
}
- .edit-button {
- background: $first-color;
- color: white;
- border: none;
- padding: 10px 20px;
- border-radius: 5px;
- cursor: pointer;
- font-weight: 600;
- transition: all 0.3s ease;
+ .settings-actions {
+ display: flex;
+ gap: 16px;
- &.cancel {
- background: #dc3545;
- }
+ .edit-button {
+ padding: 8px 16px;
+ border: none;
+ border-radius: 4px;
+ background: $first-color;
+ color: white;
+ cursor: pointer;
+ transition: background-color 0.3s;
- &:hover {
- transform: translateY(-2px);
- &.cancel {
- background: darken(#dc3545, 10%);
+ &:hover {
+ background: darken($first-color, 10%);
+ }
+
+ &.cancel {
+ background: $error-color;
+
+ &:hover {
+ background: darken($error-color, 10%);
+ }
+ }
}
- &:not(.cancel) {
- background: darken($first-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%);
+ }
}
- }
}
- }
+ }
- .message {
- padding: 10px;
+ .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: 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;
+ background: rgba($error-color, 0.1);
+ 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);
- }
- }
- }
}
+}
.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%);
+ }
+}