Compare commits

..

9 Commits

Author SHA1 Message Date
Jason Wesson
d598cf1e4b fix: replace injected Plugins directory with dependency 2023-11-08 20:12:46 +00:00
Jason Wesson
d5949f55c2 Merge master into PluggablePOCFeature (#908)
---------

Co-authored-by: jsnwesson <jsnwesson@users.noreply.github.com>
2023-11-07 20:03:44 -08:00
Jason Wesson
cd28310937 fix: hyperlink in plugin page should redirect to user profile (#897)
* add user's full name to plugn page

Co-authored-by: Jason Wesson <jwesson@2u.com>
2023-10-24 13:04:55 -07:00
Jason Wesson
b75347ad06 Merge master into PluginPOCFeature (#896) 2023-10-24 09:51:12 -07:00
Chris Deery
bd931338d8 Merge pull request #885 from openedx/PluginPOC/addTag
feat: add little tag to identify POC
2023-10-19 12:08:44 -04:00
Chris Deery
f03d5afa0d fix: update snapshot 2023-10-19 15:58:39 +00:00
Chris Deery
910e17f75d feat: add little tag to identify POC 2023-10-19 15:34:04 +00:00
Jason Wesson
2fa5cadf22 fix: rebase pluginPOCFeature with master branch (#884)
* fix(deps): update dependency @edx/paragon to v20.46.3

* chore(deps): update commitlint monorepo to v17.8.0

* fix(deps): update dependency @edx/frontend-component-footer to v12.4.0

* chore: bump frontend-platform (#869)

* fix: Add ID attribute to the main content (#845)

* chore: update browserslist DB (#871)

* fix(deps): update dependency @edx/frontend-platform to v5.6.1 (#875)

* build(deps): bump @babel/traverse from 7.22.5 to 7.23.2 (#879)

* bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.22.5 to 7.23.2.

* fix(deps): update dependency core-js to v3.33.0 (#876)
2023-10-18 11:58:21 -07:00
Jason Wesson
bd8221997e Move changes from ProfilePluginPOC to aperture/PluginPOCFeature (#883)
* build: create profile plugin page
* build: add plugins folder to Profile
* build: wrap Profile Plugin Page with Plugin

Co-authored-by: Jason Wesson <jwesson@2u.com>
2023-10-18 11:43:43 -07:00
159 changed files with 19908 additions and 28264 deletions

4
.env
View File

@@ -10,8 +10,6 @@ LOGIN_URL=''
LOGOUT_URL=''
MARKETING_SITE_BASE_URL=''
ORDER_HISTORY_URL=''
ACCOUNT_SETTINGS_URL=''
ACCOUNT_PROFILE_URL=''
REFRESH_ACCESS_TOKEN_ENDPOINT=''
SEGMENT_KEY=''
SITE_NAME=''
@@ -29,5 +27,3 @@ APP_ID=''
MFE_CONFIG_API_URL=''
SEARCH_CATALOG_URL=''
ENABLE_SKILLS_BUILDER_PROFILE=''
ENABLE_NEW_PROFILE_VIEW=''
DISABLE_VISIBILITY_EDITING=''

View File

@@ -3,9 +3,7 @@ PORT=1995
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
BASE_URL='localhost:1995'
CREDENTIALS_BASE_URL='http://localhost:18150'
ACCOUNT_SETTINGS_URL=http://localhost:1997
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ACCOUNT_PROFILE_URL=http://localhost:1995
ECOMMERCE_BASE_URL='http://localhost:18130'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
@@ -30,5 +28,3 @@ APP_ID=''
MFE_CONFIG_API_URL=''
SEARCH_CATALOG_URL='http://localhost:18000/courses'
ENABLE_SKILLS_BUILDER_PROFILE=''
ENABLE_NEW_PROFILE_VIEW=''
DISABLE_VISIBILITY_EDITING=''

View File

@@ -5,8 +5,6 @@ CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
ACCOUNT_SETTINGS_URL='http://localhost:1997'
ACCOUNT_PROFILE_URL='http://localhost:1995'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
MARKETING_SITE_BASE_URL='http://localhost:18000'
@@ -25,5 +23,3 @@ LEARNER_RECORD_MFE_BASE_URL='http://localhost:1990'
COLLECT_YEAR_OF_BIRTH=true
APP_ID=''
MFE_CONFIG_API_URL=''
ENABLE_NEW_PROFILE_VIEW=''
DISABLE_VISIBILITY_EDITING=''

View File

@@ -1,4 +1,4 @@
// eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('eslint');

1
.github/CODEOWNERS vendored
View File

@@ -1 +0,0 @@
* @openedx/2U-infinity

View File

@@ -1,7 +0,0 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -1,24 +0,0 @@
### Description
Include a description of your changes here, along with a link to any relevant Jira tickets and/or GitHub issues.
#### How Has This Been Tested?
Please describe in detail how you tested your changes.
#### Screenshots/sandbox (optional):
Include a link to the sandbox for design changes or screenshot for before and after. **Remove this section if it's not applicable.**
|Before|After|
|-------|-----|
| | |
#### Merge Checklist
* [ ] If your update includes visual changes, have they been reviewed by a designer? Send them a link to the Sandbox, if applicable.
* [ ] Is there adequate test coverage for your changes?
#### Post-merge Checklist
* [ ] Deploy the changes to prod after verifying on stage or ask **@openedx/2u-infinity** to do it.
* [ ] 🎉 🙌 Celebrate! Thanks for your contribution.

View File

@@ -14,16 +14,16 @@ jobs:
- lint
- test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: actions/checkout@v3
- name: Setup Nodejs Env
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- uses: actions/setup-node@v3
with:
node-version-file: '.nvmrc'
node-version: ${{ env.NODE_VER }}
- run: make requirements
- run: make test NPM_TESTS=build
- run: make test NPM_TESTS=${{ matrix.npm-test }}
- name: Coverage
if: ${{ matrix.npm-test == 'test' }}
uses: codecov/codecov-action@v4
- name: upload coverage
uses: codecov/codecov-action@v3
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
fail_ci_if_error: false

View File

@@ -10,4 +10,4 @@ on:
jobs:
version-check:
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
uses: openedx/.github/.github/workflows/lockfile-check.yml@master

1
.gitignore vendored
View File

@@ -17,4 +17,3 @@ temp/babel-plugin-react-intl
/temp
/.vscode
/module.config.js
src/i18n/messages

2
.nvmrc
View File

@@ -1 +1 @@
20
18

9
.tx/config Normal file
View File

@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-profile]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -1,3 +1,7 @@
export TRANSIFEX_RESOURCE = frontend-app-profile
transifex_resource = frontend-app-profile
transifex_langs = "ar,de,de_DE,es_419,fa_IR,fr,fr_CA,hi,it,it_IT,pt,pt_PT,ru,uk,zh_CN"
intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n
@@ -35,18 +39,35 @@ detect_changed_source_translations:
# Checking for changed translations...
git diff --exit-code $(i18n)
# Pushes translations to Transifex. You must run make extract_translations first.
push_translations:
# Pushing strings to Transifex...
tx push -s
# Fetching hashes from Transifex...
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
# Writing out comments to file...
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
# Pushing comments to Transifex...
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
ifeq ($(OPENEDX_ATLAS_PULL),)
# Pulls translations from Transifex.
pull_translations:
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations:
rm -rf src/i18n/messages
mkdir src/i18n/messages
cd src/i18n/messages \
&& atlas pull $(ATLAS_OPTIONS) \
translations/frontend-platform/src/i18n/messages:frontend-platform \
&& atlas pull --filter=$(transifex_langs) \
translations/paragon/src/i18n/messages:paragon \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-app-profile/src/i18n/messages:frontend-app-profile
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-app-profile
$(intl_imports) paragon frontend-component-header frontend-component-footer frontend-app-profile
endif
# This target is used by Travis.
validate-no-uncommitted-package-lock-changes:

View File

@@ -71,12 +71,6 @@ Profile MFE for local development via the `devstack`_.
Once the dev server is up, visit http://localhost:1995/u/staff.
Plugins
=======
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
Configuration
=============
@@ -98,7 +92,7 @@ frontend repository, the best place to discuss it would be in the `#wg-frontend
channel`_.
For anything non-trivial, the best path is to open an issue in this repository
with as many details about the issue you are facing as you can provide. Please tag **@openedx/2u-infinity** on any PRs or issues.
with as many details about the issue you are facing as you can provide. Please tag **@openedx/2u-aperture** on any PRs or issues.
https://github.com/openedx/frontend-app-profile/issues

View File

@@ -3,8 +3,8 @@
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: 'frontend-app-profile'
description: 'This is a micro-frontend application responsible for displaying and updating the user profiles.'
name: 'Profile'
description: 'This is a micro-frontend application responsible for the display and updating of user profiles.'
links:
- url: 'https://github.com/openedx/frontend-app-profile/blob/master/README.rst'
title: 'Documentation'
@@ -17,9 +17,8 @@ metadata:
openedx.org/arch-interest-groups: ""
# This can be multiple comma-separated projects.
openedx.org/add-to-projects: "openedx:23"
openedx.org/release: "master"
spec:
owner: group:2u-infinity
type: 'service'
lifecycle: 'production'
# (Optional) An array of different components or resources.
owner: 2U-aperture
# (Optional) An array of different components or resources.

View File

@@ -1,7 +1,7 @@
const { createConfig } = require('@openedx/frontend-build');
const { createConfig } = require('@edx/frontend-build');
module.exports = createConfig('jest', {
setupFilesAfterEnv: [
setupFiles: [
'<rootDir>/src/setupTest.js',
],
});

6
openedx.yaml Normal file
View File

@@ -0,0 +1,6 @@
# This file describes this Open edX repo, as described in OEP-2:
# https://open-edx-proposals.readthedocs.io/en/latest/oep-0002-bp-repo-metadata.html#specification
nick: prof
oeps: {}
openedx-release: {ref: master}

16012
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -29,20 +29,19 @@
],
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-header": "^5.6.0",
"@edx/frontend-platform": "8.3.1",
"@edx/openedx-atlas": "^0.6.0",
"@fortawesome/fontawesome-svg-core": "6.7.2",
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-regular-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.2.2",
"@edx/frontend-component-footer": "12.5.1",
"@edx/frontend-component-header": "4.8.0",
"@edx/frontend-platform": "5.6.1",
"@edx/frontend-plugin-framework": "openedx/frontend-plugin-framework#jwesson/install-plugins",
"@edx/paragon": "^20.44.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "0.2.0",
"@pact-foundation/pact": "^11.0.2",
"@redux-devtools/extension": "3.3.0",
"classnames": "2.5.1",
"core-js": "3.40.0",
"classnames": "2.3.2",
"core-js": "3.33.1",
"history": "5.3.0",
"lodash.camelcase": "4.3.0",
"lodash.get": "4.4.2",
@@ -51,28 +50,33 @@
"prop-types": "15.8.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react-error-boundary": "^4.0.11",
"react-helmet": "6.1.0",
"react-redux": "7.2.9",
"react-router": "6.29.0",
"react-router-dom": "6.29.0",
"react-router": "6.16.0",
"react-router-dom": "6.16.0",
"redux": "4.2.1",
"redux-devtools-extension": "2.13.9",
"redux-logger": "3.0.6",
"redux-saga": "1.3.0",
"redux-saga": "1.2.3",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "5.1.1",
"regenerator-runtime": "0.14.0",
"reselect": "4.1.8",
"universal-cookie": "4.0.4"
},
"devDependencies": {
"@commitlint/cli": "19.8.0",
"@commitlint/config-angular": "19.8.0",
"@commitlint/cli": "17.8.1",
"@commitlint/config-angular": "17.8.1",
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "13.0.4",
"@edx/reactifex": "2.2.0",
"@openedx/frontend-build": "14.3.2",
"@testing-library/jest-dom": "6.6.3",
"@testing-library/react": "12.1.5",
"glob": "11.0.1",
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
"codecov": "3.8.3",
"enzyme": "3.11.0",
"glob": "10.3.10",
"react-test-renderer": "17.0.2",
"reactifex": "1.1.1",
"redux-mock-store": "1.5.5"
"redux-mock-store": "1.5.4"
}
}

93
plugins/Plugin.jsx Normal file
View File

@@ -0,0 +1,93 @@
'use client';
import React, {
useEffect, useMemo, useState,
} from 'react';
import PropTypes from 'prop-types';
import { ErrorBoundary } from 'react-error-boundary';
import { logError } from '@edx/frontend-platform/logging';
import {
dispatchMountedEvent, dispatchReadyEvent, dispatchUnmountedEvent, useHostEvent,
} from './data/hooks';
import { PLUGIN_RESIZE } from './data/constants';
// see example-plugin-app/src/PluginOne.jsx for example of customizing errorFallback
function errorFallbackDefault() {
return (
<div>
<h2>
Oops! An error occurred. Please refresh the screen to try again.
</h2>
</div>
);
}
// eslint-disable-next-line react/function-component-definition
export default function Plugin({
children, className, style, ready, errorFallbackProp,
}) {
const [dimensions, setDimensions] = useState({
width: null,
height: null,
});
const finalStyle = useMemo(() => ({
...dimensions,
...style,
}), [dimensions, style]);
const errorFallback = errorFallbackProp || errorFallbackDefault;
// Error logging function
// Need to confirm: When an error is caught here, the logging will be sent to the child MFE's logging service
const logErrorToService = (error, info) => {
logError(error, { stack: info.componentStack });
};
useHostEvent(PLUGIN_RESIZE, ({ payload }) => {
setDimensions({
width: payload.width,
height: payload.height,
});
});
useEffect(() => {
dispatchMountedEvent();
return () => {
dispatchUnmountedEvent();
};
}, []);
useEffect(() => {
if (ready) {
dispatchReadyEvent();
}
}, [ready]);
return (
<div className={className} style={finalStyle}>
<ErrorBoundary
FallbackComponent={errorFallback}
onError={logErrorToService}
>
{children}
</ErrorBoundary>
</div>
);
}
Plugin.propTypes = {
children: PropTypes.node.isRequired,
className: PropTypes.string,
errorFallbackProp: PropTypes.func,
ready: PropTypes.bool,
style: PropTypes.object, // eslint-disable-line
};
Plugin.defaultProps = {
className: null,
errorFallbackProp: null,
style: {},
ready: true,
};

View File

@@ -0,0 +1,42 @@
'use client';
import React from 'react';
// eslint-disable-next-line import/no-extraneous-dependencies
import PluginContainerIframe from './PluginContainerIframe';
import {
IFRAME_PLUGIN,
} from './data/constants';
import { pluginConfigShape } from './data/shapes';
// eslint-disable-next-line react/function-component-definition
export default function PluginContainer({ config, ...props }) {
if (config === null) {
return null;
}
// this will allow for future plugin types to be inserted in the PluginErrorBoundary
let renderer = null;
switch (config.type) {
case IFRAME_PLUGIN:
renderer = (
<PluginContainerIframe config={config} {...props} />
);
break;
// istanbul ignore next: default isn't meaningful, just satisfying linter
default:
}
return (
renderer
);
}
PluginContainer.propTypes = {
config: pluginConfigShape,
};
PluginContainer.defaultProps = {
config: null,
};

View File

@@ -0,0 +1,99 @@
import React, {
useEffect, useState,
} from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import {
PLUGIN_MOUNTED,
PLUGIN_READY,
PLUGIN_RESIZE,
} from './data/constants';
import {
dispatchPluginEvent,
useElementSize,
usePluginEvent,
} from './data/hooks';
import { pluginConfigShape } from './data/shapes';
/**
* Feature policy for iframe, allowing access to certain courseware-related media.
*
* We must use the wildcard (*) origin for each feature, as courseware content
* may be embedded in external iframes. Notably, xblock-lti-consumer is a popular
* block that iframes external course content.
* This policy was selected in conference with the edX Security Working Group.
* Changes to it should be vetted by them (security@edx.org).
*/
export const IFRAME_FEATURE_POLICY = (
'fullscreen; microphone *; camera *; midi *; geolocation *; encrypted-media *'
);
// eslint-disable-next-line react/function-component-definition
export default function PluginContainerIframe({
config, fallback, className, ...props
}) {
const { url } = config;
const { title, scrolling } = props;
const [mounted, setMounted] = useState(false);
const [ready, setReady] = useState(false);
const [iframeRef, iframeElement, width, height] = useElementSize();
useEffect(() => {
if (mounted) {
dispatchPluginEvent(iframeElement, {
type: PLUGIN_RESIZE,
payload: {
width,
height,
},
}, url);
}
}, [iframeElement, mounted, width, height, url]);
usePluginEvent(iframeElement, PLUGIN_MOUNTED, () => {
setMounted(true);
});
usePluginEvent(iframeElement, PLUGIN_READY, () => {
setReady(true);
});
return (
<>
<iframe
ref={iframeRef}
title={title}
src={url}
allow={IFRAME_FEATURE_POLICY}
scrolling={scrolling}
referrerPolicy="origin" // The sent referrer will be limited to the origin of the referring page: its scheme, host, and port.
className={classNames(
'border border-0',
{ 'd-none': !ready },
className,
)}
{...props}
/>
{!ready && fallback}
</>
);
}
PluginContainerIframe.propTypes = {
config: pluginConfigShape,
fallback: PropTypes.node,
scrolling: PropTypes.oneOf(['auto', 'yes', 'no']),
title: PropTypes.string,
className: PropTypes.string,
};
PluginContainerIframe.defaultProps = {
config: null,
fallback: null,
scrolling: 'auto',
title: null,
className: null,
};

View File

@@ -0,0 +1,45 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
// eslint-disable-next-line import/no-extraneous-dependencies
import { FormattedMessage } from 'react-intl';
import { logError } from '@edx/frontend-platform/logging';
export default class PluginErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, info) {
logError(error, { stack: info.componentStack });
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return (
<FormattedMessage
id="plugin.load.failure.text"
defaultMessage="This content failed to load."
description="error message when an unexpected error occurs"
/>
);
}
return this.props.children;
}
}
PluginErrorBoundary.propTypes = {
children: PropTypes.node,
};
PluginErrorBoundary.defaultProps = {
children: null,
};

75
plugins/PluginSlot.jsx Normal file
View File

@@ -0,0 +1,75 @@
/* eslint-disable no-unused-vars */
import React, { forwardRef } from 'react';
import classNames from 'classnames';
import { Spinner } from '@edx/paragon';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
// import { usePluginSlot } from './data/hooks';
import PluginContainer from './PluginContainer';
const PluginSlot = forwardRef(({
as, id, intl, pluginProps, children, ...props
}, ref) => {
/* the plugins below are obtained by the id passed into PluginSlot by the Host MFE. See example/src/PluginsPage.jsx
for an example of how PluginSlot is populated, and example/src/index.jsx for a dummy JS config that holds all plugins
*/
// const { plugins, keepDefault } = usePluginSlot(id);
const { fallback } = pluginProps;
// TODO: Add internationalization to the "Loading" text on the spinner.
let finalFallback = (
<div className={classNames(pluginProps.className, 'd-flex justify-content-center align-items-center')}>
<Spinner animation="border" screenReaderText="Loading" />
</div>
);
if (fallback !== undefined) {
finalFallback = fallback;
}
let finalChildren = [];
// if (plugins.length > 0) {
// if (keepDefault) {
// finalChildren.push(children);
// }
// plugins.forEach((pluginConfig) => {
// finalChildren.push(
// <PluginContainer
// key={pluginConfig.url}
// config={pluginConfig}
// fallback={finalFallback}
// {...pluginProps}
// />,
// );
// });
// } else {
finalChildren = children;
// }
return React.createElement(
as,
{
...props,
ref,
},
finalChildren,
);
});
export default injectIntl(PluginSlot);
PluginSlot.propTypes = {
as: PropTypes.elementType,
children: PropTypes.node,
id: PropTypes.string.isRequired,
intl: intlShape.isRequired,
pluginProps: PropTypes.object, // eslint-disable-line
};
PluginSlot.defaultProps = {
as: 'div',
children: null,
pluginProps: {},
};

View File

@@ -0,0 +1,8 @@
// TODO: We expect other plugin types to be added here, such as LTI_PLUGIN and BUILD_TIME_PLUGIN.
export const IFRAME_PLUGIN = 'IFRAME_PLUGIN'; // loads iframe at the URL, rather than loading a JS file.
// Plugin lifecycle events
export const PLUGIN_MOUNTED = 'PLUGIN_MOUNTED';
export const PLUGIN_READY = 'PLUGIN_READY';
export const PLUGIN_UNMOUNTED = 'PLUGIN_UNMOUNTED';
export const PLUGIN_RESIZE = 'PLUGIN_RESIZE';

96
plugins/data/hooks.js Normal file
View File

@@ -0,0 +1,96 @@
import {
useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
} from 'react';
import { PLUGIN_MOUNTED, PLUGIN_READY, PLUGIN_UNMOUNTED } from './constants';
export function useMessageEvent(srcWindow, type, callback) {
useLayoutEffect(() => {
const listener = (event) => {
// Filter messages to those from our source window.
if (event.source === srcWindow) {
if (event.data.type === type) {
callback({ type, payload: event.data.payload });
}
}
};
if (srcWindow !== null) {
global.addEventListener('message', listener);
}
return () => {
global.removeEventListener('message', listener);
};
}, [srcWindow, type, callback]);
}
export function useHostEvent(type, callback) {
useMessageEvent(global.parent, type, callback);
}
export function usePluginEvent(iframeElement, type, callback) {
const contentWindow = iframeElement ? iframeElement.contentWindow : null;
useMessageEvent(contentWindow, type, callback);
}
export function dispatchMessageEvent(targetWindow, message, targetOrigin) {
// Checking targetOrigin falsiness here since '', null or undefined would all be reasons not to
// try to post a message to the origin.
if (targetOrigin) {
targetWindow.postMessage(message, targetOrigin);
}
}
export function dispatchPluginEvent(iframeElement, message, targetOrigin) {
dispatchMessageEvent(iframeElement.contentWindow, message, targetOrigin);
}
export function dispatchHostEvent(message) {
dispatchMessageEvent(global.parent, message, global.document.referrer);
}
export function dispatchReadyEvent() {
dispatchHostEvent({ type: PLUGIN_READY });
}
export function dispatchMountedEvent() {
dispatchHostEvent({ type: PLUGIN_MOUNTED });
}
export function dispatchUnmountedEvent() {
dispatchHostEvent({ type: PLUGIN_UNMOUNTED });
}
export function useElementSize() {
const observerRef = useRef();
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const [offset, setOffset] = useState({ x: 0, y: 0 });
const [element, setElement] = useState(null);
const measuredRef = useCallback(_element => {
setElement(_element);
}, []);
useEffect(() => {
observerRef.current = new ResizeObserver(() => {
if (element) {
setDimensions({
width: element.clientWidth,
height: element.clientHeight,
});
setOffset({
x: element.offsetLeft,
y: element.offsetTop,
});
}
});
if (element) {
observerRef.current.observe(element);
}
}, [element]);
return useMemo(
() => ([measuredRef, element, dimensions.width, dimensions.height, offset.x, offset.y]),
[measuredRef, element, dimensions, offset],
);
}

10
plugins/data/shapes.js Normal file
View File

@@ -0,0 +1,10 @@
/* eslint-disable import/prefer-default-export */
import PropTypes from 'prop-types';
import { IFRAME_PLUGIN } from './constants';
export const pluginConfigShape = PropTypes.shape({
url: PropTypes.string.isRequired,
type: PropTypes.oneOf([IFRAME_PLUGIN]).isRequired,
// This is a place for us to put any generic props we want to pass to the component. We need it.
props: PropTypes.object, // eslint-disable-line react/forbid-prop-types
});

18
plugins/index.js Normal file
View File

@@ -0,0 +1,18 @@
// export {
// usePluginSlot,
// } from './data/hooks';
export {
default as Plugin,
} from './Plugin';
export {
default as PluginContainer,
} from './PluginContainer';
export {
default as PluginSlot,
} from './PluginSlot';
export {
IFRAME_PLUGIN,
} from './data/constants';
export {
default as PluginErrorBoundary,
} from './PluginErrorBoundary';

View File

@@ -1,7 +1,7 @@
import { getConfig } from '@edx/frontend-platform';
import { applyMiddleware, createStore, compose } from 'redux';
import thunkMiddleware from 'redux-thunk';
import { composeWithDevTools } from '@redux-devtools/extension';
import { composeWithDevTools } from 'redux-devtools-extension';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';

View File

@@ -1,14 +1,9 @@
import { combineReducers } from 'redux';
import { getConfig } from '@edx/frontend-platform';
import { reducer as profilePageReducer } from '../profile';
import { reducer as newProfilePageReducer } from '../profile-v2';
const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW;
import { reducer as profilePage } from '../profile';
const createRootReducer = () => combineReducers({
profilePage: isNewProfileEnabled ? newProfilePageReducer : profilePageReducer,
profilePage,
});
export default createRootReducer;

View File

@@ -1,12 +1,9 @@
import { all } from 'redux-saga/effects';
import { getConfig } from '@edx/frontend-platform';
import { saga as profileSaga } from '../profile';
import { saga as newProfileSaga } from '../profile-v2';
const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW;
import { saga as profileSaga } from '../profile';
export default function* rootSaga() {
yield all([
isNewProfileEnabled ? newProfileSaga() : profileSaga(),
profileSaga(),
]);
}

View File

@@ -1,14 +1,14 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { Helmet } from 'react-helmet';
import { render } from '@testing-library/react';
import { mount } from 'enzyme';
import { getConfig } from '@edx/frontend-platform';
import Head from './Head';
describe('Head', () => {
const props = {};
it('should match render title tag and favicon with the site configuration values', () => {
render(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
mount(<IntlProvider locale="en"><Head {...props} /></IntlProvider>);
const helmet = Helmet.peek();
expect(helmet.title).toEqual(`Profile | ${getConfig().SITE_NAME}`);
expect(helmet.linkTags[0].rel).toEqual('shortcut icon');

View File

@@ -1 +1,44 @@
export default [];
import { messages as headerMessages } from '@edx/frontend-component-header';
import { messages as footerMessages } from '@edx/frontend-component-footer';
import { messages as paragonMessages } from '@edx/paragon';
import arMessages from './messages/ar.json';
import deMessages from './messages/de.json';
import dedeCAMessages from './messages/de_DE.json';
import es419Messages from './messages/es_419.json';
import faIRMessages from './messages/fa_IR.json';
import frCAMessages from './messages/fr_CA.json';
import itMessages from './messages/it.json';
import ititCAMessages from './messages/it_IT.json';
import frMessages from './messages/fr.json';
import hiMessages from './messages/hi.json';
import ptMessages from './messages/pt.json';
import ptptCAMessages from './messages/pt_PT.json';
import ruMessages from './messages/ru.json';
import ukMessages from './messages/uk.json';
import zhcnMessages from './messages/zh_CN.json';
// no need to import en messages-- they are in the defaultMessage field
const appMessages = {
ar: arMessages,
'es-419': es419Messages,
'fa-ir': faIRMessages,
fr: frMessages,
'zh-cn': zhcnMessages,
pt: ptMessages,
it: itMessages,
de: deMessages,
hi: hiMessages,
'fr-ca': frCAMessages,
ru: ruMessages,
uk: ukMessages,
'de-de': dedeCAMessages,
'it-it': ititCAMessages,
'pt-pt': ptptCAMessages,
};
export default [
headerMessages,
footerMessages,
paragonMessages,
appMessages,
];

57
src/i18n/messages/ar.json Normal file
View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "الملف الشخصي | {siteName}",
"profile.age.details": "لمشاركة ملفك الشخصي مع بقية متعلمي {siteName}، يجب أن تؤكد أنك عمرك يفوق 13 عامًا.",
"profile.age.set.date": "ضيط تاريخ ميلادك",
"profile.datejoined.member.since": "عضو منذ {year}",
"profile.bio.empty": "إضافة نبذة قصيرة",
"profile.bio.about.me": "نبذة عنّي",
"profile.certificate.organization.label": "من طرف",
"profile.certificate.completion.date.label": "صدرت بتاريخ {date}",
"profile.no.certificates": "ليست لديك أي شهادات يعد.",
"profile.certificates.my.certificates": "شهاداتي",
"profile.certificates.view.certificate": "معاينة الشهادة",
"profile.certificates.types.verified": "شهادة موثقة",
"profile.certificates.types.professional": "شهادة مهنية",
"profile.certificates.types.unknown": "شهادة",
"profile.country.label": "الموقع",
"profile.country.empty": "إضافة الموقع",
"profile.education.empty": "إضافة المستوى التعليمي",
"profile.education.education": "المستوى التعليمي",
"profile.education.levels.p": "دكتوراه",
"profile.education.levels.m": "ماجستير / ماستر أو شهادة مهنيّة",
"profile.education.levels.b": "بكالوريوس / ليسانس",
"profile.education.levels.a": "درجة الزمالة / دبلوم الدراسات الجامعية",
"profile.education.levels.hs": "الثانوية العامة / البكالوريا",
"profile.education.levels.jhs": "المدرسة الإعدادية / المتوسطة",
"profile.education.levels.el": "المدرسة الابتدائية / الأساسية",
"profile.education.levels.none": "دون تعليم رسمي",
"profile.education.levels.o": "نوع آخر من التعليم",
"profile.editbutton.edit": "تعديل",
"profile.formcontrols.who.can.see": "من يستطيع رؤية هذا:",
"profile.formcontrols.button.cancel": "إلغاء",
"profile.formcontrols.button.save": "حفظ",
"profile.formcontrols.button.saving": "الحفظ جارٍ",
"profile.formcontrols.button.saved": "تم الحفظ",
"profile.visibility.who.just.me": "أنا فقط",
"profile.visibility.who.everyone": "جميع من على {siteName}",
"profile.learningGoal.learningGoal": "هدف التعلم",
"profile.learningGoal.options.start_career": "أريد أن أبدأ مسيرتي المهنية",
"profile.learningGoal.options.advance_career": "أريد أن ارتقي في مسيرتي المهنية",
"profile.learningGoal.options.learn_something_new": "أريد أن أتعلم شيئًا جديدًا",
"profile.learningGoal.options.something_else": "شيء آخر",
"profile.name.full.name": "الاسم الكامل",
"profile.name.details": "هذا هو الاسم الذي يظهر في حسابك وفي شهاداتك",
"profile.name.empty": "إضافة الاسم",
"profile.preferredlanguage.empty": "إضافة اللغة",
"profile.preferredlanguage.label": "لغة التحدّث الأساسية",
"profile.profileavatar.upload-button": "تحميل صورة",
"profile.profileavatar.remove.button": "حذف",
"profile.image.alt.attribute": "صورة الملف الشخصي",
"profile.profileavatar.change-button": "تغيير",
"profile.sociallinks.add": "إضافة {network}",
"profile.sociallinks.social.links": "روابط التواصل الاجتماعي",
"profile.notfound.message": "الصفحة التي تبحث عنها غير متوفرة أو هناك خطأ في العنوان. رجاءً تحقق من العنوان و حاول مجدّدًا.",
"profile.viewMyRecords": "عرض سجلّاتي",
"profile.loading": "يتم تحميل الملف الشخصي...",
"profile.username.description": "معلومات ملفك الشخصي تظهر لك فقط. وحده اسم المستخدم الخاص بك يظهر للآخرين على {siteName}."
}

57
src/i18n/messages/de.json Normal file
View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Profile | {siteName}",
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
"profile.age.set.date": "Set your date of birth",
"profile.datejoined.member.since": "Member since {year}",
"profile.bio.empty": "Add a short bio",
"profile.bio.about.me": "About Me",
"profile.certificate.organization.label": "From",
"profile.certificate.completion.date.label": "Completed on {date}",
"profile.no.certificates": "You don't have any certificates yet.",
"profile.certificates.my.certificates": "My Certificates",
"profile.certificates.view.certificate": "View Certificate",
"profile.certificates.types.verified": "Verified Certificate",
"profile.certificates.types.professional": "Professional Certificate",
"profile.certificates.types.unknown": "Certificate",
"profile.country.label": "Location",
"profile.country.empty": "Add location",
"profile.education.empty": "Add education",
"profile.education.education": "Education",
"profile.education.levels.p": "Doctorate",
"profile.education.levels.m": "Master's or professional degree",
"profile.education.levels.b": "Bachelor's Degree",
"profile.education.levels.a": "Associate's degree",
"profile.education.levels.hs": "Secondary/high school",
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
"profile.education.levels.el": "Elementary/primary school",
"profile.education.levels.none": "No formal education",
"profile.education.levels.o": "Other education",
"profile.editbutton.edit": "Edit",
"profile.formcontrols.who.can.see": "Who can see this:",
"profile.formcontrols.button.cancel": "Cancel",
"profile.formcontrols.button.save": "Save",
"profile.formcontrols.button.saving": "Saving",
"profile.formcontrols.button.saved": "Saved",
"profile.visibility.who.just.me": "Just me",
"profile.visibility.who.everyone": "Everyone on {siteName}",
"profile.learningGoal.learningGoal": "Learning Goal",
"profile.learningGoal.options.start_career": "I want to start my career",
"profile.learningGoal.options.advance_career": "I want to advance my career",
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
"profile.learningGoal.options.something_else": "Something else",
"profile.name.full.name": "Full Name",
"profile.name.details": "This is the name that appears in your account and on your certificates.",
"profile.name.empty": "Add name",
"profile.preferredlanguage.empty": "Add language",
"profile.preferredlanguage.label": "Primary Language Spoken",
"profile.profileavatar.upload-button": "Upload Photo",
"profile.profileavatar.remove.button": "Remove",
"profile.image.alt.attribute": "profile avatar",
"profile.profileavatar.change-button": "Change",
"profile.sociallinks.add": "Add {network}",
"profile.sociallinks.social.links": "Social Links",
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"profile.viewMyRecords": "View My Records",
"profile.loading": "Profile loading...",
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
}

View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Profil | {siteName}",
"profile.age.details": "Um Ihr Profil mit anderen {siteName}-Lernern zu teilen, müssen Sie bestätigen, dass Sie über 13 Jahre alt sind.",
"profile.age.set.date": "Legen Sie Ihr Geburtsdatum fest",
"profile.datejoined.member.since": "Mitglied seit {year}",
"profile.bio.empty": "Fügen Sie Ihre Kurzbiografie hinzu",
"profile.bio.about.me": "Über mich",
"profile.certificate.organization.label": "Von",
"profile.certificate.completion.date.label": "Abgeschlossen am {date}",
"profile.no.certificates": "Sie haben bisher keine Zertifikate erhalten.",
"profile.certificates.my.certificates": "Meine Zertifikate",
"profile.certificates.view.certificate": "Zertifikat anschauen",
"profile.certificates.types.verified": "Beglaubigtes Zertifikat ",
"profile.certificates.types.professional": "Professional Certificate",
"profile.certificates.types.unknown": "Zertifikat",
"profile.country.label": "Ort",
"profile.country.empty": "Standort hinzufügen",
"profile.education.empty": "Ausbildung hinzufügen",
"profile.education.education": "Bildung",
"profile.education.levels.p": "Doktortitel",
"profile.education.levels.m": "Master oder gleichwertiger akademischer Bildungsgrad",
"profile.education.levels.b": "Bachelor",
"profile.education.levels.a": "Allgemeine Hochschulreife oder gleichwertiger Abschluss",
"profile.education.levels.hs": "Mittlere Reife",
"profile.education.levels.jhs": "Hauptschule",
"profile.education.levels.el": "Grundschule",
"profile.education.levels.none": "Keinen Bildungsabschluss",
"profile.education.levels.o": "Sonstige Bildung",
"profile.editbutton.edit": "Bearbeiten",
"profile.formcontrols.who.can.see": "Wer kann das sehen:",
"profile.formcontrols.button.cancel": "Abbrechen",
"profile.formcontrols.button.save": "Speichern",
"profile.formcontrols.button.saving": "Speichert",
"profile.formcontrols.button.saved": "Gespeichert",
"profile.visibility.who.just.me": "Nur ich",
"profile.visibility.who.everyone": "Alle auf {siteName}",
"profile.learningGoal.learningGoal": "Lernziel",
"profile.learningGoal.options.start_career": "Ich möchte meine Karriere starten",
"profile.learningGoal.options.advance_career": "Ich möchte mich beruflich weiterentwickeln",
"profile.learningGoal.options.learn_something_new": "Ich möchte etwas Neues lernen",
"profile.learningGoal.options.something_else": "Etwas anderes",
"profile.name.full.name": "Vollständiger Name",
"profile.name.details": "Dies ist der Name, der in Ihrem Konto und auf Ihren Zertifikaten erscheint.",
"profile.name.empty": "Name hinzufügen",
"profile.preferredlanguage.empty": "Sprache hinzufügen",
"profile.preferredlanguage.label": "Gesprochene Primärsprache ",
"profile.profileavatar.upload-button": "Foto hochladen",
"profile.profileavatar.remove.button": "Entfernen",
"profile.image.alt.attribute": "Profil Avatar",
"profile.profileavatar.change-button": "Ändern",
"profile.sociallinks.add": "{network} hinzufügen",
"profile.sociallinks.social.links": "Soziale Netzwerke",
"profile.notfound.message": "Die gesuchte Seite ist nicht verfügbar oder es liegt ein Fehler in der URL vor. Bitte überprüfen Sie die URL und versuchen Sie es erneut.",
"profile.viewMyRecords": "Meine Aufzeichnungen anzeigen",
"profile.loading": "Profil lädt...",
"profile.username.description": "Ihre Profilinformationen sind nur für Sie sichtbar. Nur Ihr Benutzername ist für andere auf {siteName} sichtbar."
}

View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Perfil | {siteName}",
"profile.age.details": "Para compartir el perfil con otros {siteName} estudiantes, debe confirmar que es mayor de 13 años.",
"profile.age.set.date": "Establece tu fecha de nacimiento",
"profile.datejoined.member.since": "Miembro desde {year}",
"profile.bio.empty": "Añade una breve biografía",
"profile.bio.about.me": "Sobre Mí",
"profile.certificate.organization.label": "Desde",
"profile.certificate.completion.date.label": "Completado el {date}",
"profile.no.certificates": "Todavía no ha obtenido ningún certificado.",
"profile.certificates.my.certificates": "Mis Certificados",
"profile.certificates.view.certificate": "Ver Certificado",
"profile.certificates.types.verified": "Certificado verificado",
"profile.certificates.types.professional": "Certificado profesional",
"profile.certificates.types.unknown": "Certificado",
"profile.country.label": "Ubicación",
"profile.country.empty": "Añade ubicación",
"profile.education.empty": "Añade Educación",
"profile.education.education": "Educación",
"profile.education.levels.p": "Doctorado",
"profile.education.levels.m": "Master o magíster",
"profile.education.levels.b": "Pregrado o Licenciatura",
"profile.education.levels.a": "Grado técnico - tecnológico",
"profile.education.levels.hs": "Enseñanza secundaria",
"profile.education.levels.jhs": "Formación media",
"profile.education.levels.el": "Enseñanza primaria",
"profile.education.levels.none": "Ninguna educación formal",
"profile.education.levels.o": "Otra educación",
"profile.editbutton.edit": "Editar",
"profile.formcontrols.who.can.see": "Quién puede ver esto:",
"profile.formcontrols.button.cancel": "Cancelar",
"profile.formcontrols.button.save": "Guardar",
"profile.formcontrols.button.saving": "Guardando",
"profile.formcontrols.button.saved": "Guardado",
"profile.visibility.who.just.me": "Solo yo",
"profile.visibility.who.everyone": "Todos en {siteName}",
"profile.learningGoal.learningGoal": "Objetivo de aprendizaje",
"profile.learningGoal.options.start_career": "quiero empezar mi carrera",
"profile.learningGoal.options.advance_career": "Quiero avanzar en mi carrera",
"profile.learningGoal.options.learn_something_new": "quiero aprender algo nuevo",
"profile.learningGoal.options.something_else": "Algo más",
"profile.name.full.name": "Nombre completo",
"profile.name.details": "Este es el nombre que aparecerá en tu cuenta y en tus certificados.",
"profile.name.empty": "Añade nombre",
"profile.preferredlanguage.empty": "Añadir idioma",
"profile.preferredlanguage.label": "Idioma principal que hablas",
"profile.profileavatar.upload-button": "Subir foto",
"profile.profileavatar.remove.button": "Eliminar",
"profile.image.alt.attribute": "avatar del perfil",
"profile.profileavatar.change-button": "Cambiar",
"profile.sociallinks.add": "Añade {network}",
"profile.sociallinks.social.links": "Enlaces De Redes Sociales",
"profile.notfound.message": "La página que estas buscando no está disponible o hay un error en la URL. Por favor, comprueba la URL y vuelve a intentarlo.",
"profile.viewMyRecords": "Ver mis registros",
"profile.loading": "Cargando perfil...",
"profile.username.description": "La información del perfil solo la visualiza usted. Solo el nombre de usuario es visible para los demás en {siteName}."
}

View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "پرونده کاربری {siteName}",
"profile.age.details": "برای اشتراک‌گذاری پرونده کاربری خود با سایر یادگیرندگان {siteName}، باید تأیید کنید که بیش از 13 سال سن دارید.",
"profile.age.set.date": "تنظیم تاریخ تولد",
"profile.datejoined.member.since": "عضو شده از {year}",
"profile.bio.empty": "بیوگرافی کوتاهی اضافه کنید",
"profile.bio.about.me": "درباره من",
"profile.certificate.organization.label": "از",
"profile.certificate.completion.date.label": "تکمیل شده در {date}",
"profile.no.certificates": "شما هنوز هیچ گواهی ندارید.",
"profile.certificates.my.certificates": "گواهی‌های من",
"profile.certificates.view.certificate": "نمایش گواهی",
"profile.certificates.types.verified": "گواهی تأییدشده",
"profile.certificates.types.professional": "گواهی حرفه‌ای",
"profile.certificates.types.unknown": "گواهی",
"profile.country.label": "مکان",
"profile.country.empty": "افزودن مکان",
"profile.education.empty": "افزودن تحصیلات",
"profile.education.education": "تحصیلات",
"profile.education.levels.p": "درجه دکتری",
"profile.education.levels.m": "کارشناسی ارشد یا مدرک حرفه‌ای",
"profile.education.levels.b": "مدرک کارشناسی",
"profile.education.levels.a": "مدرک کاردانی",
"profile.education.levels.hs": "متوسطه/دبیرستان",
"profile.education.levels.jhs": "مدرسه متوسطه دوره اول/ راهنمایی",
"profile.education.levels.el": "مدرسه ابتدایی",
"profile.education.levels.none": "بدون تحصیلات رسمی",
"profile.education.levels.o": "تحصیلات متفرقه",
"profile.editbutton.edit": " ویرایش",
"profile.formcontrols.who.can.see": "کسانی که می‌توانند این را ببینند:",
"profile.formcontrols.button.cancel": "لغو‌",
"profile.formcontrols.button.save": "ذخیره",
"profile.formcontrols.button.saving": "در حال ذخیره",
"profile.formcontrols.button.saved": "ذخیره شد",
"profile.visibility.who.just.me": "فقط من",
"profile.visibility.who.everyone": "هرکسی در {siteName}",
"profile.learningGoal.learningGoal": "هدف یادگیری",
"profile.learningGoal.options.start_career": "من می خواهم کارم را شروع کنم",
"profile.learningGoal.options.advance_career": "من می خواهم حرفه ام را ارتقا دهم",
"profile.learningGoal.options.learn_something_new": "می‌خواهم چیز جدیدی یاد بگیرم",
"profile.learningGoal.options.something_else": "یک چیز دیگر",
"profile.name.full.name": "نام و نام خانوادگی",
"profile.name.details": "این همان نامی است که در حساب کاربری و گواهی‌های شما درج می‌شود.",
"profile.name.empty": "افزودن نام",
"profile.preferredlanguage.empty": "افزودن زبان",
"profile.preferredlanguage.label": "زبان اصلی صحبت شده",
"profile.profileavatar.upload-button": "بارگذاری عکس",
"profile.profileavatar.remove.button": "حذف",
"profile.image.alt.attribute": "چهرک پرونده کاربری",
"profile.profileavatar.change-button": "تغییر",
"profile.sociallinks.add": "افزودن {network}",
"profile.sociallinks.social.links": "پیوندهای رسانه اجتماعی",
"profile.notfound.message": "صفحه مورد نظر شما در دسترس نیست یا خطایی در نشانی آن وجود دارد. لطفاً نشانی اینترنتی را بررسی کرده و دوباره امتحان کنید.",
"profile.viewMyRecords": "مشاهده سوابق من",
"profile.loading": "در حال بارگذاری پرونده کاربری...",
"profile.username.description": "اطلاعات پرونده کاربری فقط برای شما قابل مشاهده است. سایرین فقط نام کاربری شما را در {siteName} می‌توانند ببینند."
}

57
src/i18n/messages/fr.json Normal file
View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Profile | {siteName}",
"profile.age.details": "Pour partager votre profil avec d'autres étudiants {siteName}, vous devez confirmer que vous avez plus de 13 ans.",
"profile.age.set.date": "Définissez votre date de naissance",
"profile.datejoined.member.since": "Membre depuis {year}",
"profile.bio.empty": "Ajouter une courte biographie",
"profile.bio.about.me": "À propos de moi",
"profile.certificate.organization.label": "De",
"profile.certificate.completion.date.label": "Terminé le {date}",
"profile.no.certificates": "Vous n'avez pas encore de certificats.",
"profile.certificates.my.certificates": "Mes certificats",
"profile.certificates.view.certificate": "Voir le certificat",
"profile.certificates.types.verified": "Certificat vérifié",
"profile.certificates.types.professional": "Certificat professionnel",
"profile.certificates.types.unknown": "Certificat",
"profile.country.label": "Localisation",
"profile.country.empty": "Ajouter localisation",
"profile.education.empty": "Ajouter une éducation",
"profile.education.education": "Education",
"profile.education.levels.p": "Doctorat",
"profile.education.levels.m": "Master ou diplôme professionnel",
"profile.education.levels.b": "Diplôme de licence",
"profile.education.levels.a": "Grade de l'associé",
"profile.education.levels.hs": "Lycée / enseignement secondaire",
"profile.education.levels.jhs": "Collège / enseignement secondaire inférieur",
"profile.education.levels.el": "Enseignement primaire",
"profile.education.levels.none": "Sans diplôme",
"profile.education.levels.o": "Autre niveau d'étude",
"profile.editbutton.edit": "Modifier",
"profile.formcontrols.who.can.see": "Qui peut voir ça :",
"profile.formcontrols.button.cancel": "Annuler",
"profile.formcontrols.button.save": "Enregistrer",
"profile.formcontrols.button.saving": "Enregistrement",
"profile.formcontrols.button.saved": "Enregistré",
"profile.visibility.who.just.me": "Juste moi",
"profile.visibility.who.everyone": "Tout le monde sur {siteName}",
"profile.learningGoal.learningGoal": "Learning Goal",
"profile.learningGoal.options.start_career": "I want to start my career",
"profile.learningGoal.options.advance_career": "I want to advance my career",
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
"profile.learningGoal.options.something_else": "Something else",
"profile.name.full.name": "Nom complet",
"profile.name.details": "C'est le nom qui apparaît dans votre compte et sur vos certificats.",
"profile.name.empty": "Ajouter un nom",
"profile.preferredlanguage.empty": "Ajouter une langue",
"profile.preferredlanguage.label": "Langue principale parlée",
"profile.profileavatar.upload-button": "Envoyer la photo",
"profile.profileavatar.remove.button": "Supprimer",
"profile.image.alt.attribute": "Profil avatar",
"profile.profileavatar.change-button": "Modifier",
"profile.sociallinks.add": "Ajouter {network}",
"profile.sociallinks.social.links": "Liens vers les réseaux sociaux",
"profile.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
"profile.viewMyRecords": "Voir mes succès",
"profile.loading": "Chargement du profil....",
"profile.username.description": "Les informations de votre profil ne sont visibles que par vous. Seul votre nom d'utilisateur est visible par les autres sur {siteName}."
}

View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Profil | {siteName}",
"profile.age.details": "Pour partager votre profil avec d'autres apprenants {siteName}, vous devez confirmer que vous avez plus de 13 ans.",
"profile.age.set.date": "Entrez votre date de naissance",
"profile.datejoined.member.since": "Membre depuis {year}",
"profile.bio.empty": "Ajouter une courte biographie",
"profile.bio.about.me": "À propos de moi",
"profile.certificate.organization.label": "De",
"profile.certificate.completion.date.label": "Terminé le {date}",
"profile.no.certificates": "Vous n'avez pas encore d'attestation.",
"profile.certificates.my.certificates": "Mes Attestations",
"profile.certificates.view.certificate": "Voir votre attestation",
"profile.certificates.types.verified": "Attestation vérifiée",
"profile.certificates.types.professional": "Attestation professionnelle",
"profile.certificates.types.unknown": "Attestation",
"profile.country.label": "Adresse",
"profile.country.empty": "Ajouter un emplacement",
"profile.education.empty": "Ajouter formation",
"profile.education.education": "Formation",
"profile.education.levels.p": "Doctorat",
"profile.education.levels.m": "Maîtrise ou diplôme professionnel",
"profile.education.levels.b": "Diplôme de baccalauréat",
"profile.education.levels.a": "Diplôme d'associé",
"profile.education.levels.hs": "Lycée / enseignement secondaire",
"profile.education.levels.jhs": "Collège / enseignement secondaire inférieur",
"profile.education.levels.el": "Enseignement primaire",
"profile.education.levels.none": "Sans formation formelle",
"profile.education.levels.o": "Autre niveau de formation",
"profile.editbutton.edit": "Éditer",
"profile.formcontrols.who.can.see": "Qui peut voir ça :",
"profile.formcontrols.button.cancel": "Annuler",
"profile.formcontrols.button.save": "Sauvegarder",
"profile.formcontrols.button.saving": "Sauvegarde en cours",
"profile.formcontrols.button.saved": "Sauvegardé",
"profile.visibility.who.just.me": "Juste moi",
"profile.visibility.who.everyone": "Tout le monde sur {siteName}",
"profile.learningGoal.learningGoal": "Objectif d'apprentissage",
"profile.learningGoal.options.start_career": "Je veux commencer ma carrière",
"profile.learningGoal.options.advance_career": "Je veux faire progresser ma carrière",
"profile.learningGoal.options.learn_something_new": "Je veux apprendre quelque chose de nouveau",
"profile.learningGoal.options.something_else": "Autre chose",
"profile.name.full.name": "Nom complet",
"profile.name.details": "C'est le nom qui apparaît dans votre compte et sur vos attestations.",
"profile.name.empty": "Ajouter un nom",
"profile.preferredlanguage.empty": "Ajouter une langue",
"profile.preferredlanguage.label": "Langue principale parlée",
"profile.profileavatar.upload-button": "Téléverser une photo",
"profile.profileavatar.remove.button": "Retirer",
"profile.image.alt.attribute": "avatar de profil",
"profile.profileavatar.change-button": "Modifier",
"profile.sociallinks.add": "Ajouter {network}",
"profile.sociallinks.social.links": "Liens vers les réseaux sociaux",
"profile.notfound.message": "La page que vous recherchez n'est pas disponible ou il y a une erreur dans l'URL. Veuillez vérifier l'URL et réessayer.",
"profile.viewMyRecords": "Afficher mes dossiers",
"profile.loading": "Chargement du profil...",
"profile.username.description": "Les informations de votre profil ne sont visibles que par vous. Seul votre nom d'utilisateur est visible par les autres sur {siteName}."
}

57
src/i18n/messages/hi.json Normal file
View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Profile | {siteName}",
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
"profile.age.set.date": "Set your date of birth",
"profile.datejoined.member.since": "Member since {year}",
"profile.bio.empty": "Add a short bio",
"profile.bio.about.me": "About Me",
"profile.certificate.organization.label": "From",
"profile.certificate.completion.date.label": "Completed on {date}",
"profile.no.certificates": "You don't have any certificates yet.",
"profile.certificates.my.certificates": "My Certificates",
"profile.certificates.view.certificate": "View Certificate",
"profile.certificates.types.verified": "Verified Certificate",
"profile.certificates.types.professional": "Professional Certificate",
"profile.certificates.types.unknown": "Certificate",
"profile.country.label": "Location",
"profile.country.empty": "Add location",
"profile.education.empty": "Add education",
"profile.education.education": "Education",
"profile.education.levels.p": "Doctorate",
"profile.education.levels.m": "Master's or professional degree",
"profile.education.levels.b": "Bachelor's Degree",
"profile.education.levels.a": "Associate's degree",
"profile.education.levels.hs": "Secondary/high school",
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
"profile.education.levels.el": "Elementary/primary school",
"profile.education.levels.none": "No formal education",
"profile.education.levels.o": "Other education",
"profile.editbutton.edit": "Edit",
"profile.formcontrols.who.can.see": "Who can see this:",
"profile.formcontrols.button.cancel": "Cancel",
"profile.formcontrols.button.save": "Save",
"profile.formcontrols.button.saving": "Saving",
"profile.formcontrols.button.saved": "Saved",
"profile.visibility.who.just.me": "Just me",
"profile.visibility.who.everyone": "Everyone on {siteName}",
"profile.learningGoal.learningGoal": "Learning Goal",
"profile.learningGoal.options.start_career": "I want to start my career",
"profile.learningGoal.options.advance_career": "I want to advance my career",
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
"profile.learningGoal.options.something_else": "Something else",
"profile.name.full.name": "Full Name",
"profile.name.details": "This is the name that appears in your account and on your certificates.",
"profile.name.empty": "Add name",
"profile.preferredlanguage.empty": "Add language",
"profile.preferredlanguage.label": "Primary Language Spoken",
"profile.profileavatar.upload-button": "Upload Photo",
"profile.profileavatar.remove.button": "Remove",
"profile.image.alt.attribute": "profile avatar",
"profile.profileavatar.change-button": "Change",
"profile.sociallinks.add": "Add {network}",
"profile.sociallinks.social.links": "Social Links",
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"profile.viewMyRecords": "View My Records",
"profile.loading": "Profile loading...",
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
}

57
src/i18n/messages/it.json Normal file
View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Profile | {siteName}",
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
"profile.age.set.date": "Set your date of birth",
"profile.datejoined.member.since": "Member since {year}",
"profile.bio.empty": "Add a short bio",
"profile.bio.about.me": "About Me",
"profile.certificate.organization.label": "From",
"profile.certificate.completion.date.label": "Completed on {date}",
"profile.no.certificates": "You don't have any certificates yet.",
"profile.certificates.my.certificates": "My Certificates",
"profile.certificates.view.certificate": "View Certificate",
"profile.certificates.types.verified": "Verified Certificate",
"profile.certificates.types.professional": "Professional Certificate",
"profile.certificates.types.unknown": "Certificate",
"profile.country.label": "Location",
"profile.country.empty": "Add location",
"profile.education.empty": "Add education",
"profile.education.education": "Education",
"profile.education.levels.p": "Doctorate",
"profile.education.levels.m": "Master's or professional degree",
"profile.education.levels.b": "Bachelor's Degree",
"profile.education.levels.a": "Associate's degree",
"profile.education.levels.hs": "Secondary/high school",
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
"profile.education.levels.el": "Elementary/primary school",
"profile.education.levels.none": "No formal education",
"profile.education.levels.o": "Other education",
"profile.editbutton.edit": "Edit",
"profile.formcontrols.who.can.see": "Who can see this:",
"profile.formcontrols.button.cancel": "Cancel",
"profile.formcontrols.button.save": "Save",
"profile.formcontrols.button.saving": "Saving",
"profile.formcontrols.button.saved": "Saved",
"profile.visibility.who.just.me": "Just me",
"profile.visibility.who.everyone": "Everyone on {siteName}",
"profile.learningGoal.learningGoal": "Learning Goal",
"profile.learningGoal.options.start_career": "I want to start my career",
"profile.learningGoal.options.advance_career": "I want to advance my career",
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
"profile.learningGoal.options.something_else": "Something else",
"profile.name.full.name": "Full Name",
"profile.name.details": "This is the name that appears in your account and on your certificates.",
"profile.name.empty": "Add name",
"profile.preferredlanguage.empty": "Add language",
"profile.preferredlanguage.label": "Primary Language Spoken",
"profile.profileavatar.upload-button": "Upload Photo",
"profile.profileavatar.remove.button": "Remove",
"profile.image.alt.attribute": "profile avatar",
"profile.profileavatar.change-button": "Change",
"profile.sociallinks.add": "Add {network}",
"profile.sociallinks.social.links": "Social Links",
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"profile.viewMyRecords": "View My Records",
"profile.loading": "Profile loading...",
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
}

View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Profilo | {siteName}",
"profile.age.details": "Per condividere il tuo profilo con altri studenti di {siteName}, devi confermare di avere più di 13 anni.",
"profile.age.set.date": "Imposta la data di nascita ",
"profile.datejoined.member.since": "Membro da {year}",
"profile.bio.empty": "Aggiungi una breve biografia ",
"profile.bio.about.me": "Su di me",
"profile.certificate.organization.label": "Da ",
"profile.certificate.completion.date.label": "Completato il {date}",
"profile.no.certificates": "Non si dispone ancora di alcun certificato. ",
"profile.certificates.my.certificates": "Certificati personali ",
"profile.certificates.view.certificate": "Visualizza il certificato",
"profile.certificates.types.verified": "Certificato Verificato",
"profile.certificates.types.professional": "Certificato professionale ",
"profile.certificates.types.unknown": "Certificato ",
"profile.country.label": "Posizione",
"profile.country.empty": "Aggiungi posizione ",
"profile.education.empty": "Aggiungi titolo di studio ",
"profile.education.education": "Educazione",
"profile.education.levels.p": "Dottorato",
"profile.education.levels.m": "Laurea magistrale o titolo accademico professionale",
"profile.education.levels.b": "Laurea di primo livello ",
"profile.education.levels.a": "Diploma Professionale",
"profile.education.levels.hs": "Scuole superiori/liceo",
"profile.education.levels.jhs": "Scuole Medie",
"profile.education.levels.el": "Scuola Primaria/Elementare",
"profile.education.levels.none": "Nessun livello educativo formale",
"profile.education.levels.o": "Altro livello educativo",
"profile.editbutton.edit": "Modifica",
"profile.formcontrols.who.can.see": "Chi può visualizzare: ",
"profile.formcontrols.button.cancel": "Annulla",
"profile.formcontrols.button.save": "Salva",
"profile.formcontrols.button.saving": "Salvataggio in corso",
"profile.formcontrols.button.saved": "Salvato",
"profile.visibility.who.just.me": "Solo io ",
"profile.visibility.who.everyone": "Tutti su {siteName}",
"profile.learningGoal.learningGoal": "Obiettivo di apprendimento",
"profile.learningGoal.options.start_career": "Voglio iniziare il mio percorso",
"profile.learningGoal.options.advance_career": "Voglio avanzare nel mio percorso",
"profile.learningGoal.options.learn_something_new": "Voglio imparare qualcosa di nuovo",
"profile.learningGoal.options.something_else": "Qualcos'altro",
"profile.name.full.name": "Nome e Cognome",
"profile.name.details": "Questo è il nome visualizzato nel proprio account e nei propri certificati. ",
"profile.name.empty": "Aggiungi nome ",
"profile.preferredlanguage.empty": "Aggiungi lingua",
"profile.preferredlanguage.label": "Lingua principale ",
"profile.profileavatar.upload-button": "Carica foto",
"profile.profileavatar.remove.button": "Rimuovi",
"profile.image.alt.attribute": "avatar del profilo ",
"profile.profileavatar.change-button": "Cambia",
"profile.sociallinks.add": "Aggiungi {network}",
"profile.sociallinks.social.links": "Link social ",
"profile.notfound.message": "La pagina ricercata non è disponibile oppure è presente un errore nell'URL. Controllare l'URL e riprovare. ",
"profile.viewMyRecords": "Visualizza record personali ",
"profile.loading": "Caricamento del profilo... ",
"profile.username.description": "Le informazioni del tuo profilo sono visibili solo a te. Solo il tuo nome utente è visibile agli altri su {siteName}."
}

57
src/i18n/messages/pt.json Normal file
View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Profile | {siteName}",
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
"profile.age.set.date": "Set your date of birth",
"profile.datejoined.member.since": "Member since {year}",
"profile.bio.empty": "Add a short bio",
"profile.bio.about.me": "About Me",
"profile.certificate.organization.label": "From",
"profile.certificate.completion.date.label": "Completed on {date}",
"profile.no.certificates": "You don't have any certificates yet.",
"profile.certificates.my.certificates": "My Certificates",
"profile.certificates.view.certificate": "View Certificate",
"profile.certificates.types.verified": "Verified Certificate",
"profile.certificates.types.professional": "Professional Certificate",
"profile.certificates.types.unknown": "Certificate",
"profile.country.label": "Location",
"profile.country.empty": "Add location",
"profile.education.empty": "Add education",
"profile.education.education": "Education",
"profile.education.levels.p": "Doctorate",
"profile.education.levels.m": "Master's or professional degree",
"profile.education.levels.b": "Bachelor's Degree",
"profile.education.levels.a": "Associate's degree",
"profile.education.levels.hs": "Secondary/high school",
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
"profile.education.levels.el": "Elementary/primary school",
"profile.education.levels.none": "No formal education",
"profile.education.levels.o": "Other education",
"profile.editbutton.edit": "Edit",
"profile.formcontrols.who.can.see": "Who can see this:",
"profile.formcontrols.button.cancel": "Cancel",
"profile.formcontrols.button.save": "Save",
"profile.formcontrols.button.saving": "Saving",
"profile.formcontrols.button.saved": "Saved",
"profile.visibility.who.just.me": "Just me",
"profile.visibility.who.everyone": "Everyone on {siteName}",
"profile.learningGoal.learningGoal": "Learning Goal",
"profile.learningGoal.options.start_career": "I want to start my career",
"profile.learningGoal.options.advance_career": "I want to advance my career",
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
"profile.learningGoal.options.something_else": "Something else",
"profile.name.full.name": "Full Name",
"profile.name.details": "This is the name that appears in your account and on your certificates.",
"profile.name.empty": "Add name",
"profile.preferredlanguage.empty": "Add language",
"profile.preferredlanguage.label": "Primary Language Spoken",
"profile.profileavatar.upload-button": "Upload Photo",
"profile.profileavatar.remove.button": "Remove",
"profile.image.alt.attribute": "profile avatar",
"profile.profileavatar.change-button": "Change",
"profile.sociallinks.add": "Add {network}",
"profile.sociallinks.social.links": "Social Links",
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"profile.viewMyRecords": "View My Records",
"profile.loading": "Profile loading...",
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
}

View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Perfil | {siteName}",
"profile.age.details": "Para partilhar o seu perfil com outros estudantes da plataforma {siteName}, tem de confirmar que tem mais de 13 anos de idade.",
"profile.age.set.date": "Indique a sua data de nascimento",
"profile.datejoined.member.since": "Utilizador desde {year}",
"profile.bio.empty": "Adicionar uma breve biografia",
"profile.bio.about.me": "Sobre Mim",
"profile.certificate.organization.label": "De",
"profile.certificate.completion.date.label": "Concluído a {date}",
"profile.no.certificates": "Ainda não tem certificados.",
"profile.certificates.my.certificates": "Os Meus Certificados",
"profile.certificates.view.certificate": "Ver Certificado",
"profile.certificates.types.verified": "Certificado Validado",
"profile.certificates.types.professional": "Certificado Profissional",
"profile.certificates.types.unknown": "Certificado",
"profile.country.label": "Localização",
"profile.country.empty": "Adicionar localização",
"profile.education.empty": "Adicionar grau de escolaridade",
"profile.education.education": "Educação",
"profile.education.levels.p": "Doutoramento",
"profile.education.levels.m": "Mestrado ou Grau Profissional",
"profile.education.levels.b": "Licenciatura",
"profile.education.levels.a": "Pós-graduação",
"profile.education.levels.hs": "Secundário",
"profile.education.levels.jhs": "2ªciclo/3ºciclo",
"profile.education.levels.el": "Primária",
"profile.education.levels.none": "Sem estudos",
"profile.education.levels.o": "Outra formação",
"profile.editbutton.edit": "Editar",
"profile.formcontrols.who.can.see": "Quem pode ver isto:",
"profile.formcontrols.button.cancel": "Cancelar",
"profile.formcontrols.button.save": "Guardar",
"profile.formcontrols.button.saving": "A Guardar",
"profile.formcontrols.button.saved": "Guardado",
"profile.visibility.who.just.me": "Apenas eu",
"profile.visibility.who.everyone": "Toda a gente em {siteName}",
"profile.learningGoal.learningGoal": "Objectivo de aprendizagem",
"profile.learningGoal.options.start_career": "Quero começar a minha carreira",
"profile.learningGoal.options.advance_career": "Quero progredir na minha carreira",
"profile.learningGoal.options.learn_something_new": "Quero aprender algo novo",
"profile.learningGoal.options.something_else": "Outra coisa",
"profile.name.full.name": "Nome Completo",
"profile.name.details": "Este é o nome que aparece na sua conta e nos seus certificados.",
"profile.name.empty": "Adicionar nome",
"profile.preferredlanguage.empty": "Adicionar idioma",
"profile.preferredlanguage.label": "Língua Materna",
"profile.profileavatar.upload-button": "Carregar Fotografia",
"profile.profileavatar.remove.button": "Eliminar",
"profile.image.alt.attribute": "Icon de perfil",
"profile.profileavatar.change-button": "Alterar",
"profile.sociallinks.add": "Adicionar {network}",
"profile.sociallinks.social.links": "Links de Redes Sociais",
"profile.notfound.message": "A página que procura não está disponível ou há um erro no URL. Por favor, verifique o URL e tente novamente.",
"profile.viewMyRecords": "Ver os Meus Registos",
"profile.loading": "A carregar perfil...",
"profile.username.description": "As informações do seu perfil só são visíveis para si. Apenas o seu nome de utilizador é visível para outros em {siteName}."
}

57
src/i18n/messages/ru.json Normal file
View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Profile | {siteName}",
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
"profile.age.set.date": "Set your date of birth",
"profile.datejoined.member.since": "Member since {year}",
"profile.bio.empty": "Add a short bio",
"profile.bio.about.me": "About Me",
"profile.certificate.organization.label": "From",
"profile.certificate.completion.date.label": "Completed on {date}",
"profile.no.certificates": "You don't have any certificates yet.",
"profile.certificates.my.certificates": "My Certificates",
"profile.certificates.view.certificate": "View Certificate",
"profile.certificates.types.verified": "Verified Certificate",
"profile.certificates.types.professional": "Professional Certificate",
"profile.certificates.types.unknown": "Certificate",
"profile.country.label": "Location",
"profile.country.empty": "Add location",
"profile.education.empty": "Add education",
"profile.education.education": "Education",
"profile.education.levels.p": "Doctorate",
"profile.education.levels.m": "Master's or professional degree",
"profile.education.levels.b": "Bachelor's Degree",
"profile.education.levels.a": "Associate's degree",
"profile.education.levels.hs": "Secondary/high school",
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
"profile.education.levels.el": "Elementary/primary school",
"profile.education.levels.none": "No formal education",
"profile.education.levels.o": "Other education",
"profile.editbutton.edit": "Edit",
"profile.formcontrols.who.can.see": "Who can see this:",
"profile.formcontrols.button.cancel": "Cancel",
"profile.formcontrols.button.save": "Save",
"profile.formcontrols.button.saving": "Saving",
"profile.formcontrols.button.saved": "Saved",
"profile.visibility.who.just.me": "Just me",
"profile.visibility.who.everyone": "Everyone on {siteName}",
"profile.learningGoal.learningGoal": "Learning Goal",
"profile.learningGoal.options.start_career": "I want to start my career",
"profile.learningGoal.options.advance_career": "I want to advance my career",
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
"profile.learningGoal.options.something_else": "Something else",
"profile.name.full.name": "Full Name",
"profile.name.details": "This is the name that appears in your account and on your certificates.",
"profile.name.empty": "Add name",
"profile.preferredlanguage.empty": "Add language",
"profile.preferredlanguage.label": "Primary Language Spoken",
"profile.profileavatar.upload-button": "Upload Photo",
"profile.profileavatar.remove.button": "Remove",
"profile.image.alt.attribute": "profile avatar",
"profile.profileavatar.change-button": "Change",
"profile.sociallinks.add": "Add {network}",
"profile.sociallinks.social.links": "Social Links",
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"profile.viewMyRecords": "View My Records",
"profile.loading": "Profile loading...",
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
}

57
src/i18n/messages/uk.json Normal file
View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Profile | {siteName}",
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
"profile.age.set.date": "Set your date of birth",
"profile.datejoined.member.since": "Member since {year}",
"profile.bio.empty": "Add a short bio",
"profile.bio.about.me": "About Me",
"profile.certificate.organization.label": "From",
"profile.certificate.completion.date.label": "Completed on {date}",
"profile.no.certificates": "You don't have any certificates yet.",
"profile.certificates.my.certificates": "Мої сертифікати",
"profile.certificates.view.certificate": "View Certificate",
"profile.certificates.types.verified": "Verified Certificate",
"profile.certificates.types.professional": "Professional Certificate",
"profile.certificates.types.unknown": "Certificate",
"profile.country.label": "Location",
"profile.country.empty": "Add location",
"profile.education.empty": "Add education",
"profile.education.education": "Education",
"profile.education.levels.p": "Doctorate",
"profile.education.levels.m": "Master's or professional degree",
"profile.education.levels.b": "Bachelor's Degree",
"profile.education.levels.a": "Associate's degree",
"profile.education.levels.hs": "Secondary/high school",
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
"profile.education.levels.el": "Elementary/primary school",
"profile.education.levels.none": "No formal education",
"profile.education.levels.o": "Other education",
"profile.editbutton.edit": "Edit",
"profile.formcontrols.who.can.see": "Who can see this:",
"profile.formcontrols.button.cancel": "Cancel",
"profile.formcontrols.button.save": "Save",
"profile.formcontrols.button.saving": "Saving",
"profile.formcontrols.button.saved": "Saved",
"profile.visibility.who.just.me": "Just me",
"profile.visibility.who.everyone": "Everyone on {siteName}",
"profile.learningGoal.learningGoal": "Learning Goal",
"profile.learningGoal.options.start_career": "I want to start my career",
"profile.learningGoal.options.advance_career": "I want to advance my career",
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
"profile.learningGoal.options.something_else": "Something else",
"profile.name.full.name": "Full Name",
"profile.name.details": "This is the name that appears in your account and on your certificates.",
"profile.name.empty": "Add name",
"profile.preferredlanguage.empty": "Add language",
"profile.preferredlanguage.label": "Primary Language Spoken",
"profile.profileavatar.upload-button": "Upload Photo",
"profile.profileavatar.remove.button": "Remove",
"profile.image.alt.attribute": "profile avatar",
"profile.profileavatar.change-button": "Change",
"profile.sociallinks.add": "Add {network}",
"profile.sociallinks.social.links": "Social Links",
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"profile.viewMyRecords": "View My Records",
"profile.loading": "Profile loading...",
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
}

View File

@@ -0,0 +1,57 @@
{
"profile.page.title": "Profile | {siteName}",
"profile.age.details": "To share your profile with other {siteName} learners, you must confirm that you are over the age of 13.",
"profile.age.set.date": "Set your date of birth",
"profile.datejoined.member.since": "Member since {year}",
"profile.bio.empty": "Add a short bio",
"profile.bio.about.me": "About Me",
"profile.certificate.organization.label": "From",
"profile.certificate.completion.date.label": "Completed on {date}",
"profile.no.certificates": "You don't have any certificates yet.",
"profile.certificates.my.certificates": "My Certificates",
"profile.certificates.view.certificate": "View Certificate",
"profile.certificates.types.verified": "Verified Certificate",
"profile.certificates.types.professional": "Professional Certificate",
"profile.certificates.types.unknown": "Certificate",
"profile.country.label": "Location",
"profile.country.empty": "Add location",
"profile.education.empty": "Add education",
"profile.education.education": "Education",
"profile.education.levels.p": "Doctorate",
"profile.education.levels.m": "Master's or professional degree",
"profile.education.levels.b": "Bachelor's Degree",
"profile.education.levels.a": "Associate's degree",
"profile.education.levels.hs": "Secondary/high school",
"profile.education.levels.jhs": "Junior secondary/junior high/middle school",
"profile.education.levels.el": "Elementary/primary school",
"profile.education.levels.none": "No formal education",
"profile.education.levels.o": "Other education",
"profile.editbutton.edit": "Edit",
"profile.formcontrols.who.can.see": "Who can see this:",
"profile.formcontrols.button.cancel": "Cancel",
"profile.formcontrols.button.save": "Save",
"profile.formcontrols.button.saving": "Saving",
"profile.formcontrols.button.saved": "Saved",
"profile.visibility.who.just.me": "Just me",
"profile.visibility.who.everyone": "Everyone on {siteName}",
"profile.learningGoal.learningGoal": "Learning Goal",
"profile.learningGoal.options.start_career": "I want to start my career",
"profile.learningGoal.options.advance_career": "I want to advance my career",
"profile.learningGoal.options.learn_something_new": "I want to learn something new",
"profile.learningGoal.options.something_else": "Something else",
"profile.name.full.name": "Full Name",
"profile.name.details": "This is the name that appears in your account and on your certificates.",
"profile.name.empty": "Add name",
"profile.preferredlanguage.empty": "Add language",
"profile.preferredlanguage.label": "Primary Language Spoken",
"profile.profileavatar.upload-button": "Upload Photo",
"profile.profileavatar.remove.button": "Remove",
"profile.image.alt.attribute": "profile avatar",
"profile.profileavatar.change-button": "Change",
"profile.sociallinks.add": "Add {network}",
"profile.sociallinks.social.links": "Social Links",
"profile.notfound.message": "The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.",
"profile.viewMyRecords": "View My Records",
"profile.loading": "Profile loading...",
"profile.username.description": "Your profile information is only visible to you. Only your username is visible to others on {siteName}."
}

View File

@@ -1,8 +0,0 @@
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/footer";
@import './profile-v2/index';

View File

@@ -7,7 +7,6 @@ import {
initialize,
mergeConfig,
subscribe,
getConfig,
} from '@edx/frontend-platform';
import {
AppProvider,
@@ -16,32 +15,38 @@ import {
import React from 'react';
import ReactDOM from 'react-dom';
import { useLocation } from 'react-router-dom';
import Header from '@edx/frontend-component-header';
import FooterSlot from '@openedx/frontend-slot-footer';
import Footer from '@edx/frontend-component-footer';
import messages from './i18n';
import configureStore from './data/configureStore';
import './index.scss';
import Head from './head/Head';
import AppRoutes from './routes/AppRoutes';
subscribe(APP_READY, async () => {
const isNewProfileEnabled = getConfig().ENABLE_NEW_PROFILE_VIEW === 'true';
if (isNewProfileEnabled) {
await import('./index-v2.scss');
} else {
await import('./index.scss');
}
const RenderFooter = () => {
const location = useLocation();
return location.pathname.includes('/plugin') ? null : <Footer />;
};
const RenderHeader = () => {
const location = useLocation();
return location.pathname.includes('/plugin') ? null : <Header />;
};
subscribe(APP_READY, () => {
ReactDOM.render(
<AppProvider store={configureStore()}>
<Head />
<Header />
<RenderHeader />
<main id="main">
<AppRoutes isNewProfileEnabled={isNewProfileEnabled} />
<AppRoutes />
</main>
<FooterSlot />
<RenderFooter />
</AppProvider>,
document.getElementById('root'),
);
@@ -59,7 +64,6 @@ initialize({
mergeConfig({
COLLECT_YEAR_OF_BIRTH: process.env.COLLECT_YEAR_OF_BIRTH,
ENABLE_SKILLS_BUILDER_PROFILE: process.env.ENABLE_SKILLS_BUILDER_PROFILE,
ENABLE_NEW_PROFILE_VIEW: process.env.ENABLE_NEW_PROFILE_VIEW || null,
}, 'App loadConfig override handler');
},
},

View File

@@ -1,6 +1,6 @@
@import "~@edx/brand/paragon/fonts";
@import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core";
@import "~@edx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides";
@import "~@edx/frontend-component-header/dist/index";
@import "~@edx/frontend-component-footer/dist/footer";

View File

@@ -36,11 +36,7 @@
"dateJoined": "2017-06-07T00:44:23Z",
"email": "staff@example.com",
"isActive": true,
"languageProficiencies": [],
"levelOfEducation": null,
"name": "Lemon Seltzer",
"profileImage": {},
"socialLinks": [],
"username": "staff",
"yearOfBirth": 1901
},

View File

@@ -1,50 +0,0 @@
# Footer Slot
### Slot ID: `footer_slot`
## Description
This slot is used to replace/modify/hide the footer.
The implementation of the `FooterSlot` component lives in [the `frontend-slot-footer` repository](https://github.com/openedx/frontend-slot-footer/).
## Example
The following `env.config.jsx` will replace the default footer.
![Screenshot of Default Footer](./images/default_footer.png)
with a simple custom footer
![Screenshot of Custom Footer](./images/custom_footer.png)
```jsx
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
footer_slot: {
plugins: [
{
// Hide the default footer
op: PLUGIN_OPERATIONS.Hide,
widgetId: 'default_contents',
},
{
// Insert a custom footer
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_footer',
type: DIRECT_PLUGIN,
RenderWidget: () => (
<h1 style={{textAlign: 'center'}}>🦶</h1>
),
},
},
]
}
},
}
export default config;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,3 +0,0 @@
# `frontend-app-profile` Plugin Slots
* [`footer_slot`](./FooterSlot/)

View File

@@ -1,146 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedDate, FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink } from '@openedx/paragon';
import get from 'lodash.get';
import classNames from 'classnames';
import professionalCertificateSVG from './assets/professional-certificate.svg';
import verifiedCertificateSVG from './assets/verified-certificate.svg';
import messages from './Certificates.messages';
import { useIsOnMobileScreen } from './data/hooks';
const CertificateCard = ({
certificateType,
courseDisplayName,
courseOrganization,
modifiedDate,
downloadUrl,
courseId,
uuid,
}) => {
const intl = useIntl();
const certificateIllustration = {
professional: professionalCertificateSVG,
'no-id-professional': professionalCertificateSVG,
verified: verifiedCertificateSVG,
honor: null,
audit: null,
}[certificateType] || null;
const isMobileView = useIsOnMobileScreen();
return (
<div
key={`${modifiedDate}-${courseId}`}
className="col-auto d-flex align-items-center p-0"
>
<div className="col certificate p-4 border-light-400 bg-light-200 w-100 h-100">
<div
className="certificate-type-illustration"
style={{ backgroundImage: `url(${certificateIllustration})` }}
/>
<div className={classNames(
'd-flex flex-column position-relative p-0',
{ 'max-width-304px': isMobileView },
{ 'width-314px': !isMobileView },
)}
>
<div className="w-100 color-black">
<p className={classNames([
'mb-0 font-weight-normal',
isMobileView ? 'x-small' : 'small',
])}
>
{intl.formatMessage(get(
messages,
`profile.certificates.types.${certificateType}`,
messages['profile.certificates.types.unknown'],
))}
</p>
<p className={classNames([
'm-0 color-black',
isMobileView ? 'h5' : 'h4',
])}
>
{courseDisplayName}
</p>
<p className={classNames([
'mb-0',
isMobileView ? 'x-small' : 'small',
])}
>
<FormattedMessage
id="profile.certificate.organization.label"
defaultMessage="From"
/>
</p>
<h5 className="mb-0 color-black">{courseOrganization}</h5>
<p className={classNames([
'mb-0',
isMobileView ? 'x-small' : 'small',
])}
>
<FormattedMessage
id="profile.certificate.completion.date.label"
defaultMessage="Completed on {date}"
values={{
date: <FormattedDate value={new Date(modifiedDate)} />,
}}
/>
</p>
</div>
<div className="pt-3">
<Hyperlink
destination={downloadUrl}
target="_blank"
showLaunchIcon={false}
className={classNames(
'btn btn-primary btn-rounded font-weight-normal px-4 py-10px',
{ 'btn-sm': isMobileView },
)}
>
{intl.formatMessage(messages['profile.certificates.view.certificate'])}
</Hyperlink>
</div>
<p
className={classNames([
'mb-0 pt-3',
isMobileView ? 'x-small' : 'small',
])}
>
<FormattedMessage
id="profile.certificate.uuid"
defaultMessage="Credential ID {certificate_uuid}"
values={{
certificate_uuid: uuid,
}}
/>
</p>
</div>
</div>
</div>
);
};
CertificateCard.propTypes = {
certificateType: PropTypes.string,
courseDisplayName: PropTypes.string,
courseOrganization: PropTypes.string,
modifiedDate: PropTypes.string,
downloadUrl: PropTypes.string,
courseId: PropTypes.string.isRequired,
uuid: PropTypes.string,
};
CertificateCard.defaultProps = {
certificateType: 'unknown',
courseDisplayName: '',
courseOrganization: '',
modifiedDate: '',
downloadUrl: '',
uuid: '',
};
export default CertificateCard;

View File

@@ -1,92 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import classNames from 'classnames';
import CertificateCard from './CertificateCard';
import { certificatesSelector } from './data/selectors';
import { useIsOnTabletScreen } from './data/hooks';
const Certificates = ({ certificates }) => {
const isTabletView = useIsOnTabletScreen();
return (
<div>
<div className="col justify-content-start align-items-start g-5rem p-0">
<div className="col align-self-stretch height-42px justify-content-start align-items-start p-0">
<p className="font-weight-bold text-primary-500 m-0 h2">
<FormattedMessage
id="profile.your.certificates"
defaultMessage="Your certificates"
description="heading for the certificates section"
/>
</p>
</div>
<div className="col justify-content-start align-items-start pt-2 p-0">
<p className="font-weight-normal text-gray-800 m-0 p-0 p">
<FormattedMessage
id="profile.certificates.description"
defaultMessage="Your learner records information is only visible to you. Only your username and profile image are visible to others on {siteName}."
description="description of the certificates section"
values={{
siteName: getConfig().SITE_NAME,
}}
/>
</p>
</div>
</div>
{certificates?.length > 0 ? (
<div className="col">
<div className={classNames(
'row align-items-center pt-5 g-3rem',
{ 'justify-content-center': isTabletView },
)}
>
{certificates.map(certificate => (
<CertificateCard
key={certificate.courseId}
certificateType={certificate.certificateType}
courseDisplayName={certificate.courseDisplayName}
courseOrganization={certificate.courseOrganization}
modifiedDate={certificate.modifiedDate}
downloadUrl={certificate.downloadUrl}
courseId={certificate.courseId}
uuid={certificate.uuid}
/>
))}
</div>
</div>
) : (
<div className="pt-5">
<FormattedMessage
id="profile.no.certificates"
defaultMessage="You don't have any certificates yet."
description="displays when user has no course completion certificates"
/>
</div>
)}
</div>
);
};
Certificates.propTypes = {
certificates: PropTypes.arrayOf(PropTypes.shape({
certificateType: PropTypes.string,
courseDisplayName: PropTypes.string,
courseOrganization: PropTypes.string,
modifiedDate: PropTypes.string,
downloadUrl: PropTypes.string,
courseId: PropTypes.string.isRequired,
uuid: PropTypes.string,
})),
};
Certificates.defaultProps = {
certificates: [],
};
export default connect(
certificatesSelector,
{},
)(Certificates);

View File

@@ -1,31 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.certificates.my.certificates': {
id: 'profile.certificates.my.certificates',
defaultMessage: 'My Certificates',
description: 'A section of a user profile',
},
'profile.certificates.view.certificate': {
id: 'profile.certificates.view.certificate',
defaultMessage: 'View Certificate',
description: 'A call to action to view a certificate',
},
'profile.certificates.types.verified': {
id: 'profile.certificates.types.verified',
defaultMessage: 'Verified Certificate',
description: 'A type of certificate a user may have earned',
},
'profile.certificates.types.professional': {
id: 'profile.certificates.types.professional',
defaultMessage: 'Professional Certificate',
description: 'A type of certificate a user may have earned',
},
'profile.certificates.types.unknown': {
id: 'profile.certificates.types.unknown',
defaultMessage: 'Certificate',
description: 'The string to display when a certificate is of an unknown type',
},
});
export default messages;

View File

@@ -1,29 +0,0 @@
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
const DateJoined = ({ date }) => {
if (!date) { return null; }
return (
<span className="small mb-0 text-gray-800">
<FormattedMessage
id="profile.datejoined.member.since"
defaultMessage="Member since {year}"
description="A label for how long the user has been a member"
values={{
year: <span className="font-weight-bold"> <FormattedDate value={new Date(date)} year="numeric" /> </span>,
}}
/>
</span>
);
};
DateJoined.propTypes = {
date: PropTypes.string,
};
DateJoined.defaultProps = {
date: null,
};
export default memo(DateJoined);

View File

@@ -1,16 +0,0 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
const NotFoundPage = () => (
<div className="container-fluid d-flex py-5 justify-content-center align-items-start text-center">
<p className="my-0 py-5 text-muted max-width-32em">
<FormattedMessage
id="profile.notfound.message"
defaultMessage="The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again."
description="error message when a page does not exist"
/>
</p>
</div>
);
export default NotFoundPage;

View File

@@ -1,24 +0,0 @@
import React from 'react';
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import NotFoundPage from './NotFoundPage';
describe('NotFoundPage Snapshot Tests', () => {
it('renders correctly', () => {
const { asFragment } = render(
<IntlProvider locale="en">
<NotFoundPage />
</IntlProvider>,
);
expect(asFragment()).toMatchSnapshot();
});
it('renders with custom props', () => {
const { asFragment } = render(
<IntlProvider locale="en">
<NotFoundPage message="Custom not found message" />
</IntlProvider>,
);
expect(asFragment()).toMatchSnapshot();
});
});

View File

@@ -1,18 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
const PageLoading = ({ srMessage }) => (
<div>
<div className="d-flex justify-content-center align-items-center flex-column height-50vh">
<div className="spinner-border text-primary" role="status">
{srMessage && <span className="sr-only">{srMessage}</span>}
</div>
</div>
</div>
);
PageLoading.propTypes = {
srMessage: PropTypes.string.isRequired,
};
export default PageLoading;

View File

@@ -1,463 +0,0 @@
import React, {
useEffect, useState, useContext, useCallback,
} from 'react';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { ensureConfig } from '@edx/frontend-platform';
import { AppContext } from '@edx/frontend-platform/react';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import {
Alert, Hyperlink, OverlayTrigger, Tooltip,
} from '@openedx/paragon';
import { InfoOutline } from '@openedx/paragon/icons';
import classNames from 'classnames';
import {
fetchProfile,
saveProfile,
saveProfilePhoto,
deleteProfilePhoto,
openForm,
closeForm,
updateDraft,
} from './data/actions';
import ProfileAvatar from './forms/ProfileAvatar';
import Name from './forms/Name';
import Country from './forms/Country';
import PreferredLanguage from './forms/PreferredLanguage';
import Education from './forms/Education';
import SocialLinks from './forms/SocialLinks';
import Bio from './forms/Bio';
import DateJoined from './DateJoined';
import UserCertificateSummary from './UserCertificateSummary';
import PageLoading from './PageLoading';
import Certificates from './Certificates';
import { profilePageSelector } from './data/selectors';
import messages from './ProfilePage.messages';
import withParams from '../utils/hoc';
import { useIsOnMobileScreen, useIsOnTabletScreen } from './data/hooks';
ensureConfig(['CREDENTIALS_BASE_URL', 'LMS_BASE_URL', 'ACCOUNT_SETTINGS_URL'], 'ProfilePage');
const ProfilePage = ({ params }) => {
const dispatch = useDispatch();
const intl = useIntl();
const context = useContext(AppContext);
const {
dateJoined,
courseCertificates,
name,
visibilityName,
profileImage,
savePhotoState,
isLoadingProfile,
photoUploadError,
country,
visibilityCountry,
levelOfEducation,
visibilityLevelOfEducation,
socialLinks,
draftSocialLinksByPlatform,
visibilitySocialLinks,
languageProficiencies,
visibilityLanguageProficiencies,
bio,
visibilityBio,
saveState,
username,
} = useSelector(profilePageSelector);
const navigate = useNavigate();
const [viewMyRecordsUrl, setViewMyRecordsUrl] = useState(null);
const isMobileView = useIsOnMobileScreen();
const isTabletView = useIsOnTabletScreen();
useEffect(() => {
const { CREDENTIALS_BASE_URL } = context.config;
if (CREDENTIALS_BASE_URL) {
setViewMyRecordsUrl(`${CREDENTIALS_BASE_URL}/records`);
}
dispatch(fetchProfile(params.username));
sendTrackingLogEvent('edx.profile.viewed', {
username: params.username,
});
}, [dispatch, params.username, context.config]);
useEffect(() => {
if (!username && saveState === 'error' && navigate) {
navigate('/notfound');
}
}, [username, saveState, navigate]);
const authenticatedUserName = context.authenticatedUser.username;
const handleSaveProfilePhoto = useCallback((formData) => {
dispatch(saveProfilePhoto(authenticatedUserName, formData));
}, [dispatch, authenticatedUserName]);
const handleDeleteProfilePhoto = useCallback(() => {
dispatch(deleteProfilePhoto(authenticatedUserName));
}, [dispatch, authenticatedUserName]);
const handleClose = useCallback((formId) => {
dispatch(closeForm(formId));
}, [dispatch]);
const handleOpen = useCallback((formId) => {
dispatch(openForm(formId));
}, [dispatch]);
const handleSubmit = useCallback((formId) => {
dispatch(saveProfile(formId, authenticatedUserName));
}, [dispatch, authenticatedUserName]);
const handleChange = useCallback((fieldName, value) => {
dispatch(updateDraft(fieldName, value));
}, [dispatch]);
const isAuthenticatedUserProfile = () => params.username === authenticatedUserName;
const isBlockVisible = (blockInfo) => isAuthenticatedUserProfile()
|| (!isAuthenticatedUserProfile() && Boolean(blockInfo));
const renderViewMyRecordsButton = () => {
if (!(viewMyRecordsUrl && isAuthenticatedUserProfile())) {
return null;
}
return (
<Hyperlink
className={classNames(
'btn btn-brand bg-brand-500 btn-rounded font-weight-normal px-4 py-10px text-nowrap',
{ 'w-100': isMobileView },
)}
target="_blank"
showLaunchIcon={false}
destination={viewMyRecordsUrl}
>
{intl.formatMessage(messages['profile.viewMyRecords'])}
</Hyperlink>
);
};
const renderPhotoUploadErrorMessage = () => (
photoUploadError && (
<div className="row">
<div className="col-md-4 col-lg-3">
<Alert variant="danger" dismissible={false} show>
{photoUploadError.userMessage}
</Alert>
</div>
</div>
)
);
const commonFormProps = {
openHandler: handleOpen,
closeHandler: handleClose,
submitHandler: handleSubmit,
changeHandler: handleChange,
};
return (
<div className="profile-page">
{isLoadingProfile ? (
<PageLoading srMessage={intl.formatMessage(messages['profile.loading'])} />
) : (
<>
<div
className={classNames(
'profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100',
{ 'px-3 py-4': isMobileView },
{ 'px-120px py-5.5': !isMobileView },
)}
>
<div
className={classNames([
'col container-fluid w-100 h-100 bg-white py-0 rounded-75',
{
'px-3': isMobileView,
'px-40px': !isMobileView,
},
])}
>
<div
className={classNames([
'col h-100 w-100 px-0 justify-content-start g-15rem',
{
'py-4': isMobileView,
'py-36px': !isMobileView,
},
])}
>
<div
className={classNames([
'row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem',
isMobileView || isTabletView ? 'flex-column' : 'flex-row',
])}
>
<ProfileAvatar
className="col p-0"
src={profileImage.src}
isDefault={profileImage.isDefault}
onSave={handleSaveProfilePhoto}
onDelete={handleDeleteProfilePhoto}
savePhotoState={savePhotoState}
isEditable={isAuthenticatedUserProfile()}
/>
<div
className={classNames([
'col h-100 w-100 m-0 p-0',
isMobileView || isTabletView
? 'd-flex flex-column justify-content-center align-items-center'
: 'justify-content-start align-items-start',
])}
>
<p className="row m-0 font-weight-bold text-truncate text-primary-500 h3">
{params.username}
</p>
{isBlockVisible(name) && (
<p className="row pt-2 text-gray-800 font-weight-normal m-0 p">
{name}
</p>
)}
<div className={classNames(
'row pt-2 m-0',
isMobileView
? 'd-flex justify-content-center align-items-center flex-column'
: 'g-1rem',
)}
>
<DateJoined date={dateJoined} />
<UserCertificateSummary count={courseCertificates?.length || 0} />
</div>
</div>
<div className={classNames([
'p-0 ',
isMobileView || isTabletView ? 'col d-flex justify-content-center' : 'col-auto',
])}
>
{renderViewMyRecordsButton()}
</div>
</div>
</div>
<div className="ml-auto">
{renderPhotoUploadErrorMessage()}
</div>
</div>
</div>
<div
className={classNames([
'col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem',
isMobileView ? 'py-4 px-3' : 'px-120px py-6',
])}
>
<div className="w-100 p-0">
<div className="col justify-content-start align-items-start p-0">
<div className="col align-self-stretch height-42px justify-content-start align-items-start p-0">
<p className="font-weight-bold text-primary-500 m-0 h2">
{isMobileView ? (
<FormattedMessage
id="profile.profile.information"
defaultMessage="Profile"
description="heading for the editable profile section in mobile view"
/>
)
: (
<FormattedMessage
id="profile.profile.information"
defaultMessage="Profile information"
description="heading for the editable profile section"
/>
)}
</p>
</div>
</div>
<div
className={classNames([
'row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start',
isMobileView ? 'pt-4' : 'pt-5.5',
])}
>
<div
className={classNames([
'col p-0',
isMobileView ? 'col-12' : 'col-6',
])}
>
<div className="m-0">
<div className="row m-0 pb-1.5 align-items-center">
<p data-hj-suppress className="h5 font-weight-bold m-0">
{intl.formatMessage(messages['profile.username'])}
</p>
<OverlayTrigger
key="top"
placement="top"
overlay={(
<Tooltip variant="light" id="tooltip-top">
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.username.tooltip'])}
</p>
</Tooltip>
)}
>
<InfoOutline className="m-0 info-icon" />
</OverlayTrigger>
</div>
<h4 className="edit-section-header text-gray-700">
{params.username}
</h4>
</div>
{isBlockVisible(name) && (
<Name
name={name}
accountSettingsUrl={context.config.ACCOUNT_SETTINGS_URL}
visibilityName={visibilityName}
formId="name"
{...commonFormProps}
/>
)}
{isBlockVisible(country) && (
<Country
country={country}
visibilityCountry={visibilityCountry}
formId="country"
{...commonFormProps}
/>
)}
{isBlockVisible((languageProficiencies || []).length) && (
<PreferredLanguage
languageProficiencies={languageProficiencies || []}
visibilityLanguageProficiencies={visibilityLanguageProficiencies}
formId="languageProficiencies"
{...commonFormProps}
/>
)}
{isBlockVisible(levelOfEducation) && (
<Education
levelOfEducation={levelOfEducation}
visibilityLevelOfEducation={visibilityLevelOfEducation}
formId="levelOfEducation"
{...commonFormProps}
/>
)}
</div>
<div
className={classNames([
'col m-0 pr-0',
isMobileView ? 'pl-0 col-12' : 'pl-40px col-6',
])}
>
{isBlockVisible(bio) && (
<Bio
bio={bio}
visibilityBio={visibilityBio}
formId="bio"
{...commonFormProps}
/>
)}
{isBlockVisible((socialLinks || []).some((link) => link?.socialLink !== null)) && (
<SocialLinks
socialLinks={socialLinks || []}
draftSocialLinksByPlatform={draftSocialLinksByPlatform || {}}
visibilitySocialLinks={visibilitySocialLinks}
formId="socialLinks"
{...commonFormProps}
/>
)}
</div>
</div>
</div>
</div>
<div
className={classNames([
'col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem',
isMobileView ? 'py-4 px-3' : 'px-120px py-6',
])}
>
{isBlockVisible((courseCertificates || []).length) && (
<Certificates
certificates={courseCertificates || []}
formId="certificates"
/>
)}
</div>
</>
)}
</div>
);
};
ProfilePage.propTypes = {
params: PropTypes.shape({
username: PropTypes.string.isRequired,
}).isRequired,
requiresParentalConsent: PropTypes.bool,
dateJoined: PropTypes.string,
username: PropTypes.string,
bio: PropTypes.string,
visibilityBio: PropTypes.string,
courseCertificates: PropTypes.arrayOf(PropTypes.shape({
title: PropTypes.string,
})),
country: PropTypes.string,
visibilityCountry: PropTypes.string,
levelOfEducation: PropTypes.string,
visibilityLevelOfEducation: PropTypes.string,
languageProficiencies: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
})),
visibilityLanguageProficiencies: PropTypes.string,
name: PropTypes.string,
visibilityName: PropTypes.string,
socialLinks: PropTypes.arrayOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
draftSocialLinksByPlatform: PropTypes.objectOf(PropTypes.shape({
platform: PropTypes.string,
socialLink: PropTypes.string,
})),
visibilitySocialLinks: PropTypes.string,
profileImage: PropTypes.shape({
src: PropTypes.string,
isDefault: PropTypes.bool,
}),
saveState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
savePhotoState: PropTypes.oneOf([null, 'pending', 'complete', 'error']),
isLoadingProfile: PropTypes.bool,
photoUploadError: PropTypes.objectOf(PropTypes.string),
};
ProfilePage.defaultProps = {
saveState: null,
username: '',
savePhotoState: null,
photoUploadError: {},
profileImage: {},
name: null,
levelOfEducation: null,
country: null,
socialLinks: [],
draftSocialLinksByPlatform: {},
bio: null,
languageProficiencies: [],
courseCertificates: [],
requiresParentalConsent: null,
dateJoined: null,
visibilityName: null,
visibilityCountry: null,
visibilityLevelOfEducation: null,
visibilitySocialLinks: null,
visibilityLanguageProficiencies: null,
visibilityBio: null,
isLoadingProfile: false,
};
export default withParams(ProfilePage);

View File

@@ -1,26 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.viewMyRecords': {
id: 'profile.viewMyRecords',
defaultMessage: 'View My Records',
description: 'A link to go view my academic records',
},
'profile.loading': {
id: 'profile.loading',
defaultMessage: 'Profile loading...',
description: 'Message displayed when the profile data is loading.',
},
'profile.username': {
id: 'profile.username',
defaultMessage: 'Username',
description: 'Label for the username field.',
},
'profile.username.tooltip': {
id: 'profile.username.tooltip',
defaultMessage: 'The name that identifies you on edX. You cannot change your username.',
description: 'Tooltip for the username field.',
},
});
export default messages;

View File

@@ -1,516 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import * as analytics from '@edx/frontend-platform/analytics';
import { AppContext } from '@edx/frontend-platform/react';
import { configure as configureI18n, IntlProvider } from '@edx/frontend-platform/i18n';
import { render } from '@testing-library/react';
import React from 'react';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import {
MemoryRouter,
Routes,
Route,
useNavigate,
} from 'react-router-dom';
import messages from '../i18n';
import ProfilePage from './ProfilePage';
import loadingApp from './__mocks__/loadingApp.mockStore';
import viewOwnProfile from './__mocks__/viewOwnProfile.mockStore';
import viewOtherProfile from './__mocks__/viewOtherProfile.mockStore';
import invalidUser from './__mocks__/invalidUser.mockStore';
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn(),
}));
const mockStore = configureMockStore([thunk]);
const storeMocks = {
loadingApp,
viewOwnProfile,
viewOtherProfile,
invalidUser,
};
const requiredProfilePageProps = {
params: { username: 'staff' },
};
Object.defineProperty(global.document, 'cookie', {
writable: true,
value: `${getConfig().LANGUAGE_PREFERENCE_COOKIE_NAME}=en`,
});
jest.mock('@edx/frontend-platform/auth', () => ({
configure: () => {},
getAuthenticatedUser: () => null,
fetchAuthenticatedUser: () => null,
getAuthenticatedHttpClient: jest.fn(),
AUTHENTICATED_USER_CHANGED: 'user_changed',
}));
jest.mock('@edx/frontend-platform/analytics', () => ({
configure: () => {},
identifyAnonymousUser: jest.fn(),
identifyAuthenticatedUser: jest.fn(),
sendTrackingLogEvent: jest.fn(),
}));
configureI18n({
loggingService: { logError: jest.fn() },
config: {
ENVIRONMENT: 'production',
LANGUAGE_PREFERENCE_COOKIE_NAME: 'yum',
},
messages,
});
beforeEach(() => {
analytics.sendTrackingLogEvent.mockReset();
useNavigate.mockReset();
});
const ProfilePageWrapper = ({
contextValue, store, params,
}) => (
<AppContext.Provider value={contextValue}>
<IntlProvider locale="en">
<Provider store={store}>
<MemoryRouter initialEntries={[`/profile/${params.username}`]}>
<Routes>
<Route
path="/profile/:username"
element={<ProfilePage {...requiredProfilePageProps} params={params} />}
/>
</Routes>
</MemoryRouter>
</Provider>
</IntlProvider>
</AppContext.Provider>
);
ProfilePageWrapper.defaultProps = {
// eslint-disable-next-line react/default-props-match-prop-types
params: { username: 'staff' },
};
ProfilePageWrapper.propTypes = {
contextValue: PropTypes.shape({}).isRequired,
store: PropTypes.shape({}).isRequired,
params: PropTypes.shape({
username: PropTypes.string.isRequired,
}).isRequired,
};
describe('<ProfilePage />', () => {
describe('Renders correctly in various states', () => {
it('app loading', () => {
const contextValue = {
authenticatedUser: { userId: null, username: null, administrator: false },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.loadingApp)}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
it('viewing own profile', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOwnProfile)}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
it('viewing other profile with all fields', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore({
...storeMocks.viewOtherProfile,
profilePage: {
...storeMocks.viewOtherProfile.profilePage,
account: {
...storeMocks.viewOtherProfile.profilePage.account,
name: 'Verified User',
country: 'US',
bio: 'About me',
courseCertificates: [{ title: 'Course 1' }],
levelOfEducation: 'bachelors',
languageProficiencies: [{ code: 'en' }],
socialLinks: [{ platform: 'twitter', socialLink: 'https://twitter.com/user' }],
},
preferences: {
...storeMocks.viewOtherProfile.profilePage.preferences,
visibilityName: 'all_users',
visibilityCountry: 'all_users',
visibilityLevelOfEducation: 'all_users',
visibilityLanguageProficiencies: 'all_users',
visibilitySocialLinks: 'all_users',
visibilityBio: 'all_users',
},
},
})}
params={{ username: 'verified' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
it('without credentials service', () => {
const config = getConfig();
config.CREDENTIALS_BASE_URL = '';
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOwnProfile)}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
it('successfully redirected to not found page', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const navigate = jest.fn();
useNavigate.mockReturnValue(navigate);
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.invalidUser)}
params={{ username: 'staffTest' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
expect(navigate).toHaveBeenCalledWith('/notfound');
});
});
describe('handles analytics', () => {
it('calls sendTrackingLogEvent when mounting', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
render(
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.loadingApp)}
params={{ username: 'test-username' }}
/>,
);
expect(analytics.sendTrackingLogEvent).toHaveBeenCalledTimes(1);
expect(analytics.sendTrackingLogEvent).toHaveBeenCalledWith('edx.profile.viewed', {
username: 'test-username',
});
});
});
describe('handles navigation', () => {
it('navigates to notfound on save error with no username', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const navigate = jest.fn();
useNavigate.mockReturnValue(navigate);
render(
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.invalidUser)}
params={{ username: 'staffTest' }}
/>,
);
expect(navigate).toHaveBeenCalledWith('/notfound');
});
});
describe('form fields', () => {
it('renders all form fields for own profile', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const { getByText } = render(
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOwnProfile)}
/>,
);
expect(getByText('Full name')).toBeInTheDocument();
expect(getByText('Country')).toBeInTheDocument();
expect(getByText('Bio')).toBeInTheDocument();
expect(getByText('Education')).toBeInTheDocument();
expect(getByText('Primary language spoken')).toBeInTheDocument();
});
});
describe('handles invalid user', () => {
it('navigates to not found page for invalid user', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const navigate = jest.fn();
useNavigate.mockReturnValue(navigate);
render(
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.invalidUser)}
params={{ username: 'invalidUser' }}
/>,
);
expect(navigate).toHaveBeenCalledWith('/notfound');
});
});
describe('handles empty profile', () => {
it('renders empty profile state', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'empty' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only username', () => {
it('renders profile with only username', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyUsername' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with no social links', () => {
it('renders profile without social links', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'noSocialLinks' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only social links', () => {
it('renders profile with only social links', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlySocialLinks' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only bio', () => {
it('renders profile with only bio', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyBio' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only country', () => {
it('renders profile with only country', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyCountry' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only level of education', () => {
it('renders profile with only level of education', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyLevelOfEducation' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only language proficiencies', () => {
it('renders profile with only language proficiencies', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyLanguageProficiencies' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only course certificates', () => {
it('renders profile with only course certificates', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyCourseCertificates' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only name', () => {
it('renders profile with only name', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyName' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with only username and no other fields', () => {
it('renders profile with only username', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: 'onlyUsernameNoFields' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
describe('handles profile with no fields and no username', () => {
it('renders empty profile state', () => {
const contextValue = {
authenticatedUser: { userId: 123, username: 'staff', administrator: true },
config: getConfig(),
};
const component = (
<ProfilePageWrapper
contextValue={contextValue}
store={mockStore(storeMocks.viewOtherProfile)}
params={{ username: '' }}
/>
);
const { container: tree } = render(component);
expect(tree).toMatchSnapshot();
});
});
});

View File

@@ -1,27 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
const UserCertificateSummary = ({ count = 0 }) => {
if (count) {
return (
<span className="small m-0 text-gray-800">
<FormattedMessage
id="profile.certificatecount"
defaultMessage="{certificate_count} certifications"
description="A label for many certificates a user has"
values={{
certificate_count: <span className="font-weight-bold">{count}</span>,
}}
/>
</span>
);
}
return null;
};
UserCertificateSummary.propTypes = {
count: PropTypes.number,
};
export default UserCertificateSummary;

View File

@@ -1,42 +0,0 @@
module.exports = {
userAccount: {
loading: false,
error: null,
username: 'staff',
email: null,
bio: null,
name: null,
country: null,
socialLinks: null,
profileImage: {
imageUrlMedium: null,
imageUrlLarge: null
},
levelOfEducation: null,
learningGoal: null
},
profilePage: {
errors: {},
saveState: 'error',
savePhotoState: null,
currentlyEditingField: null,
account: {
username: '',
socialLinks: []
},
preferences: {},
courseCertificates: [],
drafts: {},
isLoadingProfile: false,
isAuthenticatedUserProfile: true,
countriesCodesList: ['US', 'CA', 'GB', 'ME']
},
router: {
location: {
pathname: '/u/staffTest',
search: '',
hash: ''
},
action: 'POP'
}
};

View File

@@ -1,42 +0,0 @@
module.exports = {
userAccount: {
loading: false,
error: null,
username: 'staff',
email: null,
bio: null,
name: null,
country: null,
socialLinks: null,
profileImage: {
imageUrlMedium: null,
imageUrlLarge: null
},
levelOfEducation: null,
learningGoal: null
},
profilePage: {
errors: {},
saveState: null,
savePhotoState: null,
currentlyEditingField: null,
account: {
username: 'staff',
socialLinks: []
},
preferences: {},
courseCertificates: [],
drafts: {},
isLoadingProfile: true,
isAuthenticatedUserProfile: true,
countriesCodesList: ['US', 'CA', 'GB', 'ME']
},
router: {
location: {
pathname: '/u/staff',
search: '',
hash: ''
},
action: 'POP'
}
};

View File

@@ -1,139 +0,0 @@
module.exports = {
userAccount: {
loading: false,
error: null,
username: 'staff',
email: 'staff@example.com',
bio: 'This is my bio',
name: 'Lemon Seltzer',
country: 'ME',
socialLinks: [
{
platform: 'facebook',
socialLink: 'https://www.facebook.com/aloha'
},
{
platform: 'twitter',
socialLink: 'https://www.twitter.com/ALOHA'
}
],
profileImage: {
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
hasImage: true
},
levelOfEducation: 'el',
mailingAddress: null,
extendedProfile: [],
dateJoined: '2017-06-07T00:44:23Z',
accomplishmentsShared: false,
isActive: true,
yearOfBirth: 1901,
goals: null,
languageProficiencies: [
{
code: 'yo'
}
],
courseCertificates: null,
requiresParentalConsent: false,
secondaryEmail: null,
timeZone: null,
gender: null,
accountPrivacy: 'custom',
learningGoal: null,
},
profilePage: {
errors: {},
saveState: 'pending',
savePhotoState: null,
currentlyEditingField: 'bio',
isAuthenticatedUserProfile: true,
account: {
mailingAddress: null,
profileImage: {
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
hasImage: true
},
extendedProfile: [],
dateJoined: '2017-06-07T00:44:23Z',
accomplishmentsShared: false,
email: 'staff@example.com',
username: 'staff',
bio: 'This is my bio',
isActive: true,
yearOfBirth: 1901,
goals: null,
languageProficiencies: [
{
code: 'yo'
}
],
courseCertificates: null,
requiresParentalConsent: false,
name: 'Lemon Seltzer',
secondaryEmail: null,
country: 'ME',
socialLinks: [
{
platform: 'facebook',
socialLink: 'https://www.facebook.com/aloha'
},
{
platform: 'twitter',
socialLink: 'https://www.twitter.com/ALOHA'
}
],
timeZone: null,
levelOfEducation: 'el',
gender: null,
accountPrivacy: 'custom',
learningGoal: null,
},
preferences: {
visibilityUserLocation: 'all_users',
visibilitySocialLinks: 'all_users',
visibilityCertificates: 'private',
visibilityLevelOfEducation: 'private',
visibilityCourseCertificates: 'all_users',
prefLang: 'en',
visibilityBio: 'all_users',
visibilityName: 'private',
visibilityLanguageProficiencies: 'all_users',
visibilityCountry: 'all_users',
accountPrivacy: 'custom',
visibilityLearningGoal: 'private',
},
courseCertificates: [
{
username: 'staff',
status: 'downloadable',
courseDisplayName: 'edX Demonstration Course',
grade: '0.89',
courseId: 'course-v1:edX+DemoX+Demo_Course',
courseOrganization: 'edX',
modifiedDate: '2019-03-04T19:31:39.930255Z',
isPassing: true,
downloadUrl: 'http://www.example.com/',
certificateType: 'verified',
createdDate: '2019-03-04T19:31:39.896806Z'
}
],
drafts: {},
isLoadingProfile: false,
disabledCountries: [],
},
router: {
location: {
pathname: '/u/staff',
search: '',
hash: ''
},
action: 'POP'
}
};

View File

@@ -1,105 +0,0 @@
module.exports = {
userAccount: {
loading: false,
error: null,
username: 'staff',
email: 'staff@example.com',
bio: 'This is my bio',
name: 'Lemon Seltzer',
country: 'ME',
socialLinks: [
{
platform: 'facebook',
socialLink: 'https://www.facebook.com/aloha'
},
{
platform: 'twitter',
socialLink: 'https://www.twitter.com/ALOHA'
}
],
profileImage: {
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
hasImage: true
},
levelOfEducation: 'el',
mailingAddress: null,
extendedProfile: [],
dateJoined: '2017-06-07T00:44:23Z',
accomplishmentsShared: false,
isActive: true,
yearOfBirth: 1901,
goals: null,
languageProficiencies: [
{
code: 'yo'
}
],
courseCertificates: null,
requiresParentalConsent: false,
secondaryEmail: null,
timeZone: null,
gender: null,
accountPrivacy: 'custom',
learningGoal: 'advance_career',
},
profilePage: {
errors: {},
saveState: null,
savePhotoState: null,
currentlyEditingField: null,
isAuthenticatedUserProfile: false,
account: {
mailingAddress: null,
profileImage: {
imageUrlFull: 'http://localhost:18000/static/images/profiles/default_500.png',
imageUrlLarge: 'http://localhost:18000/static/images/profiles/default_120.png',
imageUrlMedium: 'http://localhost:18000/static/images/profiles/default_50.png',
imageUrlSmall: 'http://localhost:18000/static/images/profiles/default_30.png',
hasImage: false
},
extendedProfile: [],
dateJoined: '2017-06-07T00:44:19Z',
accomplishmentsShared: false,
email: 'verified@example.com',
username: 'verified',
bio: null,
isActive: true,
yearOfBirth: null,
goals: null,
languageProficiencies: [],
courseCertificates: null,
requiresParentalConsent: true,
name: '',
secondaryEmail: null,
country: null,
socialLinks: [],
timeZone: null,
levelOfEducation: null,
gender: null,
accountPrivacy: 'private'
},
preferences: {
visibilityName: 'all_users',
visibilityCountry: 'all_users',
visibilityLevelOfEducation: 'all_users',
visibilityLanguageProficiencies: 'all_users',
visibilitySocialLinks: 'all_users',
visibilityBio: 'all_users'
},
courseCertificates: [],
drafts: {},
isLoadingProfile: false,
countriesCodesList: ['US', 'CA', 'GB', 'ME']
},
router: {
location: {
pathname: '/u/verified',
search: '',
hash: ''
},
action: 'POP'
}
};

View File

@@ -1,139 +0,0 @@
module.exports = {
userAccount: {
loading: false,
error: null,
username: 'staff',
email: 'staff@example.com',
bio: 'This is my bio',
name: 'Lemon Seltzer',
country: 'ME',
socialLinks: [
{
platform: 'facebook',
socialLink: 'https://www.facebook.com/aloha'
},
{
platform: 'twitter',
socialLink: 'https://www.twitter.com/ALOHA'
}
],
profileImage: {
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
hasImage: true
},
levelOfEducation: 'el',
mailingAddress: null,
extendedProfile: [],
dateJoined: '2017-06-07T00:44:23Z',
accomplishmentsShared: false,
isActive: true,
yearOfBirth: 1901,
goals: null,
languageProficiencies: [
{
code: 'yo'
}
],
courseCertificates: null,
requiresParentalConsent: false,
secondaryEmail: null,
timeZone: null,
gender: null,
accountPrivacy: 'custom',
learningGoal: 'advance_career'
},
profilePage: {
errors: {},
saveState: null,
savePhotoState: null,
currentlyEditingField: null,
isAuthenticatedUserProfile: true,
account: {
mailingAddress: null,
profileImage: {
imageUrlFull: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012',
imageUrlLarge: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_120.jpg?v=1552495012',
imageUrlMedium: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_50.jpg?v=1552495012',
imageUrlSmall: 'http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_30.jpg?v=1552495012',
hasImage: true
},
extendedProfile: [],
dateJoined: '2017-06-07T00:44:23Z',
accomplishmentsShared: false,
email: 'staff@example.com',
username: 'staff',
bio: 'This is my bio',
isActive: true,
yearOfBirth: 1901,
goals: null,
languageProficiencies: [
{
code: 'yo'
}
],
courseCertificates: null,
requiresParentalConsent: false,
name: 'Lemon Seltzer',
secondaryEmail: null,
country: 'ME',
socialLinks: [
{
platform: 'facebook',
socialLink: 'https://www.facebook.com/aloha'
},
{
platform: 'twitter',
socialLink: 'https://www.twitter.com/ALOHA'
}
],
timeZone: null,
levelOfEducation: 'el',
gender: null,
accountPrivacy: 'custom',
learningGoal: 'advance_career'
},
preferences: {
visibilityUserLocation: 'all_users',
visibilitySocialLinks: 'all_users',
visibilityCertificates: 'private',
visibilityLevelOfEducation: 'private',
visibilityCourseCertificates: 'all_users',
prefLang: 'en',
visibilityBio: 'all_users',
visibilityName: 'private',
visibilityLanguageProficiencies: 'all_users',
visibilityCountry: 'all_users',
accountPrivacy: 'custom',
visibilityLearningGoal: 'private',
},
courseCertificates: [
{
username: 'staff',
status: 'downloadable',
courseDisplayName: 'edX Demonstration Course',
grade: '0.89',
courseId: 'course-v1:edX+DemoX+Demo_Course',
courseOrganization: 'edX',
modifiedDate: '2019-03-04T19:31:39.930255Z',
isPassing: true,
downloadUrl: 'http://www.example.com/',
certificateType: 'verified',
createdDate: '2019-03-04T19:31:39.896806Z'
}
],
drafts: {},
isLoadingProfile: false,
countriesCodesList: ['US', 'CA', 'GB', 'ME']
},
router: {
location: {
pathname: '/u/staff',
search: '',
hash: ''
},
action: 'POP'
}
};

View File

@@ -1,29 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotFoundPage Snapshot Tests renders correctly 1`] = `
<DocumentFragment>
<div
class="container-fluid d-flex py-5 justify-content-center align-items-start text-center"
>
<p
class="my-0 py-5 text-muted max-width-32em"
>
The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.
</p>
</div>
</DocumentFragment>
`;
exports[`NotFoundPage Snapshot Tests renders with custom props 1`] = `
<DocumentFragment>
<div
class="container-fluid d-flex py-5 justify-content-center align-items-start text-center"
>
<p
class="my-0 py-5 text-muted max-width-32em"
>
The page you're looking for is unavailable or there's an error in the URL. Please check the URL and try again.
</p>
</div>
</DocumentFragment>
`;

View File

@@ -1,3671 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ProfilePage /> Renders correctly in various states app loading 1`] = `
<div>
<div
class="profile-page"
>
<div>
<div
class="d-flex justify-content-center align-items-center flex-column height-50vh"
>
<div
class="spinner-border text-primary"
role="status"
>
<span
class="sr-only"
>
Profile loading...
</span>
</div>
</div>
</div>
</div>
</div>
`;
exports[`<ProfilePage /> Renders correctly in various states successfully redirected to not found page 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
staffTest
</p>
<div
class="row pt-2 m-0 g-1rem"
/>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
staffTest
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> Renders correctly in various states viewing other profile with all fields 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
verified
</p>
<p
class="row pt-2 text-gray-800 font-weight-normal m-0 p"
>
Verified User
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
verified
</h4>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Full name
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Verified User
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
/>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Country
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
United States of America
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
/>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Primary language spoken
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
English
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
/>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Education
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Other education
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
/>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
>
<div
class="pgn-transition-replace-group position-relative pt-0"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Bio
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
About me
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
/>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative p-0"
>
<div
style="padding: .1px 0px;"
>
<div>
<div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
X
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
https://twitter.com/user
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
/>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> Renders correctly in various states viewing own profile 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<img
alt="profile avatar"
class="w-100 h-100 d-block rounded-circle overflow-hidden object-fit-cover"
data-hj-suppress="true"
src="http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012"
/>
</div>
<div
class="profile-avatar-button"
>
<div
class="pgn__dropdown pgn__dropdown-light dropdown"
data-testid="dropdown"
>
<button
aria-expanded="false"
aria-haspopup="true"
class="btn-icon btn-icon-inverse-primary btn-icon-md btn-icon-inverse-primary-active shadow-sm pgn__dropdown-toggle-iconbutton"
id="dropdown-toggle-with-iconbutton"
type="button"
>
<span
class="btn-icon__icon-container"
>
<span
class="pgn__icon btn-icon__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"
fill="currentColor"
/>
<path
d="M9 2 7.17 4H2v16h20V4h-5.17L15 2H9Zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5Z"
fill="currentColor"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
staff
</p>
<p
class="row pt-2 text-gray-800 font-weight-normal m-0 p"
>
Lemon Seltzer
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
<span
class="small m-0 text-gray-800"
>
<span
class="font-weight-bold"
>
1
</span>
certifications
</span>
</div>
</div>
<div
class="p-0 col-auto"
>
<a
class="pgn__hyperlink default-link standalone-link btn btn-brand bg-brand-500 btn-rounded font-weight-normal px-4 py-10px text-nowrap"
href="http://localhost:18150/records"
rel="noopener noreferrer"
target="_blank"
>
View My Records
</a>
</div>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
staff
</h4>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Full name
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Lemon Seltzer
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Country
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Montenegro
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Primary language spoken
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Yoruba
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Education
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Elementary/primary school
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
>
<div
class="pgn-transition-replace-group position-relative pt-0"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Bio
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
This is my bio
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative p-0"
>
<div
style="padding: .1px 0px;"
>
<div>
<div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
X
</p>
<div
class="w-100 overflowWrap-breakWord"
>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
https://www.twitter.com/ALOHA
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Facebook
</p>
<div
class="w-100 overflowWrap-breakWord"
>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
https://www.facebook.com/aloha
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
LinkedIn
</p>
<div
class="p-0 m-0"
>
<button
class="p-0 text-left btn btn-link lh-36px"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-plus fa-xs mr-1"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
fill="currentColor"
/>
</svg>
Add
LinkedIn
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div>
<div
class="col justify-content-start align-items-start g-5rem p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Your certificates
</p>
</div>
<div
class="col justify-content-start align-items-start pt-2 p-0"
>
<p
class="font-weight-normal text-gray-800 m-0 p-0 p"
>
Your learner records information is only visible to you. Only your username and profile image are visible to others on localhost.
</p>
</div>
</div>
<div
class="col"
>
<div
class="row align-items-center pt-5 g-3rem"
>
<div
class="col-auto d-flex align-items-center p-0"
>
<div
class="col certificate p-4 border-light-400 bg-light-200 w-100 h-100"
>
<div
class="certificate-type-illustration"
style="background-image: url(icon/mock/path);"
/>
<div
class="d-flex flex-column position-relative p-0 width-314px"
>
<div
class="w-100 color-black"
>
<p
class="mb-0 font-weight-normal small"
>
Verified Certificate
</p>
<p
class="m-0 color-black h4"
>
edX Demonstration Course
</p>
<p
class="mb-0 small"
>
From
</p>
<h5
class="mb-0 color-black"
>
edX
</h5>
<p
class="mb-0 small"
>
Completed on
3/4/2019
</p>
</div>
<div
class="pt-3"
>
<a
class="pgn__hyperlink default-link standalone-link btn btn-primary btn-rounded font-weight-normal px-4 py-10px"
href="http://www.example.com/"
rel="noopener noreferrer"
target="_blank"
>
View Certificate
</a>
</div>
<p
class="mb-0 pt-3 small"
>
Credential ID
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`<ProfilePage /> Renders correctly in various states without credentials service 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<img
alt="profile avatar"
class="w-100 h-100 d-block rounded-circle overflow-hidden object-fit-cover"
data-hj-suppress="true"
src="http://localhost:18000/media/profile-images/d2a9bdc2ba165dcefc73265c54bf9a20_500.jpg?v=1552495012"
/>
</div>
<div
class="profile-avatar-button"
>
<div
class="pgn__dropdown pgn__dropdown-light dropdown"
data-testid="dropdown"
>
<button
aria-expanded="false"
aria-haspopup="true"
class="btn-icon btn-icon-inverse-primary btn-icon-md btn-icon-inverse-primary-active shadow-sm pgn__dropdown-toggle-iconbutton"
id="dropdown-toggle-with-iconbutton"
type="button"
>
<span
class="btn-icon__icon-container"
>
<span
class="pgn__icon btn-icon__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6Z"
fill="currentColor"
/>
<path
d="M9 2 7.17 4H2v16h20V4h-5.17L15 2H9Zm3 15c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5Z"
fill="currentColor"
/>
</svg>
</span>
</span>
</button>
</div>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
staff
</p>
<p
class="row pt-2 text-gray-800 font-weight-normal m-0 p"
>
Lemon Seltzer
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
<span
class="small m-0 text-gray-800"
>
<span
class="font-weight-bold"
>
1
</span>
certifications
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
staff
</h4>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Full name
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Lemon Seltzer
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Country
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Montenegro
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Primary language spoken
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Yoruba
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative pt-40px"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Education
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
Elementary/primary school
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
>
<div
class="pgn-transition-replace-group position-relative pt-0"
>
<div
style="padding: .1px 0px;"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Bio
</p>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
This is my bio
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pgn-transition-replace-group position-relative p-0"
>
<div
style="padding: .1px 0px;"
>
<div>
<div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
X
</p>
<div
class="w-100 overflowWrap-breakWord"
>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
https://www.twitter.com/ALOHA
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
Facebook
</p>
<div
class="w-100 overflowWrap-breakWord"
>
<div
class="row m-0 p-0 d-flex flex-nowrap align-items-center"
>
<div
class="m-0 p-0 col-auto"
>
<h4
class="edit-section-header text-gray-700"
>
https://www.facebook.com/aloha
</h4>
</div>
<div
class="col-auto m-0 p-0 d-flex align-items-center col-auto"
>
<button
class="p-1.5 btn btn-link btn-sm"
type="button"
>
<svg
class="text-gray-700"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m14.06 9.02.92.92L5.92 19H5v-.92l9.06-9.06ZM17.66 3c-.25 0-.51.1-.7.29l-1.83 1.83 3.75 3.75 1.83-1.83a.996.996 0 0 0 0-1.41l-2.34-2.34c-.2-.2-.45-.29-.71-.29Zm-3.6 3.19L3 17.25V21h3.75L17.81 9.94l-3.75-3.75Z"
fill="currentColor"
/>
</svg>
</button>
</div>
</div>
<div
class="row m-0 p-0"
/>
</div>
</div>
<div
class="pt-40px"
>
<p
class="h5 font-weight-bold m-0 pb-1.5"
data-hj-suppress="true"
>
LinkedIn
</p>
<div
class="p-0 m-0"
>
<button
class="p-0 text-left btn btn-link lh-36px"
tabindex="0"
type="button"
>
<svg
aria-hidden="true"
class="svg-inline--fa fa-plus fa-xs mr-1"
data-icon="plus"
data-prefix="fas"
focusable="false"
role="img"
viewBox="0 0 448 512"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M256 80c0-17.7-14.3-32-32-32s-32 14.3-32 32l0 144L48 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l144 0 0 144c0 17.7 14.3 32 32 32s32-14.3 32-32l0-144 144 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-144 0 0-144z"
fill="currentColor"
/>
</svg>
Add
LinkedIn
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div>
<div
class="col justify-content-start align-items-start g-5rem p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Your certificates
</p>
</div>
<div
class="col justify-content-start align-items-start pt-2 p-0"
>
<p
class="font-weight-normal text-gray-800 m-0 p-0 p"
>
Your learner records information is only visible to you. Only your username and profile image are visible to others on localhost.
</p>
</div>
</div>
<div
class="col"
>
<div
class="row align-items-center pt-5 g-3rem"
>
<div
class="col-auto d-flex align-items-center p-0"
>
<div
class="col certificate p-4 border-light-400 bg-light-200 w-100 h-100"
>
<div
class="certificate-type-illustration"
style="background-image: url(icon/mock/path);"
/>
<div
class="d-flex flex-column position-relative p-0 width-314px"
>
<div
class="w-100 color-black"
>
<p
class="mb-0 font-weight-normal small"
>
Verified Certificate
</p>
<p
class="m-0 color-black h4"
>
edX Demonstration Course
</p>
<p
class="mb-0 small"
>
From
</p>
<h5
class="mb-0 color-black"
>
edX
</h5>
<p
class="mb-0 small"
>
Completed on
3/4/2019
</p>
</div>
<div
class="pt-3"
>
<a
class="pgn__hyperlink default-link standalone-link btn btn-primary btn-rounded font-weight-normal px-4 py-10px"
href="http://www.example.com/"
rel="noopener noreferrer"
target="_blank"
>
View Certificate
</a>
</div>
<p
class="mb-0 pt-3 small"
>
Credential ID
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`<ProfilePage /> handles empty profile renders empty profile state 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
empty
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
empty
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with no fields and no username renders empty profile state 1`] = `<div />`;
exports[`<ProfilePage /> handles profile with no social links renders profile without social links 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
noSocialLinks
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
noSocialLinks
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only bio renders profile with only bio 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyBio
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyBio
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only country renders profile with only country 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyCountry
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyCountry
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only course certificates renders profile with only course certificates 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyCourseCertificates
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyCourseCertificates
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only language proficiencies renders profile with only language proficiencies 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyLanguageProficiencies
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyLanguageProficiencies
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only level of education renders profile with only level of education 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyLevelOfEducation
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyLevelOfEducation
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only name renders profile with only name 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyName
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyName
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only social links renders profile with only social links 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlySocialLinks
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlySocialLinks
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only username and no other fields renders profile with only username 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyUsernameNoFields
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyUsernameNoFields
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;
exports[`<ProfilePage /> handles profile with only username renders profile with only username 1`] = `
<div>
<div
class="profile-page"
>
<div
class="profile-page-bg-banner bg-primary d-md-block align-items-center h-100 w-100 px-120px py-5.5"
>
<div
class="col container-fluid w-100 h-100 bg-white py-0 rounded-75 px-40px"
>
<div
class="col h-100 w-100 px-0 justify-content-start g-15rem py-36px"
>
<div
class="row-auto d-flex flex-wrap align-items-center h-100 w-100 justify-content-start g-15rem flex-row"
>
<div
class="profile-avatar-wrap position-relative"
>
<div
class="profile-avatar rounded-circle bg-light"
>
<div
aria-hidden="true"
class="text-muted"
data-testid="IconMock"
focusable="false"
role="img"
viewbox="0 0 24 24"
/>
</div>
<form
enctype="multipart/form-data"
>
<input
accept=".jpg, .jpeg, .png"
class="d-none form-control-file"
id="photo-file"
name="file"
type="file"
/>
</form>
</div>
<div
class="col h-100 w-100 m-0 p-0 justify-content-start align-items-start"
>
<p
class="row m-0 font-weight-bold text-truncate text-primary-500 h3"
>
onlyUsername
</p>
<div
class="row pt-2 m-0 g-1rem"
>
<span
class="small mb-0 text-gray-800"
>
Member since
<span
class="font-weight-bold"
>
2017
</span>
</span>
</div>
</div>
<div
class="p-0 col-auto"
/>
</div>
</div>
<div
class="ml-auto"
/>
</div>
</div>
<div
class="col d-inline-flex h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
>
<div
class="w-100 p-0"
>
<div
class="col justify-content-start align-items-start p-0"
>
<div
class="col align-self-stretch height-42px justify-content-start align-items-start p-0"
>
<p
class="font-weight-bold text-primary-500 m-0 h2"
>
Profile information
</p>
</div>
</div>
<div
class="row m-0 px-0 w-100 d-inline-flex align-items-start justify-content-start pt-5.5"
>
<div
class="col p-0 col-6"
>
<div
class="m-0"
>
<div
class="row m-0 pb-1.5 align-items-center"
>
<p
class="h5 font-weight-bold m-0"
data-hj-suppress="true"
>
Username
</p>
<svg
class="m-0 info-icon"
fill="none"
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M11 7h2v2h-2V7Zm0 4h2v6h-2v-6Zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2Zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8Z"
fill="currentColor"
/>
</svg>
</div>
<h4
class="edit-section-header text-gray-700"
>
onlyUsername
</h4>
</div>
</div>
<div
class="col m-0 pr-0 pl-40px col-6"
/>
</div>
</div>
</div>
<div
class="col container-fluid d-inline-flex bg-color-grey-FBFAF9 h-100 w-100 align-items-start justify-content-start g-3rem px-120px py-6"
/>
</div>
</div>
`;

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
<title>avatar</title>
<desc>Created with Sketch.</desc>
<g id="avatar" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M4.10255106,18.1351061 C4.7170266,16.0581859 8.01891846,14.4720277 12,14.4720277 C15.9810815,14.4720277 19.2829734,16.0581859 19.8974489,18.1351061 C21.215206,16.4412566 22,14.3122775 22,12 C22,6.4771525 17.5228475,2 12,2 C6.4771525,2 2,6.4771525 2,12 C2,14.3122775 2.78479405,16.4412566 4.10255106,18.1351061 Z M12,24 C5.372583,24 0,18.627417 0,12 C0,5.372583 5.372583,0 12,0 C18.627417,0 24,5.372583 24,12 C24,18.627417 18.627417,24 12,24 Z M12,13 C9.790861,13 8,11.209139 8,9 C8,6.790861 9.790861,5 12,5 C14.209139,5 16,6.790861 16,9 C16,11.209139 14.209139,13 12,13 Z" fill="currentColor"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1006 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="110px" height="100px" viewBox="0 0 110 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 53.2 (72643) - https://sketchapp.com -->
<title>micro-masters</title>
<desc>Created with Sketch.</desc>
<g id="micro-masters" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<polygon id="Fill-14" fill="#005585" points="1.00481933 99 1 96.14832 108.995272 96 109 98.85168"></polygon>
<polygon id="Fill-16" fill="#005585" points="1.00481933 54 1 51.14832 108.995272 51 109 53.85168"></polygon>
<polygon id="Fill-18" fill="#005585" points="24.9998189 88 4.22751315 66.9102771 4.22751315 87.0451102 1 87.0451102 1 59 24.9998189 83.3666894 49 59 49 87.0451102 45.7724869 87.0451102 45.7724869 66.9102771"></polygon>
<polygon id="Fill-20" fill="#005585" points="83.0003622 88 62.2275375 66.9102771 62.2275375 87.0451102 59 87.0451102 59 59 83.0003622 83.3666894 107 59 107 87.0451102 103.772372 87.0451102 103.772372 66.9102771"></polygon>
<path d="M46.8337141,5.73372796 C40.9959838,9.0009662 32.7599012,14.7887333 26.6032377,23.8303431 C22.1430738,22.980271 18.7577893,22.0941759 16.6724707,21.4890245 C24.4296997,13.0587208 35.0333668,7.38581116 46.8337141,5.73372796 L46.8337141,5.73372796 Z M52.9498983,26.3252576 C44.0609738,26.2888724 36.3049246,25.4607944 30.1183101,24.4518768 C37.8086485,13.8504137 48.4460785,8.26376038 52.9498983,6.25307565 L52.9498983,26.3252576 Z M56.1856092,6.53148511 C60.9824046,8.72165163 70.9689895,14.1856636 78.3079931,24.2371866 C70.4533778,25.5777336 63.0182586,26.1827945 56.1856092,26.3036256 L56.1856092,6.53148511 Z M91.2530147,21.4114572 C88.0536989,22.2655118 84.9019418,22.9909512 81.8101774,23.5971888 C75.749448,14.7672824 67.7047798,9.0930151 61.9038076,5.84261176 C73.3677956,7.61959908 83.6636931,13.2007313 91.2530147,21.4114572 L91.2530147,21.4114572 Z M5.35796556,42 C7.1655563,35.3542865 10.2683027,29.3264862 14.378947,24.1676747 C16.125002,24.7123653 19.7105047,25.7399279 24.7732291,26.7529184 C22.2125965,31.1374149 20.1687556,36.1942214 19.0530339,42 L22.3555447,42 C23.5090228,36.4059248 25.5817257,31.5545765 28.1556093,27.3837747 C34.7214222,28.5242036 43.1689773,29.5005374 52.9498983,29.5419006 L52.9498983,42 L56.1856092,42 L56.1856092,29.5223504 C63.6041191,29.395636 71.7140444,28.7101116 80.2902074,27.16121 C82.9053872,31.3760904 85.0043199,36.3025621 86.1572534,42 L89.460218,42 C88.3437702,36.0858806 86.2630805,30.9538601 83.6533464,26.5180443 C86.9031251,25.8529752 90.216981,25.0530458 93.5773971,24.1181656 C97.7090979,29.2882909 100.82809,35.3331976 102.641853,42 L106,42 C100.00834,18.4385583 78.6589648,2 53.9991832,2 C29.3299624,2 7.9913882,18.4352094 2,42 L5.35796556,42 Z" id="Fill-22" fill="#005585"></path>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,137 +0,0 @@
import { AsyncActionType } from '../utils';
export const FETCH_PROFILE = new AsyncActionType('PROFILE', 'FETCH_PROFILE');
export const SAVE_PROFILE = new AsyncActionType('PROFILE', 'SAVE_PROFILE');
export const SAVE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'SAVE_PROFILE_PHOTO');
export const DELETE_PROFILE_PHOTO = new AsyncActionType('PROFILE', 'DELETE_PROFILE_PHOTO');
export const OPEN_FORM = 'OPEN_FORM';
export const CLOSE_FORM = 'CLOSE_FORM';
export const UPDATE_DRAFT = 'UPDATE_DRAFT';
export const RESET_DRAFTS = 'RESET_DRAFTS';
export const fetchProfile = username => ({
type: FETCH_PROFILE.BASE,
payload: { username },
});
export const fetchProfileBegin = () => ({
type: FETCH_PROFILE.BEGIN,
});
export const fetchProfileSuccess = (
account,
preferences,
courseCertificates,
isAuthenticatedUserProfile,
) => ({
type: FETCH_PROFILE.SUCCESS,
account,
preferences,
courseCertificates,
isAuthenticatedUserProfile,
});
export const fetchProfileReset = () => ({
type: FETCH_PROFILE.RESET,
});
export const saveProfile = (formId, username) => ({
type: SAVE_PROFILE.BASE,
payload: {
formId,
username,
},
});
export const saveProfileBegin = () => ({
type: SAVE_PROFILE.BEGIN,
});
export const saveProfileSuccess = (account, preferences) => ({
type: SAVE_PROFILE.SUCCESS,
payload: {
account,
preferences,
},
});
export const saveProfileReset = () => ({
type: SAVE_PROFILE.RESET,
});
export const saveProfileFailure = errors => ({
type: SAVE_PROFILE.FAILURE,
payload: { errors },
});
export const saveProfilePhoto = (username, formData) => ({
type: SAVE_PROFILE_PHOTO.BASE,
payload: {
username,
formData,
},
});
export const saveProfilePhotoBegin = () => ({
type: SAVE_PROFILE_PHOTO.BEGIN,
});
export const saveProfilePhotoSuccess = profileImage => ({
type: SAVE_PROFILE_PHOTO.SUCCESS,
payload: { profileImage },
});
export const saveProfilePhotoReset = () => ({
type: SAVE_PROFILE_PHOTO.RESET,
});
export const saveProfilePhotoFailure = error => ({
type: SAVE_PROFILE_PHOTO.FAILURE,
payload: { error },
});
export const deleteProfilePhoto = username => ({
type: DELETE_PROFILE_PHOTO.BASE,
payload: {
username,
},
});
export const deleteProfilePhotoBegin = () => ({
type: DELETE_PROFILE_PHOTO.BEGIN,
});
export const deleteProfilePhotoSuccess = profileImage => ({
type: DELETE_PROFILE_PHOTO.SUCCESS,
payload: { profileImage },
});
export const deleteProfilePhotoReset = () => ({
type: DELETE_PROFILE_PHOTO.RESET,
});
export const openForm = formId => ({
type: OPEN_FORM,
payload: {
formId,
},
});
export const closeForm = formId => ({
type: CLOSE_FORM,
payload: {
formId,
},
});
export const updateDraft = (name, value) => ({
type: UPDATE_DRAFT,
payload: {
name,
value,
},
});
export const resetDrafts = () => ({
type: RESET_DRAFTS,
});

View File

@@ -1,98 +0,0 @@
import {
SAVE_PROFILE_PHOTO,
saveProfilePhotoBegin,
saveProfilePhotoSuccess,
saveProfilePhotoFailure,
saveProfilePhotoReset,
saveProfilePhoto,
DELETE_PROFILE_PHOTO,
deleteProfilePhotoBegin,
deleteProfilePhotoSuccess,
deleteProfilePhotoReset,
deleteProfilePhoto,
} from './actions';
describe('SAVE profile photo actions', () => {
it('should create an action to signal the start of a profile photo save', () => {
const formData = 'multipart form data';
const expectedAction = {
type: SAVE_PROFILE_PHOTO.BASE,
payload: {
username: 'myusername',
formData,
},
};
expect(saveProfilePhoto('myusername', formData)).toEqual(expectedAction);
});
it('should create an action to signal user profile photo save beginning', () => {
const expectedAction = {
type: SAVE_PROFILE_PHOTO.BEGIN,
};
expect(saveProfilePhotoBegin()).toEqual(expectedAction);
});
it('should create an action to signal user profile photo save success', () => {
const newPhotoData = { hasImage: true };
const expectedAction = {
type: SAVE_PROFILE_PHOTO.SUCCESS,
payload: {
profileImage: newPhotoData,
},
};
expect(saveProfilePhotoSuccess(newPhotoData)).toEqual(expectedAction);
});
it('should create an action to signal user profile photo save reset', () => {
const expectedAction = {
type: SAVE_PROFILE_PHOTO.RESET,
};
expect(saveProfilePhotoReset()).toEqual(expectedAction);
});
it('should create an action to signal user profile photo save failure', () => {
const error = 'Test failure';
const expectedAction = {
type: SAVE_PROFILE_PHOTO.FAILURE,
payload: { error },
};
expect(saveProfilePhotoFailure(error)).toEqual(expectedAction);
});
});
describe('DELETE profile photo actions', () => {
it('should create an action to signal the start of a profile photo deletion', () => {
const expectedAction = {
type: DELETE_PROFILE_PHOTO.BASE,
payload: {
username: 'myusername',
},
};
expect(deleteProfilePhoto('myusername')).toEqual(expectedAction);
});
it('should create an action to signal user profile photo deletion beginning', () => {
const expectedAction = {
type: DELETE_PROFILE_PHOTO.BEGIN,
};
expect(deleteProfilePhotoBegin()).toEqual(expectedAction);
});
it('should create an action to signal user profile photo deletion success', () => {
const defaultPhotoData = { hasImage: false };
const expectedAction = {
type: DELETE_PROFILE_PHOTO.SUCCESS,
payload: {
profileImage: defaultPhotoData,
},
};
expect(deleteProfilePhotoSuccess(defaultPhotoData)).toEqual(expectedAction);
});
it('should create an action to signal user profile photo deletion reset', () => {
const expectedAction = {
type: DELETE_PROFILE_PHOTO.RESET,
};
expect(deleteProfilePhotoReset()).toEqual(expectedAction);
});
});

View File

@@ -1,33 +0,0 @@
const EDUCATION_LEVELS = [
'p',
'm',
'b',
'a',
'hs',
'jhs',
'el',
'none',
'other',
];
const SOCIAL = {
linkedin: {
title: 'LinkedIn',
},
twitter: {
title: 'Twitter',
},
facebook: {
title: 'Facebook',
},
};
const FIELD_LABELS = {
COUNTRY: 'country',
};
export {
EDUCATION_LEVELS,
SOCIAL,
FIELD_LABELS,
};

View File

@@ -1,34 +0,0 @@
import { breakpoints, useWindowSize } from '@openedx/paragon';
import { getConfig } from '@edx/frontend-platform';
export function useIsOnTabletScreen() {
const windowSize = useWindowSize();
return windowSize.width <= breakpoints.medium.minWidth;
}
export function useIsOnMobileScreen() {
const windowSize = useWindowSize();
return windowSize.width <= breakpoints.small.minWidth;
}
export function useIsVisibilityEnabled() {
return getConfig().DISABLE_VISIBILITY_EDITING === 'true';
}
export function useHandleChange(changeHandler) {
return (e) => {
const { name, value } = e.target;
changeHandler(name, value);
};
}
export function useHandleSubmit(submitHandler, formId) {
return (e) => {
e.preventDefault();
submitHandler(formId);
};
}
export function useCloseOpenHandler(handler, formId) {
return () => handler(formId);
}

View File

@@ -1,7 +0,0 @@
const mockData = {
learningGoal: 'advance_career',
editMode: 'static',
visibilityLearningGoal: 'private',
};
export default mockData;

View File

@@ -1,84 +0,0 @@
// This test file simply creates a contract that defines
// expectations and correct responses from the Pact stub server.
import path from 'path';
import { PactV3, MatchersV3 } from '@pact-foundation/pact';
import { initializeMockApp, getConfig, setConfig } from '@edx/frontend-platform';
import { getAccount } from './services';
const expectedUserInfo200 = {
username: 'staff',
email: 'staff@example.com',
bio: 'This is my bio',
name: 'Lemon Seltzer',
country: 'ME',
dateJoined: '2017-06-07T00:44:23Z',
isActive: true,
yearOfBirth: 1901,
languageProficiencies: [],
levelOfEducation: null,
profileImage: {},
socialLinks: [],
};
const provider = new PactV3({
log: path.resolve(process.cwd(), 'src/pact-logs/pact.log'),
dir: path.resolve(process.cwd(), 'src/pacts'),
consumer: 'frontend-app-profile',
provider: 'edx-platform',
});
describe('getAccount for one username', () => {
beforeAll(async () => {
initializeMockApp();
});
it('returns a HTTP 200 and user information', async () => {
const username200 = 'staff';
await provider.addInteraction({
states: [{ description: "I have a user's basic information" }],
uponReceiving: "A request for user's basic information",
withRequest: {
method: 'GET',
path: `/api/user/v1/accounts/${username200}`,
headers: {},
},
willRespondWith: {
status: 200,
headers: {},
body: MatchersV3.like(expectedUserInfo200),
},
});
return provider.executeTest(async (mockserver) => {
setConfig({
...getConfig(),
LMS_BASE_URL: mockserver.url,
});
const response = await getAccount(username200);
expect(response).toEqual(expectedUserInfo200);
});
});
it('Account does not exist', async () => {
const username404 = 'staff_not_found';
await provider.addInteraction({
states: [{ description: "Account and user's information does not exist" }],
uponReceiving: "A request for user's basic information",
withRequest: {
method: 'GET',
path: `/api/user/v1/accounts/${username404}`,
},
willRespondWith: {
status: 404,
},
});
await provider.executeTest(async (mockserver) => {
setConfig({
...getConfig(),
LMS_BASE_URL: mockserver.url,
});
await expect(getAccount(username404).then((response) => response.data)).rejects.toThrow('Request failed with status code 404');
});
});
});

View File

@@ -1,181 +0,0 @@
import {
SAVE_PROFILE,
SAVE_PROFILE_PHOTO,
DELETE_PROFILE_PHOTO,
CLOSE_FORM,
OPEN_FORM,
FETCH_PROFILE,
UPDATE_DRAFT,
RESET_DRAFTS,
} from './actions';
export const initialState = {
errors: {},
saveState: null,
savePhotoState: null,
currentlyEditingField: null,
account: {
socialLinks: [],
languageProficiencies: [],
name: '',
bio: '',
country: '',
levelOfEducation: '',
profileImage: {},
yearOfBirth: '',
},
preferences: {
visibilityName: '',
visibilityBio: '',
visibilityCountry: '',
visibilityLevelOfEducation: '',
visibilitySocialLinks: '',
visibilityLanguageProficiencies: '',
},
courseCertificates: [],
drafts: {},
isLoadingProfile: true,
isAuthenticatedUserProfile: false,
disabledCountries: ['RU'],
countriesCodesList: [],
};
const profilePage = (state = initialState, action = {}) => {
switch (action.type) {
case FETCH_PROFILE.BEGIN:
return {
...state,
// TODO: uncomment this line after ARCH-438 Image Post API returns the url
// is complete. Right now we refetch the whole profile causing us to show a full reload
// instead of a partial one.
// isLoadingProfile: true,
};
case FETCH_PROFILE.SUCCESS:
return {
...state,
account: {
...state.account,
...action.account,
socialLinks: action.account.socialLinks || [],
languageProficiencies: action.account.languageProficiencies || [],
},
preferences: action.preferences,
courseCertificates: action.courseCertificates || [],
isLoadingProfile: false,
isAuthenticatedUserProfile: action.isAuthenticatedUserProfile,
countriesCodesList: action.countriesCodesList || [],
};
case SAVE_PROFILE.BEGIN:
return {
...state,
saveState: 'pending',
errors: {},
};
case SAVE_PROFILE.SUCCESS:
return {
...state,
saveState: 'complete',
errors: {},
account: action.payload.account !== null ? {
...state.account,
...action.payload.account,
socialLinks: action.payload.account.socialLinks || [],
languageProficiencies: action.payload.account.languageProficiencies || [],
} : state.account,
preferences: { ...state.preferences, ...action.payload.preferences },
};
case SAVE_PROFILE.FAILURE:
return {
...state,
saveState: 'error',
isLoadingProfile: false,
errors: { ...state.errors, ...action.payload.errors },
};
case SAVE_PROFILE.RESET:
return {
...state,
saveState: null,
isLoadingProfile: false,
errors: {},
};
case SAVE_PROFILE_PHOTO.BEGIN:
return {
...state,
savePhotoState: 'pending',
errors: {},
};
case SAVE_PROFILE_PHOTO.SUCCESS:
return {
...state,
account: { ...state.account, profileImage: action.payload.profileImage },
savePhotoState: 'complete',
errors: {},
};
case SAVE_PROFILE_PHOTO.FAILURE:
return {
...state,
savePhotoState: 'error',
errors: { ...state.errors, photo: action.payload.error },
};
case SAVE_PROFILE_PHOTO.RESET:
return {
...state,
savePhotoState: null,
errors: {},
};
case DELETE_PROFILE_PHOTO.BEGIN:
return {
...state,
savePhotoState: 'pending',
errors: {},
};
case DELETE_PROFILE_PHOTO.SUCCESS:
return {
...state,
account: { ...state.account, profileImage: action.payload.profileImage },
savePhotoState: 'complete',
errors: {},
};
case DELETE_PROFILE_PHOTO.FAILURE:
return {
...state,
savePhotoState: 'error',
errors: { ...state.errors, ...action.payload.errors },
};
case DELETE_PROFILE_PHOTO.RESET:
return {
...state,
savePhotoState: null,
errors: {},
};
case UPDATE_DRAFT:
return {
...state,
drafts: { ...state.drafts, [action.payload.name]: action.payload.value },
};
case RESET_DRAFTS:
return {
...state,
drafts: {},
};
case OPEN_FORM:
return {
...state,
currentlyEditingField: action.payload.formId,
drafts: {},
};
case CLOSE_FORM:
if (action.payload.formId === state.currentlyEditingField) {
return {
...state,
currentlyEditingField: null,
drafts: {},
};
}
return state;
default:
return state;
}
};
export default profilePage;

View File

@@ -1,309 +0,0 @@
import profilePage, { initialState } from './reducers';
import {
SAVE_PROFILE,
SAVE_PROFILE_PHOTO,
DELETE_PROFILE_PHOTO,
CLOSE_FORM,
OPEN_FORM,
FETCH_PROFILE,
UPDATE_DRAFT,
RESET_DRAFTS,
} from './actions';
describe('profilePage reducer', () => {
it('should return the initial state by default', () => {
expect(profilePage(undefined, {})).toEqual(initialState);
});
describe('FETCH_PROFILE actions', () => {
it('should handle FETCH_PROFILE.BEGIN', () => {
const action = { type: FETCH_PROFILE.BEGIN };
const expectedState = {
...initialState,
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle FETCH_PROFILE.SUCCESS', () => {
const action = {
type: FETCH_PROFILE.SUCCESS,
account: {
name: 'John Doe',
bio: 'Software Engineer',
country: 'US',
levelOfEducation: 'bachelors',
socialLinks: [{ platform: 'twitter', link: 'twitter.com/johndoe' }],
languageProficiencies: [{ code: 'en', name: 'English' }],
profileImage: { url: 'profile.jpg' },
yearOfBirth: 1990,
},
preferences: {
visibilityName: 'public',
visibilityBio: 'public',
visibilityCountry: 'public',
visibilityLevelOfEducation: 'public',
visibilitySocialLinks: 'public',
visibilityLanguageProficiencies: 'public',
},
courseCertificates: ['cert1', 'cert2'],
isAuthenticatedUserProfile: true,
countriesCodesList: ['US', 'CA'],
};
const expectedState = {
...initialState,
account: {
...initialState.account,
...action.account,
socialLinks: action.account.socialLinks,
languageProficiencies: action.account.languageProficiencies,
},
preferences: action.preferences,
courseCertificates: action.courseCertificates,
isLoadingProfile: false,
isAuthenticatedUserProfile: action.isAuthenticatedUserProfile,
countriesCodesList: action.countriesCodesList,
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
});
describe('SAVE_PROFILE actions', () => {
it('should handle SAVE_PROFILE.BEGIN', () => {
const action = { type: SAVE_PROFILE.BEGIN };
const expectedState = {
...initialState,
saveState: 'pending',
errors: {},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle SAVE_PROFILE.SUCCESS', () => {
const action = {
type: SAVE_PROFILE.SUCCESS,
payload: {
account: {
name: 'Jane Doe',
bio: 'Updated bio',
socialLinks: [{ platform: 'linkedin', link: 'linkedin.com/janedoe' }],
languageProficiencies: [{ code: 'es', name: 'Spanish' }],
},
preferences: {
visibilityName: 'private',
visibilityBio: 'private',
},
},
};
const expectedState = {
...initialState,
saveState: 'complete',
errors: {},
account: {
...initialState.account,
...action.payload.account,
socialLinks: action.payload.account.socialLinks,
languageProficiencies: action.payload.account.languageProficiencies,
},
preferences: {
...initialState.preferences,
...action.payload.preferences,
},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle SAVE_PROFILE.FAILURE', () => {
const action = {
type: SAVE_PROFILE.FAILURE,
payload: { errors: { save: 'Failed to save profile' } },
};
const expectedState = {
...initialState,
saveState: 'error',
isLoadingProfile: false,
errors: { save: action.payload.errors.save },
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle SAVE_PROFILE.RESET', () => {
const action = { type: SAVE_PROFILE.RESET };
const expectedState = {
...initialState,
saveState: null,
isLoadingProfile: false,
errors: {},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
});
describe('SAVE_PROFILE_PHOTO actions', () => {
it('should handle SAVE_PROFILE_PHOTO.BEGIN', () => {
const action = { type: SAVE_PROFILE_PHOTO.BEGIN };
const expectedState = {
...initialState,
savePhotoState: 'pending',
errors: {},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle SAVE_PROFILE_PHOTO.SUCCESS', () => {
const action = {
type: SAVE_PROFILE_PHOTO.SUCCESS,
payload: { profileImage: { url: 'new-image-url.jpg' } },
};
const expectedState = {
...initialState,
account: { ...initialState.account, profileImage: action.payload.profileImage },
savePhotoState: 'complete',
errors: {},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle SAVE_PROFILE_PHOTO.FAILURE', () => {
const action = {
type: SAVE_PROFILE_PHOTO.FAILURE,
payload: { error: 'Photo upload failed' },
};
const expectedState = {
...initialState,
savePhotoState: 'error',
errors: { photo: action.payload.error },
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle SAVE_PROFILE_PHOTO.RESET', () => {
const action = { type: SAVE_PROFILE_PHOTO.RESET };
const expectedState = {
...initialState,
savePhotoState: null,
errors: {},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
});
describe('DELETE_PROFILE_PHOTO actions', () => {
it('should handle DELETE_PROFILE_PHOTO.BEGIN', () => {
const action = { type: DELETE_PROFILE_PHOTO.BEGIN };
const expectedState = {
...initialState,
savePhotoState: 'pending',
errors: {},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle DELETE_PROFILE_PHOTO.SUCCESS', () => {
const action = {
type: DELETE_PROFILE_PHOTO.SUCCESS,
payload: { profileImage: { url: 'default-image-url.jpg' } },
};
const expectedState = {
...initialState,
account: { ...initialState.account, profileImage: action.payload.profileImage },
savePhotoState: 'complete',
errors: {},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle DELETE_PROFILE_PHOTO.FAILURE', () => {
const action = {
type: DELETE_PROFILE_PHOTO.FAILURE,
payload: { errors: { delete: 'Failed to delete photo' } },
};
const expectedState = {
...initialState,
savePhotoState: 'error',
errors: { delete: action.payload.errors.delete },
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle DELETE_PROFILE_PHOTO.RESET', () => {
const action = { type: DELETE_PROFILE_PHOTO.RESET };
const expectedState = {
...initialState,
savePhotoState: null,
errors: {},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
});
describe('Draft and Form actions', () => {
it('should handle UPDATE_DRAFT', () => {
const action = {
type: UPDATE_DRAFT,
payload: { name: 'bio', value: 'New bio draft' },
};
const expectedState = {
...initialState,
drafts: { bio: 'New bio draft' },
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle RESET_DRAFTS', () => {
const initialStateWithDrafts = {
...initialState,
drafts: { bio: 'New bio draft', name: 'New name' },
};
const action = { type: RESET_DRAFTS };
const expectedState = {
...initialStateWithDrafts,
drafts: {},
};
expect(profilePage(initialStateWithDrafts, action)).toEqual(expectedState);
});
it('should handle OPEN_FORM', () => {
const action = {
type: OPEN_FORM,
payload: { formId: 'bioForm' },
};
const expectedState = {
...initialState,
currentlyEditingField: 'bioForm',
drafts: {},
};
expect(profilePage(initialState, action)).toEqual(expectedState);
});
it('should handle CLOSE_FORM when formId matches currentlyEditingField', () => {
const initialStateWithForm = {
...initialState,
currentlyEditingField: 'bioForm',
drafts: { bio: 'New bio draft' },
};
const action = {
type: CLOSE_FORM,
payload: { formId: 'bioForm' },
};
const expectedState = {
...initialStateWithForm,
currentlyEditingField: null,
drafts: {},
};
expect(profilePage(initialStateWithForm, action)).toEqual(expectedState);
});
it('should not handle CLOSE_FORM when formId does not match currentlyEditingField', () => {
const initialStateWithForm = {
...initialState,
currentlyEditingField: 'bioForm',
drafts: { bio: 'New bio draft' },
};
const action = {
type: CLOSE_FORM,
payload: { formId: 'nameForm' },
};
expect(profilePage(initialStateWithForm, action)).toEqual(initialStateWithForm);
});
});
});

View File

@@ -1,191 +0,0 @@
import { history } from '@edx/frontend-platform';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import pick from 'lodash.pick';
import {
all,
call,
delay,
put,
select,
takeEvery,
} from 'redux-saga/effects';
import {
closeForm,
deleteProfilePhotoBegin,
deleteProfilePhotoReset,
deleteProfilePhotoSuccess,
DELETE_PROFILE_PHOTO,
fetchProfileBegin,
fetchProfileReset,
fetchProfileSuccess,
FETCH_PROFILE,
resetDrafts,
saveProfileBegin,
saveProfileFailure,
saveProfileReset,
saveProfileSuccess,
SAVE_PROFILE,
saveProfilePhotoBegin,
saveProfilePhotoReset,
saveProfilePhotoSuccess,
SAVE_PROFILE_PHOTO,
} from './actions';
import { handleSaveProfileSelector, userAccountSelector } from './selectors';
import * as ProfileApiService from './services';
export function* handleFetchProfile(action) {
const { username } = action.payload;
const userAccount = yield select(userAccountSelector);
const isAuthenticatedUserProfile = username === getAuthenticatedUser().username;
let preferences = {};
let account = userAccount;
let courseCertificates = null;
let countriesCodesList = [];
try {
yield put(fetchProfileBegin());
const calls = [
call(ProfileApiService.getAccount, username),
call(ProfileApiService.getCourseCertificates, username),
call(ProfileApiService.getCountryList),
];
if (isAuthenticatedUserProfile) {
calls.push(call(ProfileApiService.getPreferences, username));
}
const result = yield all(calls);
if (isAuthenticatedUserProfile) {
[account, courseCertificates, countriesCodesList, preferences] = result;
} else {
[account, courseCertificates, countriesCodesList] = result;
}
if (isAuthenticatedUserProfile && result[0].accountPrivacy === 'all_users') {
yield call(ProfileApiService.patchPreferences, action.payload.username, {
account_privacy: 'custom',
'visibility.name': 'all_users',
'visibility.bio': 'all_users',
'visibility.course_certificates': 'all_users',
'visibility.country': 'all_users',
'visibility.date_joined': 'all_users',
'visibility.level_of_education': 'all_users',
'visibility.language_proficiencies': 'all_users',
'visibility.social_links': 'all_users',
'visibility.time_zone': 'all_users',
});
}
yield put(fetchProfileSuccess(
account,
preferences,
courseCertificates,
isAuthenticatedUserProfile,
countriesCodesList,
));
yield put(fetchProfileReset());
} catch (e) {
if (e.response.status === 404) {
history.push('/notfound');
} else {
throw e;
}
}
}
export function* handleSaveProfile(action) {
try {
const { drafts, preferences } = yield select(handleSaveProfileSelector);
const accountDrafts = pick(drafts, [
'bio',
'country',
'levelOfEducation',
'languageProficiencies',
'name',
'socialLinks',
]);
const preferencesDrafts = pick(drafts, [
'visibilityBio',
'visibilityCountry',
'visibilityLevelOfEducation',
'visibilityLanguageProficiencies',
'visibilityName',
'visibilitySocialLinks',
]);
if (Object.keys(preferencesDrafts).length > 0) {
preferencesDrafts.accountPrivacy = 'custom';
}
yield put(saveProfileBegin());
let accountResult = null;
if (Object.keys(accountDrafts).length > 0) {
accountResult = yield call(
ProfileApiService.patchProfile,
action.payload.username,
accountDrafts,
);
}
let preferencesResult = preferences;
if (Object.keys(preferencesDrafts).length > 0) {
yield call(ProfileApiService.patchPreferences, action.payload.username, preferencesDrafts);
// TODO: Temporary deoptimization since the patchPreferences call doesn't return anything.
preferencesResult = yield call(ProfileApiService.getPreferences, action.payload.username);
}
yield put(saveProfileSuccess(accountResult, preferencesResult));
yield delay(1000);
yield put(closeForm(action.payload.formId));
yield delay(300);
yield put(saveProfileReset());
yield put(resetDrafts());
} catch (e) {
if (e.processedData && e.processedData.fieldErrors) {
yield put(saveProfileFailure(e.processedData.fieldErrors));
} else {
yield put(saveProfileReset());
throw e;
}
}
}
export function* handleSaveProfilePhoto(action) {
const { username, formData } = action.payload;
try {
yield put(saveProfilePhotoBegin());
const photoResult = yield call(ProfileApiService.postProfilePhoto, username, formData);
yield put(saveProfilePhotoSuccess(photoResult));
yield put(saveProfilePhotoReset());
} catch (e) {
yield put(saveProfilePhotoReset());
}
}
export function* handleDeleteProfilePhoto(action) {
const { username } = action.payload;
try {
yield put(deleteProfilePhotoBegin());
const photoResult = yield call(ProfileApiService.deleteProfilePhoto, username);
yield put(deleteProfilePhotoSuccess(photoResult));
yield put(deleteProfilePhotoReset());
} catch (e) {
yield put(deleteProfilePhotoReset());
}
}
export default function* profileSaga() {
yield takeEvery(FETCH_PROFILE.BASE, handleFetchProfile);
yield takeEvery(SAVE_PROFILE.BASE, handleSaveProfile);
yield takeEvery(SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto);
yield takeEvery(DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto);
}

View File

@@ -1,167 +0,0 @@
import {
takeEvery,
put,
call,
delay,
select,
all,
} from 'redux-saga/effects';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import * as profileActions from './actions';
import { handleSaveProfileSelector, userAccountSelector } from './selectors';
jest.mock('./services', () => ({
getProfile: jest.fn(),
patchProfile: jest.fn(),
postProfilePhoto: jest.fn(),
deleteProfilePhoto: jest.fn(),
getPreferences: jest.fn(),
getAccount: jest.fn(),
getCourseCertificates: jest.fn(),
getCountryList: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(),
}));
/* eslint-disable import/first */
import profileSaga, {
handleFetchProfile,
handleSaveProfile,
handleSaveProfilePhoto,
handleDeleteProfilePhoto,
} from './sagas';
import * as ProfileApiService from './services';
/* eslint-enable import/first */
describe('RootSaga', () => {
describe('profileSaga', () => {
it('should pass actions to the correct sagas', () => {
const gen = profileSaga();
expect(gen.next().value)
.toEqual(takeEvery(profileActions.FETCH_PROFILE.BASE, handleFetchProfile));
expect(gen.next().value)
.toEqual(takeEvery(profileActions.SAVE_PROFILE.BASE, handleSaveProfile));
expect(gen.next().value)
.toEqual(takeEvery(profileActions.SAVE_PROFILE_PHOTO.BASE, handleSaveProfilePhoto));
expect(gen.next().value)
.toEqual(takeEvery(profileActions.DELETE_PROFILE_PHOTO.BASE, handleDeleteProfilePhoto));
expect(gen.next().value).toBeUndefined();
});
});
describe('handleFetchProfile', () => {
it('should fetch certificates and preferences for the current user profile', () => {
const userAccount = {
username: 'gonzo',
other: 'data',
};
getAuthenticatedUser.mockReturnValue(userAccount);
const selectorData = {
userAccount,
};
const action = profileActions.fetchProfile('gonzo');
const gen = handleFetchProfile(action);
const result = [userAccount, [1, 2, 3], [], { preferences: 'stuff' }];
expect(gen.next().value).toEqual(select(userAccountSelector));
expect(gen.next(selectorData).value).toEqual(put(profileActions.fetchProfileBegin()));
expect(gen.next().value).toEqual(all([
call(ProfileApiService.getAccount, 'gonzo'),
call(ProfileApiService.getCourseCertificates, 'gonzo'),
call(ProfileApiService.getCountryList),
call(ProfileApiService.getPreferences, 'gonzo'),
]));
expect(gen.next(result).value)
.toEqual(put(profileActions.fetchProfileSuccess(userAccount, result[3], result[1], true, [])));
expect(gen.next().value).toEqual(put(profileActions.fetchProfileReset()));
expect(gen.next().value).toBeUndefined();
});
it('should fetch certificates and profile for some other user profile', () => {
const userAccount = {
username: 'gonzo',
other: 'data',
};
const countriesCodesList = [{ code: 'AX' }, { code: 'AL' }];
getAuthenticatedUser.mockReturnValue(userAccount);
const selectorData = {
userAccount,
};
const action = profileActions.fetchProfile('booyah');
const gen = handleFetchProfile(action);
const result = [{}, [1, 2, 3], countriesCodesList];
expect(gen.next().value).toEqual(select(userAccountSelector));
expect(gen.next(selectorData).value).toEqual(put(profileActions.fetchProfileBegin()));
expect(gen.next().value).toEqual(all([
call(ProfileApiService.getAccount, 'booyah'),
call(ProfileApiService.getCourseCertificates, 'booyah'),
call(ProfileApiService.getCountryList),
]));
expect(gen.next(result).value)
.toEqual(put(profileActions.fetchProfileSuccess(result[0], {}, result[1], false, countriesCodesList)));
expect(gen.next().value).toEqual(put(profileActions.fetchProfileReset()));
expect(gen.next().value).toBeUndefined();
});
});
describe('handleSaveProfile', () => {
const selectorData = {
username: 'my username',
drafts: {
name: 'Full Name',
},
preferences: {},
};
it('should successfully process a saveProfile request if there are no exceptions', () => {
const action = profileActions.saveProfile('ze form id', 'my username');
const gen = handleSaveProfile(action);
const profile = {
name: 'Full Name',
levelOfEducation: 'b',
};
expect(gen.next().value).toEqual(select(handleSaveProfileSelector));
expect(gen.next(selectorData).value).toEqual(put(profileActions.saveProfileBegin()));
expect(gen.next().value).toEqual(call(ProfileApiService.patchProfile, 'my username', {
name: 'Full Name',
}));
expect(gen.next(profile).value).toEqual(put(profileActions.saveProfileSuccess(profile, {})));
expect(gen.next().value).toEqual(delay(1000));
expect(gen.next().value).toEqual(put(profileActions.closeForm('ze form id')));
expect(gen.next().value).toEqual(delay(300));
expect(gen.next().value).toEqual(put(profileActions.saveProfileReset()));
expect(gen.next().value).toEqual(put(profileActions.resetDrafts()));
expect(gen.next().value).toBeUndefined();
});
it('should successfully publish a failure action on exception', () => {
const error = new Error('uhoh');
error.processedData = {
fieldErrors: {
uhoh: 'not good',
},
};
const action = profileActions.saveProfile(
'ze form id',
'my username',
);
const gen = handleSaveProfile(action);
expect(gen.next().value).toEqual(select(handleSaveProfileSelector));
expect(gen.next(selectorData).value).toEqual(put(profileActions.saveProfileBegin()));
const result = gen.throw(error);
expect(result.value).toEqual(put(profileActions.saveProfileFailure({ uhoh: 'not good' })));
expect(gen.next().value).toBeUndefined();
});
});
});

View File

@@ -1,338 +0,0 @@
import { createSelector } from 'reselect';
import {
getLocale,
getLanguageList,
getCountryList,
getCountryMessages,
getLanguageMessages,
} from '@edx/frontend-platform/i18n';
export const formIdSelector = (state, props) => props.formId;
export const userAccountSelector = state => state.userAccount;
export const profileAccountSelector = state => state.profilePage.account;
export const profileDraftsSelector = state => state.profilePage.drafts;
export const accountPrivacySelector = state => state.profilePage.preferences.accountPrivacy;
export const profilePreferencesSelector = state => state.profilePage.preferences;
export const profileCourseCertificatesSelector = state => state.profilePage.courseCertificates;
export const saveStateSelector = state => state.profilePage.saveState;
export const savePhotoStateSelector = state => state.profilePage.savePhotoState;
export const isLoadingProfileSelector = state => state.profilePage.isLoadingProfile;
export const currentlyEditingFieldSelector = state => state.profilePage.currentlyEditingField;
export const accountErrorsSelector = state => state.profilePage.errors;
export const isAuthenticatedUserProfileSelector = state => state.profilePage.isAuthenticatedUserProfile;
export const countriesCodesListSelector = state => state.profilePage.countriesCodesList;
export const editableFormModeSelector = createSelector(
profileAccountSelector,
isAuthenticatedUserProfileSelector,
profileCourseCertificatesSelector,
formIdSelector,
currentlyEditingFieldSelector,
(account, isAuthenticatedUserProfile, certificates, formId, currentlyEditingField) => {
let propExists = account[formId] != null && account[formId].length > 0;
propExists = formId === 'certificates' ? certificates.length > 0 : propExists;
if (!isAuthenticatedUserProfile) {
return 'static';
}
if (formId === currentlyEditingField) {
return 'editing';
}
if (!propExists) {
return 'empty';
}
return 'editable';
},
);
export const accountDraftsFieldSelector = createSelector(
formIdSelector,
profileDraftsSelector,
(formId, drafts) => drafts[formId],
);
export const visibilityDraftsFieldSelector = createSelector(
formIdSelector,
profileDraftsSelector,
(formId, drafts) => drafts[`visibility${formId.charAt(0).toUpperCase() + formId.slice(1)}`],
);
export const formErrorSelector = createSelector(
accountErrorsSelector,
formIdSelector,
(errors, formId) => (errors[formId] ? errors[formId].userMessage : null),
);
export const editableFormSelector = createSelector(
editableFormModeSelector,
formErrorSelector,
saveStateSelector,
(editMode, error, saveState) => ({
editMode,
error,
saveState,
}),
);
export const localeSelector = () => getLocale();
export const countryMessagesSelector = createSelector(
localeSelector,
locale => getCountryMessages(locale),
);
export const languageMessagesSelector = createSelector(
localeSelector,
locale => getLanguageMessages(locale),
);
export const sortedLanguagesSelector = createSelector(
localeSelector,
locale => getLanguageList(locale),
);
export const sortedCountriesSelector = createSelector(
localeSelector,
countriesCodesListSelector,
profileAccountSelector,
(locale, countriesCodesList, profileAccount) => {
const countryList = getCountryList(locale);
const userCountry = profileAccount.country;
return countryList.filter(({ code }) => code === userCountry || countriesCodesList.find(x => x === code));
},
);
export const preferredLanguageSelector = createSelector(
editableFormSelector,
sortedLanguagesSelector,
languageMessagesSelector,
(editableForm, sortedLanguages, languageMessages) => ({
...editableForm,
sortedLanguages,
languageMessages,
}),
);
export const countrySelector = createSelector(
editableFormSelector,
sortedCountriesSelector,
countryMessagesSelector,
countriesCodesListSelector,
profileAccountSelector,
(editableForm, translatedCountries, countryMessages, countriesCodesList, account) => ({
...editableForm,
translatedCountries,
countryMessages,
countriesCodesList,
committedCountry: account.country,
}),
);
export const certificatesSelector = createSelector(
editableFormSelector,
profileCourseCertificatesSelector,
(editableForm, certificates) => ({
...editableForm,
certificates,
value: certificates,
}),
);
export const profileImageSelector = createSelector(
profileAccountSelector,
account => (account.profileImage != null
? {
src: account.profileImage.imageUrlFull,
isDefault: !account.profileImage.hasImage,
}
: {}),
);
export const handleSaveProfileSelector = createSelector(
profileDraftsSelector,
profilePreferencesSelector,
(drafts, preferences) => ({
drafts,
preferences,
}),
);
const socialLinksByPlatformSelector = createSelector(
profileAccountSelector,
(account) => {
const linksByPlatform = {};
if (Array.isArray(account.socialLinks)) {
account.socialLinks.forEach((socialLink) => {
linksByPlatform[socialLink.platform] = socialLink;
});
}
return linksByPlatform;
},
);
const draftSocialLinksByPlatformSelector = createSelector(
profileDraftsSelector,
(drafts) => {
const linksByPlatform = {};
if (Array.isArray(drafts.socialLinks)) {
drafts.socialLinks.forEach((socialLink) => {
linksByPlatform[socialLink.platform] = socialLink;
});
}
return linksByPlatform;
},
);
export const formSocialLinksSelector = createSelector(
socialLinksByPlatformSelector,
draftSocialLinksByPlatformSelector,
(linksByPlatform, draftLinksByPlatform) => {
const knownPlatforms = ['twitter', 'facebook', 'linkedin'];
const socialLinks = [];
knownPlatforms.forEach((platform) => {
if (draftLinksByPlatform[platform] !== undefined) {
socialLinks.push(draftLinksByPlatform[platform]);
} else if (linksByPlatform[platform] !== undefined) {
socialLinks.push(linksByPlatform[platform]);
} else {
socialLinks.push({
platform,
socialLink: null,
});
}
});
return socialLinks;
},
);
export const visibilitiesSelector = createSelector(
profilePreferencesSelector,
accountPrivacySelector,
(preferences, accountPrivacy) => {
switch (accountPrivacy) {
case 'custom':
return {
visibilityBio: preferences.visibilityBio || 'all_users',
visibilityCountry: preferences.visibilityCountry || 'all_users',
visibilityLevelOfEducation: preferences.visibilityLevelOfEducation || 'all_users',
visibilityLanguageProficiencies: preferences.visibilityLanguageProficiencies || 'all_users',
visibilityName: preferences.visibilityName || 'all_users',
visibilitySocialLinks: preferences.visibilitySocialLinks || 'all_users',
};
case 'private':
return {
visibilityBio: 'private',
visibilityCountry: 'private',
visibilityLevelOfEducation: 'private',
visibilityLanguageProficiencies: 'private',
visibilityName: 'private',
visibilitySocialLinks: 'private',
};
case 'all_users':
default:
return {
visibilityBio: 'all_users',
visibilityCountry: 'all_users',
visibilityLevelOfEducation: 'all_users',
visibilityLanguageProficiencies: 'all_users',
visibilityName: 'all_users',
visibilitySocialLinks: 'all_users',
};
}
},
);
function chooseFormValue(draft, committed) {
return draft !== undefined ? draft : committed;
}
export const formValuesSelector = createSelector(
profileAccountSelector,
visibilitiesSelector,
profileDraftsSelector,
profileCourseCertificatesSelector,
formSocialLinksSelector,
(account, visibilities, drafts, courseCertificates, socialLinks) => ({
bio: chooseFormValue(drafts.bio, account.bio),
visibilityBio: chooseFormValue(drafts.visibilityBio, visibilities.visibilityBio),
courseCertificates,
country: chooseFormValue(drafts.country, account.country),
visibilityCountry: chooseFormValue(drafts.visibilityCountry, visibilities.visibilityCountry),
levelOfEducation: chooseFormValue(drafts.levelOfEducation, account.levelOfEducation),
visibilityLevelOfEducation: chooseFormValue(
drafts.visibilityLevelOfEducation,
visibilities.visibilityLevelOfEducation,
),
languageProficiencies: chooseFormValue(
drafts.languageProficiencies,
account.languageProficiencies,
),
visibilityLanguageProficiencies: chooseFormValue(
drafts.visibilityLanguageProficiencies,
visibilities.visibilityLanguageProficiencies,
),
name: chooseFormValue(drafts.name, account.name),
visibilityName: chooseFormValue(drafts.visibilityName, visibilities.visibilityName),
socialLinks,
visibilitySocialLinks: chooseFormValue(
drafts.visibilitySocialLinks,
visibilities.visibilitySocialLinks,
),
}),
);
export const profilePageSelector = createSelector(
profileAccountSelector,
formValuesSelector,
profileImageSelector,
saveStateSelector,
savePhotoStateSelector,
isLoadingProfileSelector,
draftSocialLinksByPlatformSelector,
accountErrorsSelector,
isAuthenticatedUserProfileSelector,
(
account,
formValues,
profileImage,
saveState,
savePhotoState,
isLoadingProfile,
draftSocialLinksByPlatform,
errors,
isAuthenticatedUserProfile,
) => ({
username: account.username,
profileImage,
requiresParentalConsent: account.requiresParentalConsent,
dateJoined: account.dateJoined,
yearOfBirth: account.yearOfBirth,
bio: formValues.bio,
visibilityBio: formValues.visibilityBio,
courseCertificates: formValues.courseCertificates,
country: formValues.country,
visibilityCountry: formValues.visibilityCountry,
levelOfEducation: formValues.levelOfEducation,
visibilityLevelOfEducation: formValues.visibilityLevelOfEducation,
languageProficiencies: formValues.languageProficiencies,
visibilityLanguageProficiencies: formValues.visibilityLanguageProficiencies,
name: formValues.name,
visibilityName: formValues.visibilityName,
socialLinks: formValues.socialLinks,
visibilitySocialLinks: formValues.visibilitySocialLinks,
draftSocialLinksByPlatform,
saveState,
savePhotoState,
isLoadingProfile,
photoUploadError: errors.photo || null,
isAuthenticatedUserProfile,
}),
);

View File

@@ -1,168 +0,0 @@
import { ensureConfig, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient as getHttpClient } from '@edx/frontend-platform/auth';
import { logError } from '@edx/frontend-platform/logging';
import { camelCaseObject, convertKeyNames, snakeCaseObject } from '../utils';
import { FIELD_LABELS } from './constants';
ensureConfig(['LMS_BASE_URL'], 'Profile API service');
function processAccountData(data) {
const processedData = camelCaseObject(data);
return {
...processedData,
socialLinks: Array.isArray(processedData.socialLinks) ? processedData.socialLinks : [],
languageProficiencies: Array.isArray(processedData.languageProficiencies)
? processedData.languageProficiencies : [],
name: processedData.name || null,
bio: processedData.bio || null,
country: processedData.country || null,
levelOfEducation: processedData.levelOfEducation || null,
profileImage: processedData.profileImage || {},
yearOfBirth: processedData.yearOfBirth || null,
};
}
function processAndThrowError(error, errorDataProcessor) {
const processedError = Object.create(error);
if (error.response && error.response.data && typeof error.response.data === 'object') {
processedError.processedData = errorDataProcessor(error.response.data);
throw processedError;
} else {
throw error;
}
}
export async function getAccount(username) {
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`);
return processAccountData(data);
}
export async function patchProfile(username, params) {
const processedParams = snakeCaseObject(params);
const { data } = await getHttpClient()
.patch(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}`, processedParams, {
headers: {
'Content-Type': 'application/merge-patch+json',
},
})
.catch((error) => {
processAndThrowError(error, processAccountData);
});
return processAccountData(data);
}
export async function postProfilePhoto(username, formData) {
// eslint-disable-next-line no-unused-vars
const { data } = await getHttpClient().post(
`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`,
formData,
{
headers: {
'Content-Type': 'multipart/form-data',
},
},
).catch((error) => {
processAndThrowError(error, camelCaseObject);
});
// TODO: Someday in the future the POST photo endpoint
// will return the new values. At that time we should
// use the commented line below instead of the separate
// getAccount request that follows.
// return camelCaseObject(data);
const updatedData = await getAccount(username);
return updatedData.profileImage;
}
export async function deleteProfilePhoto(username) {
// eslint-disable-next-line no-unused-vars
const { data } = await getHttpClient().delete(`${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${username}/image`);
// TODO: Someday in the future the POST photo endpoint
// will return the new values. At that time we should
// use the commented line below instead of the separate
// getAccount request that follows.
// return camelCaseObject(data);
const updatedData = await getAccount(username);
return updatedData.profileImage;
}
export async function getPreferences(username) {
const { data } = await getHttpClient().get(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`);
return camelCaseObject(data);
}
export async function patchPreferences(username, params) {
let processedParams = snakeCaseObject(params);
processedParams = convertKeyNames(processedParams, {
visibility_bio: 'visibility.bio',
visibility_course_certificates: 'visibility.course_certificates',
visibility_country: 'visibility.country',
visibility_date_joined: 'visibility.date_joined',
visibility_level_of_education: 'visibility.level_of_education',
visibility_language_proficiencies: 'visibility.language_proficiencies',
visibility_name: 'visibility.name',
visibility_social_links: 'visibility.social_links',
visibility_time_zone: 'visibility.time_zone',
});
await getHttpClient().patch(`${getConfig().LMS_BASE_URL}/api/user/v1/preferences/${username}`, processedParams, {
headers: { 'Content-Type': 'application/merge-patch+json' },
});
return params; // TODO: Once the server returns the updated preferences object, return that.
}
function transformCertificateData(data) {
const transformedData = [];
data.forEach((cert) => {
// download_url may be full url or absolute path.
// note: using the URL() api breaks in ie 11
const urlIsPath = typeof cert.download_url === 'string'
&& cert.download_url.search(/http[s]?:\/\//) !== 0;
const downloadUrl = urlIsPath
? `${getConfig().LMS_BASE_URL}${cert.download_url}`
: cert.download_url;
transformedData.push({
...camelCaseObject(cert),
certificateType: cert.certificate_type,
downloadUrl,
});
});
return transformedData;
}
export async function getCourseCertificates(username) {
const url = `${getConfig().LMS_BASE_URL}/api/certificates/v0/certificates/${username}/`;
try {
const { data } = await getHttpClient().get(url);
return transformCertificateData(data);
} catch (e) {
logError(e);
return [];
}
}
function extractCountryList(data) {
return data?.fields
.find(({ name }) => name === FIELD_LABELS.COUNTRY)
?.options?.map(({ value }) => (value)) || [];
}
export async function getCountryList() {
const url = `${getConfig().LMS_BASE_URL}/user_api/v1/account/registration/`;
try {
const { data } = await getHttpClient().get(url);
return extractCountryList(data);
} catch (e) {
logError(e);
return [];
}
}

View File

@@ -1,151 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
import classNames from 'classnames';
import messages from './Bio.messages';
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
import { editableFormSelector } from '../data/selectors';
import {
useCloseOpenHandler,
useHandleChange,
useHandleSubmit,
useIsOnMobileScreen,
useIsVisibilityEnabled,
} from '../data/hooks';
const Bio = ({
formId,
bio,
visibilityBio,
editMode,
saveState,
error,
changeHandler,
submitHandler,
closeHandler,
openHandler,
}) => {
const isMobileView = useIsOnMobileScreen();
const isVisibilityEnabled = useIsVisibilityEnabled();
const intl = useIntl();
const handleChange = useHandleChange(changeHandler);
const handleSubmit = useHandleSubmit(submitHandler, formId);
const handleOpen = useCloseOpenHandler(openHandler, formId);
const handleClose = useCloseOpenHandler(closeHandler, formId);
return (
<SwitchContent
className={classNames([
isMobileView ? 'pt-40px' : 'pt-0',
])}
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={handleSubmit}>
<Form.Group
controlId={formId}
className="m-0 pb-3"
isInvalid={error !== null}
>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-2.5">
{intl.formatMessage(messages['profile.bio.about.me'])}
</p>
<textarea
className="form-control py-10px"
id={formId}
name={formId}
value={bio}
onChange={handleChange}
/>
{error !== null && (
<Form.Control.Feedback hasIcon={false}>
{error}
</Form.Control.Feedback>
)}
</Form.Group>
<FormControls
visibilityId="visibilityBio"
saveState={saveState}
visibility={visibilityBio}
cancelHandler={handleClose}
changeHandler={handleChange}
/>
</form>
</div>
),
editable: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.bio.about.me'])}
</p>
<EditableItemHeader
content={bio}
showEditButton
onClickEdit={handleOpen}
showVisibility={visibilityBio !== null && isVisibilityEnabled}
visibility={visibilityBio}
/>
</>
),
empty: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.bio.about.me'])}
</p>
<EmptyContent onClick={handleOpen}>
<FormattedMessage
id="profile.bio.empty"
defaultMessage="Add a short bio"
description="instructions when the user hasn't written an About Me"
/>
</EmptyContent>
</>
),
static: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.bio.about.me'])}
</p>
<EditableItemHeader content={bio} />
</>
),
}}
/>
);
};
Bio.propTypes = {
formId: PropTypes.string.isRequired,
bio: PropTypes.string,
visibilityBio: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
};
Bio.defaultProps = {
editMode: 'static',
saveState: null,
bio: null,
visibilityBio: 'private',
error: null,
};
export default connect(
editableFormSelector,
{},
)(Bio);

View File

@@ -1,11 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.bio.about.me': {
id: 'profile.bio.about.me',
defaultMessage: 'Bio',
description: 'A section of a user profile',
},
});
export default messages;

View File

@@ -1,163 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
import messages from './Country.messages';
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
import { countrySelector } from '../data/selectors';
import {
useCloseOpenHandler,
useHandleChange,
useHandleSubmit,
useIsVisibilityEnabled,
} from '../data/hooks';
const Country = ({
formId,
country,
visibilityCountry,
editMode,
saveState,
error,
translatedCountries,
countriesCodesList,
countryMessages,
changeHandler,
submitHandler,
closeHandler,
openHandler,
}) => {
const isVisibilityEnabled = useIsVisibilityEnabled();
const intl = useIntl();
const handleChange = useHandleChange(changeHandler);
const handleSubmit = useHandleSubmit(submitHandler, formId);
const handleOpen = useCloseOpenHandler(openHandler, formId);
const handleClose = useCloseOpenHandler(closeHandler, formId);
const isDisabledCountry = (countryCode) => countriesCodesList.length > 0
&& !countriesCodesList.find(code => code === countryCode);
return (
<SwitchContent
className="pt-40px"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={handleSubmit}>
<Form.Group
controlId={formId}
className="m-0 pb-3"
isInvalid={error !== null}
>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-2.5">
{intl.formatMessage(messages['profile.country.label'])}
</p>
<select
data-hj-suppress
className="form-control py-10px"
type="select"
id={formId}
name={formId}
value={country}
onChange={handleChange}
>
<option value=""> </option>
{translatedCountries.map(({ code, name }) => (
<option key={code} value={code} disabled={isDisabledCountry(code)}>
{name}
</option>
))}
</select>
{error !== null && (
<Form.Control.Feedback hasIcon={false}>
{error}
</Form.Control.Feedback>
)}
</Form.Group>
<FormControls
visibilityId="visibilityCountry"
saveState={saveState}
visibility={visibilityCountry}
cancelHandler={handleClose}
changeHandler={handleChange}
/>
</form>
</div>
),
editable: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.country.label'])}
</p>
<EditableItemHeader
content={countryMessages[country]}
showEditButton
onClickEdit={handleOpen}
showVisibility={visibilityCountry !== null && isVisibilityEnabled}
visibility={visibilityCountry}
/>
</>
),
empty: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.country.label'])}
</p>
<EmptyContent onClick={handleOpen}>
{intl.formatMessage(messages['profile.country.empty'])}
</EmptyContent>
</>
),
static: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.country.label'])}
</p>
<EditableItemHeader content={countryMessages[country]} />
</>
),
}}
/>
);
};
Country.propTypes = {
formId: PropTypes.string.isRequired,
country: PropTypes.string,
visibilityCountry: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
translatedCountries: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})).isRequired,
countriesCodesList: PropTypes.arrayOf(PropTypes.string).isRequired,
countryMessages: PropTypes.objectOf(PropTypes.string).isRequired,
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
};
Country.defaultProps = {
editMode: 'static',
saveState: null,
country: null,
visibilityCountry: 'private',
error: null,
};
export default connect(
countrySelector,
{},
)(Country);

View File

@@ -1,16 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.country.label': {
id: 'profile.country.label',
defaultMessage: 'Country',
description: 'The label for a country in a user profile.',
},
'profile.country.empty': {
id: 'profile.country.empty',
defaultMessage: 'Add country',
description: 'The affordance to add country location to a users profile.',
},
});
export default messages;

View File

@@ -1,171 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import get from 'lodash.get';
import { Form } from '@openedx/paragon';
import messages from './Education.messages';
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
import { EDUCATION_LEVELS } from '../data/constants';
import { editableFormSelector } from '../data/selectors';
import {
useCloseOpenHandler,
useHandleChange,
useHandleSubmit,
useIsVisibilityEnabled,
} from '../data/hooks';
const Education = ({
formId,
levelOfEducation,
visibilityLevelOfEducation,
editMode,
saveState,
error,
changeHandler,
submitHandler,
closeHandler,
openHandler,
}) => {
const isVisibilityEnabled = useIsVisibilityEnabled();
const intl = useIntl();
const handleChange = useHandleChange(changeHandler);
const handleSubmit = useHandleSubmit(submitHandler, formId);
const handleOpen = useCloseOpenHandler(openHandler, formId);
const handleClose = useCloseOpenHandler(closeHandler, formId);
return (
<SwitchContent
className="pt-40px"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={handleSubmit}>
<Form.Group
controlId={formId}
className="m-0 pb-3"
isInvalid={error !== null}
>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-2.5">
{intl.formatMessage(messages['profile.education.education'])}
</p>
<select
data-hj-suppress
className="form-control py-10px"
id={formId}
name={formId}
value={levelOfEducation}
onChange={handleChange}
>
<option value=""> </option>
{EDUCATION_LEVELS.map(level => (
<option key={level} value={level}>
{intl.formatMessage(get(
messages,
`profile.education.levels.${level}`,
messages['profile.education.levels.o'],
))}
</option>
))}
</select>
{error !== null && (
<Form.Control.Feedback hasIcon={false}>
{error}
</Form.Control.Feedback>
)}
</Form.Group>
<FormControls
visibilityId="visibilityLevelOfEducation"
saveState={saveState}
visibility={visibilityLevelOfEducation}
cancelHandler={handleClose}
changeHandler={handleChange}
/>
</form>
</div>
),
editable: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.education.education'])}
</p>
<EditableItemHeader
content={intl.formatMessage(get(
messages,
`profile.education.levels.${levelOfEducation}`,
messages['profile.education.levels.o'],
))}
showEditButton
onClickEdit={handleOpen}
showVisibility={visibilityLevelOfEducation !== null && isVisibilityEnabled}
visibility={visibilityLevelOfEducation}
/>
</>
),
empty: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.education.education'])}
</p>
<EmptyContent onClick={handleOpen}>
<FormattedMessage
id="profile.education.empty"
defaultMessage="Add level of education"
description="instructions when the user doesn't have their level of education set"
/>
</EmptyContent>
</>
),
static: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.education.education'])}
</p>
<EditableItemHeader
content={intl.formatMessage(get(
messages,
`profile.education.levels.${levelOfEducation}`,
messages['profile.education.levels.o'],
))}
/>
</>
),
}}
/>
);
};
Education.propTypes = {
formId: PropTypes.string.isRequired,
levelOfEducation: PropTypes.string,
visibilityLevelOfEducation: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
};
Education.defaultProps = {
editMode: 'static',
saveState: null,
levelOfEducation: null,
visibilityLevelOfEducation: 'private',
error: null,
};
export default connect(
editableFormSelector,
{},
)(Education);

View File

@@ -1,56 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.education.education': {
id: 'profile.education.education',
defaultMessage: 'Education',
description: 'A section of a user profile',
},
'profile.education.levels.p': {
id: 'profile.education.levels.p',
defaultMessage: 'Doctorate',
description: 'Selected by the user if their highest level of education is a doctorate degree.',
},
'profile.education.levels.m': {
id: 'profile.education.levels.m',
defaultMessage: "Master's or professional degree",
description: "Selected by the user if their highest level of education is a master's or professional degree from a college or university.",
},
'profile.education.levels.b': {
id: 'profile.education.levels.b',
defaultMessage: "Bachelor's Degree",
description: "Selected by the user if their highest level of education is a four year college or university bachelor's degree.",
},
'profile.education.levels.a': {
id: 'profile.education.levels.a',
defaultMessage: "Associate's degree",
description: "Selected by the user if their highest level of education is an associate's degree. 1-2 years of college or university.",
},
'profile.education.levels.hs': {
id: 'profile.education.levels.hs',
defaultMessage: 'Secondary/high school',
description: 'Selected by the user if their highest level of education is secondary or high school. 9-12 years of education.',
},
'profile.education.levels.jhs': {
id: 'profile.education.levels.jhs',
defaultMessage: 'Junior secondary/junior high/middle school',
description: 'Selected by the user if their highest level of education is junior or middle school. 6-8 years of education.',
},
'profile.education.levels.el': {
id: 'profile.education.levels.el',
defaultMessage: 'Elementary/primary school',
description: 'Selected by the user if their highest level of education is elementary or primary school. 1-5 years of education.',
},
'profile.education.levels.none': {
id: 'profile.education.levels.none',
defaultMessage: 'No formal education',
description: 'Selected by the user to describe their education.',
},
'profile.education.levels.o': {
id: 'profile.education.levels.o',
defaultMessage: 'Other education',
description: 'Selected by the user if they have a type of education not described by the other choices.',
},
});
export default messages;

View File

@@ -1,192 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { InfoOutline } from '@openedx/paragon/icons';
import { Hyperlink, OverlayTrigger, Tooltip } from '@openedx/paragon';
import messages from './Name.messages';
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
import { editableFormSelector } from '../data/selectors';
import {
useCloseOpenHandler,
useHandleChange,
useHandleSubmit,
useIsVisibilityEnabled,
} from '../data/hooks';
const Name = ({
formId,
name,
visibilityName,
editMode,
saveState,
changeHandler,
submitHandler,
closeHandler,
openHandler,
accountSettingsUrl,
}) => {
const isVisibilityEnabled = useIsVisibilityEnabled();
const intl = useIntl();
const handleChange = useHandleChange(changeHandler);
const handleSubmit = useHandleSubmit(submitHandler, formId);
const handleOpen = useCloseOpenHandler(openHandler, formId);
const handleClose = useCloseOpenHandler(closeHandler, formId);
return (
<SwitchContent
className="pt-40px"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={handleSubmit}>
<div className="form-group">
<div className="row m-0 pb-2.5 align-items-center">
<p data-hj-suppress className="h5 font-weight-bold m-0">
{intl.formatMessage(messages['profile.name.full.name'])}
</p>
<OverlayTrigger
key="top"
placement="top"
overlay={(
<Tooltip variant="light" id="tooltip-top">
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.name.tooltip'])}
</p>
</Tooltip>
)}
>
<InfoOutline className="m-0 info-icon" />
</OverlayTrigger>
</div>
<EditableItemHeader content={name} />
<h4 className="font-weight-normal">
<Hyperlink destination={accountSettingsUrl} target="_blank">
{intl.formatMessage(messages['profile.name.redirect'])}
</Hyperlink>
</h4>
</div>
<FormControls
visibilityId="visibilityName"
saveState={saveState}
visibility={visibilityName}
cancelHandler={handleClose}
changeHandler={handleChange}
/>
</form>
</div>
),
editable: (
<>
<div className="row m-0 pb-1.5 align-items-center">
<p data-hj-suppress className="h5 font-weight-bold m-0">
{intl.formatMessage(messages['profile.name.full.name'])}
</p>
<OverlayTrigger
key="top"
placement="top"
overlay={(
<Tooltip variant="light" id="tooltip-top">
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.name.tooltip'])}
</p>
</Tooltip>
)}
>
<InfoOutline className="m-0 info-icon" />
</OverlayTrigger>
</div>
<EditableItemHeader
content={name}
showEditButton
onClickEdit={handleOpen}
showVisibility={visibilityName !== null && isVisibilityEnabled}
visibility={visibilityName}
/>
</>
),
empty: (
<>
<div className="row m-0 pb-1.5 align-items-center">
<p data-hj-suppress className="h5 font-weight-bold m-0">
{intl.formatMessage(messages['profile.name.full.name'])}
</p>
<OverlayTrigger
key="top"
placement="top"
overlay={(
<Tooltip variant="light" id="tooltip-top">
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.name.tooltip'])}
</p>
</Tooltip>
)}
>
<InfoOutline className="m-0 info-icon" />
</OverlayTrigger>
</div>
<EmptyContent onClick={handleOpen}>
{intl.formatMessage(messages['profile.name.empty'])}
</EmptyContent>
</>
),
static: (
<>
<div className="row m-0 pb-1.5 align-items-center">
<p data-hj-suppress className="h5 font-weight-bold m-0">
{intl.formatMessage(messages['profile.name.full.name'])}
</p>
<OverlayTrigger
key="top"
placement="top"
overlay={(
<Tooltip variant="light" id="tooltip-top">
<p className="h5 font-weight-normal m-0 p-0">
{intl.formatMessage(messages['profile.name.tooltip'])}
</p>
</Tooltip>
)}
>
<InfoOutline className="m-0 info-icon" />
</OverlayTrigger>
</div>
<EditableItemHeader content={name} />
</>
),
}}
/>
);
};
Name.propTypes = {
formId: PropTypes.string.isRequired,
name: PropTypes.string,
visibilityName: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
accountSettingsUrl: PropTypes.string.isRequired,
};
Name.defaultProps = {
editMode: 'static',
saveState: null,
name: null,
visibilityName: 'private',
};
export default connect(
editableFormSelector,
{},
)(Name);

View File

@@ -1,26 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
'profile.name.full.name': {
id: 'profile.name.full.name',
defaultMessage: 'Full name',
description: 'A section of a user profile',
},
'profile.name.empty': {
id: 'profile.name.empty',
defaultMessage: 'Add full name',
description: 'The affordance to add a name to a users profile.',
},
'profile.name.tooltip': {
id: 'profile.name.tooltip',
defaultMessage: 'The name that is used for ID verification and that appears on your certificates',
description: 'Tooltip for the full name field.',
},
'profile.name.redirect': {
id: 'profile.name.redirect',
defaultMessage: 'Edit full name from the Accounts page',
description: 'Redirect message for editing the name from the Accounts page.',
},
});
export default messages;

View File

@@ -1,166 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
import messages from './PreferredLanguage.messages';
import FormControls from './elements/FormControls';
import EditableItemHeader from './elements/EditableItemHeader';
import EmptyContent from './elements/EmptyContent';
import SwitchContent from './elements/SwitchContent';
import { preferredLanguageSelector } from '../data/selectors';
import {
useCloseOpenHandler,
useHandleSubmit,
useIsVisibilityEnabled,
} from '../data/hooks';
const PreferredLanguage = ({
formId,
languageProficiencies,
visibilityLanguageProficiencies,
editMode,
saveState,
error,
sortedLanguages,
languageMessages,
changeHandler,
submitHandler,
closeHandler,
openHandler,
}) => {
const isVisibilityEnabled = useIsVisibilityEnabled();
const intl = useIntl();
const handleChange = ({ target: { name, value } }) => {
let newValue = value;
if (name === formId) {
newValue = value ? [{ code: value }] : [];
}
changeHandler(name, newValue);
};
const handleSubmit = useHandleSubmit(submitHandler, formId);
const handleOpen = useCloseOpenHandler(openHandler, formId);
const handleClose = useCloseOpenHandler(closeHandler, formId);
const value = languageProficiencies.length ? languageProficiencies[0].code : '';
return (
<SwitchContent
className="pt-40px"
expression={editMode}
cases={{
editing: (
<div role="dialog" aria-labelledby={`${formId}-label`}>
<form onSubmit={handleSubmit}>
<Form.Group
controlId={formId}
className="m-0 pb-3"
isInvalid={error !== null}
>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-2.5">
{intl.formatMessage(messages['profile.preferredlanguage.label'])}
</p>
<select
data-hj-suppress
id={formId}
name={formId}
className="form-control py-10px"
value={value}
onChange={handleChange}
>
<option value=""> </option>
{sortedLanguages.map(({ code, name }) => (
<option key={code} value={code}>{name}</option>
))}
</select>
{error !== null && (
<Form.Control.Feedback hasIcon={false}>
{error}
</Form.Control.Feedback>
)}
</Form.Group>
<FormControls
visibilityId="visibilityLanguageProficiencies"
saveState={saveState}
visibility={visibilityLanguageProficiencies}
cancelHandler={handleClose}
changeHandler={handleChange}
/>
</form>
</div>
),
editable: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.preferredlanguage.label'])}
</p>
<EditableItemHeader
content={languageMessages[value]}
showEditButton
onClickEdit={handleOpen}
showVisibility={visibilityLanguageProficiencies !== null && isVisibilityEnabled}
visibility={visibilityLanguageProficiencies}
/>
</>
),
empty: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.preferredlanguage.label'])}
</p>
<EmptyContent onClick={handleOpen}>
{intl.formatMessage(messages['profile.preferredlanguage.empty'])}
</EmptyContent>
</>
),
static: (
<>
<p data-hj-suppress className="h5 font-weight-bold m-0 pb-1.5">
{intl.formatMessage(messages['profile.preferredlanguage.label'])}
</p>
<EditableItemHeader content={languageMessages[value]} />
</>
),
}}
/>
);
};
PreferredLanguage.propTypes = {
formId: PropTypes.string.isRequired,
languageProficiencies: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.shape({ code: PropTypes.string })),
PropTypes.oneOf(['']),
]),
visibilityLanguageProficiencies: PropTypes.oneOf(['private', 'all_users']),
editMode: PropTypes.oneOf(['editing', 'editable', 'empty', 'static']),
saveState: PropTypes.string,
error: PropTypes.string,
sortedLanguages: PropTypes.arrayOf(PropTypes.shape({
code: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})).isRequired,
languageMessages: PropTypes.objectOf(PropTypes.string).isRequired,
changeHandler: PropTypes.func.isRequired,
submitHandler: PropTypes.func.isRequired,
closeHandler: PropTypes.func.isRequired,
openHandler: PropTypes.func.isRequired,
};
PreferredLanguage.defaultProps = {
editMode: 'static',
saveState: null,
languageProficiencies: [],
visibilityLanguageProficiencies: 'private',
error: null,
};
export default connect(
preferredLanguageSelector,
{},
)(PreferredLanguage);

Some files were not shown because too many files have changed in this diff Show More