Merge pull request 'RC-4-connect-firebase-auth' (#2) from RC-4-connect-firebase-auth into dev

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2023-06-23 19:19:37 +03:00
37 changed files with 307 additions and 282 deletions

View File

@@ -7,7 +7,7 @@ const aliases = (prefix = `src`) => ({
'app/shared-components': `${prefix}/app/shared-components`, 'app/shared-components': `${prefix}/app/shared-components`,
'app/configs': `${prefix}/app/configs`, 'app/configs': `${prefix}/app/configs`,
'app/theme-layouts': `${prefix}/app/theme-layouts`, 'app/theme-layouts': `${prefix}/app/theme-layouts`,
'app/AppContext': `${prefix}/app/AppContext`, 'app/contexts/AppContext': `${prefix}/app/contexts/AppContext`,
}); });
module.exports = aliases; module.exports = aliases;

View File

@@ -1,16 +1,34 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": "./", "baseUrl": "./",
"paths": { "paths": {
"@fuse/*": ["./src/@fuse/*"], "@fuse/*": [
"@history*": ["./src/@history"], "./src/@fuse/*"
"@lodash": ["./src/@lodash"], ],
"@mock-api": ["./src/@mock-api"], "@history*": [
"app/store/*": ["./src/app/store/*"], "./src/@history"
"app/shared-components/*": ["./src/app/shared-components/*"], ],
"app/configs/*": ["./src/app/configs/*"], "@lodash": [
"app/theme-layouts/*": ["./src/app/theme-layouts/*"], "./src/@lodash"
"app/AppContext": ["./src/app/AppContext"] ],
} "@mock-api": [
"./src/@mock-api"
],
"app/store/*": [
"./src/app/store/*"
],
"app/shared-components/*": [
"./src/app/shared-components/*"
],
"app/configs/*": [
"./src/app/configs/*"
],
"app/theme-layouts/*": [
"./src/app/theme-layouts/*"
],
"app/contexts/AppContext": [
"./src/app/contexts/AppContext"
]
}
} }
} }

View File

@@ -1,5 +1,5 @@
import FuseUtils from '@fuse/utils'; import FuseUtils from '@fuse/utils';
import AppContext from 'app/AppContext'; import AppContext from 'src/app/contexts/AppContext';
import { Component } from 'react'; import { Component } from 'react';
import { matchRoutes } from 'react-router-dom'; import { matchRoutes } from 'react-router-dom';
import withRouter from '@fuse/core/withRouter'; import withRouter from '@fuse/core/withRouter';

View File

@@ -1,6 +1,6 @@
import { useDeepCompareEffect } from '@fuse/hooks'; import { useDeepCompareEffect } from '@fuse/hooks';
import _ from '@lodash'; import _ from '@lodash';
import AppContext from 'app/AppContext'; import AppContext from 'src/app/contexts/AppContext';
import { import {
generateSettings, generateSettings,
selectFuseCurrentSettings, selectFuseCurrentSettings,

View File

@@ -14,7 +14,7 @@ import { selectMainTheme } from 'app/store/fuse/settingsSlice';
import FuseAuthorization from '@fuse/core/FuseAuthorization'; import FuseAuthorization from '@fuse/core/FuseAuthorization';
import settingsConfig from 'app/configs/settingsConfig'; import settingsConfig from 'app/configs/settingsConfig';
import withAppProviders from './withAppProviders'; import withAppProviders from './withAppProviders';
import { AuthProvider } from './auth/AuthContext'; import { AuthProvider } from './contexts/AuthContext';
// import axios from 'axios'; // import axios from 'axios';
/** /**

View File

@@ -1 +0,0 @@
export { default as authRoles } from './authRoles';

View File

@@ -1,3 +0,0 @@
import JwtService from './jwtService';
export default JwtService;

View File

@@ -1,151 +0,0 @@
import FuseUtils from '@fuse/utils/FuseUtils';
import axios from 'axios';
import jwtDecode from 'jwt-decode';
import jwtServiceConfig from './jwtServiceConfig';
/* eslint-disable camelcase */
class JwtService extends FuseUtils.EventEmitter {
init() {
this.setInterceptors();
this.handleAuthentication();
}
setInterceptors = () => {
axios.interceptors.response.use(
(response) => {
return response;
},
(err) => {
return new Promise((resolve, reject) => {
if (err.response.status === 401 && err.config && !err.config.__isRetryRequest) {
// if you ever get an unauthorized response, logout the user
this.emit('onAutoLogout', 'Invalid access_token');
this.setSession(null);
}
throw err;
});
}
);
};
handleAuthentication = () => {
const access_token = this.getAccessToken();
if (!access_token) {
this.emit('onNoAccessToken');
return;
}
if (this.isAuthTokenValid(access_token)) {
this.setSession(access_token);
this.emit('onAutoLogin', true);
} else {
this.setSession(null);
this.emit('onAutoLogout', 'access_token expired');
}
};
createUser = (data) => {
return new Promise((resolve, reject) => {
axios.post(jwtServiceConfig.signUp, data).then((response) => {
if (response.data.user) {
this.setSession(response.data.access_token);
resolve(response.data.user);
this.emit('onLogin', response.data.user);
} else {
reject(response.data.error);
}
});
});
};
signInWithEmailAndPassword = (email, password) => {
return new Promise((resolve, reject) => {
axios
.get(jwtServiceConfig.signIn, {
data: {
email,
password,
},
})
.then((response) => {
if (response.data.user) {
this.setSession(response.data.access_token);
resolve(response.data.user);
this.emit('onLogin', response.data.user);
} else {
reject(response.data.error);
}
});
});
};
signInWithToken = () => {
return new Promise((resolve, reject) => {
axios
.get(jwtServiceConfig.accessToken, {
data: {
access_token: this.getAccessToken(),
},
})
.then((response) => {
if (response.data.user) {
this.setSession(response.data.access_token);
resolve(response.data.user);
} else {
this.logout();
reject(new Error('Failed to login with token.'));
}
})
.catch((error) => {
this.logout();
reject(new Error('Failed to login with token.'));
});
});
};
updateUserData = (user) => {
return axios.post(jwtServiceConfig.updateUser, {
user,
});
};
setSession = (access_token) => {
if (access_token) {
localStorage.setItem('jwt_access_token', access_token);
axios.defaults.headers.common.Authorization = `Bearer ${access_token}`;
} else {
localStorage.removeItem('jwt_access_token');
delete axios.defaults.headers.common.Authorization;
}
};
logout = () => {
this.setSession(null);
this.emit('onLogout', 'Logged out');
};
isAuthTokenValid = (access_token) => {
if (!access_token) {
return false;
}
const decoded = jwtDecode(access_token);
const currentTime = Date.now() / 1000;
if (decoded.exp < currentTime) {
console.warn('access token expired');
return false;
}
return true;
};
getAccessToken = () => {
return window.localStorage.getItem('jwt_access_token');
};
}
const instance = new JwtService();
export default instance;

View File

@@ -1,8 +0,0 @@
const jwtServiceConfig = {
signIn: 'api/auth/sign-in',
signUp: 'api/auth/sign-up',
accessToken: 'api/auth/access-token',
updateUser: 'api/auth/user/update',
};
export default jwtServiceConfig;

View File

@@ -19,7 +19,7 @@ const settingsConfig = {
To make whole app accessible without authorization by default set defaultAuth: null To make whole app accessible without authorization by default set defaultAuth: null
*** The individual route configs which has auth option won't be overridden. *** The individual route configs which has auth option won't be overridden.
*/ */
defaultAuth: ['admin'], defaultAuth: ['admin', 'staff', 'user'],
/* /*
Default redirect url for the logged-in user, Default redirect url for the logged-in user,
*/ */

View File

@@ -4,7 +4,7 @@ import { useDispatch } from 'react-redux';
import FuseSplashScreen from '@fuse/core/FuseSplashScreen'; import FuseSplashScreen from '@fuse/core/FuseSplashScreen';
import { showMessage } from 'app/store/fuse/messageSlice'; import { showMessage } from 'app/store/fuse/messageSlice';
import { logoutUser, setUser } from 'app/store/userSlice'; import { logoutUser, setUser } from 'app/store/userSlice';
import jwtService from './services/jwtService'; import { authService, firebase } from '../services';
const AuthContext = React.createContext(); const AuthContext = React.createContext();
@@ -14,44 +14,35 @@ function AuthProvider({ children }) {
const dispatch = useDispatch(); const dispatch = useDispatch();
useEffect(() => { useEffect(() => {
jwtService.on('onAutoLogin', () => { authService.on('onLogout', () => {
dispatch(showMessage({ message: 'Signing in with JWT' }));
/**
* Sign in and retrieve user data with stored token
*/
jwtService
.signInWithToken()
.then((user) => {
success(user, 'Signed in with JWT');
})
.catch((error) => {
pass(error.message);
});
});
jwtService.on('onLogin', (user) => {
success(user, 'Signed in');
});
jwtService.on('onLogout', () => {
pass('Signed out'); pass('Signed out');
dispatch(logoutUser()); dispatch(logoutUser());
}); });
jwtService.on('onAutoLogout', (message) => { authService.init(firebase.auth, firebase.db);
pass(message);
dispatch(logoutUser()); authService.onAuthStateChanged((authUser) => {
dispatch(showMessage({ message: 'Signing...' }));
if (authUser) {
authService
.getUserData(authUser.uid)
.then((user) => {
if (user) {
success(user, 'Signed in');
} else {
const { displayName, photoURL, email } = authUser;
success({ role: 'user', data: { displayName, photoURL, email } }, 'Signed in');
}
})
.catch((error) => {
pass(error.message);
});
} else {
pass('Signed out');
}
}); });
jwtService.on('onNoAccessToken', () => {
pass();
});
jwtService.init();
function success(user, message) { function success(user, message) {
if (message) { if (message) {
dispatch(showMessage({ message })); dispatch(showMessage({ message }));

View File

@@ -1,7 +1,7 @@
import i18next from 'i18next'; import i18next from 'i18next';
import ForgotPasswordPage from './ForgotPasswordPage'; import ForgotPasswordPage from './ForgotPasswordPage';
import authRoles from '../../../auth/authRoles'; import authRoles from '../../../configs/authRoles';
import en from './i18n/en'; import en from './i18n/en';
i18next.addResourceBundle('en', 'forgotPasswordPage', en); i18next.addResourceBundle('en', 'forgotPasswordPage', en);

View File

@@ -7,6 +7,7 @@ import Typography from '@mui/material/Typography';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { authService } from 'src/app/services';
import * as yup from 'yup'; import * as yup from 'yup';
import LeftSideCanvas from '../shared-components/LeftSideCanvas'; import LeftSideCanvas from '../shared-components/LeftSideCanvas';
@@ -19,7 +20,7 @@ function ForgotPasswordPage({ t }) {
email: yup.string().email(t('email_error')).required(t('email_error')), email: yup.string().email(t('email_error')).required(t('email_error')),
}); });
const { control, formState, handleSubmit, reset } = useForm({ const { control, formState, handleSubmit, setError } = useForm({
mode: 'onChange', mode: 'onChange',
defaultValues, defaultValues,
resolver: yupResolver(schema), resolver: yupResolver(schema),
@@ -27,8 +28,13 @@ function ForgotPasswordPage({ t }) {
const { isValid, dirtyFields, errors } = formState; const { isValid, dirtyFields, errors } = formState;
function onSubmit() { function onSubmit({ email }) {
reset(defaultValues); authService.sendPasswordResetEmail(email).catch((error) => {
setError('root', {
type: 'manual',
message: error.message,
});
});
} }
return ( return (

View File

@@ -1,7 +1,7 @@
import i18next from 'i18next'; import i18next from 'i18next';
import SignInPage from './SignInPage'; import SignInPage from './SignInPage';
import authRoles from '../../../auth/authRoles'; import authRoles from '../../../configs/authRoles';
import en from './i18n/en'; import en from './i18n/en';
i18next.addResourceBundle('en', 'signInPage', en); i18next.addResourceBundle('en', 'signInPage', en);

View File

@@ -10,13 +10,14 @@ import Typography from '@mui/material/Typography';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { authService } from 'src/app/services';
import * as yup from 'yup'; import * as yup from 'yup';
import LeftSideCanvas from '../shared-components/LeftSideCanvas'; import LeftSideCanvas from '../shared-components/LeftSideCanvas';
const defaultValues = { const defaultValues = {
email: '', email: '',
password: '', password: '',
remember: true, remember: false,
}; };
function SignInPage({ t }) { function SignInPage({ t }) {
@@ -25,7 +26,7 @@ function SignInPage({ t }) {
password: yup.string().required(t('password_error')).min(8, t('password_error')), password: yup.string().required(t('password_error')).min(8, t('password_error')),
}); });
const { control, formState, handleSubmit, reset } = useForm({ const { control, formState, handleSubmit, setError } = useForm({
mode: 'onChange', mode: 'onChange',
defaultValues, defaultValues,
resolver: yupResolver(schema), resolver: yupResolver(schema),
@@ -33,8 +34,13 @@ function SignInPage({ t }) {
const { isValid, dirtyFields, errors } = formState; const { isValid, dirtyFields, errors } = formState;
function onSubmit() { function onSubmit(data) {
reset(defaultValues); authService.signInWithEmailAndPassword(data).catch((error) => {
setError('root', {
type: 'manual',
message: error.message,
});
});
} }
return ( return (
@@ -145,6 +151,7 @@ function SignInPage({ t }) {
> >
{t('sign_in_btn')} {t('sign_in_btn')}
</Button> </Button>
{errors.root && <p>{errors.root.message}</p>}
</div> </div>
</form> </form>
</div> </div>

View File

@@ -1,12 +1,12 @@
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import Paper from '@mui/material/Paper'; import Paper from '@mui/material/Paper';
import { useEffect } from 'react'; import { useEffect } from 'react';
import JwtService from '../../../auth/services/jwtService'; import { authService } from 'src/app/services';
function SignOutPage() { function SignOutPage() {
useEffect(() => { useEffect(() => {
setTimeout(() => { setTimeout(() => {
JwtService.logout(); authService.logout();
}, 1000); }, 1000);
}, []); }, []);

View File

@@ -1,7 +1,7 @@
import i18next from 'i18next'; import i18next from 'i18next';
import SignUpPage from './SignUpPage'; import SignUpPage from './SignUpPage';
import authRoles from '../../../auth/authRoles'; import authRoles from '../../../configs/authRoles';
import en from './i18n/en'; import en from './i18n/en';
i18next.addResourceBundle('en', 'signUpPage', en); i18next.addResourceBundle('en', 'signUpPage', en);

View File

@@ -11,6 +11,7 @@ import Typography from '@mui/material/Typography';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
import { withTranslation } from 'react-i18next'; import { withTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { authService } from 'src/app/services';
import * as yup from 'yup'; import * as yup from 'yup';
import LeftSideCanvas from '../shared-components/LeftSideCanvas'; import LeftSideCanvas from '../shared-components/LeftSideCanvas';
@@ -31,7 +32,7 @@ function SignUpPage({ t }) {
acceptTermsConditions: yup.boolean().oneOf([true], t('accept_terms_error')), acceptTermsConditions: yup.boolean().oneOf([true], t('accept_terms_error')),
}); });
const { control, formState, handleSubmit, reset } = useForm({ const { control, formState, handleSubmit, setError } = useForm({
mode: 'onChange', mode: 'onChange',
defaultValues, defaultValues,
resolver: yupResolver(schema), resolver: yupResolver(schema),
@@ -39,8 +40,19 @@ function SignUpPage({ t }) {
const { isValid, dirtyFields, errors } = formState; const { isValid, dirtyFields, errors } = formState;
function onSubmit() { function onSubmit({ name, password, email }) {
reset(defaultValues); authService
.createUser({
displayName: name,
password,
email,
})
.catch((error) => {
setError('root', {
type: 'manual',
message: error.message,
});
});
} }
return ( return (

View File

@@ -12,6 +12,7 @@ const HomeConfig = {
style: 'layout2', style: 'layout2',
}, },
}, },
auth: null,
routes: [ routes: [
{ {
path: '/', path: '/',

View File

@@ -1,6 +1,6 @@
import i18next from 'i18next'; import i18next from 'i18next';
import { authRoles } from 'src/app/auth'; import authRoles from '../../../configs/authRoles';
import Dashboard from './Dashboard'; import Dashboard from './Dashboard';
import en from './i18n/en'; import en from './i18n/en';

View File

@@ -1,6 +1,6 @@
import i18next from 'i18next'; import i18next from 'i18next';
import { authRoles } from 'src/app/auth'; import authRoles from '../../../configs/authRoles';
import Favorites from './Favorites'; import Favorites from './Favorites';
import en from './i18n/en'; import en from './i18n/en';

View File

@@ -1,6 +1,6 @@
import i18next from 'i18next'; import i18next from 'i18next';
import { authRoles } from 'src/app/auth'; import authRoles from '../../../configs/authRoles';
import History from './History'; import History from './History';
import en from './i18n/en'; import en from './i18n/en';

View File

@@ -1,6 +1,6 @@
import i18next from 'i18next'; import i18next from 'i18next';
import { authRoles } from 'src/app/auth'; import authRoles from '../../../configs/authRoles';
import en from './i18n/en'; import en from './i18n/en';
import Profile from './Profile'; import Profile from './Profile';

View File

@@ -0,0 +1,137 @@
import FuseUtils from '@fuse/utils/FuseUtils';
import _ from '@lodash';
import * as firebaseAuth from 'firebase/auth';
import * as firebaseDb from 'firebase/database';
export default class AuthService extends FuseUtils.EventEmitter {
#auth;
#db;
init(authInstance, dbInstance) {
this.#auth = authInstance;
this.#db = dbInstance;
}
createUser = ({ displayName, email, password }) => {
return new Promise((resolve, reject) => {
firebaseAuth
.createUserWithEmailAndPassword(this.#auth, email, password)
.then((userCredential) => firebaseAuth.updateProfile(userCredential.user, { displayName }))
.then(() => {
const userRef = firebaseDb.ref(this.#db, `users/${this.#auth.currentUser.uid}`);
const value = { role: 'user', data: { displayName, email } };
return firebaseDb.set(userRef, value);
})
.then(() => {
resolve();
})
.catch((error) => {
reject(error);
});
});
};
signInWithEmailAndPassword = ({ email, password, remember }) => {
const persistence = remember
? firebaseAuth.browserLocalPersistence
: firebaseAuth.browserSessionPersistence;
return new Promise((resolve, reject) => {
firebaseAuth
.setPersistence(this.#auth, persistence)
.then(() => firebaseAuth.signInWithEmailAndPassword(this.#auth, email, password))
.then((userCredential) => {
resolve(userCredential.user);
const { user } = userCredential;
this.emit('onLogin', {
role: 'user',
data: { displayName: user.displayName, photoURL: user.photoURL, email: user.email },
});
})
.catch((error) => {
reject(error);
});
});
};
updateUserData = (user) => {
return new Promise((resolve, reject) => {
if (_.isEmpty(user)) {
reject(Error('User data is empty'));
}
const userRef = firebaseDb.ref(this.#db, `users/${this.#auth.currentUser.uid}`);
const value = { data: { ...user } };
firebaseDb
.set(userRef, value)
.then(() => {
if (user.email) {
return firebaseAuth.updateEmail(this.#auth, user.email);
}
return null;
})
.then(() => {
resolve();
})
.catch((error) => {
reject(error);
});
});
};
logout = () => {
this.#auth
.signOut()
.then(() => {
this.emit('onLogout', 'Logged out');
})
.catch((error) => {
console.warn('Logout error: ', error);
});
};
getUserData = (userId) => {
return new Promise((resolve, reject) => {
const userRef = firebaseDb.ref(this.#db, `users/${userId}`);
firebaseDb.onValue(
userRef,
(snapshot) => {
const user = snapshot.val();
resolve(user);
},
(error) => {
reject(error);
}
);
});
};
sendPasswordResetEmail = (email) => {
return new Promise((resolve, reject) => {
if (email) {
reject(Error('Email is empty'));
}
firebaseAuth
.sendPasswordResetEmail(this.#auth, email)
.then(() => {
resolve();
})
.catch((error) => {
reject(error);
});
});
};
onAuthStateChanged = (callback) => {
if (!this.#auth) {
return;
}
this.#auth.onAuthStateChanged(callback);
};
}

View File

@@ -0,0 +1,15 @@
import { initializeApp } from 'firebase/app';
import { getAuth } from 'firebase/auth';
import { getDatabase } from 'firebase/database';
import { getStorage } from 'firebase/storage';
export default class FirebaseService {
#app;
constructor(config) {
this.app = initializeApp(config);
this.auth = getAuth(this.app);
this.db = getDatabase(this.app);
this.storage = getStorage(this.app);
}
}

18
src/app/services/index.js Normal file
View File

@@ -0,0 +1,18 @@
import AuthService from './authService';
import FirebaseService from './firebaseService';
// TODO: change to firebase secrets or to use firebase functions
const firebaseConfig = {
apiKey: 'AIzaSyBqMGmOF0-DkYDpnsmZwpf5S8w5cL3fBb8',
authDomain: 'rental-calculator-13a9e.firebaseapp.com',
databaseURL: 'https://rental-calculator-13a9e-default-rtdb.firebaseio.com',
projectId: 'rental-calculator-13a9e',
storageBucket: 'rental-calculator-13a9e.appspot.com',
messagingSenderId: '479612883365',
appId: '1:479612883365:web:fde2d2632ce4c42ce5184c',
measurementId: 'G-JW7J8ZQ9FJ',
};
const firebase = new FirebaseService(firebaseConfig);
const authService = new AuthService();
export { authService, firebase };

View File

@@ -5,7 +5,7 @@ import _ from '@lodash';
import { setInitialSettings } from 'app/store/fuse/settingsSlice'; import { setInitialSettings } from 'app/store/fuse/settingsSlice';
import { showMessage } from 'app/store/fuse/messageSlice'; import { showMessage } from 'app/store/fuse/messageSlice';
import settingsConfig from 'app/configs/settingsConfig'; import settingsConfig from 'app/configs/settingsConfig';
import jwtService from '../auth/services/jwtService'; import { authService } from '../services';
export const setUser = createAsyncThunk('user/setUser', async (user, { dispatch, getState }) => { export const setUser = createAsyncThunk('user/setUser', async (user, { dispatch, getState }) => {
/* /*
@@ -30,24 +30,6 @@ export const updateUserSettings = createAsyncThunk(
} }
); );
export const updateUserShortcuts = createAsyncThunk(
'user/updateShortucts',
async (shortcuts, { dispatch, getState }) => {
const { user } = getState();
const newUser = {
...user,
data: {
...user.data,
shortcuts,
},
};
dispatch(updateUserData(newUser));
return newUser;
}
);
export const logoutUser = () => async (dispatch, getState) => { export const logoutUser = () => async (dispatch, getState) => {
const { user } = getState(); const { user } = getState();
@@ -71,10 +53,10 @@ export const updateUserData = (user) => async (dispatch, getState) => {
return; return;
} }
jwtService authService
.updateUserData(user) .updateUserData(user)
.then(() => { .then(() => {
dispatch(showMessage({ message: 'User data saved with api' })); dispatch(showMessage({ message: 'User data saved' }));
}) })
.catch((error) => { .catch((error) => {
dispatch(showMessage({ message: error.message })); dispatch(showMessage({ message: error.message }));
@@ -87,7 +69,6 @@ const initialState = {
displayName: 'John Doe', displayName: 'John Doe',
photoURL: 'assets/images/avatars/brian-hughes.jpg', photoURL: 'assets/images/avatars/brian-hughes.jpg',
email: 'johndoe@withinpixels.com', email: 'johndoe@withinpixels.com',
shortcuts: ['apps.calendar', 'apps.mailbox', 'apps.contacts', 'apps.tasks'],
}, },
}; };
@@ -99,7 +80,6 @@ const userSlice = createSlice({
}, },
extraReducers: { extraReducers: {
[updateUserSettings.fulfilled]: (state, action) => action.payload, [updateUserSettings.fulfilled]: (state, action) => action.payload,
[updateUserShortcuts.fulfilled]: (state, action) => action.payload,
[setUser.fulfilled]: (state, action) => action.payload, [setUser.fulfilled]: (state, action) => action.payload,
}, },
}); });
@@ -108,6 +88,4 @@ export const { userLoggedOut } = userSlice.actions;
export const selectUser = ({ user }) => user; export const selectUser = ({ user }) => user;
export const selectUserShortcuts = ({ user }) => user.data.shortcuts;
export default userSlice.reducer; export default userSlice.reducer;

View File

@@ -2,7 +2,7 @@ import FuseDialog from '@fuse/core/FuseDialog';
import FuseMessage from '@fuse/core/FuseMessage'; import FuseMessage from '@fuse/core/FuseMessage';
import FuseSuspense from '@fuse/core/FuseSuspense'; import FuseSuspense from '@fuse/core/FuseSuspense';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import AppContext from 'app/AppContext'; import AppContext from 'src/app/contexts/AppContext';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice'; import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import { memo, useContext } from 'react'; import { memo, useContext } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';

View File

@@ -1,10 +1,11 @@
import FuseSuspense from '@fuse/core/FuseSuspense'; import FuseSuspense from '@fuse/core/FuseSuspense';
import AppContext from 'app/AppContext'; import AppContext from 'src/app/contexts/AppContext';
import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice'; import { selectFuseCurrentLayoutConfig } from 'app/store/fuse/settingsSlice';
import i18next from 'i18next'; import i18next from 'i18next';
import { memo, useContext } from 'react'; import { memo, useContext } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useRoutes } from 'react-router-dom'; import { useRoutes } from 'react-router-dom';
import { useAuth } from 'src/app/contexts/AuthContext';
import FooterLayout2 from './components/FooterLayout2'; import FooterLayout2 from './components/FooterLayout2';
import HeaderLayout2 from './components/HeaderLayout2'; import HeaderLayout2 from './components/HeaderLayout2';
@@ -14,12 +15,13 @@ i18next.addResourceBundle('en', 'layout2', en);
function Layout2(props) { function Layout2(props) {
const config = useSelector(selectFuseCurrentLayoutConfig); const config = useSelector(selectFuseCurrentLayoutConfig);
const authContext = useAuth();
const appContext = useContext(AppContext); const appContext = useContext(AppContext);
const { routes } = appContext; const { routes } = appContext;
return ( return (
<> <>
{config.header.display && <HeaderLayout2 />} {config.header.display && <HeaderLayout2 isAuthenticated={authContext.isAuthenticated} />}
<main id="fuse-main" className="flex flex-col min-h-full min-w-full mt-72"> <main id="fuse-main" className="flex flex-col min-h-full min-w-full mt-72">
<FuseSuspense>{useRoutes(routes)}</FuseSuspense> <FuseSuspense>{useRoutes(routes)}</FuseSuspense>
{props.children} {props.children}

View File

@@ -7,8 +7,8 @@ function FooterLayout2() {
const { t } = useTranslation('layout2'); const { t } = useTranslation('layout2');
return ( return (
<footer className="w-full bg-gray-900"> <footer className="flex items-center justify-center w-full bg-gray-900">
<div className="flex gap-96 max-w-screen-2xl px-10 py-52"> <div className="flex gap-96 w-full max-w-screen-xl px-10 py-52">
<ul className="flex flex-col gap-16 mr-96"> <ul className="flex flex-col gap-16 mr-96">
<li> <li>
<Link to="/"> <Link to="/">

View File

@@ -4,12 +4,12 @@ import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import linksConfigLayout2 from './linksLayout2Config'; import linksConfigLayout2 from './linksLayout2Config';
function HeaderLayout2() { function HeaderLayout2(props) {
const { t } = useTranslation('layout2'); const { t } = useTranslation('layout2');
return ( return (
<header className="fixed z-50 flex items-center justify-center w-full h-72 px-10 bg-white"> <header className="fixed z-50 flex items-center justify-center w-full h-72 px-10 bg-white">
<div className="flex justify-between max-w-screen-2xl w-full"> <div className="flex justify-between max-w-screen-xl w-full">
<Link to="/"> <Link to="/">
<img <img
className="max-w-[88px] max-h-[45px]" className="max-w-[88px] max-h-[45px]"
@@ -17,7 +17,7 @@ function HeaderLayout2() {
alt={t('logo_alt')} alt={t('logo_alt')}
/> />
</Link> </Link>
<nav className="flex gap-72 items-center"> <nav className="flex grow justify-center gap-72 items-center">
{linksConfigLayout2.map((path) => ( {linksConfigLayout2.map((path) => (
<Link <Link
className="text-lg leading-5 text-slate-800 no-underline" className="text-lg leading-5 text-slate-800 no-underline"
@@ -28,18 +28,20 @@ function HeaderLayout2() {
</Link> </Link>
))} ))}
</nav> </nav>
<div className="flex gap-32 items-center"> {props.isAuthenticated || (
<Link className="text-indigo-400" to="/sign-up"> <div className="flex gap-32 items-center">
{t('sign_up')} <Link className="text-indigo-400" to="/sign-in">
</Link> {t('sign_in')}
<Link </Link>
className="flex gap-7 items-center px-24 py-10 text-lg leading-5 text-white bg-indigo-400 rounded-2xl" <Link
to="/sign-in" className="flex gap-7 items-center px-24 py-10 text-lg leading-5 text-white bg-indigo-400 rounded-2xl"
> to="/sign-up"
<span>{t('sign_in')}</span> >
<FuseSvgIcon>heroicons-outline:login</FuseSvgIcon> <span>{t('sign_up')}</span>
</Link> <FuseSvgIcon>heroicons-outline:login</FuseSvgIcon>
</div> </Link>
</div>
)}
</div> </div>
</header> </header>
); );

View File

@@ -3,8 +3,8 @@ const locale = {
'about-us': 'About Us', 'about-us': 'About Us',
blog: 'Blog', blog: 'Blog',
contacts: 'Contacts', contacts: 'Contacts',
sign_up: 'Log In', sign_in: 'Log In',
sign_in: 'Register', sign_up: 'Register',
logo_alt: 'Logo', logo_alt: 'Logo',
}; };

View File

@@ -40,7 +40,7 @@ function UserNavbarHeader(props) {
src={user.data.photoURL} src={user.data.photoURL}
alt={user.data.displayName} alt={user.data.displayName}
> >
{user.data.displayName.charAt(0)} {user.data.displayName?.charAt(0)}
</Avatar> </Avatar>
</div> </div>
<Typography className="username text-14 whitespace-nowrap font-medium"> <Typography className="username text-14 whitespace-nowrap font-medium">

View File

@@ -9,7 +9,7 @@ import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import { StyledEngineProvider } from '@mui/material/styles'; import { StyledEngineProvider } from '@mui/material/styles';
import routes from 'app/configs/routesConfig'; import routes from 'app/configs/routesConfig';
import store from './store'; import store from './store';
import AppContext from './AppContext'; import AppContext from './contexts/AppContext';
const withAppProviders = (Component) => (props) => { const withAppProviders = (Component) => (props) => {
const WrapperComponent = () => ( const WrapperComponent = () => (

View File

@@ -1192,7 +1192,8 @@ module.exports = {
sm: '600px', sm: '600px',
md: '960px', md: '960px',
lg: '1280px', lg: '1280px',
xl: '1920px', xl: '1420px',
'2xl': '1920px',
print: { raw: 'print' }, print: { raw: 'print' },
}, },
scrollMargin: ({ theme }) => ({ scrollMargin: ({ theme }) => ({