diff --git a/cypress/fixtures/members.js b/cypress/fixtures/members.js
index ea7c44085..f597f5304 100644
--- a/cypress/fixtures/members.js
+++ b/cypress/fixtures/members.js
@@ -1,6 +1,20 @@
export const MEMBERS = {
- ANNA: { id: 'anna-id', name: 'anna', email: 'anna@email.com' },
- BOB: { id: 'bob-id', name: 'bob', email: 'bob@email.com' },
+ ANNA: {
+ id: 'ecafbd2a-5642-31fb-ae93-0242ac130002',
+ name: 'anna',
+ email: 'anna@email.com',
+ createdAt: '2021-04-13 14:56:34.749946',
+ extra: {
+ lang: 'fr',
+ },
+ },
+ BOB: {
+ id: 'ecafbd2a-5688-11eb-fb52-3212ac130002',
+ name: 'bob',
+ email: 'bob@email.com',
+ createdAt: '2021-04-13 14:56:34.749946',
+ extra: { lang: 'en' },
+ },
};
export const CURRENT_USER = MEMBERS.ANNA;
diff --git a/cypress/integration/authentication.spec.js b/cypress/integration/authentication.spec.js
index 6c4c16ebd..daee7db8f 100644
--- a/cypress/integration/authentication.spec.js
+++ b/cypress/integration/authentication.spec.js
@@ -9,6 +9,7 @@ import {
HEADER_APP_BAR_ID,
HEADER_USER_ID,
ITEM_MAIN_CLASS,
+ REDIRECTION_CONTENT_ID,
USER_MENU_SIGN_OUT_OPTION_ID,
} from '../../src/config/selectors';
import { SAMPLE_ITEMS } from '../fixtures/items';
@@ -16,7 +17,6 @@ import { CURRENT_USER } from '../fixtures/members';
import {
REQUEST_FAILURE_LOADING_TIME,
PAGE_LOAD_WAITING_PAUSE,
- REDIRECTION_CONTENT,
REDIRECTION_TIME,
} from '../support/constants';
import { REDIRECT_URL_LOCAL_STORAGE_KEY } from '../../src/config/constants';
@@ -33,7 +33,7 @@ describe('Authentication', () => {
'equal',
HOME_PATH,
);
- cy.get('html').should('contain', REDIRECTION_CONTENT);
+ cy.get(`#${REDIRECTION_CONTENT_ID}`).should('exist');
});
it('Shared Items', () => {
cy.visit(SHARED_ITEMS_PATH);
@@ -42,7 +42,7 @@ describe('Authentication', () => {
'equal',
SHARED_ITEMS_PATH,
);
- cy.get('html').should('contain', REDIRECTION_CONTENT);
+ cy.get(`#${REDIRECTION_CONTENT_ID}`).should('exist');
});
});
@@ -53,11 +53,13 @@ describe('Authentication', () => {
it('Signing Off redirect to sign in route', () => {
cy.visit(HOME_PATH);
- // user name in header
cy.get(`#${HEADER_USER_ID}`).click();
cy.get(`#${USER_MENU_SIGN_OUT_OPTION_ID}`).click();
cy.wait(REQUEST_FAILURE_LOADING_TIME);
- cy.get('html').should('contain', REDIRECTION_CONTENT);
+
+ // should refetch current member just after signing out
+ // this current member will be unauthorized and thus redirect
+ cy.wait(['@signOut', '@getCurrentMember']);
});
describe('Load page correctly', () => {
diff --git a/cypress/integration/memberProfile.spec.js b/cypress/integration/memberProfile.spec.js
new file mode 100644
index 000000000..efd822782
--- /dev/null
+++ b/cypress/integration/memberProfile.spec.js
@@ -0,0 +1,57 @@
+import { MEMBER_PROFILE_PATH } from '../../src/config/paths';
+import { langs } from '../../src/config/i18n';
+import {
+ MEMBER_PROFILE_MEMBER_ID_ID,
+ MEMBER_PROFILE_MEMBER_NAME_ID,
+ MEMBER_PROFILE_EMAIL_ID,
+ MEMBER_PROFILE_LANGUAGE_SWITCH_ID,
+ MEMBER_PROFILE_INSCRIPTION_DATE_ID,
+ MEMBER_PROFILE_MEMBER_ID_COPY_BUTTON_ID,
+} from '../../src/config/selectors';
+import { CURRENT_USER } from '../fixtures/members';
+import { formatDate } from '../../src/utils/date';
+
+describe('Member Profile', () => {
+ beforeEach(() => {
+ cy.setUpApi();
+ cy.visit(MEMBER_PROFILE_PATH);
+ });
+
+ it('Layout', () => {
+ const { id, name, email, extra, createdAt } = CURRENT_USER;
+ cy.get(`#${MEMBER_PROFILE_MEMBER_ID_ID}`).should('contain', id);
+ cy.get(`#${MEMBER_PROFILE_MEMBER_NAME_ID}`).should('contain', name);
+ cy.get(`#${MEMBER_PROFILE_EMAIL_ID}`).should('contain', email);
+ cy.get(`#${MEMBER_PROFILE_INSCRIPTION_DATE_ID}`).should(
+ 'contain',
+ formatDate(createdAt),
+ );
+ cy.get(`#${MEMBER_PROFILE_LANGUAGE_SWITCH_ID}`).should(
+ 'contain',
+ langs[extra.lang],
+ );
+ });
+
+ it('Changing Language edits user', () => {
+ const { id } = CURRENT_USER;
+
+ cy.get(`#${MEMBER_PROFILE_LANGUAGE_SWITCH_ID}`).select('en');
+
+ cy.wait('@editMember').then(({ request: { body, url } }) => {
+ expect(url).to.contain(id);
+ expect(body?.extra?.lang).to.equal('en');
+ });
+ });
+
+ it('Copy member ID to clipboard', () => {
+ const { id } = CURRENT_USER;
+
+ cy.get(`#${MEMBER_PROFILE_MEMBER_ID_COPY_BUTTON_ID}`).click();
+
+ cy.window().then((win) => {
+ win.navigator.clipboard.readText().then((text) => {
+ expect(text).to.equal(id);
+ });
+ });
+ });
+});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
index 1da80a19a..9bfcfee54 100644
--- a/cypress/plugins/index.js
+++ b/cypress/plugins/index.js
@@ -27,6 +27,7 @@ module.exports = (on, config) => {
root: '#root',
},
API_HOST: process.env.REACT_APP_API_HOST,
+ AUTHENTICATION_HOST: process.env.REACT_APP_AUTHENTICATION_HOST,
S3_FILES_HOST:
// calls to this host are mocked, but still should be reachable
// set an s3 host or fake it by using the same host as the api's
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 265101745..6aab84b3d 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -33,6 +33,8 @@ import {
mockGetTags,
mockPostItemTag,
mockPutItemLogin,
+ mockEditMember,
+ mockGetSharedItems,
} from './server';
import './commands/item';
import './commands/navigation';
@@ -62,12 +64,15 @@ Cypress.Commands.add(
postItemTagError = false,
postItemLoginError = false,
putItemLoginError = false,
+ editMemberError = false,
} = {}) => {
const cachedItems = JSON.parse(JSON.stringify(items));
const cachedMembers = JSON.parse(JSON.stringify(members));
mockGetOwnItems(cachedItems);
+ mockGetSharedItems({ items: cachedItems, member: currentMember });
+
mockPostItem(cachedItems, postItemError);
mockDeleteItem(cachedItems, deleteItemError);
@@ -118,6 +123,8 @@ Cypress.Commands.add(
mockGetItemTags(items);
mockPostItemTag(items, postItemTagError);
+
+ mockEditMember(members, editMemberError);
},
);
diff --git a/cypress/support/constants.js b/cypress/support/constants.js
index 0bdf33424..8b5998b4a 100644
--- a/cypress/support/constants.js
+++ b/cypress/support/constants.js
@@ -3,7 +3,7 @@ export const EDIT_ITEM_PAUSE = 1000;
export const ITEM_LOGIN_PAUSE = 1000;
export const NAVIGATE_PAUSE = 500;
export const PAGE_LOAD_WAITING_PAUSE = 3000;
-export const REQUEST_FAILURE_LOADING_TIME = 3000;
+export const REQUEST_FAILURE_LOADING_TIME = 1500;
export const TREE_VIEW_PAUSE = 2000;
export const REDIRECTION_CONTENT = 'hello';
diff --git a/cypress/support/server.js b/cypress/support/server.js
index 6de8c21fe..b31f21dcf 100644
--- a/cypress/support/server.js
+++ b/cypress/support/server.js
@@ -23,7 +23,6 @@ import {
getItemLoginSchema,
buildItemLoginSchemaExtra,
} from '../../src/utils/itemExtra';
-import { REDIRECTION_CONTENT } from './constants';
import { SETTINGS } from '../../src/config/constants';
import { ITEM_LOGIN_TAG } from '../fixtures/itemTags';
@@ -53,15 +52,18 @@ const {
GET_TAGS_ROUTE,
buildPutItemLoginSchema,
buildPostItemTagRoute,
+ buildPatchMember,
+ SHARE_ITEM_WITH_ROUTE,
} = API_ROUTES;
const API_HOST = Cypress.env('API_HOST');
const S3_FILES_HOST = Cypress.env('S3_FILES_HOST');
+const AUTHENTICATION_HOST = Cypress.env('AUTHENTICATION_HOST');
export const redirectionReply = {
- headers: { 'content-type': 'text/html' },
+ headers: { 'content-type': 'application/json' },
statusCode: StatusCodes.OK,
- body: REDIRECTION_CONTENT,
+ body: null,
};
export const mockGetCurrentMember = (
@@ -97,6 +99,19 @@ export const mockGetOwnItems = (items) => {
).as('getOwnItems');
};
+export const mockGetSharedItems = ({ items, member }) => {
+ cy.intercept(
+ {
+ method: DEFAULT_GET.method,
+ url: `${API_HOST}/${SHARE_ITEM_WITH_ROUTE}`,
+ },
+ (req) => {
+ const own = items.filter(({ creator }) => creator !== member.id);
+ req.reply(own);
+ },
+ ).as('getSharedItems');
+};
+
export const mockPostItem = (items, shouldThrowError) => {
cy.intercept(
{
@@ -336,6 +351,22 @@ export const mockGetMemberBy = (members, shouldThrowError) => {
).as('getMemberBy');
};
+export const mockEditMember = (members, shouldThrowError) => {
+ cy.intercept(
+ {
+ method: DEFAULT_PATCH.method,
+ url: new RegExp(`${API_HOST}/${buildPatchMember(ID_FORMAT)}`),
+ },
+ ({ reply }) => {
+ if (shouldThrowError) {
+ return reply({ statusCode: StatusCodes.BAD_REQUEST });
+ }
+
+ return reply('edit member');
+ },
+ ).as('editMember');
+};
+
// mock upload item for default and s3 upload methods
export const mockUploadItem = (items, shouldThrowError) => {
cy.intercept(
@@ -425,7 +456,7 @@ export const mockSignInRedirection = () => {
cy.intercept(
{
method: DEFAULT_GET.method,
- url: new RegExp(buildSignInPath()),
+ url: `${AUTHENTICATION_HOST}/${buildSignInPath()}`,
},
({ reply }) => {
reply(redirectionReply);
diff --git a/package.json b/package.json
index 9ef0112d4..205e0c4ee 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"private": true,
"dependencies": {
"@graasp/query-client": "git://github.com/graasp/graasp-query-client.git",
- "@graasp/ui": "git://github.com/graasp/graasp-ui.git",
+ "@graasp/ui": "git://github.com/graasp/graasp-ui.git#master",
"@material-ui/core": "4.11.2",
"@material-ui/icons": "4.11.2",
"@material-ui/lab": "4.0.0-alpha.57",
diff --git a/src/components/App.js b/src/components/App.js
index 2d45fa4ed..72276c7f1 100644
--- a/src/components/App.js
+++ b/src/components/App.js
@@ -6,40 +6,44 @@ import {
SHARED_ITEMS_PATH,
buildItemPath,
REDIRECT_PATH,
+ MEMBER_PROFILE_PATH,
} from '../config/paths';
import Home from './main/Home';
import ItemScreen from './main/ItemScreen';
import SharedItems from './SharedItems';
-import Main from './main/Main';
import Authorization from './common/Authorization';
import ModalProviders from './context/ModalProviders';
import ItemLoginAuthorization from './common/ItemLoginAuthorization';
import Redirect from './main/Redirect';
+import MemberProfileScreen from './member/MemberProfileScreen';
const App = () => (
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
);
diff --git a/src/components/SharedItems.js b/src/components/SharedItems.js
index ce964b3c6..980d0ca0e 100644
--- a/src/components/SharedItems.js
+++ b/src/components/SharedItems.js
@@ -10,6 +10,7 @@ import ErrorAlert from './common/ErrorAlert';
import Items from './main/Items';
import { hooks } from '../config/queryClient';
import Loader from './common/Loader';
+import Main from './main/Main';
const SharedItems = () => {
const { t } = useTranslation();
@@ -24,14 +25,14 @@ const SharedItems = () => {
}
return (
- <>
+
- >
+
);
};
diff --git a/src/components/common/Authorization.js b/src/components/common/Authorization.js
index 895023bcc..106ad1841 100644
--- a/src/components/common/Authorization.js
+++ b/src/components/common/Authorization.js
@@ -4,9 +4,11 @@ import { API_ROUTES } from '@graasp/query-client';
import {
AUTHENTICATION_HOST,
REDIRECT_URL_LOCAL_STORAGE_KEY,
+ NODE_ENV,
} from '../../config/constants';
import { hooks } from '../../config/queryClient';
import Loader from './Loader';
+import RedirectPage from './RedirectionContent';
const Authorization = () => (ChildComponent) => {
const ComposedComponent = (props) => {
@@ -29,14 +31,26 @@ const Authorization = () => (ChildComponent) => {
}
// check authorization
- if (isError || !currentMember) {
- // save current url for later redirection after sign in
- localStorage.setItem(REDIRECT_URL_LOCAL_STORAGE_KEY, pathname);
- redirectToSignIn();
+ if (currentMember && !isError) {
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return ;
}
- // eslint-disable-next-line react/jsx-props-no-spreading
- return ;
+ // save current url for later redirection after sign in
+ localStorage.setItem(REDIRECT_URL_LOCAL_STORAGE_KEY, pathname);
+
+ // do not redirect in test environment to fully load a page
+ // eslint-disable-next-line no-unused-expressions
+ NODE_ENV !== 'test' && redirectToSignIn();
+
+ // redirect page if redirection is not working
+ return (
+
+ );
};
return ComposedComponent;
};
diff --git a/src/components/common/RedirectionContent.js b/src/components/common/RedirectionContent.js
new file mode 100644
index 000000000..f16978fde
--- /dev/null
+++ b/src/components/common/RedirectionContent.js
@@ -0,0 +1,23 @@
+import React from 'react';
+import { Typography } from '@material-ui/core';
+import PropTypes from 'prop-types';
+import { Link } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+import { REDIRECTION_CONTENT_ID } from '../../config/selectors';
+
+const RedirectionContent = ({ link }) => {
+ const { t } = useTranslation();
+ return (
+
+
+ {t('Click here if you are not automatically redirected')}
+
+
+ );
+};
+
+RedirectionContent.propTypes = {
+ link: PropTypes.string.isRequired,
+};
+
+export default RedirectionContent;
diff --git a/src/components/common/SettingsHeader.js b/src/components/common/SettingsHeader.js
index 589571252..46d7a9c1c 100644
--- a/src/components/common/SettingsHeader.js
+++ b/src/components/common/SettingsHeader.js
@@ -5,6 +5,7 @@ import Avatar from '@material-ui/core/Avatar';
import Menu from '@material-ui/core/Menu';
import Tooltip from '@material-ui/core/Tooltip';
import Box from '@material-ui/core/Box';
+import { useHistory } from 'react-router';
import { useTranslation } from 'react-i18next';
import truncate from 'lodash.truncate';
import { MUTATION_KEYS, API_ROUTES } from '@graasp/query-client';
@@ -19,6 +20,7 @@ import {
USER_MENU_SIGN_OUT_OPTION_ID,
} from '../../config/selectors';
import Loader from './Loader';
+import { MEMBER_PROFILE_PATH } from '../../config/paths';
const useStyles = makeStyles((theme) => ({
wrapper: {
@@ -34,9 +36,10 @@ const useStyles = makeStyles((theme) => ({
},
}));
-function SettingsHeader() {
+const SettingsHeader = () => {
const { data: user, isLoading } = hooks.useCurrentMember();
const classes = useStyles();
+ const { push } = useHistory();
const { t } = useTranslation();
const [anchorEl, setAnchorEl] = useState(null);
const { mutate: signOut } = useMutation(MUTATION_KEYS.SIGN_OUT);
@@ -54,6 +57,10 @@ function SettingsHeader() {
handleClose();
};
+ const goToProfile = () => {
+ push(MEMBER_PROFILE_PATH);
+ };
+
const renderMenu = () => {
if (!user || user.isEmpty()) {
return (
@@ -67,9 +74,12 @@ function SettingsHeader() {
}
return (
-
+ <>
+
+
+ >
);
};
@@ -107,6 +117,6 @@ function SettingsHeader() {
>
);
-}
+};
export default SettingsHeader;
diff --git a/src/components/item/form/FolderForm.js b/src/components/item/form/FolderForm.js
index 64499d973..848958b5a 100644
--- a/src/components/item/form/FolderForm.js
+++ b/src/components/item/form/FolderForm.js
@@ -39,11 +39,6 @@ FolderForm.propTypes = {
}),
}),
onChange: PropTypes.func.isRequired,
- classes: PropTypes.shape({
- shortInputField: PropTypes.string.isRequired,
- dialogContent: PropTypes.string.isRequired,
- addedMargin: PropTypes.string.isRequired,
- }).isRequired,
item: PropTypes.shape({
name: PropTypes.string,
description: PropTypes.string,
diff --git a/src/components/main/Home.js b/src/components/main/Home.js
index a718957f7..8f1a6e9ec 100644
--- a/src/components/main/Home.js
+++ b/src/components/main/Home.js
@@ -10,6 +10,7 @@ import FileUploader from './FileUploader';
import { HOME_ERROR_ALERT_ID, OWNED_ITEMS_ID } from '../../config/selectors';
import Loader from '../common/Loader';
import ErrorAlert from '../common/ErrorAlert';
+import Main from './Main';
const Home = () => {
const { t } = useTranslation();
@@ -25,11 +26,11 @@ const Home = () => {
}
return (
- <>
+
- >
+
);
};
diff --git a/src/components/main/ItemScreen.js b/src/components/main/ItemScreen.js
index 253c2cf31..dde5b3041 100644
--- a/src/components/main/ItemScreen.js
+++ b/src/components/main/ItemScreen.js
@@ -25,6 +25,7 @@ import Loader from '../common/Loader';
import ErrorAlert from '../common/ErrorAlert';
import { API_HOST } from '../../config/constants';
import { ItemLayoutModeContext } from '../context/ItemLayoutModeContext';
+import Main from './Main';
const { useChildren, useItem, useFileContent, useS3FileContent } = hooks;
@@ -144,7 +145,11 @@ const ItemScreen = () => {
return ;
}
- return {renderContent()};
+ return (
+
+ {renderContent()}
+
+ );
};
export default ItemScreen;
diff --git a/src/components/main/Main.js b/src/components/main/Main.js
index 3cac10e03..c48b02dc3 100644
--- a/src/components/main/Main.js
+++ b/src/components/main/Main.js
@@ -1,12 +1,19 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import Drawer from '@material-ui/core/Drawer';
import clsx from 'clsx';
+import { Loader } from '@graasp/ui';
import { makeStyles } from '@material-ui/core/styles';
+import { useTranslation } from 'react-i18next';
import { CssBaseline } from '@material-ui/core';
import PropTypes from 'prop-types';
-import { HEADER_HEIGHT, LEFT_MENU_WIDTH } from '../../config/constants';
+import {
+ DEFAULT_LANG,
+ HEADER_HEIGHT,
+ LEFT_MENU_WIDTH,
+} from '../../config/constants';
import MainMenu from './MainMenu';
import Header from '../layout/Header';
+import { hooks } from '../../config/queryClient';
const useStyles = makeStyles((theme) => ({
root: {
@@ -55,9 +62,21 @@ const useStyles = makeStyles((theme) => ({
}));
const Main = ({ children }) => {
+ const { i18n } = useTranslation();
const classes = useStyles();
const [open, setOpen] = React.useState(false);
+ const { data: member, isLoading } = hooks.useCurrentMember();
+
+ useEffect(() => {
+ i18n.changeLanguage(member?.get('extra')?.lang || DEFAULT_LANG);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [member?.get('extra')?.lang]);
+
+ if (isLoading) {
+ return ;
+ }
+
const toggleDrawer = (isOpen) => {
setOpen(isOpen);
};
diff --git a/src/components/main/Redirect.js b/src/components/main/Redirect.js
index 118f3b749..3b2c61e58 100644
--- a/src/components/main/Redirect.js
+++ b/src/components/main/Redirect.js
@@ -1,29 +1,18 @@
import React from 'react';
-import { Typography } from '@material-ui/core';
-import { Link } from 'react-router-dom';
import { useHistory } from 'react-router';
-import { useTranslation } from 'react-i18next';
import { HOME_PATH } from '../../config/paths';
import { REDIRECT_URL_LOCAL_STORAGE_KEY } from '../../config/constants';
+import RedirectionContent from '../common/RedirectionContent';
const Redirect = () => {
const { push } = useHistory();
- const { t } = useTranslation();
const nextPath =
localStorage.getItem(REDIRECT_URL_LOCAL_STORAGE_KEY) ?? HOME_PATH;
push(nextPath);
- return (
-
-
-
- {t('Click here if you are not automatically redirected')}
-
-
-
- );
+ return ;
};
export default Redirect;
diff --git a/src/components/member/LanguageSwitch.js b/src/components/member/LanguageSwitch.js
new file mode 100644
index 000000000..1dfc9319d
--- /dev/null
+++ b/src/components/member/LanguageSwitch.js
@@ -0,0 +1,41 @@
+import React from 'react';
+import { MUTATION_KEYS } from '@graasp/query-client';
+import PropTypes from 'prop-types';
+import { FormControl, Select } from '@material-ui/core';
+import { langs } from '../../config/i18n';
+import { useMutation } from '../../config/queryClient';
+
+const LanguageSwitch = ({ id, memberId, lang }) => {
+ const { mutate: editMember } = useMutation(MUTATION_KEYS.EDIT_MEMBER);
+
+ const handleChange = (event) => {
+ editMember({
+ id: memberId,
+ extra: {
+ lang: event.target.value,
+ },
+ });
+ };
+
+ return (
+
+
+
+ );
+};
+
+LanguageSwitch.propTypes = {
+ id: PropTypes.string,
+ memberId: PropTypes.string.isRequired,
+ lang: PropTypes.string.isRequired,
+};
+
+LanguageSwitch.defaultProps = {
+ id: null,
+};
+
+export default LanguageSwitch;
diff --git a/src/components/member/MemberProfileScreen.js b/src/components/member/MemberProfileScreen.js
new file mode 100644
index 000000000..9a983faf5
--- /dev/null
+++ b/src/components/member/MemberProfileScreen.js
@@ -0,0 +1,142 @@
+import React from 'react';
+import { Grid, IconButton, makeStyles, Typography } from '@material-ui/core';
+import { Loader } from '@graasp/ui';
+import FileCopyIcon from '@material-ui/icons/FileCopy';
+import Card from '@material-ui/core/Card';
+import { useTranslation } from 'react-i18next';
+import { ReactComponent as GraaspLogo } from '../../resources/graasp-logo.svg';
+import { hooks } from '../../config/queryClient';
+import LanguageSwitch from './LanguageSwitch';
+import { formatDate } from '../../utils/date';
+import { DEFAULT_LANG } from '../../config/constants';
+import { copyToClipboard } from '../../utils/clipboard';
+import {
+ MEMBER_PROFILE_MEMBER_ID_ID,
+ MEMBER_PROFILE_EMAIL_ID,
+ MEMBER_PROFILE_MEMBER_NAME_ID,
+ MEMBER_PROFILE_INSCRIPTION_DATE_ID,
+ MEMBER_PROFILE_LANGUAGE_SWITCH_ID,
+ MEMBER_PROFILE_MEMBER_ID_COPY_BUTTON_ID,
+} from '../../config/selectors';
+import notifier from '../../middlewares/notifier';
+import { COPY_MEMBER_ID_TO_CLIPBOARD } from '../../types/clipboard';
+import Main from '../main/Main';
+
+const useStyles = makeStyles((theme) => ({
+ profileTable: {
+ margin: theme.spacing(1, 0),
+ },
+ // todo: this will be replaced by a default image or the member avatar
+ logo: {
+ background: 'grey',
+ textAlign: 'center',
+ },
+}));
+
+const MemberProfileScreen = () => {
+ const { t } = useTranslation();
+ const classes = useStyles();
+ const { data: member, isLoading } = hooks.useCurrentMember();
+
+ if (isLoading) {
+ return ;
+ }
+
+ const copyIdToClipboard = () => {
+ copyToClipboard(member.get('id'), {
+ onSuccess: () => {
+ notifier({ type: COPY_MEMBER_ID_TO_CLIPBOARD.SUCCESS, payload: {} });
+ },
+ onError: () => {
+ notifier({ type: COPY_MEMBER_ID_TO_CLIPBOARD.FAILURE, payload: {} });
+ },
+ });
+ };
+
+ return (
+
+
+
+ {/* use the member avatar */}
+
+
+
+
+
+
+ {member.get('name')}
+
+
+ {/* todo: display only as light user */}
+
+
+ {t('Member ID')}
+
+
+
+ {member.get('id')}
+
+
+
+
+
+
+
+
+ {t('Email')}
+
+
+
+ {member.get('email')}
+
+
+
+
+
+ {t('Member Since')}
+
+
+
+ {formatDate(member.get('createdAt'))}
+
+
+
+
+
+ {t('Language')}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default MemberProfileScreen;
diff --git a/src/config/constants.js b/src/config/constants.js
index b66ad953a..09076ebb1 100644
--- a/src/config/constants.js
+++ b/src/config/constants.js
@@ -60,6 +60,7 @@ export const MIME_TYPES = {
};
export const DRAWER_WIDTH = 300;
export const DEFAULT_LOCALE = 'en-US';
+export const DEFAULT_LANG = 'en';
export const DEFAULT_PERMISSION_LEVEL = PERMISSION_LEVELS.WRITE;
diff --git a/src/config/messages.js b/src/config/messages.js
index 9a44e97d2..7a984e016 100644
--- a/src/config/messages.js
+++ b/src/config/messages.js
@@ -38,3 +38,11 @@ export const POST_ITEM_TAG_ERROR_MESSAGE =
'There was an error while posting the tag.';
export const ITEM_LOGIN_SIGN_IN_ERROR_MESSAGE =
'There was an error while signing in.';
+export const EDIT_MEMBER_ERROR_MESSAGE =
+ 'There was an error updating the member';
+export const EDIT_MEMBER_SUCCESS_MESSAGE =
+ 'The member was updated successfully';
+export const COPY_MEMBER_ID_TO_CLIPBOARD_SUCCESS_MESSAGE =
+ 'Member ID is successfully copied!';
+export const COPY_MEMBER_ID_TO_CLIPBOARD_ERROR_MESSAGE =
+ 'An error occured while copying the member ID';
diff --git a/src/config/paths.js b/src/config/paths.js
index e7c58ef80..e32497b4f 100644
--- a/src/config/paths.js
+++ b/src/config/paths.js
@@ -4,3 +4,4 @@ export const SIGN_UP_PATH = '/signUp';
export const ITEMS_PATH = '/items';
export const buildItemPath = (id = ':itemId') => `${ITEMS_PATH}/${id}`;
export const REDIRECT_PATH = '/redirect';
+export const MEMBER_PROFILE_PATH = '/profile';
diff --git a/src/config/selectors.js b/src/config/selectors.js
index 1ae3c893b..4b28b22c3 100644
--- a/src/config/selectors.js
+++ b/src/config/selectors.js
@@ -82,3 +82,12 @@ export const ITEM_FORM_APP_URL_ID = 'itemFormAppUrl';
export const VIEW_ITEM_EDIT_ITEM_BUTTON_ID = 'viewItemEditItemButton';
export const TEXT_EDITOR_CLASS = 'ql-editor';
export const buildSaveButtonId = (id) => `saveButton-${id}`;
+export const MEMBER_PROFILE_MEMBER_ID_ID = 'memberProfileMemberId';
+export const MEMBER_PROFILE_MEMBER_NAME_ID = 'memberProfileMemberName';
+export const MEMBER_PROFILE_EMAIL_ID = 'memberProfileEmail';
+export const MEMBER_PROFILE_INSCRIPTION_DATE_ID =
+ 'memberProfileInscriptionDate';
+export const MEMBER_PROFILE_LANGUAGE_SWITCH_ID = 'memberProfileLanguageSwitch';
+export const MEMBER_PROFILE_MEMBER_ID_COPY_BUTTON_ID =
+ 'memberProfileMemberIdCopyButton';
+export const REDIRECTION_CONTENT_ID = 'redirectionContent';
diff --git a/src/langs/en.json b/src/langs/en.json
index 1d00b4eb4..0f6395cca 100644
--- a/src/langs/en.json
+++ b/src/langs/en.json
@@ -77,6 +77,11 @@
"Create Shortcut": "Create Shortcut",
"Sign Out": "Sign Out",
"Confirm deleting item.": "Confirm deleting item",
- "This item will be deleted permanently.": "This item will be deleted permanently."
+ "This item will be deleted permanently.": "This item will be deleted permanently.",
+ "Language": "Language",
+ "Storage Used": "Storage Used",
+ "Member Since": "Member Since",
+ "Profile": "Profile",
+ "Member ID": "Member ID"
}
}
diff --git a/src/langs/fr.json b/src/langs/fr.json
index da3bec4a9..8fcc16fb9 100644
--- a/src/langs/fr.json
+++ b/src/langs/fr.json
@@ -77,6 +77,11 @@
"Create Shortcut": "Créer un Raccourci",
"Sign Out": "Déconnexion",
"Confirm deleting item.": "Confirmer la suppression de l'élément",
- "This item will be deleted permanently.": "Cet élément sera supprimé définitivement."
+ "This item will be deleted permanently.": "Cet élément sera supprimé définitivement.",
+ "Language": "Langue",
+ "Storage Used": "Mémoire utilisée",
+ "Member Since": "Membre depuis",
+ "Profile": "Profil",
+ "Member ID": "ID de Membre"
}
}
diff --git a/src/middlewares/notifier.js b/src/middlewares/notifier.js
index dc85cd5e0..8b17dbdf1 100644
--- a/src/middlewares/notifier.js
+++ b/src/middlewares/notifier.js
@@ -25,7 +25,12 @@ import {
POST_ITEM_TAG_ERROR_MESSAGE,
DELETE_ITEM_TAG_ERROR_MESSAGE,
ITEM_LOGIN_SIGN_IN_ERROR_MESSAGE,
+ EDIT_MEMBER_ERROR_MESSAGE,
+ EDIT_MEMBER_SUCCESS_MESSAGE,
+ COPY_MEMBER_ID_TO_CLIPBOARD_SUCCESS_MESSAGE,
+ COPY_MEMBER_ID_TO_CLIPBOARD_ERROR_MESSAGE,
} from '../config/messages';
+import { COPY_MEMBER_ID_TO_CLIPBOARD } from '../types/clipboard';
const {
createItemRoutine,
@@ -40,12 +45,21 @@ const {
postItemTagRoutine,
deleteItemTagRoutine,
postItemLoginRoutine,
+ editMemberRoutine,
} = routines;
export default ({ type, payload }) => {
let message = null;
switch (type) {
// error messages
+ case COPY_MEMBER_ID_TO_CLIPBOARD.FAILURE: {
+ message = COPY_MEMBER_ID_TO_CLIPBOARD_ERROR_MESSAGE;
+ break;
+ }
+ case editMemberRoutine.FAILURE: {
+ message = EDIT_MEMBER_ERROR_MESSAGE;
+ break;
+ }
case createItemRoutine.FAILURE: {
message = CREATE_ITEM_ERROR_MESSAGE;
break;
@@ -92,6 +106,10 @@ export default ({ type, payload }) => {
break;
}
// success messages
+ case editMemberRoutine.SUCCESS: {
+ message = EDIT_MEMBER_SUCCESS_MESSAGE;
+ break;
+ }
case createItemRoutine.SUCCESS: {
message = CREATE_ITEM_SUCCESS_MESSAGE;
break;
@@ -125,6 +143,10 @@ export default ({ type, payload }) => {
message = SIGN_OUT_SUCCESS_MESSAGE;
break;
}
+ case COPY_MEMBER_ID_TO_CLIPBOARD.SUCCESS: {
+ message = COPY_MEMBER_ID_TO_CLIPBOARD_SUCCESS_MESSAGE;
+ break;
+ }
// progress messages
// todo: this might be handled differently
diff --git a/src/types/clipboard.js b/src/types/clipboard.js
new file mode 100644
index 000000000..3766542c1
--- /dev/null
+++ b/src/types/clipboard.js
@@ -0,0 +1,6 @@
+// todo: use create routine from utils
+// eslint-disable-next-line import/prefer-default-export
+export const COPY_MEMBER_ID_TO_CLIPBOARD = {
+ SUCCESS: 'COPY_MEMBER_ID_TO_CLIPBOARD/SUCCESS',
+ FAILURE: 'COPY_MEMBER_ID_TO_CLIPBOARD/FAILURE',
+};
diff --git a/src/utils/clipboard.js b/src/utils/clipboard.js
new file mode 100644
index 000000000..900e09dcf
--- /dev/null
+++ b/src/utils/clipboard.js
@@ -0,0 +1,15 @@
+// eslint-disable-next-line import/prefer-default-export
+export const copyToClipboard = (string, { onSuccess, onError }) => {
+ // check can write to clipboard
+ navigator.permissions.query({ name: 'clipboard-write' }).then((result) => {
+ if (result.state === 'granted' || result.state === 'prompt') {
+ // write to clipboard
+ navigator.clipboard
+ .writeText(string)
+ .then(() => onSuccess?.())
+ .catch(() => onError?.());
+ } else {
+ onError?.();
+ }
+ });
+};
diff --git a/yarn.lock b/yarn.lock
index cac18c44d..b487bc53b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1583,9 +1583,9 @@
minimatch "^3.0.4"
strip-json-comments "^3.1.1"
-"@graasp/query-client@git://github.com/graasp/graasp-query-client.git#5/patchMember":
+"@graasp/query-client@git://github.com/graasp/graasp-query-client.git":
version "0.1.0"
- resolved "git://github.com/graasp/graasp-query-client.git#5397cd3fbb27d7703c61d57a34c7c2f1083280ff"
+ resolved "git://github.com/graasp/graasp-query-client.git#05bde46fec71ed84aa6ca4ae0e5753ba78c677ae"
dependencies:
http-status-codes "2.1.4"
immutable "4.0.0-rc.12"
@@ -1594,9 +1594,9 @@
react-query "3.16.0"
uuid "8.3.2"
-"@graasp/ui@git://github.com/graasp/graasp-ui.git#14/captions":
+"@graasp/ui@git://github.com/graasp/graasp-ui.git#master":
version "0.2.0"
- resolved "git://github.com/graasp/graasp-ui.git#e8659334d6fe8a8e28fa4c3f41a467f58de79a69"
+ resolved "git://github.com/graasp/graasp-ui.git#97c65d25464dc8ba944ce35722d2bad5a16d834f"
dependencies:
clsx "1.1.1"
immutable "4.0.0-rc.12"
@@ -2313,9 +2313,9 @@
integrity sha512-fZQQafSREFyuZcdWFAExYjBiCL7AUCdgsk80iO0q4yihYYdcIiH28CcuPTGFgLOCC8RlW49GSQxdHwZP+I7CNg==
"@types/node@*":
- version "15.12.3"
- resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.3.tgz#2817bf5f25bc82f56579018c53f7d41b1830b1af"
- integrity sha512-SNt65CPCXvGNDZ3bvk1TQ0Qxoe3y1RKH88+wZ2Uf05dduBCqqFQ76ADP9pbT+Cpvj60SkRppMCh2Zo8tDixqjQ==
+ version "15.12.4"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-15.12.4.tgz#e1cf817d70a1e118e81922c4ff6683ce9d422e26"
+ integrity sha512-zrNj1+yqYF4WskCMOHwN+w9iuD12+dGm0rQ35HLl9/Ouuq52cEtd0CH9qMgrdNmi5ejC1/V7vKEXYubB+65DkA==
"@types/normalize-package-data@^2.4.0":
version "2.4.0"
@@ -4257,9 +4257,9 @@ caniuse-api@^3.0.0:
lodash.uniq "^4.5.0"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001219:
- version "1.0.30001238"
- resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001238.tgz#e6a8b45455c5de601718736d0242feef0ecdda15"
- integrity sha512-bZGam2MxEt7YNsa2VwshqWQMwrYs5tR5WZQRYSuFxsBQunWjBuXhN4cS9nV5FFb1Z9y+DoQcQ0COyQbv6A+CKw==
+ version "1.0.30001239"
+ resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001239.tgz#66e8669985bb2cb84ccb10f68c25ce6dd3e4d2b8"
+ integrity sha512-cyBkXJDMeI4wthy8xJ2FvDU6+0dtcZSJW3voUF8+e9f1bBeuvyZfc3PNbkOETyhbR+dGCPzn9E7MA3iwzusOhQ==
capture-exit@^2.0.0:
version "2.0.0"
@@ -4963,7 +4963,7 @@ conventional-recommended-bump@6.0.11:
meow "^8.0.0"
q "^1.5.1"
-convert-source-map@1.7.0, convert-source-map@^1.1.0, convert-source-map@^1.3.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
+convert-source-map@1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.7.0.tgz#17a2cb882d7f77d3490585e2ce6c524424a3a442"
integrity sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==
@@ -4975,6 +4975,13 @@ convert-source-map@^0.3.3:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190"
integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA=
+convert-source-map@^1.1.0, convert-source-map@^1.3.0, convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0:
+ version "1.8.0"
+ resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.8.0.tgz#f3373c32d21b4d780dd8004514684fb791ca4369"
+ integrity sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==
+ dependencies:
+ safe-buffer "~5.1.1"
+
convert-source-map@~1.1.0:
version "1.1.3"
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.1.3.tgz#4829c877e9fe49b3161f3bf3673888e204699860"
@@ -5008,17 +5015,17 @@ copy-descriptor@^0.1.0:
integrity sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=
core-js-compat@^3.1.1, core-js-compat@^3.14.0, core-js-compat@^3.6.2:
- version "3.14.0"
- resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.14.0.tgz#b574dabf29184681d5b16357bd33d104df3d29a5"
- integrity sha512-R4NS2eupxtiJU+VwgkF9WTpnSfZW4pogwKHd8bclWU2sp93Pr5S1uYJI84cMOubJRou7bcfL0vmwtLslWN5p3A==
+ version "3.15.0"
+ resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.15.0.tgz#e14a371123db9d1c5b41206d3f420643d238b8fa"
+ integrity sha512-8X6lWsG+s7IfOKzV93a7fRYfWRZobOfjw5V5rrq43Vh/W+V6qYxl7Akalsvgab4PFT/4L/pjQbdBUEM36NXKrw==
dependencies:
browserslist "^4.16.6"
semver "7.0.0"
core-js-pure@^3.14.0:
- version "3.14.0"
- resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.14.0.tgz#72bcfacba74a65ffce04bf94ae91d966e80ee553"
- integrity sha512-YVh+LN2FgNU0odThzm61BsdkwrbrchumFq3oztnE9vTKC4KS2fvnPmcx8t6jnqAyOTCTF4ZSiuK8Qhh7SNcL4g==
+ version "3.15.0"
+ resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.15.0.tgz#c19349ae0be197b8bcf304acf4d91c5e29ae2091"
+ integrity sha512-RO+LFAso8DB6OeBX9BAcEGvyth36QtxYon1OyVsITNVtSKr/Hos0BXZwnsOJ7o+O6KHtK+O+cJIEj9NGg6VwFA==
core-js@^2.4.0:
version "2.6.12"
@@ -5026,9 +5033,9 @@ core-js@^2.4.0:
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.6.1, core-js@^3.6.5:
- version "3.14.0"
- resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.14.0.tgz#62322b98c71cc2018b027971a69419e2425c2a6c"
- integrity sha512-3s+ed8er9ahK+zJpp9ZtuVcDoFzHNiZsPbNAAE4KXgrRHbjSqqNN6xGSXq6bq7TZIbKj4NLrLb6bJ5i+vSVjHA==
+ version "3.15.0"
+ resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.15.0.tgz#db9554ebce0b6fd90dc9b1f2465c841d2d055044"
+ integrity sha512-GUbtPllXMYRzIgHNZ4dTYTcUemls2cni83Q4Q/TrFONHfhcg9oEGOtaGHfb0cpzec60P96UKPvMkjX1jET8rUw==
core-util-is@1.0.2, core-util-is@~1.0.0:
version "1.0.2"
@@ -6409,9 +6416,9 @@ eslint-webpack-plugin@^2.1.0:
schema-utils "^3.0.0"
eslint@^7.11.0:
- version "7.28.0"
- resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.28.0.tgz#435aa17a0b82c13bb2be9d51408b617e49c1e820"
- integrity sha512-UMfH0VSjP0G4p3EWirscJEQ/cHqnT/iuH6oNZOB94nBjWbMnhGEPxsZm1eyIW0C/9jLI0Fow4W5DXLjEI7mn1g==
+ version "7.29.0"
+ resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.29.0.tgz#ee2a7648f2e729485e4d0bd6383ec1deabc8b3c0"
+ integrity sha512-82G/JToB9qIy/ArBzIWG9xvvwL3R86AlCjtGw+A29OMZDqhTybz/MByORSukGxeI+YPCR4coYyITKk8BFH9nDA==
dependencies:
"@babel/code-frame" "7.12.11"
"@eslint/eslintrc" "^0.4.2"
@@ -15365,9 +15372,9 @@ yargs-parser@^18.1.2:
decamelize "^1.2.0"
yargs-parser@^20.2.2, yargs-parser@^20.2.3:
- version "20.2.7"
- resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.7.tgz#61df85c113edfb5a7a4e36eb8aa60ef423cbc90a"
- integrity sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==
+ version "20.2.9"
+ resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
+ integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==
yargs@^13.3.2:
version "13.3.2"