mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat: move profile settings to profile page (#56135)
Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com> Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
This commit is contained in:
@@ -332,7 +332,8 @@
|
||||
"total-points": "Total Points:",
|
||||
"points": "{{count}} point on {{date}}",
|
||||
"points_plural": "{{count}} points on {{date}}",
|
||||
"page-number": "{{pageNumber}} of {{totalPages}}"
|
||||
"page-number": "{{pageNumber}} of {{totalPages}}",
|
||||
"edit-my-profile": "Edit My Profile"
|
||||
},
|
||||
"footer": {
|
||||
"tax-exempt-status": "freeCodeCamp is a donor-supported tax-exempt 501(c)(3) charitable organization (United States Federal Tax Identification Number: 82-0779546).",
|
||||
@@ -772,7 +773,8 @@
|
||||
"code-example": "{{codeName}} code example",
|
||||
"opens-new-window": "Opens in new window",
|
||||
"rsa-checkbox": "I have tried the Read-Search-Ask method",
|
||||
"similar-questions-checkbox": "I have searched for similar questions that have already been answered on the forum"
|
||||
"similar-questions-checkbox": "I have searched for similar questions that have already been answered on the forum",
|
||||
"edit-my-profile": "Edit my profile"
|
||||
},
|
||||
"flash": {
|
||||
"honest-first": "To claim a certification, you must first agree to our academic honesty policy",
|
||||
|
||||
@@ -7,15 +7,25 @@ import FourOhFour from '../components/FourOhFour';
|
||||
import Loader from '../components/helpers/loader';
|
||||
import Profile from '../components/profile/profile';
|
||||
import { fetchProfileForUser } from '../redux/actions';
|
||||
|
||||
import {
|
||||
submitNewAbout,
|
||||
updateMyPortfolio,
|
||||
updateMySocials
|
||||
} from '../redux/settings/actions';
|
||||
import {
|
||||
usernameSelector,
|
||||
userByNameSelector,
|
||||
userProfileFetchStateSelector
|
||||
} from '../redux/selectors';
|
||||
import { User } from '../redux/prop-types';
|
||||
import { Socials } from '../components/profile/components/internet';
|
||||
|
||||
interface ShowProfileOrFourOhFourProps {
|
||||
fetchProfileForUser: (username: string) => void;
|
||||
updateMyPortfolio: () => void;
|
||||
submitNewAbout: () => void;
|
||||
updateMySocials: (formValues: Socials) => void;
|
||||
fetchState: {
|
||||
pending: boolean;
|
||||
complete: boolean;
|
||||
@@ -53,14 +63,23 @@ const makeMapStateToProps =
|
||||
|
||||
const mapDispatchToProps: {
|
||||
fetchProfileForUser: ShowProfileOrFourOhFourProps['fetchProfileForUser'];
|
||||
submitNewAbout: () => void;
|
||||
updateMyPortfolio: () => void;
|
||||
updateMySocials: (formValues: Socials) => void;
|
||||
} = {
|
||||
fetchProfileForUser
|
||||
fetchProfileForUser,
|
||||
submitNewAbout,
|
||||
updateMyPortfolio,
|
||||
updateMySocials
|
||||
};
|
||||
|
||||
function ShowProfileOrFourOhFour({
|
||||
requestedUser,
|
||||
maybeUser,
|
||||
fetchProfileForUser,
|
||||
submitNewAbout,
|
||||
updateMyPortfolio,
|
||||
updateMySocials,
|
||||
isSessionUser,
|
||||
showLoading
|
||||
}: ShowProfileOrFourOhFourProps) {
|
||||
@@ -85,7 +104,13 @@ function ShowProfileOrFourOhFour({
|
||||
<FourOhFour />
|
||||
)
|
||||
) : (
|
||||
<Profile isSessionUser={isSessionUser} user={requestedUser} />
|
||||
<Profile
|
||||
isSessionUser={isSessionUser}
|
||||
user={requestedUser}
|
||||
submitNewAbout={submitNewAbout}
|
||||
updateMyPortfolio={updateMyPortfolio}
|
||||
updateMySocials={updateMySocials}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,10 @@ import envData from '../../config/env.json';
|
||||
import { createFlashMessage } from '../components/Flash/redux';
|
||||
import { Loader, Spacer } from '../components/helpers';
|
||||
import Certification from '../components/settings/certification';
|
||||
import About from '../components/settings/about';
|
||||
import MiscSettings from '../components/settings/misc-settings';
|
||||
import DangerZone from '../components/settings/danger-zone';
|
||||
import Email from '../components/settings/email';
|
||||
import Honesty from '../components/settings/honesty';
|
||||
import Internet, { Socials } from '../components/settings/internet';
|
||||
import Portfolio from '../components/settings/portfolio';
|
||||
import Privacy from '../components/settings/privacy';
|
||||
import { type ThemeProps, Themes } from '../components/settings/theme';
|
||||
import UserToken from '../components/settings/user-token';
|
||||
@@ -31,9 +29,7 @@ import { User } from '../redux/prop-types';
|
||||
import {
|
||||
submitNewAbout,
|
||||
updateMyHonesty,
|
||||
updateMyPortfolio,
|
||||
updateMyQuincyEmail,
|
||||
updateMySocials,
|
||||
updateMySound,
|
||||
updateMyTheme,
|
||||
updateMyKeyboardShortcuts,
|
||||
@@ -47,12 +43,9 @@ type ShowSettingsProps = Pick<ThemeProps, 'toggleNightMode'> & {
|
||||
isSignedIn: boolean;
|
||||
navigate: (location: string) => void;
|
||||
showLoading: boolean;
|
||||
submitNewAbout: () => void;
|
||||
toggleSoundMode: (sound: boolean) => void;
|
||||
toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void;
|
||||
updateSocials: (formValues: Socials) => void;
|
||||
updateIsHonest: () => void;
|
||||
updatePortfolio: () => void;
|
||||
updateQuincyEmail: (isSendQuincyEmail: boolean) => void;
|
||||
user: User;
|
||||
verifyCert: typeof verifyCert;
|
||||
@@ -81,9 +74,7 @@ const mapDispatchToProps = {
|
||||
toggleSoundMode: (sound: boolean) => updateMySound({ sound }),
|
||||
toggleKeyboardShortcuts: (keyboardShortcuts: boolean) =>
|
||||
updateMyKeyboardShortcuts({ keyboardShortcuts }),
|
||||
updateSocials: (formValues: Socials) => updateMySocials(formValues),
|
||||
updateIsHonest: updateMyHonesty,
|
||||
updatePortfolio: updateMyPortfolio,
|
||||
updateQuincyEmail: (sendQuincyEmail: boolean) =>
|
||||
updateMyQuincyEmail({ sendQuincyEmail }),
|
||||
verifyCert
|
||||
@@ -94,7 +85,6 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
const {
|
||||
createFlashMessage,
|
||||
isSignedIn,
|
||||
submitNewAbout,
|
||||
toggleNightMode,
|
||||
toggleSoundMode,
|
||||
toggleKeyboardShortcuts,
|
||||
@@ -124,23 +114,12 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
isHonest,
|
||||
sendQuincyEmail,
|
||||
username,
|
||||
about,
|
||||
picture,
|
||||
theme,
|
||||
keyboardShortcuts,
|
||||
location,
|
||||
name,
|
||||
githubProfile,
|
||||
linkedin,
|
||||
twitter,
|
||||
website,
|
||||
portfolio
|
||||
keyboardShortcuts
|
||||
},
|
||||
navigate,
|
||||
showLoading,
|
||||
updateQuincyEmail,
|
||||
updateSocials,
|
||||
updatePortfolio,
|
||||
updateIsHonest,
|
||||
verifyCert,
|
||||
userToken
|
||||
@@ -170,19 +149,13 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
>
|
||||
{t('settings.for', { username: username })}
|
||||
</h1>
|
||||
<About
|
||||
about={about}
|
||||
<MiscSettings
|
||||
currentTheme={theme}
|
||||
location={location}
|
||||
name={name}
|
||||
picture={picture}
|
||||
sound={sound}
|
||||
keyboardShortcuts={keyboardShortcuts}
|
||||
submitNewAbout={submitNewAbout}
|
||||
sound={sound}
|
||||
toggleKeyboardShortcuts={toggleKeyboardShortcuts}
|
||||
toggleNightMode={toggleNightMode}
|
||||
toggleSoundMode={toggleSoundMode}
|
||||
toggleKeyboardShortcuts={toggleKeyboardShortcuts}
|
||||
username={username}
|
||||
/>
|
||||
<Spacer size='medium' />
|
||||
<Privacy />
|
||||
@@ -194,16 +167,6 @@ export function ShowSettings(props: ShowSettingsProps): JSX.Element {
|
||||
updateQuincyEmail={updateQuincyEmail}
|
||||
/>
|
||||
<Spacer size='medium' />
|
||||
<Internet
|
||||
githubProfile={githubProfile}
|
||||
linkedin={linkedin}
|
||||
twitter={twitter}
|
||||
updateSocials={updateSocials}
|
||||
website={website}
|
||||
/>
|
||||
<Spacer size='medium' />
|
||||
<Portfolio portfolio={portfolio} updatePortfolio={updatePortfolio} />
|
||||
<Spacer size='medium' />
|
||||
<Honesty isHonest={isHonest} updateIsHonest={updateIsHonest} />
|
||||
<Spacer size='medium' />
|
||||
<Certification
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
For some reason this `div` is needed in order to overidde
|
||||
the CSS rules of `ui-components`'s Alert.
|
||||
/*
|
||||
For some reason this `div` is needed in order to overidde
|
||||
the CSS rules of `ui-components`'s Alert.
|
||||
*/
|
||||
div.flash-message {
|
||||
display: flex;
|
||||
|
||||
@@ -175,10 +175,14 @@ exports[`<Profile/> renders correctly 1`] = `
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<h1>
|
||||
@
|
||||
string
|
||||
</h1>
|
||||
<div
|
||||
class="profile-edit-container"
|
||||
>
|
||||
<h1>
|
||||
@
|
||||
string
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="spacer"
|
||||
|
||||
+25
-53
@@ -10,34 +10,25 @@ import type { TFunction } from 'i18next';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import isURL from 'validator/lib/isURL';
|
||||
|
||||
import { FullWidthRow, Spacer } from '../helpers';
|
||||
import BlockSaveButton from '../helpers/form/block-save-button';
|
||||
import type { CamperProps } from '../profile/components/camper';
|
||||
import SoundSettings from './sound';
|
||||
import ThemeSettings, { type ThemeProps } from './theme';
|
||||
import UsernameSettings from './username';
|
||||
import KeyboardShortcutsSettings from './keyboard-shortcuts';
|
||||
import SectionHeader from './section-header';
|
||||
import ScrollbarWidthSettings from './scrollbar-width';
|
||||
import { FullWidthRow } from '../../helpers';
|
||||
import BlockSaveButton from '../../helpers/form/block-save-button';
|
||||
import SectionHeader from '../../settings/section-header';
|
||||
import type { CamperProps } from './camper';
|
||||
|
||||
type AboutProps = ThemeProps &
|
||||
Omit<
|
||||
CamperProps,
|
||||
| 'linkedin'
|
||||
| 'joinDate'
|
||||
| 'isDonating'
|
||||
| 'githubProfile'
|
||||
| 'twitter'
|
||||
| 'website'
|
||||
| 'yearsTopContributor'
|
||||
> & {
|
||||
sound: boolean;
|
||||
keyboardShortcuts: boolean;
|
||||
submitNewAbout: (formValues: FormValues) => void;
|
||||
t: TFunction;
|
||||
toggleSoundMode: (sound: boolean) => void;
|
||||
toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void;
|
||||
};
|
||||
type AboutProps = Omit<
|
||||
CamperProps,
|
||||
| 'linkedin'
|
||||
| 'joinDate'
|
||||
| 'isDonating'
|
||||
| 'githubProfile'
|
||||
| 'twitter'
|
||||
| 'website'
|
||||
| 'yearsTopContributor'
|
||||
> & {
|
||||
t: TFunction;
|
||||
submitNewAbout: (formValues: FormValues) => void;
|
||||
setIsEditing: (isEditing: boolean) => void;
|
||||
};
|
||||
|
||||
type FormValues = Pick<AboutProps, 'name' | 'location' | 'picture' | 'about'>;
|
||||
|
||||
@@ -81,6 +72,10 @@ class AboutSettings extends Component<AboutProps, AboutState> {
|
||||
};
|
||||
}
|
||||
|
||||
toggleEditing = () => {
|
||||
this.props.setIsEditing(false);
|
||||
};
|
||||
|
||||
componentDidUpdate() {
|
||||
const { name, location, picture, about } = this.props;
|
||||
const { formValues, formClicked } = this.state;
|
||||
@@ -119,10 +114,12 @@ class AboutSettings extends Component<AboutProps, AboutState> {
|
||||
const { formValues } = this.state;
|
||||
const { submitNewAbout } = this.props;
|
||||
if (this.state.isPictureUrlValid === true && !this.isFormPristine()) {
|
||||
this.toggleEditing();
|
||||
return this.setState({ formClicked: true }, () =>
|
||||
submitNewAbout(formValues)
|
||||
);
|
||||
} else {
|
||||
this.toggleEditing();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -198,20 +195,9 @@ class AboutSettings extends Component<AboutProps, AboutState> {
|
||||
const {
|
||||
formValues: { name, location, picture, about }
|
||||
} = this.state;
|
||||
const {
|
||||
currentTheme,
|
||||
sound,
|
||||
keyboardShortcuts,
|
||||
username,
|
||||
t,
|
||||
toggleNightMode,
|
||||
toggleSoundMode,
|
||||
toggleKeyboardShortcuts
|
||||
} = this.props;
|
||||
const { t } = this.props;
|
||||
return (
|
||||
<>
|
||||
<UsernameSettings username={username} />
|
||||
<Spacer size='medium' />
|
||||
<SectionHeader>{t('settings.headings.personal-info')}</SectionHeader>
|
||||
<FullWidthRow>
|
||||
<form
|
||||
@@ -282,20 +268,6 @@ class AboutSettings extends Component<AboutProps, AboutState> {
|
||||
</BlockSaveButton>
|
||||
</form>
|
||||
</FullWidthRow>
|
||||
<Spacer size='medium' />
|
||||
<FullWidthRow>
|
||||
<ThemeSettings
|
||||
currentTheme={currentTheme}
|
||||
toggleNightMode={toggleNightMode}
|
||||
/>
|
||||
<SoundSettings sound={sound} toggleSoundMode={toggleSoundMode} />
|
||||
<KeyboardShortcutsSettings
|
||||
keyboardShortcuts={keyboardShortcuts}
|
||||
toggleKeyboardShortcuts={toggleKeyboardShortcuts}
|
||||
explain={t('settings.shortcuts-explained')}
|
||||
/>
|
||||
<ScrollbarWidthSettings />
|
||||
</FullWidthRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
import React from 'react';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faCalendar, faLocationDot } from '@fortawesome/free-solid-svg-icons';
|
||||
import {
|
||||
faCalendar,
|
||||
faLocationDot,
|
||||
faPen
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
import { AvatarRenderer, FullWidthRow, Spacer } from '../../helpers';
|
||||
import { parseDate } from './utils';
|
||||
import SocialIcons from './social-icons';
|
||||
import { type CamperProps } from './camper';
|
||||
|
||||
const Bio = ({
|
||||
joinDate,
|
||||
location,
|
||||
@@ -19,7 +23,9 @@ const Bio = ({
|
||||
website,
|
||||
isDonating,
|
||||
yearsTopContributor,
|
||||
picture
|
||||
picture,
|
||||
setIsEditing,
|
||||
isSessionUser
|
||||
}: CamperProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -35,7 +41,19 @@ const Bio = ({
|
||||
picture={picture}
|
||||
/>
|
||||
</div>
|
||||
<h1>@{username}</h1>
|
||||
<div className='profile-edit-container'>
|
||||
<h1>@{username}</h1>
|
||||
{isSessionUser && (
|
||||
<Button
|
||||
onClick={() => setIsEditing(true)}
|
||||
size='small'
|
||||
className='button-fit'
|
||||
aria-label={t('aria.edit-my-profile')}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPen} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{name && <h2>{name}</h2>}
|
||||
<Spacer size={'small'} />
|
||||
{about && <p>{about}</p>}
|
||||
|
||||
@@ -40,6 +40,17 @@
|
||||
color: var(--quaternary-color);
|
||||
}
|
||||
|
||||
.profile-edit-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
.profile-edit-container h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.profile-meta-container div {
|
||||
margin-inline-end: 12px;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,10 @@ export type CamperProps = Pick<
|
||||
| 'picture'
|
||||
| 'name'
|
||||
| 'joinDate'
|
||||
>;
|
||||
> & {
|
||||
setIsEditing: (value: boolean) => void;
|
||||
isSessionUser: boolean;
|
||||
};
|
||||
|
||||
function Camper({
|
||||
name,
|
||||
@@ -35,11 +38,12 @@ function Camper({
|
||||
joinDate,
|
||||
linkedin,
|
||||
twitter,
|
||||
website
|
||||
website,
|
||||
isSessionUser,
|
||||
setIsEditing
|
||||
}: CamperProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const isTopContributor = yearsTopContributor.filter(Boolean).length > 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='bio-container'>
|
||||
@@ -56,6 +60,8 @@ function Camper({
|
||||
isDonating={isDonating}
|
||||
yearsTopContributor={yearsTopContributor}
|
||||
picture={picture}
|
||||
setIsEditing={setIsEditing}
|
||||
isSessionUser={isSessionUser}
|
||||
/>
|
||||
</div>
|
||||
{(isDonating || isTopContributor) && (
|
||||
|
||||
+11
-4
@@ -12,11 +12,11 @@ import {
|
||||
type FormGroupProps
|
||||
} from '@freecodecamp/ui';
|
||||
|
||||
import { maybeUrlRE } from '../../utils';
|
||||
import { maybeUrlRE } from '../../../utils';
|
||||
|
||||
import { FullWidthRow } from '../helpers';
|
||||
import BlockSaveButton from '../helpers/form/block-save-button';
|
||||
import SectionHeader from './section-header';
|
||||
import { FullWidthRow } from '../../helpers';
|
||||
import BlockSaveButton from '../../helpers/form/block-save-button';
|
||||
import SectionHeader from '../../settings/section-header';
|
||||
|
||||
export interface Socials {
|
||||
githubProfile: string;
|
||||
@@ -28,6 +28,7 @@ export interface Socials {
|
||||
interface InternetProps extends Socials {
|
||||
t: TFunction;
|
||||
updateSocials: (formValues: Socials) => void;
|
||||
setIsEditing: (isEditing: boolean) => void;
|
||||
}
|
||||
|
||||
type InternetState = {
|
||||
@@ -61,6 +62,10 @@ class InternetSettings extends Component<InternetProps, InternetState> {
|
||||
};
|
||||
}
|
||||
|
||||
toggleEditing = () => {
|
||||
this.props.setIsEditing(false);
|
||||
};
|
||||
|
||||
componentDidUpdate() {
|
||||
const {
|
||||
githubProfile = '',
|
||||
@@ -134,8 +139,10 @@ class InternetSettings extends Component<InternetProps, InternetState> {
|
||||
const { formValues } = this.state;
|
||||
|
||||
const { updateSocials } = this.props;
|
||||
this.toggleEditing();
|
||||
return updateSocials({ ...formValues });
|
||||
}
|
||||
this.toggleEditing();
|
||||
return null;
|
||||
};
|
||||
|
||||
+13
-5
@@ -12,13 +12,13 @@ import {
|
||||
} from '@freecodecamp/ui';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import isURL from 'validator/lib/isURL';
|
||||
import { PortfolioProjectData } from '../../redux/prop-types';
|
||||
import { PortfolioProjectData } from '../../../redux/prop-types';
|
||||
|
||||
import { hasProtocolRE } from '../../utils';
|
||||
import { hasProtocolRE } from '../../../utils';
|
||||
|
||||
import { FullWidthRow, Spacer } from '../helpers';
|
||||
import BlockSaveButton from '../helpers/form/block-save-button';
|
||||
import SectionHeader from './section-header';
|
||||
import { FullWidthRow, Spacer } from '../../helpers';
|
||||
import BlockSaveButton from '../../helpers/form/block-save-button';
|
||||
import SectionHeader from '../../settings/section-header';
|
||||
|
||||
type PortfolioProps = {
|
||||
picture?: string;
|
||||
@@ -26,6 +26,7 @@ type PortfolioProps = {
|
||||
t: TFunction;
|
||||
updatePortfolio: (obj: { portfolio: PortfolioProjectData[] }) => void;
|
||||
username?: string;
|
||||
setIsEditing: (isEditing: boolean) => void;
|
||||
};
|
||||
|
||||
type PortfolioState = {
|
||||
@@ -65,6 +66,10 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
|
||||
};
|
||||
}
|
||||
|
||||
toggleEditing = () => {
|
||||
this.props.setIsEditing(false);
|
||||
};
|
||||
|
||||
createOnChangeHandler =
|
||||
(id: string, key: 'description' | 'image' | 'title' | 'url') =>
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@@ -90,6 +95,7 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
|
||||
this.setState({ unsavedItemId: null });
|
||||
}
|
||||
this.props.updatePortfolio({ portfolio });
|
||||
this.toggleEditing();
|
||||
};
|
||||
|
||||
handleAdd = () => {
|
||||
@@ -107,6 +113,7 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
|
||||
}),
|
||||
() => this.updateItem(id)
|
||||
);
|
||||
this.toggleEditing();
|
||||
};
|
||||
|
||||
isFormPristine = (id: string) => {
|
||||
@@ -252,6 +259,7 @@ class PortfolioSettings extends Component<PortfolioProps, PortfolioState> {
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>, id: string) => {
|
||||
e.preventDefault();
|
||||
if (isButtonDisabled) return null;
|
||||
this.toggleEditing();
|
||||
return this.updateItem(id);
|
||||
};
|
||||
return (
|
||||
@@ -21,12 +21,17 @@ interface SocialIconsProps {
|
||||
website: string;
|
||||
}
|
||||
|
||||
function LinkedInIcon(linkedIn: string, username: string): JSX.Element {
|
||||
interface IconProps {
|
||||
href: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
function LinkedInIcon({ href, username }: IconProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a
|
||||
aria-label={t('aria.linkedin', { username: username })}
|
||||
href={linkedIn}
|
||||
aria-label={t('aria.linkedin', { username })}
|
||||
href={href}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
@@ -35,12 +40,12 @@ function LinkedInIcon(linkedIn: string, username: string): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function GitHubIcon(ghURL: string, username: string): JSX.Element {
|
||||
function GitHubIcon({ href, username }: IconProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a
|
||||
aria-label={t('aria.github', { username: username })}
|
||||
href={ghURL}
|
||||
aria-label={t('aria.github', { username })}
|
||||
href={href}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
@@ -49,12 +54,12 @@ function GitHubIcon(ghURL: string, username: string): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function WebsiteIcon(website: string, username: string): JSX.Element {
|
||||
function WebsiteIcon({ href, username }: IconProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a
|
||||
aria-label={t('aria.website', { username: username })}
|
||||
href={website}
|
||||
aria-label={t('aria.website', { username })}
|
||||
href={href}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
@@ -63,12 +68,12 @@ function WebsiteIcon(website: string, username: string): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function TwitterIcon(handle: string, username: string): JSX.Element {
|
||||
function TwitterIcon({ href, username }: IconProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<a
|
||||
aria-label={t('aria.twitter', { username: username })}
|
||||
href={handle}
|
||||
aria-label={t('aria.twitter', { username })}
|
||||
href={href}
|
||||
rel='noopener noreferrer'
|
||||
target='_blank'
|
||||
>
|
||||
@@ -87,10 +92,12 @@ function SocialIcons(props: SocialIconsProps): JSX.Element | null {
|
||||
return (
|
||||
<Row>
|
||||
<Col className='social-icons-row'>
|
||||
{linkedin ? LinkedInIcon(linkedin, username) : null}
|
||||
{githubProfile ? GitHubIcon(githubProfile, username) : null}
|
||||
{website ? WebsiteIcon(website, username) : null}
|
||||
{twitter ? TwitterIcon(twitter, username) : null}
|
||||
{linkedin ? <LinkedInIcon href={linkedin} username={username} /> : null}
|
||||
{githubProfile ? (
|
||||
<GitHubIcon href={githubProfile} username={username} />
|
||||
) : null}
|
||||
{website ? <WebsiteIcon href={website} username={username} /> : null}
|
||||
{twitter ? <TwitterIcon href={twitter} username={username} /> : null}
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
+12
-5
@@ -8,14 +8,14 @@ import { bindActionCreators } from 'redux';
|
||||
import type { Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
import { isValidUsername } from '../../../../shared/utils/validate';
|
||||
import { usernameValidationSelector } from '../../redux/settings/selectors';
|
||||
import { isValidUsername } from '../../../../../shared/utils/validate';
|
||||
import { usernameValidationSelector } from '../../../redux/settings/selectors';
|
||||
import {
|
||||
validateUsername,
|
||||
submitNewUsername
|
||||
} from '../../redux/settings/actions';
|
||||
import BlockSaveButton from '../helpers/form/block-save-button';
|
||||
import FullWidthRow from '../helpers/full-width-row';
|
||||
} from '../../../redux/settings/actions';
|
||||
import BlockSaveButton from '../../helpers/form/block-save-button';
|
||||
import FullWidthRow from '../../helpers/full-width-row';
|
||||
|
||||
type UsernameProps = {
|
||||
isValidUsername: boolean;
|
||||
@@ -24,6 +24,7 @@ type UsernameProps = {
|
||||
username: string;
|
||||
validateUsername: (name: string) => void;
|
||||
validating: boolean;
|
||||
setIsEditing: (isEditing: boolean) => void;
|
||||
};
|
||||
|
||||
type UsernameState = {
|
||||
@@ -101,6 +102,10 @@ class UsernameSettings extends Component<UsernameProps, UsernameState> {
|
||||
return null;
|
||||
}
|
||||
|
||||
toggleEditing = () => {
|
||||
this.props.setIsEditing(false);
|
||||
};
|
||||
|
||||
handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const { submitNewUsername } = this.props;
|
||||
@@ -109,6 +114,8 @@ class UsernameSettings extends Component<UsernameProps, UsernameState> {
|
||||
characterValidation: { valid }
|
||||
} = this.state;
|
||||
|
||||
this.toggleEditing();
|
||||
|
||||
return this.setState({ submitClicked: true }, () =>
|
||||
valid ? submitNewUsername(formValue) : null
|
||||
);
|
||||
@@ -0,0 +1,5 @@
|
||||
.button-fit {
|
||||
width: 30px;
|
||||
height: fit-content;
|
||||
padding: 0 !important;
|
||||
}
|
||||
@@ -1,21 +1,39 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Alert, Container, Row } from '@freecodecamp/ui';
|
||||
import { Alert, Container, Modal, Row } from '@freecodecamp/ui';
|
||||
import { FullWidthRow, Link, Spacer } from '../helpers';
|
||||
import Portfolio from './components/portfolio';
|
||||
|
||||
import UsernameSettings from './components/username';
|
||||
import About from './components/about';
|
||||
import Internet, { Socials } from './components/internet';
|
||||
import { User } from './../../redux/prop-types';
|
||||
import Timeline from './components/time-line';
|
||||
import Camper from './components/camper';
|
||||
import Certifications from './components/certifications';
|
||||
import Stats from './components/stats';
|
||||
import HeatMap from './components/heat-map';
|
||||
import './profile.css';
|
||||
import { PortfolioProjects } from './components/portfolio-projects';
|
||||
|
||||
interface ProfileProps {
|
||||
isSessionUser: boolean;
|
||||
user: User;
|
||||
updateMyPortfolio: () => void;
|
||||
updateMySocials: (formValues: Socials) => void;
|
||||
submitNewAbout: () => void;
|
||||
}
|
||||
|
||||
interface EditModalProps {
|
||||
user: User;
|
||||
isEditing: boolean;
|
||||
isSessionUser: boolean;
|
||||
setIsEditing: (isEditing: boolean) => void;
|
||||
updateMySocials: (formValues: Socials) => void;
|
||||
updateMyPortfolio: () => void;
|
||||
submitNewAbout: () => void;
|
||||
}
|
||||
interface MessageProps {
|
||||
isSessionUser: boolean;
|
||||
@@ -32,6 +50,64 @@ const UserMessage = ({ t }: Pick<MessageProps, 't'>) => {
|
||||
);
|
||||
};
|
||||
|
||||
const EditModal = ({
|
||||
user,
|
||||
isEditing,
|
||||
isSessionUser,
|
||||
setIsEditing,
|
||||
updateMyPortfolio,
|
||||
updateMySocials,
|
||||
submitNewAbout
|
||||
}: EditModalProps) => {
|
||||
const {
|
||||
portfolio,
|
||||
username,
|
||||
about,
|
||||
location,
|
||||
name,
|
||||
picture,
|
||||
githubProfile,
|
||||
linkedin,
|
||||
twitter,
|
||||
website
|
||||
} = user;
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Modal onClose={() => setIsEditing(false)} open={isEditing} size='large'>
|
||||
<Modal.Header>{t('profile.edit-my-profile')}</Modal.Header>
|
||||
<Modal.Body alignment='left'>
|
||||
<UsernameSettings username={username} setIsEditing={setIsEditing} />
|
||||
<Spacer size='medium' />
|
||||
<About
|
||||
about={about}
|
||||
location={location}
|
||||
name={name}
|
||||
picture={picture}
|
||||
username={username}
|
||||
submitNewAbout={submitNewAbout}
|
||||
setIsEditing={setIsEditing}
|
||||
isSessionUser={isSessionUser}
|
||||
/>
|
||||
<Spacer size='medium' />
|
||||
<Internet
|
||||
githubProfile={githubProfile}
|
||||
linkedin={linkedin}
|
||||
twitter={twitter}
|
||||
updateSocials={updateMySocials}
|
||||
setIsEditing={setIsEditing}
|
||||
website={website}
|
||||
/>
|
||||
<Spacer size='medium' />
|
||||
<Portfolio
|
||||
portfolio={portfolio}
|
||||
updatePortfolio={updateMyPortfolio}
|
||||
setIsEditing={setIsEditing}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
const VisitorMessage = ({
|
||||
t,
|
||||
username
|
||||
@@ -53,7 +129,15 @@ const Message = ({ isSessionUser, t, username }: MessageProps) => {
|
||||
return <VisitorMessage t={t} username={username} />;
|
||||
};
|
||||
|
||||
function UserProfile({ user }: { user: ProfileProps['user'] }): JSX.Element {
|
||||
function UserProfile({
|
||||
user,
|
||||
isSessionUser,
|
||||
updateMyPortfolio,
|
||||
updateMySocials,
|
||||
submitNewAbout
|
||||
}: ProfileProps): JSX.Element {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const {
|
||||
profileUI: {
|
||||
showAbout,
|
||||
@@ -86,6 +170,17 @@ function UserProfile({ user }: { user: ProfileProps['user'] }): JSX.Element {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isSessionUser && (
|
||||
<EditModal
|
||||
user={user}
|
||||
isEditing={isEditing}
|
||||
isSessionUser={isSessionUser}
|
||||
setIsEditing={setIsEditing}
|
||||
updateMyPortfolio={updateMyPortfolio}
|
||||
updateMySocials={updateMySocials}
|
||||
submitNewAbout={submitNewAbout}
|
||||
/>
|
||||
)}
|
||||
<Camper
|
||||
about={showAbout ? about : ''}
|
||||
githubProfile={githubProfile}
|
||||
@@ -99,13 +194,15 @@ function UserProfile({ user }: { user: ProfileProps['user'] }): JSX.Element {
|
||||
username={username}
|
||||
website={website}
|
||||
yearsTopContributor={yearsTopContributor}
|
||||
isSessionUser={isSessionUser}
|
||||
setIsEditing={setIsEditing}
|
||||
/>
|
||||
{showPoints ? <Stats points={points} calendar={calendar} /> : null}
|
||||
{showHeatMap ? <HeatMap calendar={calendar} /> : null}
|
||||
{showCerts ? <Certifications username={username} /> : null}
|
||||
{showPortfolio ? (
|
||||
<PortfolioProjects portfolioProjects={portfolio} />
|
||||
) : null}
|
||||
{showCerts ? <Certifications username={username} /> : null}
|
||||
{showTimeLine ? (
|
||||
<Timeline completedMap={completedChallenges} username={username} />
|
||||
) : null}
|
||||
@@ -114,7 +211,13 @@ function UserProfile({ user }: { user: ProfileProps['user'] }): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
function Profile({ user, isSessionUser }: ProfileProps): JSX.Element {
|
||||
function Profile({
|
||||
user,
|
||||
isSessionUser,
|
||||
updateMyPortfolio,
|
||||
updateMySocials,
|
||||
submitNewAbout
|
||||
}: ProfileProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
profileUI: { isLocked },
|
||||
@@ -134,7 +237,15 @@ function Profile({ user, isSessionUser }: ProfileProps): JSX.Element {
|
||||
{isLocked && (
|
||||
<Message username={username} isSessionUser={isSessionUser} t={t} />
|
||||
)}
|
||||
{showUserProfile && <UserProfile user={user} />}
|
||||
{showUserProfile && (
|
||||
<UserProfile
|
||||
user={user}
|
||||
isSessionUser={isSessionUser}
|
||||
updateMyPortfolio={updateMyPortfolio}
|
||||
updateMySocials={updateMySocials}
|
||||
submitNewAbout={submitNewAbout}
|
||||
/>
|
||||
)}
|
||||
{!isSessionUser && (
|
||||
<Row className='text-center'>
|
||||
<Link to={`/user/${username}/report-user`}>
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Spacer, FullWidthRow } from '../helpers';
|
||||
import ThemeSettings, { ThemeProps } from '../../components/settings/theme';
|
||||
import SoundSettings from '../../components/settings/sound';
|
||||
import KeyboardShortcutsSettings from '../../components/settings/keyboard-shortcuts';
|
||||
import ScrollbarWidthSettings from '../../components/settings/scrollbar-width';
|
||||
|
||||
type MiscSettingsProps = ThemeProps & {
|
||||
currentTheme: string;
|
||||
keyboardShortcuts: boolean;
|
||||
sound: boolean;
|
||||
toggleKeyboardShortcuts: (keyboardShortcuts: boolean) => void;
|
||||
toggleNightMode: () => void;
|
||||
toggleSoundMode: (sound: boolean) => void;
|
||||
};
|
||||
|
||||
const MiscSettings = ({
|
||||
currentTheme,
|
||||
keyboardShortcuts,
|
||||
sound,
|
||||
toggleKeyboardShortcuts,
|
||||
toggleNightMode,
|
||||
toggleSoundMode
|
||||
}: MiscSettingsProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Spacer size='medium' />
|
||||
<FullWidthRow>
|
||||
<ThemeSettings
|
||||
currentTheme={currentTheme}
|
||||
toggleNightMode={toggleNightMode}
|
||||
/>
|
||||
<SoundSettings sound={sound} toggleSoundMode={toggleSoundMode} />
|
||||
<KeyboardShortcutsSettings
|
||||
keyboardShortcuts={keyboardShortcuts}
|
||||
toggleKeyboardShortcuts={toggleKeyboardShortcuts}
|
||||
explain={t('settings.shortcuts-explained')?.toString()}
|
||||
/>
|
||||
<ScrollbarWidthSettings />
|
||||
</FullWidthRow>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MiscSettings;
|
||||
@@ -23,7 +23,15 @@ test.describe('Public profile certifications', () => {
|
||||
test('Should show claimed certifications if the username includes uppercase characters', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto('/settings');
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
|
||||
await page.getByLabel('Username').fill('CertifiedBoozer');
|
||||
await page.getByRole('button', { name: 'Save' }).nth(0).click();
|
||||
await expect(page.getByTestId('flash-message')).toContainText(
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import translations from '../client/i18n/locales/english/translations.json';
|
||||
|
||||
test.use({ storageState: 'playwright/.auth/certified-user.json' });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page.getByRole('button', { name: 'Preview custom 404 page' }).click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
});
|
||||
|
||||
test('edit profile modal should render correctly', async ({ page }) => {
|
||||
// Internet Presence
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: translations.settings.headings.internet
|
||||
})
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('internet-presence')).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: translations.settings.headings.internet
|
||||
})
|
||||
).toBeVisible();
|
||||
|
||||
// Personal Information
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: translations.settings.headings['personal-info']
|
||||
})
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('camper-identity')).toBeVisible();
|
||||
const savePersonalInfoButton = page.getByRole('button', {
|
||||
name: translations.settings.headings['personal-info']
|
||||
});
|
||||
await expect(savePersonalInfoButton).toBeVisible();
|
||||
await expect(savePersonalInfoButton).toBeDisabled();
|
||||
await expect(
|
||||
page.getByLabel(translations.settings.labels.name, { exact: true })
|
||||
).toHaveValue('Full Stack User');
|
||||
await expect(
|
||||
page.getByLabel(translations.settings.labels.location)
|
||||
).toHaveValue('');
|
||||
await expect(
|
||||
page.getByLabel(translations.settings.labels.picture)
|
||||
).toHaveValue('');
|
||||
await expect(page.getByLabel(translations.settings.labels.about)).toHaveValue(
|
||||
''
|
||||
);
|
||||
});
|
||||
@@ -4,7 +4,15 @@ test.describe('Picture input field', () => {
|
||||
test.use({ storageState: 'playwright/.auth/certified-user.json' });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
});
|
||||
|
||||
test('Should be possible to type', async ({ page }) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { test, expect } from '@playwright/test';
|
||||
import translations from '../client/i18n/locales/english/translations.json';
|
||||
|
||||
@@ -14,10 +15,20 @@ const settingsPageElement = {
|
||||
test.use({ storageState: 'playwright/.auth/certified-user.json' });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
// Reset input values
|
||||
execSync('node ./tools/scripts/seed/seed-demo-user --certified-user');
|
||||
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page.getByRole('button', { name: 'Preview custom 404 page' }).click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
});
|
||||
|
||||
test.describe('Your Internet Presence', () => {
|
||||
test.skip(({ browserName }) => browserName === 'webkit', 'flaky on Safari');
|
||||
test('should display the section with save button being disabled', async ({
|
||||
page
|
||||
}) => {
|
||||
@@ -70,7 +81,8 @@ test.describe('Your Internet Presence', () => {
|
||||
});
|
||||
|
||||
test(`should update ${social.name} URL`, async ({ page }) => {
|
||||
const socialInput = page.getByLabel(social.label);
|
||||
const socialInput = page.getByRole('textbox', { name: social.label });
|
||||
await expect(socialInput).toBeVisible();
|
||||
await socialInput.fill(social.url);
|
||||
const socialCheckmark = page.getByTestId(social.checkTestId);
|
||||
await expect(socialCheckmark).toBeVisible();
|
||||
@@ -81,21 +93,9 @@ test.describe('Your Internet Presence', () => {
|
||||
|
||||
await expect(saveButton).toBeVisible();
|
||||
await saveButton.click();
|
||||
await expect(
|
||||
page.getByTestId(settingsPageElement.flashMessageAlert)
|
||||
).toContainText('We have updated your social links');
|
||||
|
||||
// clear value before next test
|
||||
await socialInput.clear();
|
||||
await Promise.all([
|
||||
page.waitForResponse(
|
||||
response =>
|
||||
response.url().includes('update-my-socials') &&
|
||||
response.status() === 200
|
||||
),
|
||||
saveButton.click()
|
||||
]);
|
||||
await expect(socialCheckmark).toBeHidden();
|
||||
await expect(page.getByRole('alert').first()).toContainText(
|
||||
'We have updated your social links'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+10
-2
@@ -14,7 +14,15 @@ test.afterAll(() => {
|
||||
|
||||
test.describe('Add Portfolio Item', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
await page.goto('/developmentuser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
});
|
||||
|
||||
test('The title has validation', async ({ page }) => {
|
||||
@@ -134,7 +142,7 @@ test.describe('Add Portfolio Item', () => {
|
||||
await page
|
||||
.getByRole('button', { name: 'Save this portfolio item' })
|
||||
.click();
|
||||
await expect(page.getByTestId('flash-message')).toContainText(
|
||||
await expect(page.getByRole('alert').first()).toContainText(
|
||||
/We have updated your portfolio/
|
||||
);
|
||||
});
|
||||
|
||||
+1
-91
@@ -6,9 +6,7 @@ import { alertToBeVisible } from './utils/alerts';
|
||||
|
||||
const settingsTestIds = {
|
||||
settingsHeading: 'settings-heading',
|
||||
internetPresence: 'internet-presence',
|
||||
portfolioItems: 'portfolio-items',
|
||||
camperIdentity: 'camper-identity'
|
||||
portfolioItems: 'portfolio-items'
|
||||
};
|
||||
|
||||
const settingsObject = {
|
||||
@@ -173,47 +171,6 @@ test.describe('Settings - Certified User', () => {
|
||||
});
|
||||
await expect(downloadButton).toBeVisible();
|
||||
|
||||
// Internet Presence
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: translations.settings.headings.internet
|
||||
})
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId(settingsTestIds.internetPresence)
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('button', {
|
||||
name: translations.settings.headings.internet
|
||||
})
|
||||
).toBeVisible();
|
||||
|
||||
// Personal Information
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: translations.settings.headings['personal-info']
|
||||
})
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId(settingsTestIds.camperIdentity)
|
||||
).toBeVisible();
|
||||
const savePersonalInfoButton = page.getByRole('button', {
|
||||
name: translations.settings.headings['personal-info']
|
||||
});
|
||||
await expect(savePersonalInfoButton).toBeVisible();
|
||||
await expect(savePersonalInfoButton).toBeDisabled();
|
||||
await expect(
|
||||
page.getByLabel(translations.settings.labels.name, { exact: true })
|
||||
).toHaveValue('Full Stack User');
|
||||
await expect(
|
||||
page.getByLabel(translations.settings.labels.location)
|
||||
).toHaveValue('');
|
||||
await expect(
|
||||
page.getByLabel(translations.settings.labels.picture)
|
||||
).toHaveValue('');
|
||||
await expect(
|
||||
page.getByLabel(translations.settings.labels.about)
|
||||
).toHaveValue('');
|
||||
await expect(
|
||||
page
|
||||
.getByRole('group', {
|
||||
@@ -299,53 +256,6 @@ test.describe('Settings - Certified User', () => {
|
||||
})
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Should allow empty string in any field in about settings', async ({
|
||||
page
|
||||
}) => {
|
||||
const saveButton = page.getByRole('button', {
|
||||
name: translations.settings.headings['personal-info']
|
||||
});
|
||||
|
||||
const nameInput = page.getByLabel(translations.settings.labels.name, {
|
||||
exact: true
|
||||
});
|
||||
const locationInput = page.getByLabel(
|
||||
translations.settings.labels.location
|
||||
);
|
||||
const pictureInput = page.getByLabel(translations.settings.labels.picture);
|
||||
const aboutInput = page.getByLabel(translations.settings.labels.about);
|
||||
const updatedAlert = page.getByText(translations.flash['updated-about-me']);
|
||||
|
||||
await nameInput.fill('Quincy Larson');
|
||||
await locationInput.fill('USA');
|
||||
await pictureInput.fill(
|
||||
'https://cdn.freecodecamp.org/platform/english/images/quincy-larson-signature.svg'
|
||||
);
|
||||
await aboutInput.fill('Teacher at freeCodeCamp');
|
||||
|
||||
await expect(saveButton).not.toBeDisabled();
|
||||
await saveButton.click();
|
||||
await expect(updatedAlert).toBeVisible();
|
||||
// clear the alert to make sure it's gone before we save again.
|
||||
await updatedAlert.getByRole('button').click();
|
||||
|
||||
await nameInput.fill('');
|
||||
await locationInput.fill('');
|
||||
await pictureInput.fill('');
|
||||
await aboutInput.fill('');
|
||||
|
||||
await expect(saveButton).not.toBeDisabled();
|
||||
await saveButton.click();
|
||||
await expect(updatedAlert).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
|
||||
await expect(nameInput).toHaveValue('');
|
||||
await expect(locationInput).toHaveValue('');
|
||||
await expect(pictureInput).toHaveValue('');
|
||||
await expect(aboutInput).toHaveValue('');
|
||||
});
|
||||
});
|
||||
|
||||
// In order to claim the Full Stack cert, the user needs to complete 6 certs.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { test, expect } from '@playwright/test';
|
||||
import translations from '../client/i18n/locales/english/translations.json';
|
||||
|
||||
test.use({ storageState: 'playwright/.auth/certified-user.json' });
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
execSync(
|
||||
'node ./tools/scripts/seed/seed-demo-user --certified-user --set-false isFullStackCert'
|
||||
);
|
||||
await page.goto('/settings');
|
||||
});
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/certifieduser');
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page.getByRole('button', { name: 'Preview custom 404 page' }).click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
});
|
||||
|
||||
test.afterAll(() => {
|
||||
execSync('node ./tools/scripts/seed/seed-demo-user --certified-user');
|
||||
});
|
||||
|
||||
test('Should allow empty string in any field in about settings', async ({
|
||||
page
|
||||
}) => {
|
||||
test.setTimeout(20000);
|
||||
|
||||
const saveButton = page.getByRole('button', {
|
||||
name: translations.settings.headings['personal-info']
|
||||
});
|
||||
|
||||
const nameInput = page.getByLabel(translations.settings.labels.name, {
|
||||
exact: true
|
||||
});
|
||||
const locationInput = page.getByLabel(translations.settings.labels.location);
|
||||
const pictureInput = page.getByLabel(translations.settings.labels.picture);
|
||||
const aboutInput = page.getByLabel(translations.settings.labels.about);
|
||||
const updatedAlert = page
|
||||
.getByRole('alert')
|
||||
.filter({ hasText: translations.flash['updated-about-me'] })
|
||||
.first();
|
||||
|
||||
await nameInput.fill('Quincy Larson');
|
||||
await locationInput.fill('USA');
|
||||
await pictureInput.fill(
|
||||
'https://cdn.freecodecamp.org/platform/english/images/quincy-larson-signature.svg'
|
||||
);
|
||||
await aboutInput.fill('Teacher at freeCodeCamp');
|
||||
|
||||
await expect(saveButton).not.toBeDisabled();
|
||||
await saveButton.click();
|
||||
await expect(updatedAlert).toBeVisible();
|
||||
// clear the alert to make sure it's gone before we save again.
|
||||
await updatedAlert.getByRole('button').click();
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
await nameInput.fill('');
|
||||
await locationInput.fill('');
|
||||
await pictureInput.fill('');
|
||||
await aboutInput.fill('');
|
||||
|
||||
await expect(saveButton).not.toBeDisabled();
|
||||
await saveButton.click();
|
||||
await expect(updatedAlert).toBeVisible();
|
||||
|
||||
await page.reload();
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page.getByRole('button', { name: 'Preview custom 404 page' }).click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
await expect(nameInput).toHaveValue('');
|
||||
await expect(locationInput).toHaveValue('');
|
||||
await expect(pictureInput).toHaveValue('');
|
||||
await expect(aboutInput).toHaveValue('');
|
||||
});
|
||||
+39
-25
@@ -16,9 +16,19 @@ const settingsObject = {
|
||||
errorCode: '404'
|
||||
};
|
||||
|
||||
let currentUsername = 'certifieduser';
|
||||
|
||||
test.describe('Username Settings Validation', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/settings');
|
||||
await page.goto(`/${currentUsername}`);
|
||||
|
||||
if (!process.env.CI) {
|
||||
await page
|
||||
.getByRole('button', { name: 'Preview custom 404 page' })
|
||||
.click();
|
||||
}
|
||||
|
||||
await page.getByRole('button', { name: 'Edit my profile' }).click();
|
||||
});
|
||||
|
||||
test('Should display Username Input and Save Button', async ({ page }) => {
|
||||
@@ -87,17 +97,18 @@ test.describe('Username Settings Validation', () => {
|
||||
const saveButton = page.getByRole('button', {
|
||||
name: translations.settings.labels.username
|
||||
});
|
||||
const flashText = translations.flash['username-updated'].replace(
|
||||
settingsObject.usernamePlaceholder,
|
||||
settingsObject.usernameAvailable
|
||||
);
|
||||
|
||||
await inputLabel.fill(settingsObject.usernameAvailable);
|
||||
await expect(saveButton).not.toBeDisabled();
|
||||
await saveButton.click();
|
||||
await expect(
|
||||
page.getByText(
|
||||
translations.flash['username-updated'].replace(
|
||||
settingsObject.usernamePlaceholder,
|
||||
settingsObject.usernameAvailable
|
||||
)
|
||||
)
|
||||
page.getByRole('alert').filter({ hasText: flashText }).first()
|
||||
).toBeVisible();
|
||||
currentUsername = settingsObject.usernameAvailable;
|
||||
});
|
||||
|
||||
test('should update username in lowercase and reflect in the UI', async ({
|
||||
@@ -107,17 +118,18 @@ test.describe('Username Settings Validation', () => {
|
||||
const saveButton = page.getByRole('button', {
|
||||
name: translations.settings.labels.username
|
||||
});
|
||||
const flashText = translations.flash['username-updated'].replace(
|
||||
settingsObject.usernamePlaceholder,
|
||||
settingsObject.usernameUpdateToLowerCase
|
||||
);
|
||||
|
||||
await inputLabel.fill(settingsObject.usernameUpdateToLowerCase);
|
||||
await expect(saveButton).not.toBeDisabled();
|
||||
await saveButton.click();
|
||||
await expect(
|
||||
page.getByText(
|
||||
translations.flash['username-updated'].replace(
|
||||
settingsObject.usernamePlaceholder,
|
||||
settingsObject.usernameUpdateToLowerCase
|
||||
)
|
||||
)
|
||||
page.getByRole('alert').filter({ hasText: flashText }).first()
|
||||
).toBeVisible();
|
||||
currentUsername = settingsObject.usernameUpdateToLowerCase;
|
||||
});
|
||||
|
||||
test('should update username in uppercase and reflect in the UI', async ({
|
||||
@@ -127,23 +139,29 @@ test.describe('Username Settings Validation', () => {
|
||||
const saveButton = page.getByRole('button', {
|
||||
name: translations.settings.labels.username
|
||||
});
|
||||
const flashText = translations.flash['username-updated'].replace(
|
||||
settingsObject.usernamePlaceholder,
|
||||
settingsObject.usernameUpdateToUpperCase
|
||||
);
|
||||
|
||||
await inputLabel.fill(settingsObject.usernameUpdateToUpperCase);
|
||||
await expect(saveButton).not.toBeDisabled();
|
||||
await saveButton.click();
|
||||
await expect(
|
||||
page.getByText(
|
||||
translations.flash['username-updated'].replace(
|
||||
settingsObject.usernamePlaceholder,
|
||||
settingsObject.usernameUpdateToUpperCase
|
||||
)
|
||||
)
|
||||
page.getByRole('alert').filter({ hasText: flashText }).first()
|
||||
).toBeVisible();
|
||||
currentUsername = settingsObject.usernameUpdateToUpperCase;
|
||||
});
|
||||
|
||||
test('should update username by pressing enter', async ({ page }) => {
|
||||
const inputLabel = page.getByLabel(translations.settings.labels.username);
|
||||
await inputLabel.fill(settingsObject.testUser);
|
||||
|
||||
const flashText = translations.flash['username-updated'].replace(
|
||||
settingsObject.usernamePlaceholder,
|
||||
settingsObject.testUser
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.getByText(translations.settings.username.available)
|
||||
).toBeVisible();
|
||||
@@ -151,13 +169,9 @@ test.describe('Username Settings Validation', () => {
|
||||
await inputLabel.press('Enter');
|
||||
|
||||
await expect(
|
||||
page.getByText(
|
||||
translations.flash['username-updated'].replace(
|
||||
settingsObject.usernamePlaceholder,
|
||||
settingsObject.testUser
|
||||
)
|
||||
)
|
||||
page.getByRole('alert').filter({ hasText: flashText }).first()
|
||||
).toBeVisible();
|
||||
currentUsername = settingsObject.testUser;
|
||||
});
|
||||
|
||||
test('should not be able to update username to the same username', async ({
|
||||
|
||||
Reference in New Issue
Block a user