Merge pull request 'RC-8-create-profile-page' (#4) from RC-8-create-profile-page into dev
Reviewed-on: #4
This commit was merged in pull request #4.
This commit is contained in:
21
database.rules.json
Normal file
21
database.rules.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"rules": {
|
||||||
|
".read": false,
|
||||||
|
".write": false,
|
||||||
|
"users": {
|
||||||
|
"$userId": {
|
||||||
|
".read": "auth.uid === $userId",
|
||||||
|
".write": "auth.uid === $userId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// // readable node
|
||||||
|
// "messages": {
|
||||||
|
// ".read": true
|
||||||
|
// },
|
||||||
|
// // readable and writable node
|
||||||
|
// "messages": {
|
||||||
|
// ".read": true,
|
||||||
|
// ".write": true
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -53,7 +53,7 @@ const inputGlobalStyles = (
|
|||||||
// },
|
// },
|
||||||
// },
|
// },
|
||||||
'[class*="MuiOutlinedInput-root"]': {
|
'[class*="MuiOutlinedInput-root"]': {
|
||||||
borderRadius: theme.size?.inputRadius,
|
borderRadius: `${theme.spacing('10px')}!important`,
|
||||||
},
|
},
|
||||||
'[class^="border"]': {
|
'[class^="border"]': {
|
||||||
borderColor: theme.palette.divider,
|
borderColor: theme.palette.divider,
|
||||||
|
|||||||
@@ -48,9 +48,6 @@ const themesConfig = {
|
|||||||
dark: '#b71c1c',
|
dark: '#b71c1c',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
size: {
|
|
||||||
inputRadius: '10px',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
defaultDark: {
|
defaultDark: {
|
||||||
palette: {
|
palette: {
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ function SignUpPage({ t }) {
|
|||||||
className="mb-28"
|
className="mb-28"
|
||||||
label={t('name')}
|
label={t('name')}
|
||||||
autoFocus
|
autoFocus
|
||||||
type="name"
|
type="text"
|
||||||
error={!!errors.name}
|
error={!!errors.name}
|
||||||
helperText={errors?.name?.message}
|
helperText={errors?.name?.message}
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
|
|||||||
@@ -1,36 +1,386 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import FusePageSimple from '@fuse/core/FusePageSimple';
|
import FusePageSimple from '@fuse/core/FusePageSimple';
|
||||||
import DemoContent from '@fuse/core/DemoContent';
|
import _ from '@lodash';
|
||||||
|
import FuseSvgIcon from '@fuse/core/FuseSvgIcon/FuseSvgIcon';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import Button from '@mui/material/Button';
|
||||||
|
import Paper from '@mui/material/Paper';
|
||||||
|
import TextField from '@mui/material/TextField';
|
||||||
|
import { styled } from '@mui/material/styles';
|
||||||
|
import { selectUser, updateUserSettings } from 'app/store/userSlice';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { withTranslation } from 'react-i18next';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
import { toBase64 } from 'src/app/utils';
|
||||||
|
|
||||||
|
const MAX_PICTURE_SIZE = 5000000;
|
||||||
|
const AVAILABLE_MEDIA_TYPES = ['image/png', 'image/jpeg'];
|
||||||
|
|
||||||
const Root = styled(FusePageSimple)(({ theme }) => ({
|
const Root = styled(FusePageSimple)(({ theme }) => ({
|
||||||
'& .FusePageSimple-header': {
|
'& .FusePageSimple-header': {},
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
borderBottomWidth: 1,
|
|
||||||
borderStyle: 'solid',
|
|
||||||
borderColor: theme.palette.divider,
|
|
||||||
},
|
|
||||||
'& .FusePageSimple-toolbar': {},
|
'& .FusePageSimple-toolbar': {},
|
||||||
'& .FusePageSimple-content': {},
|
'& .FusePageSimple-content': {
|
||||||
|
backgroundColor: theme.palette.background.default,
|
||||||
|
},
|
||||||
'& .FusePageSimple-sidebarHeader': {},
|
'& .FusePageSimple-sidebarHeader': {},
|
||||||
'& .FusePageSimple-sidebarContent': {},
|
'& .FusePageSimple-sidebarContent': {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
function ProfilePage(props) {
|
function ProfilePage({ t }) {
|
||||||
const { t } = useTranslation('profilePage');
|
const dispatch = useDispatch();
|
||||||
|
const user = useSelector(selectUser);
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
photoURL: '',
|
||||||
|
firstName: '',
|
||||||
|
lastName: '',
|
||||||
|
displayName: '',
|
||||||
|
email: '',
|
||||||
|
mobileNumber: '',
|
||||||
|
information: '',
|
||||||
|
address: '',
|
||||||
|
...user.data,
|
||||||
|
};
|
||||||
|
const schema = yup.object().shape({
|
||||||
|
photoURL: yup.string().notRequired(),
|
||||||
|
firstName: yup
|
||||||
|
.string()
|
||||||
|
.max(150, t('max_length_error', { length: 150 }))
|
||||||
|
.trim()
|
||||||
|
.notRequired(),
|
||||||
|
lastName: yup
|
||||||
|
.string()
|
||||||
|
.max(150, t('max_length_error', { length: 150 }))
|
||||||
|
.trim()
|
||||||
|
.notRequired(),
|
||||||
|
displayName: yup
|
||||||
|
.string()
|
||||||
|
.required(t('display_name_error'))
|
||||||
|
.max(30, t('max_length_error', { length: 30 }))
|
||||||
|
.trim(),
|
||||||
|
email: yup
|
||||||
|
.string()
|
||||||
|
.matches(/^[\w-.]+@([\w-]+\.)+[\w-]{2,4}$/g, t('email_error'))
|
||||||
|
.required(t('email_error'))
|
||||||
|
.max(40, t('max_length_error', { length: 40 })),
|
||||||
|
mobileNumber: yup
|
||||||
|
.string()
|
||||||
|
.matches(/^(?=(.*\d){7,})\+?(\d[\d-. ]+)?(\([\d-. ]+\))?[\d-. ]+\d$/, {
|
||||||
|
message: t('mobile_number_error'),
|
||||||
|
excludeEmptyString: true,
|
||||||
|
})
|
||||||
|
.max(20, t('max_length_error', { length: 20 }))
|
||||||
|
.notRequired(),
|
||||||
|
information: yup
|
||||||
|
.string()
|
||||||
|
.max(300, t('max_length_error', { length: 300 }))
|
||||||
|
.trim()
|
||||||
|
.notRequired(),
|
||||||
|
address: yup
|
||||||
|
.string()
|
||||||
|
.max(150, t('max_length_error', { length: 150 }))
|
||||||
|
.trim()
|
||||||
|
.notRequired(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { control, formState, watch, setValue, handleSubmit, setError, reset } = useForm({
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues,
|
||||||
|
resolver: yupResolver(schema),
|
||||||
|
});
|
||||||
|
const { dirtyFields, errors } = formState;
|
||||||
|
|
||||||
|
const uploadPicture = async (event) => {
|
||||||
|
const { target } = event;
|
||||||
|
if (target.files && target.files[0]) {
|
||||||
|
const file = target.files[0];
|
||||||
|
if (file.size > MAX_PICTURE_SIZE) {
|
||||||
|
return setError('photoURL', { type: 'custom', message: t('picture_size_error') });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AVAILABLE_MEDIA_TYPES.includes(file.type)) {
|
||||||
|
return setError('photoURL', {
|
||||||
|
type: 'custom',
|
||||||
|
message: t('picture_extensions_error'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const base64 = await toBase64(file);
|
||||||
|
setValue('photoURL', base64, { shouldDirty: true });
|
||||||
|
} else {
|
||||||
|
setError('photoURL', { type: 'custom', message: 'Choose a file please' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deletePicture = () => setValue('photoURL', '', { shouldDirty: true });
|
||||||
|
|
||||||
|
const onSubmit = (data) => {
|
||||||
|
dispatch(updateUserSettings(data)).catch((error) => {
|
||||||
|
setError('root', {
|
||||||
|
type: 'manual',
|
||||||
|
message: error.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
reset(data);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Root
|
<Root
|
||||||
// header={
|
|
||||||
// <div className="p-24">
|
|
||||||
// <h4>{t('TITLE')}</h4>
|
|
||||||
// </div>
|
|
||||||
// }
|
|
||||||
content={
|
content={
|
||||||
<div className="p-24">
|
<div className="w-full p-60">
|
||||||
<h4>Content</h4>
|
<form name="profileForm" noValidate onSubmit={handleSubmit(onSubmit)}>
|
||||||
<br />
|
<div className="flex items-end mb-68">
|
||||||
<DemoContent />
|
<Paper className="relative w-[240px] h-[240px] mr-20 bg-common-disabled overflow-hidden">
|
||||||
|
{watch('photoURL') && (
|
||||||
|
<img
|
||||||
|
src={watch('photoURL')}
|
||||||
|
alt="user"
|
||||||
|
className="absolute w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Paper>
|
||||||
|
<div className="grid grid-cols-[200px_minmax(0,_1fr)] gap-x-40 gap-y-28">
|
||||||
|
<Controller
|
||||||
|
name="photoURL"
|
||||||
|
control={control}
|
||||||
|
render={({ field: { name, ref, onBlur } }) => (
|
||||||
|
<label htmlFor="input-file">
|
||||||
|
<input
|
||||||
|
id="input-file"
|
||||||
|
name={name}
|
||||||
|
ref={ref}
|
||||||
|
type="file"
|
||||||
|
accept={AVAILABLE_MEDIA_TYPES.join(', ')}
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onBlur={onBlur}
|
||||||
|
onChange={uploadPicture}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
component="span"
|
||||||
|
color="secondary"
|
||||||
|
className="text-lg rounded-xl border-2 border-secondary-main shadow hover:shadow-hover hover:shadow-secondary-main hover:border-2 ease-in-out duration-300"
|
||||||
|
aria-label={t('upload_picture_btn')}
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
>
|
||||||
|
{t('upload_picture_btn')}
|
||||||
|
</Button>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant="text"
|
||||||
|
color="secondary"
|
||||||
|
className="justify-self-start min-w-fit text-base"
|
||||||
|
aria-label={t('delete_picture')}
|
||||||
|
disabled={!watch('photoURL')}
|
||||||
|
size="small"
|
||||||
|
onClick={deletePicture}
|
||||||
|
>
|
||||||
|
<FuseSvgIcon className="mr-10">heroicons-outline:trash</FuseSvgIcon>
|
||||||
|
{t('delete_picture')}
|
||||||
|
</Button>
|
||||||
|
<ul className="col-span-2 text-lg">
|
||||||
|
<li className="bullet">{t('first_picture_req')}</li>
|
||||||
|
<li className="bullet">{t('second_picture_req')}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-6 gap-x-24 gap-y-32 w-full mb-48">
|
||||||
|
<Controller
|
||||||
|
name="firstName"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-2"
|
||||||
|
label={t('first_name')}
|
||||||
|
type="text"
|
||||||
|
error={!!errors.firstName}
|
||||||
|
helperText={errors?.firstName?.message}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="lastName"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-2"
|
||||||
|
label={t('last_name')}
|
||||||
|
type="text"
|
||||||
|
error={!!errors.lastName}
|
||||||
|
helperText={errors?.lastName?.message}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="displayName"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-2"
|
||||||
|
label={t('display_name')}
|
||||||
|
type="text"
|
||||||
|
error={!!errors.displayName}
|
||||||
|
helperText={errors?.displayName?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="email"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-3"
|
||||||
|
label={t('email')}
|
||||||
|
type="email"
|
||||||
|
error={!!errors.email}
|
||||||
|
helperText={errors?.email?.message}
|
||||||
|
variant="outlined"
|
||||||
|
required
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="mobileNumber"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-3"
|
||||||
|
label={t('mobile_number')}
|
||||||
|
type="tel"
|
||||||
|
error={!!errors.mobileNumber}
|
||||||
|
helperText={errors?.mobileNumber?.message}
|
||||||
|
variant="outlined"
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="information"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-3"
|
||||||
|
label={t('information')}
|
||||||
|
type="text"
|
||||||
|
error={!!errors.information}
|
||||||
|
helperText={errors?.information?.message}
|
||||||
|
variant="outlined"
|
||||||
|
multiline
|
||||||
|
rows={7}
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
// eslint-disable-next-line react/jsx-no-duplicate-props
|
||||||
|
inputProps={{
|
||||||
|
sx: {
|
||||||
|
padding: '16.5px 14px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Controller
|
||||||
|
name="address"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
className="col-span-3"
|
||||||
|
label={t('address')}
|
||||||
|
type="text"
|
||||||
|
error={!!errors.address}
|
||||||
|
helperText={errors?.address?.message}
|
||||||
|
variant="outlined"
|
||||||
|
multiline
|
||||||
|
rows={7}
|
||||||
|
fullWidth
|
||||||
|
InputProps={{
|
||||||
|
sx: {
|
||||||
|
background: (theme) => theme.palette.background.paper,
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
// eslint-disable-next-line react/jsx-no-duplicate-props
|
||||||
|
inputProps={{
|
||||||
|
sx: {
|
||||||
|
padding: '16.5px 14px',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col items-center justify-center gap-10 w-full">
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
color="secondary"
|
||||||
|
className="max-w-320 text-base uppercase rounded-xl"
|
||||||
|
aria-label={t('save_changes')}
|
||||||
|
disabled={_.isEmpty(dirtyFields) || !_.isEmpty(errors)}
|
||||||
|
type="submit"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{t('save_changes')}
|
||||||
|
</Button>
|
||||||
|
{errors.root?.message && (
|
||||||
|
<p className="text-l text-error-main">{errors.root?.message}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
scroll="content"
|
scroll="content"
|
||||||
@@ -38,4 +388,4 @@ function ProfilePage(props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ProfilePage;
|
export default withTranslation('profilePage')(ProfilePage);
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
import { lazy } from 'react';
|
||||||
import i18next from 'i18next';
|
import i18next from 'i18next';
|
||||||
|
|
||||||
import authRoles from '../../../configs/authRoles';
|
import authRoles from '../../../configs/authRoles';
|
||||||
import en from './i18n/en';
|
import en from './i18n/en';
|
||||||
import Profile from './Profile';
|
|
||||||
|
|
||||||
i18next.addResourceBundle('en', 'profilePage', en);
|
i18next.addResourceBundle('en', 'profilePage', en);
|
||||||
|
|
||||||
|
const Profile = lazy(() => import('./Profile'));
|
||||||
|
|
||||||
const ProfileConfig = {
|
const ProfileConfig = {
|
||||||
settings: {
|
settings: {
|
||||||
layout: {
|
layout: {
|
||||||
@@ -22,28 +24,3 @@ const ProfileConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default ProfileConfig;
|
export default ProfileConfig;
|
||||||
|
|
||||||
/**
|
|
||||||
* Lazy load Example
|
|
||||||
*/
|
|
||||||
/*
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
const Example = lazy(() => import('./Example'));
|
|
||||||
|
|
||||||
const ExampleConfig = {
|
|
||||||
settings: {
|
|
||||||
layout: {
|
|
||||||
config: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
routes: [
|
|
||||||
{
|
|
||||||
path: 'example',
|
|
||||||
element: <Example />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ExampleConfig;
|
|
||||||
*/
|
|
||||||
|
|||||||
@@ -1,3 +1,25 @@
|
|||||||
const locale = {};
|
const locale = {
|
||||||
|
upload_picture_btn: 'Upload New Picture',
|
||||||
|
delete_picture: 'Delete',
|
||||||
|
first_picture_req: 'Lorem ipsum dolor st ut nec.',
|
||||||
|
second_picture_req: 'Lorem ipsum dolor sit amet consectetur. Etiam tristique feugiat ut nec.',
|
||||||
|
picture_size_error: 'The file is too large',
|
||||||
|
picture_extensions_error: 'We only support jpeg and png file extensions',
|
||||||
|
first_name: 'First Name',
|
||||||
|
last_name: 'Last Name',
|
||||||
|
display_name: 'Display Name',
|
||||||
|
display_name_error:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
|
||||||
|
email: 'Email',
|
||||||
|
email_error:
|
||||||
|
'Lorem ipsum dolor sit amet consectetur. Eget pellentesque id consequat consectetur eu quis.',
|
||||||
|
mobile_number: 'Mobile Number',
|
||||||
|
mobile_number_error: 'The mobile number is not correct',
|
||||||
|
information: 'Biographical Information',
|
||||||
|
address: 'Address',
|
||||||
|
save_changes: 'save changes',
|
||||||
|
max_length_error: 'The maximum length is {{length}}',
|
||||||
|
min_length_error: 'The minimum length is {{length}}',
|
||||||
|
};
|
||||||
|
|
||||||
export default locale;
|
export default locale;
|
||||||
|
|||||||
@@ -63,13 +63,12 @@ export default class AuthService extends FuseUtils.EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const userRef = firebaseDb.ref(this.#db, `users/${this.#auth.currentUser.uid}`);
|
const userRef = firebaseDb.ref(this.#db, `users/${this.#auth.currentUser.uid}`);
|
||||||
const value = { data: { ...user } };
|
|
||||||
|
|
||||||
firebaseDb
|
firebaseDb
|
||||||
.set(userRef, value)
|
.set(userRef, user)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (user.email) {
|
if (user.data.email !== this.#auth.currentUser.email) {
|
||||||
return firebaseAuth.updateEmail(this.#auth, user.email);
|
return firebaseAuth.updateEmail(this.#auth, user.data.email);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ export const updateUserSettings = createAsyncThunk(
|
|||||||
'user/updateSettings',
|
'user/updateSettings',
|
||||||
async (settings, { dispatch, getState }) => {
|
async (settings, { dispatch, getState }) => {
|
||||||
const { user } = getState();
|
const { user } = getState();
|
||||||
const newUser = _.merge({}, user, { data: { settings } });
|
const newUser = _.merge({}, user, { data: { ...settings } });
|
||||||
|
|
||||||
dispatch(updateUserData(newUser));
|
dispatch(updateUserData(newUser));
|
||||||
|
|
||||||
@@ -67,7 +67,6 @@ const initialState = {
|
|||||||
role: [], // guest
|
role: [], // guest
|
||||||
data: {
|
data: {
|
||||||
displayName: 'John Doe',
|
displayName: 'John Doe',
|
||||||
photoURL: 'assets/images/avatars/brian-hughes.jpg',
|
|
||||||
email: 'johndoe@withinpixels.com',
|
email: 'johndoe@withinpixels.com',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
7
src/app/utils/index.js
Normal file
7
src/app/utils/index.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const toBase64 = (value) =>
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.readAsDataURL(value);
|
||||||
|
reader.onload = () => resolve(reader.result);
|
||||||
|
reader.onerror = reject;
|
||||||
|
});
|
||||||
@@ -10,3 +10,22 @@
|
|||||||
@import 'prism.css';
|
@import 'prism.css';
|
||||||
|
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
|
|
||||||
|
@layer components {
|
||||||
|
.bullet {
|
||||||
|
position: relative;
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bullet::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
transform: translateY(50%);
|
||||||
|
left: 0;
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
background-color: #4d53ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user