mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2025-12-19 18:18:27 -05:00
feat(client): add settings side nav (#63034)
Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Co-authored-by: ahmad abdolsaheb <ahmad.abdolsaheb@gmail.com>
This commit is contained in:
@@ -281,7 +281,7 @@
|
|||||||
"disabled": "Your certifications will be disabled, if set to private.",
|
"disabled": "Your certifications will be disabled, if set to private.",
|
||||||
"private-name": "Your name will not appear on your certifications, if this is set to private.",
|
"private-name": "Your name will not appear on your certifications, if this is set to private.",
|
||||||
"claim-legacy": "Once you've earned the following freeCodeCamp certifications, you'll be able to claim the {{cert}}:",
|
"claim-legacy": "Once you've earned the following freeCodeCamp certifications, you'll be able to claim the {{cert}}:",
|
||||||
"for": "Account Settings for {{username}}",
|
"for": "Settings for {{username}}",
|
||||||
"sound-mode": "This adds the pleasant sound of acoustic guitar throughout the website. You'll get musical feedback as you type in the editor, complete challenges, claim certifications, and more.",
|
"sound-mode": "This adds the pleasant sound of acoustic guitar throughout the website. You'll get musical feedback as you type in the editor, complete challenges, claim certifications, and more.",
|
||||||
"sound-volume": "Campfire Volume:",
|
"sound-volume": "Campfire Volume:",
|
||||||
"scrollbar-width": "Editor Scrollbar Width",
|
"scrollbar-width": "Editor Scrollbar Width",
|
||||||
@@ -328,12 +328,13 @@
|
|||||||
"keyboard-shortcuts": "Enable Keyboard Shortcuts"
|
"keyboard-shortcuts": "Enable Keyboard Shortcuts"
|
||||||
},
|
},
|
||||||
"headings": {
|
"headings": {
|
||||||
|
"account": "Account",
|
||||||
"certs": "Certifications",
|
"certs": "Certifications",
|
||||||
"legacy-certs": "Legacy Certifications",
|
"legacy-certs": "Legacy Certifications",
|
||||||
"honesty": "Academic Honesty Policy",
|
"honesty": "Academic Honesty Policy",
|
||||||
"internet": "Your Internet Presence",
|
"internet": "Your Internet Presence",
|
||||||
"portfolio": "Portfolio Settings",
|
"portfolio": "Portfolio Settings",
|
||||||
"privacy": "Privacy Settings",
|
"privacy": "Privacy",
|
||||||
"personal-info": "Personal Information"
|
"personal-info": "Personal Information"
|
||||||
},
|
},
|
||||||
"danger": {
|
"danger": {
|
||||||
@@ -362,7 +363,7 @@
|
|||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"missing": "You do not have an email associated with this account.",
|
"missing": "You do not have an email associated with this account.",
|
||||||
"heading": "Email Settings",
|
"heading": "Email",
|
||||||
"not-verified": "Your email has not been verified.",
|
"not-verified": "Your email has not been verified.",
|
||||||
"check": "Please check your email, or <0>request a new verification email here</0>.",
|
"check": "Please check your email, or <0>request a new verification email here</0>.",
|
||||||
"current": "Current Email",
|
"current": "Current Email",
|
||||||
|
|||||||
71
client/src/client-only-routes/show-settings.css
Normal file
71
client/src/client-only-routes/show-settings.css
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
.settings-container {
|
||||||
|
display: flex;
|
||||||
|
min-height: calc(100vh - var(--header-height));
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-main {
|
||||||
|
flex: 4;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar-nav ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar-nav ul li {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar-nav {
|
||||||
|
flex: 1;
|
||||||
|
position: sticky;
|
||||||
|
top: var(--header-height);
|
||||||
|
padding: 1rem 0;
|
||||||
|
overflow-y: scroll;
|
||||||
|
height: calc(100vh - var(--header-height));
|
||||||
|
border-right: 3px solid var(--tertiary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar-nav .sidebar-nav-section-heading {
|
||||||
|
display: block;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
padding-left: 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar-nav .sidebar-nav-anchor-btn {
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 1rem 0rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-sidebar-nav .sidebar-nav-section-heading.active,
|
||||||
|
.settings-sidebar-nav .sidebar-nav-anchor-btn.active {
|
||||||
|
box-shadow: inset 3px 0 0 0 var(--highlight-color);
|
||||||
|
background-color: var(--tertiary-background);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 980px) {
|
||||||
|
.settings-sidebar-nav {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-container {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-user-token-heading,
|
||||||
|
.settings-danger-zone-heading,
|
||||||
|
.settings-exam-token-heading {
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -3,22 +3,22 @@ import Helmet from 'react-helmet';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { scroller } from 'react-scroll';
|
|
||||||
|
|
||||||
import { Container, Spacer } from '@freecodecamp/ui';
|
|
||||||
|
|
||||||
|
import { Spacer } from '@freecodecamp/ui';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
import { scroller, Element as ScrollElement } from 'react-scroll';
|
||||||
import envData from '../../config/env.json';
|
import envData from '../../config/env.json';
|
||||||
import { createFlashMessage } from '../components/Flash/redux';
|
import { createFlashMessage } from '../components/Flash/redux';
|
||||||
import { Loader } from '../components/helpers';
|
import { Loader } from '../components/helpers';
|
||||||
import Certification from '../components/settings/certification';
|
import Certification from '../components/settings/certification';
|
||||||
import MiscSettings from '../components/settings/misc-settings';
|
import Account from '../components/settings/account';
|
||||||
import DangerZone from '../components/settings/danger-zone';
|
import DangerZone from '../components/settings/danger-zone';
|
||||||
import Email from '../components/settings/email';
|
import Email from '../components/settings/email';
|
||||||
import Honesty from '../components/settings/honesty';
|
import Honesty from '../components/settings/honesty';
|
||||||
import Privacy from '../components/settings/privacy';
|
import Privacy from '../components/settings/privacy';
|
||||||
import UserToken from '../components/settings/user-token';
|
import UserToken from '../components/settings/user-token';
|
||||||
import ExamToken from '../components/settings/exam-token';
|
import ExamToken from '../components/settings/exam-token';
|
||||||
|
import SettingsSidebarNav from '../components/settings/settings-sidebar-nav';
|
||||||
import { hardGoTo as navigate } from '../redux/actions';
|
import { hardGoTo as navigate } from '../redux/actions';
|
||||||
import {
|
import {
|
||||||
signInLoadingSelector,
|
signInLoadingSelector,
|
||||||
@@ -37,6 +37,8 @@ import {
|
|||||||
resetMyEditorLayout
|
resetMyEditorLayout
|
||||||
} from '../redux/settings/actions';
|
} from '../redux/settings/actions';
|
||||||
|
|
||||||
|
import './show-settings.css';
|
||||||
|
|
||||||
const { apiLocation } = envData;
|
const { apiLocation } = envData;
|
||||||
|
|
||||||
// TODO: update types for actions
|
// TODO: update types for actions
|
||||||
@@ -171,78 +173,99 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet title={`${t('buttons.settings')} | freeCodeCamp.org`} />
|
<Helmet title={`${t('buttons.settings')} | freeCodeCamp.org`} />
|
||||||
<Container>
|
<div className='settings-container' id='settings-container'>
|
||||||
<main>
|
<SettingsSidebarNav userToken={userToken} />
|
||||||
|
<main className='settings-main'>
|
||||||
<Spacer size='l' />
|
<Spacer size='l' />
|
||||||
<h1
|
<ScrollElement name='username'>
|
||||||
id='content-start'
|
<h1
|
||||||
className='text-center'
|
id='content-start'
|
||||||
style={{ overflowWrap: 'break-word' }}
|
className='text-center'
|
||||||
data-playwright-test-label='settings-heading'
|
style={{ overflowWrap: 'break-word' }}
|
||||||
>
|
data-playwright-test-label='settings-heading'
|
||||||
{t('settings.for', { username: username })}
|
>
|
||||||
</h1>
|
{t('settings.for', { username: username })}
|
||||||
<MiscSettings
|
</h1>
|
||||||
keyboardShortcuts={keyboardShortcuts}
|
</ScrollElement>
|
||||||
sound={sound}
|
|
||||||
editorLayout={editorLayout}
|
|
||||||
resetEditorLayout={resetEditorLayout}
|
|
||||||
toggleKeyboardShortcuts={toggleKeyboardShortcuts}
|
|
||||||
toggleSoundMode={toggleSoundMode}
|
|
||||||
/>
|
|
||||||
<Spacer size='m' />
|
<Spacer size='m' />
|
||||||
<Privacy />
|
<ScrollElement name='account'>
|
||||||
|
<Account
|
||||||
|
keyboardShortcuts={keyboardShortcuts}
|
||||||
|
sound={sound}
|
||||||
|
editorLayout={editorLayout}
|
||||||
|
resetEditorLayout={resetEditorLayout}
|
||||||
|
toggleKeyboardShortcuts={toggleKeyboardShortcuts}
|
||||||
|
toggleSoundMode={toggleSoundMode}
|
||||||
|
/>
|
||||||
|
</ScrollElement>
|
||||||
<Spacer size='m' />
|
<Spacer size='m' />
|
||||||
<Email
|
<ScrollElement name='privacy'>
|
||||||
email={email}
|
<Privacy />
|
||||||
isEmailVerified={isEmailVerified}
|
</ScrollElement>
|
||||||
sendQuincyEmail={sendQuincyEmail}
|
|
||||||
updateQuincyEmail={updateQuincyEmail}
|
|
||||||
/>
|
|
||||||
<Spacer size='m' />
|
<Spacer size='m' />
|
||||||
<Honesty isHonest={isHonest} updateIsHonest={updateIsHonest} />
|
<ScrollElement name='email'>
|
||||||
|
<Email
|
||||||
|
email={email}
|
||||||
|
isEmailVerified={isEmailVerified}
|
||||||
|
sendQuincyEmail={sendQuincyEmail}
|
||||||
|
updateQuincyEmail={updateQuincyEmail}
|
||||||
|
/>
|
||||||
|
</ScrollElement>
|
||||||
<Spacer size='m' />
|
<Spacer size='m' />
|
||||||
<ExamToken email={email} />
|
<ScrollElement name='honesty'>
|
||||||
<Certification
|
<Honesty isHonest={isHonest} updateIsHonest={updateIsHonest} />
|
||||||
completedChallenges={completedChallenges}
|
</ScrollElement>
|
||||||
createFlashMessage={createFlashMessage}
|
<Spacer size='m' />
|
||||||
is2018DataVisCert={is2018DataVisCert}
|
<ScrollElement name='exam-token'>
|
||||||
isA2EnglishCert={isA2EnglishCert}
|
<ExamToken email={email} />
|
||||||
isApisMicroservicesCert={isApisMicroservicesCert}
|
</ScrollElement>
|
||||||
isBackEndCert={isBackEndCert}
|
<Spacer size='m' />
|
||||||
isDataAnalysisPyCertV7={isDataAnalysisPyCertV7}
|
<ScrollElement name='certifications'>
|
||||||
isDataVisCert={isDataVisCert}
|
<Certification
|
||||||
isCollegeAlgebraPyCertV8={isCollegeAlgebraPyCertV8}
|
completedChallenges={completedChallenges}
|
||||||
isFoundationalCSharpCertV8={isFoundationalCSharpCertV8}
|
createFlashMessage={createFlashMessage}
|
||||||
isFrontEndCert={isFrontEndCert}
|
is2018DataVisCert={is2018DataVisCert}
|
||||||
isFrontEndLibsCert={isFrontEndLibsCert}
|
isA2EnglishCert={isA2EnglishCert}
|
||||||
isFullStackCert={isFullStackCert}
|
isApisMicroservicesCert={isApisMicroservicesCert}
|
||||||
isJavascriptCertV9={isJavascriptCertV9}
|
isBackEndCert={isBackEndCert}
|
||||||
isHonest={isHonest}
|
isDataAnalysisPyCertV7={isDataAnalysisPyCertV7}
|
||||||
isInfosecCertV7={isInfosecCertV7}
|
isDataVisCert={isDataVisCert}
|
||||||
isInfosecQaCert={isInfosecQaCert}
|
isCollegeAlgebraPyCertV8={isCollegeAlgebraPyCertV8}
|
||||||
isJsAlgoDataStructCert={isJsAlgoDataStructCert}
|
isFoundationalCSharpCertV8={isFoundationalCSharpCertV8}
|
||||||
isMachineLearningPyCertV7={isMachineLearningPyCertV7}
|
isFrontEndCert={isFrontEndCert}
|
||||||
isQaCertV7={isQaCertV7}
|
isFrontEndLibsCert={isFrontEndLibsCert}
|
||||||
isRelationalDatabaseCertV8={isRelationalDatabaseCertV8}
|
isFullStackCert={isFullStackCert}
|
||||||
isRespWebDesignCert={isRespWebDesignCert}
|
isJavascriptCertV9={isJavascriptCertV9}
|
||||||
isRespWebDesignCertV9={isRespWebDesignCertV9}
|
isHonest={isHonest}
|
||||||
isSciCompPyCertV7={isSciCompPyCertV7}
|
isInfosecCertV7={isInfosecCertV7}
|
||||||
isJsAlgoDataStructCertV8={isJsAlgoDataStructCertV8}
|
isInfosecQaCert={isInfosecQaCert}
|
||||||
username={username}
|
isJsAlgoDataStructCert={isJsAlgoDataStructCert}
|
||||||
verifyCert={verifyCert}
|
isMachineLearningPyCertV7={isMachineLearningPyCertV7}
|
||||||
isEmailVerified={isEmailVerified}
|
isQaCertV7={isQaCertV7}
|
||||||
/>
|
isRelationalDatabaseCertV8={isRelationalDatabaseCertV8}
|
||||||
|
isRespWebDesignCert={isRespWebDesignCert}
|
||||||
|
isRespWebDesignCertV9={isRespWebDesignCertV9}
|
||||||
|
isSciCompPyCertV7={isSciCompPyCertV7}
|
||||||
|
isJsAlgoDataStructCertV8={isJsAlgoDataStructCertV8}
|
||||||
|
username={username}
|
||||||
|
verifyCert={verifyCert}
|
||||||
|
isEmailVerified={isEmailVerified}
|
||||||
|
/>
|
||||||
|
<Spacer size='m' />
|
||||||
|
</ScrollElement>
|
||||||
{userToken && (
|
{userToken && (
|
||||||
<>
|
<>
|
||||||
|
<ScrollElement name='user-token'>
|
||||||
|
<UserToken />
|
||||||
|
</ScrollElement>
|
||||||
<Spacer size='m' />
|
<Spacer size='m' />
|
||||||
<UserToken />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Spacer size='m' />
|
<ScrollElement name='danger-zone'>
|
||||||
<DangerZone />
|
<DangerZone />
|
||||||
|
</ScrollElement>
|
||||||
</main>
|
</main>
|
||||||
</Container>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import { Button, Spacer } from '@freecodecamp/ui';
|
import { Button, Spacer } from '@freecodecamp/ui';
|
||||||
import { FullWidthRow } from '../helpers';
|
import { FullWidthRow } from '../helpers';
|
||||||
|
|
||||||
import SoundSettings from '../../components/settings/sound';
|
import SoundSettings from './sound';
|
||||||
import KeyboardShortcutsSettings from '../../components/settings/keyboard-shortcuts';
|
import KeyboardShortcutsSettings from './keyboard-shortcuts';
|
||||||
import ScrollbarWidthSettings from '../../components/settings/scrollbar-width';
|
import ScrollbarWidthSettings from './scrollbar-width';
|
||||||
|
import SectionHeader from './section-header';
|
||||||
|
|
||||||
type MiscSettingsProps = {
|
type MiscSettingsProps = {
|
||||||
keyboardShortcuts: boolean;
|
keyboardShortcuts: boolean;
|
||||||
@@ -27,8 +28,8 @@ const MiscSettings = ({
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='account-settings'>
|
||||||
<Spacer size='m' />
|
<SectionHeader>{t('settings.headings.account')}</SectionHeader>
|
||||||
<FullWidthRow>
|
<FullWidthRow>
|
||||||
<SoundSettings sound={sound} toggleSoundMode={toggleSoundMode} />
|
<SoundSettings sound={sound} toggleSoundMode={toggleSoundMode} />
|
||||||
<KeyboardShortcutsSettings
|
<KeyboardShortcutsSettings
|
||||||
@@ -51,7 +52,7 @@ const MiscSettings = ({
|
|||||||
{t('settings.reset-editor-layout')}
|
{t('settings.reset-editor-layout')}
|
||||||
</Button>
|
</Button>
|
||||||
</FullWidthRow>
|
</FullWidthRow>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -155,60 +155,66 @@ const LegacyFullStack = (props: CertificationSettingsProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FullWidthRow key={certSlug}>
|
<Element name={`cert-${certSlug}`}>
|
||||||
<Spacer size='m' />
|
<FullWidthRow key={certSlug}>
|
||||||
<h3 className='text-center'>
|
<Spacer size='m' />
|
||||||
{t(`certification.title.${Certification.LegacyFullStack}-cert`)}
|
<h3 className='text-center'>
|
||||||
</h3>
|
{t(`certification.title.${Certification.LegacyFullStack}`)}
|
||||||
<div>
|
</h3>
|
||||||
<p>
|
<div>
|
||||||
{t('settings.claim-legacy', {
|
<p>
|
||||||
cert: t(`certification.title.${Certification.LegacyFullStack}-cert`)
|
{t('settings.claim-legacy', {
|
||||||
})}
|
cert: t(
|
||||||
</p>
|
`certification.title.${Certification.LegacyFullStack}-cert`
|
||||||
<ul>
|
)
|
||||||
<li>{t(`certification.title.${Certification.RespWebDesign}`)}</li>
|
})}
|
||||||
<li>{t(`certification.title.${Certification.JsAlgoDataStruct}`)}</li>
|
</p>
|
||||||
<li>{t(`certification.title.${Certification.FrontEndDevLibs}`)}</li>
|
<ul>
|
||||||
<li>{t(`certification.title.${Certification.DataVis}`)}</li>
|
<li>{t(`certification.title.${Certification.RespWebDesign}`)}</li>
|
||||||
<li>{t(`certification.title.${Certification.BackEndDevApis}`)}</li>
|
<li>
|
||||||
<li>{t(`certification.title.${Certification.LegacyInfoSecQa}`)}</li>
|
{t(`certification.title.${Certification.JsAlgoDataStruct}`)}
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
<li>{t(`certification.title.${Certification.FrontEndDevLibs}`)}</li>
|
||||||
|
<li>{t(`certification.title.${Certification.DataVis}`)}</li>
|
||||||
|
<li>{t(`certification.title.${Certification.BackEndDevApis}`)}</li>
|
||||||
|
<li>{t(`certification.title.${Certification.LegacyInfoSecQa}`)}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{isFullStackCert ? (
|
{isFullStackCert ? (
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
variant='primary'
|
variant='primary'
|
||||||
block={true}
|
block={true}
|
||||||
href={certLocation}
|
href={certLocation}
|
||||||
id={'button-' + certSlug}
|
id={'button-' + certSlug}
|
||||||
target='_blank'
|
target='_blank'
|
||||||
>
|
>
|
||||||
{t('buttons.show-cert')}{' '}
|
{t('buttons.show-cert')}{' '}
|
||||||
<span className='sr-only'>
|
<span className='sr-only'>
|
||||||
{t(`certification.title.${Certification.LegacyFullStack}`)}
|
{t(`certification.title.${Certification.LegacyFullStack}`)}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
size='small'
|
size='small'
|
||||||
variant='primary'
|
variant='primary'
|
||||||
block={true}
|
block={true}
|
||||||
disabled={!fullStackClaimable}
|
disabled={!fullStackClaimable}
|
||||||
id={'button-' + certSlug}
|
id={'button-' + certSlug}
|
||||||
onClick={handleClaim(certSlug)}
|
onClick={handleClaim(certSlug)}
|
||||||
>
|
>
|
||||||
{t('buttons.claim-cert')}{' '}
|
{t('buttons.claim-cert')}{' '}
|
||||||
<span className='sr-only'>
|
<span className='sr-only'>
|
||||||
{t(`certification.title.${Certification.LegacyFullStack}`)}
|
{t(`certification.title.${Certification.LegacyFullStack}`)}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Spacer size='m' />
|
<Spacer size='m' />
|
||||||
</FullWidthRow>
|
</FullWidthRow>
|
||||||
|
</Element>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -385,7 +391,9 @@ function CertificationSettings(props: CertificationSettingsProps) {
|
|||||||
<Certification key={cert} certSlug={cert} t={t} />
|
<Certification key={cert} certSlug={cert} t={t} />
|
||||||
))}
|
))}
|
||||||
<Spacer size='m' />
|
<Spacer size='m' />
|
||||||
<SectionHeader>{t('settings.headings.legacy-certs')}</SectionHeader>
|
<Element name='legacy-certifications'>
|
||||||
|
<SectionHeader>{t('settings.headings.legacy-certs')}</SectionHeader>
|
||||||
|
</Element>
|
||||||
<LegacyFullStack {...props} />
|
<LegacyFullStack {...props} />
|
||||||
{legacyCertifications.map(cert => (
|
{legacyCertifications.map(cert => (
|
||||||
<Certification key={cert} certSlug={cert} t={t} />
|
<Certification key={cert} certSlug={cert} t={t} />
|
||||||
|
|||||||
@@ -43,7 +43,11 @@ function DangerZone({ deleteAccount, resetProgress, t }: DangerZoneProps) {
|
|||||||
return (
|
return (
|
||||||
<FullWidthRow className='text-center'>
|
<FullWidthRow className='text-center'>
|
||||||
<Panel variant='danger' id='danger-zone'>
|
<Panel variant='danger' id='danger-zone'>
|
||||||
<Panel.Heading>{t('settings.danger.heading')}</Panel.Heading>
|
<Panel.Heading>
|
||||||
|
<h2 className='settings-danger-zone-heading'>
|
||||||
|
{t('settings.danger.heading')}
|
||||||
|
</h2>
|
||||||
|
</Panel.Heading>
|
||||||
<Spacer size='m' />
|
<Spacer size='m' />
|
||||||
<p>{t('settings.danger.be-careful')}</p>
|
<p>{t('settings.danger.be-careful')}</p>
|
||||||
<FullWidthRow>
|
<FullWidthRow>
|
||||||
|
|||||||
@@ -95,7 +95,11 @@ function ExamToken({ email }: ExamTokenProps) {
|
|||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
<Panel variant='info' id='exam-environment-authorization-token'>
|
<Panel variant='info' id='exam-environment-authorization-token'>
|
||||||
<Panel.Heading>{t('exam-token.exam-token')}</Panel.Heading>
|
<Panel.Heading>
|
||||||
|
<h2 className='settings-exam-token-heading'>
|
||||||
|
{t('exam-token.exam-token')}
|
||||||
|
</h2>
|
||||||
|
</Panel.Heading>
|
||||||
<Panel.Body>
|
<Panel.Body>
|
||||||
<p>{t('exam-token.note')}</p>
|
<p>{t('exam-token.note')}</p>
|
||||||
<strong>{t('exam-token.invalidation-2')}</strong>
|
<strong>{t('exam-token.invalidation-2')}</strong>
|
||||||
|
|||||||
232
client/src/components/settings/settings-sidebar-nav.tsx
Normal file
232
client/src/components/settings/settings-sidebar-nav.tsx
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Link as ScrollLink } from 'react-scroll';
|
||||||
|
import {
|
||||||
|
currentCertifications,
|
||||||
|
legacyCertifications,
|
||||||
|
legacyFullStackCertification,
|
||||||
|
upcomingCertifications
|
||||||
|
} from '../../../../shared-dist/config/certification-settings';
|
||||||
|
import env from '../../../config/env.json';
|
||||||
|
|
||||||
|
type SettingsSidebarNavProps = {
|
||||||
|
userToken: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { showUpcomingChanges } = env;
|
||||||
|
|
||||||
|
function SettingsSidebarNav({
|
||||||
|
userToken
|
||||||
|
}: SettingsSidebarNavProps): JSX.Element {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const allLegacyCertifications = [
|
||||||
|
...legacyFullStackCertification,
|
||||||
|
...legacyCertifications
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className='settings-sidebar-nav'>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<ScrollLink
|
||||||
|
to='account'
|
||||||
|
href='#account'
|
||||||
|
className='sidebar-nav-section-heading'
|
||||||
|
smooth={true}
|
||||||
|
offset={-48}
|
||||||
|
duration={300}
|
||||||
|
spy={true}
|
||||||
|
hashSpy={true}
|
||||||
|
activeClass='active'
|
||||||
|
>
|
||||||
|
{t('settings.headings.account')}
|
||||||
|
</ScrollLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ScrollLink
|
||||||
|
to='privacy'
|
||||||
|
href='#privacy'
|
||||||
|
className='sidebar-nav-section-heading'
|
||||||
|
smooth={true}
|
||||||
|
offset={-48}
|
||||||
|
duration={300}
|
||||||
|
spy={true}
|
||||||
|
hashSpy={true}
|
||||||
|
activeClass='active'
|
||||||
|
>
|
||||||
|
{t('settings.headings.privacy')}
|
||||||
|
</ScrollLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ScrollLink
|
||||||
|
to='email'
|
||||||
|
href='#email'
|
||||||
|
className='sidebar-nav-section-heading'
|
||||||
|
smooth={true}
|
||||||
|
offset={-48}
|
||||||
|
duration={300}
|
||||||
|
spy={true}
|
||||||
|
hashSpy={true}
|
||||||
|
activeClass='active'
|
||||||
|
>
|
||||||
|
{t('settings.email.heading')}
|
||||||
|
</ScrollLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ScrollLink
|
||||||
|
to='honesty'
|
||||||
|
href='#honesty'
|
||||||
|
className='sidebar-nav-section-heading'
|
||||||
|
smooth={true}
|
||||||
|
offset={-48}
|
||||||
|
duration={300}
|
||||||
|
spy={true}
|
||||||
|
hashSpy={true}
|
||||||
|
activeClass='active'
|
||||||
|
>
|
||||||
|
{t('settings.headings.honesty')}
|
||||||
|
</ScrollLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ScrollLink
|
||||||
|
to='exam-token'
|
||||||
|
href='#exam-token'
|
||||||
|
className='sidebar-nav-section-heading'
|
||||||
|
smooth={true}
|
||||||
|
offset={-48}
|
||||||
|
duration={300}
|
||||||
|
spy={true}
|
||||||
|
hashSpy={true}
|
||||||
|
activeClass='active'
|
||||||
|
>
|
||||||
|
{t('exam-token.exam-token')}
|
||||||
|
</ScrollLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ScrollLink
|
||||||
|
to='certifications'
|
||||||
|
href='#certifications'
|
||||||
|
className='sidebar-nav-section-heading'
|
||||||
|
smooth={true}
|
||||||
|
offset={-48}
|
||||||
|
duration={300}
|
||||||
|
spy={true}
|
||||||
|
hashSpy={true}
|
||||||
|
activeClass='active'
|
||||||
|
>
|
||||||
|
{t('settings.headings.certs')}
|
||||||
|
</ScrollLink>
|
||||||
|
<ul>
|
||||||
|
{currentCertifications.map(slug => (
|
||||||
|
<li key={slug}>
|
||||||
|
<ScrollLink
|
||||||
|
to={`cert-${slug}`}
|
||||||
|
href={`#cert-${slug}`}
|
||||||
|
className={'sidebar-nav-anchor-btn'}
|
||||||
|
smooth={true}
|
||||||
|
offset={-48}
|
||||||
|
duration={300}
|
||||||
|
spy={true}
|
||||||
|
hashSpy={true}
|
||||||
|
activeClass='active'
|
||||||
|
>
|
||||||
|
{t(`certification.title.${slug}`, slug)}
|
||||||
|
</ScrollLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<ScrollLink
|
||||||
|
to='legacy-certifications'
|
||||||
|
href='#legacy-certifications'
|
||||||
|
className='sidebar-nav-section-heading'
|
||||||
|
smooth={true}
|
||||||
|
offset={-48}
|
||||||
|
duration={300}
|
||||||
|
spy={true}
|
||||||
|
hashSpy={true}
|
||||||
|
activeClass='active'
|
||||||
|
>
|
||||||
|
{t('settings.headings.legacy-certs')}
|
||||||
|
</ScrollLink>
|
||||||
|
<ul>
|
||||||
|
{allLegacyCertifications.map(slug => (
|
||||||
|
<li key={slug}>
|
||||||
|
<ScrollLink
|
||||||
|
to={`cert-${slug}`}
|
||||||
|
href={`#cert-${slug}`}
|
||||||
|
className={'sidebar-nav-anchor-btn'}
|
||||||
|
smooth={true}
|
||||||
|
offset={-48}
|
||||||
|
duration={300}
|
||||||
|
spy={true}
|
||||||
|
hashSpy={true}
|
||||||
|
activeClass='active'
|
||||||
|
>
|
||||||
|
{t(`certification.title.${slug}`, slug)}
|
||||||
|
</ScrollLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<ul>
|
||||||
|
{showUpcomingChanges &&
|
||||||
|
upcomingCertifications.map(slug => (
|
||||||
|
<li key={slug}>
|
||||||
|
<ScrollLink
|
||||||
|
to={`cert-${slug}`}
|
||||||
|
href={`#cert-${slug}`}
|
||||||
|
className={'sidebar-nav-anchor-btn'}
|
||||||
|
smooth={true}
|
||||||
|
offset={-48}
|
||||||
|
duration={300}
|
||||||
|
spy={true}
|
||||||
|
hashSpy={true}
|
||||||
|
activeClass='active'
|
||||||
|
>
|
||||||
|
{t(`certification.title.${slug}`, slug)}
|
||||||
|
</ScrollLink>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
{userToken && (
|
||||||
|
<li>
|
||||||
|
<ScrollLink
|
||||||
|
to='user-token'
|
||||||
|
href='#user-token'
|
||||||
|
className='sidebar-nav-section-heading'
|
||||||
|
smooth={true}
|
||||||
|
offset={-48}
|
||||||
|
duration={300}
|
||||||
|
spy={true}
|
||||||
|
hashSpy={true}
|
||||||
|
activeClass='active'
|
||||||
|
>
|
||||||
|
{t('user-token.title')}
|
||||||
|
</ScrollLink>
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
<li>
|
||||||
|
<ScrollLink
|
||||||
|
to='danger-zone'
|
||||||
|
href='#danger-zone'
|
||||||
|
className='sidebar-nav-section-heading'
|
||||||
|
smooth={true}
|
||||||
|
offset={-48}
|
||||||
|
duration={300}
|
||||||
|
spy={true}
|
||||||
|
hashSpy={true}
|
||||||
|
activeClass='active'
|
||||||
|
>
|
||||||
|
{t('settings.danger.heading')}
|
||||||
|
</ScrollLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsSidebarNav.displayName = 'SettingsSidebarNav';
|
||||||
|
|
||||||
|
export default SettingsSidebarNav;
|
||||||
@@ -29,7 +29,11 @@ class UserToken extends Component<UserTokenProps> {
|
|||||||
return (
|
return (
|
||||||
<FullWidthRow>
|
<FullWidthRow>
|
||||||
<Panel variant='info' className='text-center'>
|
<Panel variant='info' className='text-center'>
|
||||||
<Panel.Heading>{t('user-token.title')}</Panel.Heading>
|
<Panel.Heading>
|
||||||
|
<h2 className='settings-user-token-heading'>
|
||||||
|
{t('user-token.title')}
|
||||||
|
</h2>
|
||||||
|
</Panel.Heading>
|
||||||
<Spacer size='m' />
|
<Spacer size='m' />
|
||||||
<Panel.Body>
|
<Panel.Body>
|
||||||
<p>{t('user-token.delete-p1')}</p>
|
<p>{t('user-token.delete-p1')}</p>
|
||||||
|
|||||||
61
e2e/settings-sidenav.spec.ts
Normal file
61
e2e/settings-sidenav.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test.use({ storageState: 'playwright/.auth/certified-user.json' });
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
// Set viewport to desktop size to ensure sideNav is visible
|
||||||
|
await page.setViewportSize({ width: 1280, height: 720 });
|
||||||
|
await page.goto('/settings');
|
||||||
|
|
||||||
|
// Wait for the main heading to appear
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { level: 1, name: 'Settings for certifieduser' })
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Settings SideNav Component', () => {
|
||||||
|
test('should display the settings sideNav with links to all main sections', async ({
|
||||||
|
page,
|
||||||
|
isMobile
|
||||||
|
}) => {
|
||||||
|
test.setTimeout(30000);
|
||||||
|
test.skip(isMobile, 'Sidebar is hidden on mobile');
|
||||||
|
|
||||||
|
const sideNav = page.getByRole('complementary');
|
||||||
|
const main = page.getByRole('main');
|
||||||
|
|
||||||
|
// Get all h2 and h3 heading in the main section
|
||||||
|
const h2Texts = await main
|
||||||
|
.getByRole('heading', { level: 2 })
|
||||||
|
.allTextContents();
|
||||||
|
const h3Texts = await main
|
||||||
|
.getByRole('heading', { level: 3 })
|
||||||
|
.allTextContents();
|
||||||
|
|
||||||
|
const headingTexts = [...h2Texts, ...h3Texts];
|
||||||
|
|
||||||
|
// Make sure the sideNav contains the same number of links as headings
|
||||||
|
const sideNavLinks = sideNav.getByRole('link');
|
||||||
|
await expect(sideNavLinks).toHaveCount(headingTexts.length);
|
||||||
|
|
||||||
|
// For each heading text, find the link by accessible name and test click behavior
|
||||||
|
for (const headingText of headingTexts) {
|
||||||
|
const link = sideNav.getByRole('link', {
|
||||||
|
name: headingText,
|
||||||
|
exact: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(link).toBeVisible();
|
||||||
|
|
||||||
|
// Get the href and assert the URL ends with it after click
|
||||||
|
const href = await link.getAttribute('href');
|
||||||
|
await link.click();
|
||||||
|
|
||||||
|
// Wait for scroll animation
|
||||||
|
// Playwright performs click very fast, which could lead to URL check before scroll ends
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
await expect(page).toHaveURL(new RegExp(href + '$'));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -232,7 +232,7 @@ test.describe('Settings - Certified User', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Danger Zone
|
// Danger Zone
|
||||||
await expect(page.getByText('Danger Zone')).toBeVisible();
|
await expect(page.getByRole('main').getByText('Danger Zone')).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(
|
page.getByText(
|
||||||
'Please be careful. Changes in this section are permanent.'
|
'Please be careful. Changes in this section are permanent.'
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ test.describe('Initially', () => {
|
|||||||
test('should not render', async ({ page }) => {
|
test('should not render', async ({ page }) => {
|
||||||
await page.goto('/settings');
|
await page.goto('/settings');
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('User Token', { exact: true })
|
page.getByRole('main').getByText('User Token', { exact: true })
|
||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -32,7 +32,9 @@ test.describe('After creating token', () => {
|
|||||||
|
|
||||||
await page.goto('/settings');
|
await page.goto('/settings');
|
||||||
// Set `exact` to `true` to only match the panel heading
|
// Set `exact` to `true` to only match the panel heading
|
||||||
await expect(page.getByText('User Token', { exact: true })).toBeVisible();
|
await expect(
|
||||||
|
page.getByRole('main').getByText('User Token', { exact: true })
|
||||||
|
).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(
|
page.getByText(
|
||||||
'Your user token is used to save your progress on curriculum sections that use a virtual machine. If you suspect it has been compromised, you can delete it without losing any progress. A new one will be created automatically the next time you open a project.'
|
'Your user token is used to save your progress on curriculum sections that use a virtual machine. If you suspect it has been compromised, you can delete it without losing any progress. A new one will be created automatically the next time you open a project.'
|
||||||
@@ -42,7 +44,7 @@ test.describe('After creating token', () => {
|
|||||||
|
|
||||||
await alertToBeVisible(page, translations.flash['token-deleted']);
|
await alertToBeVisible(page, translations.flash['token-deleted']);
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('User Token', { exact: true })
|
page.getByRole('main').getByText('User Token', { exact: true })
|
||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user