mirror of
https://github.com/kingomarnajjar/flex-template-web.git
synced 2026-07-27 19:42:11 +10:00
ProfilePage page with user bio
This commit is contained in:
parent
c424054e73
commit
689697fc4b
9 changed files with 410 additions and 28 deletions
|
|
@ -38,7 +38,7 @@ describe('Application', () => {
|
|||
'/s': 'Search results for map search',
|
||||
'/l/listing-title-slug/1234': 'Loading listing…',
|
||||
'/l/1234': 'Loading listing…',
|
||||
'/u/1234': 'Profile page with display name: 1234',
|
||||
'/u/1234': 'Profile page',
|
||||
'/login': 'Log in',
|
||||
'/signup': 'Sign up',
|
||||
'/recover-password': 'Request a new password',
|
||||
|
|
|
|||
121
src/containers/ProfilePage/ProfilePage.css
Normal file
121
src/containers/ProfilePage/ProfilePage.css
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
@import '../../marketplace.css';
|
||||
|
||||
.loading,
|
||||
.error {
|
||||
margin-top: 5px;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 4px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--failColor);
|
||||
}
|
||||
|
||||
.aside {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.asideContent {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
border-bottom: 1px solid var(--matterColorNegative);
|
||||
|
||||
@media (--viewportLarge) {
|
||||
flex-direction: column;
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin: 30px 26px 29px 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin: 0 96px 44px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mobileHeading {
|
||||
flex-shrink: 0;
|
||||
margin: 47px 0 0 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 49px 0 0 0;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.editLinkMobile {
|
||||
margin-top: 17px;
|
||||
|
||||
/* Pull the link to the end of the container */
|
||||
margin-left: auto;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.editLinkDesktop {
|
||||
display: none;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.desktopHeading {
|
||||
display: none;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
display: block;
|
||||
margin: 4px 0 23px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.bio {
|
||||
/* Preserve newlines, but collapse other whitespace */
|
||||
white-space: pre-line;
|
||||
|
||||
max-width: 563px;
|
||||
margin: 5px 0 24px 0;
|
||||
|
||||
@media (--viewportMedium) {
|
||||
margin: 4px 0 51px 0;
|
||||
}
|
||||
@media (--viewportLarge) {
|
||||
margin: 0 0 56px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.listingsContainer {
|
||||
border-top: 1px solid var(--matterColorNegative);
|
||||
}
|
||||
|
||||
.listingsTitle {
|
||||
@apply --marketplaceH3FontStyles;
|
||||
color: var(--matterColorAnti);
|
||||
|
||||
@media (--viewportLarge) {
|
||||
margin-top: 19px;
|
||||
}
|
||||
}
|
||||
|
||||
.withBioMissingAbove {
|
||||
border-top: none;
|
||||
|
||||
@media (--viewportLarge) {
|
||||
border-top: 1px solid var(--matterColorNegative);
|
||||
}
|
||||
}
|
||||
64
src/containers/ProfilePage/ProfilePage.duck.js
Normal file
64
src/containers/ProfilePage/ProfilePage.duck.js
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import { addMarketplaceEntities } from '../../ducks/marketplaceData.duck';
|
||||
import { fetchCurrentUser } from '../../ducks/user.duck';
|
||||
import { storableError } from '../../util/errors';
|
||||
|
||||
// ================ Action types ================ //
|
||||
|
||||
export const SHOW_USER_REQUEST = 'app/ProfilePage/SHOW_USER_REQUEST';
|
||||
export const SHOW_USER_SUCCESS = 'app/ProfilePage/SHOW_USER_SUCCESS';
|
||||
export const SHOW_USER_ERROR = 'app/ProfilePage/SHOW_USER_ERROR';
|
||||
|
||||
// ================ Reducer ================ //
|
||||
|
||||
const initialState = {
|
||||
userId: null,
|
||||
userShowInProgress: false,
|
||||
userShowError: null,
|
||||
};
|
||||
|
||||
export default function profilePageReducer(state = initialState, action = {}) {
|
||||
const { type, payload } = action;
|
||||
switch (type) {
|
||||
case SHOW_USER_REQUEST:
|
||||
return { ...state, userShowInProgress: true, userShowError: null, userId: payload.userId };
|
||||
case SHOW_USER_SUCCESS:
|
||||
return { ...state, userShowInProgress: false };
|
||||
case SHOW_USER_ERROR:
|
||||
return { ...state, userShowInProgress: false, userShowError: payload };
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
// ================ Action creators ================ //
|
||||
|
||||
export const showUserRequest = userId => ({
|
||||
type: SHOW_USER_REQUEST,
|
||||
payload: { userId },
|
||||
});
|
||||
|
||||
export const showUserSuccess = () => ({
|
||||
type: SHOW_USER_SUCCESS,
|
||||
});
|
||||
|
||||
export const showUserError = e => ({
|
||||
type: SHOW_USER_ERROR,
|
||||
error: true,
|
||||
payload: e,
|
||||
});
|
||||
|
||||
// ================ Thunks ================ //
|
||||
|
||||
export const showUser = userId => (dispatch, getState, sdk) => {
|
||||
dispatch(showUserRequest(userId));
|
||||
dispatch(fetchCurrentUser());
|
||||
return sdk.users
|
||||
.show({ id: userId, include: ['profileImage'] })
|
||||
.then(response => {
|
||||
dispatch(addMarketplaceEntities(response));
|
||||
dispatch(showUserSuccess());
|
||||
return response;
|
||||
})
|
||||
.catch(e => showUserError(storableError(e)));
|
||||
};
|
||||
|
|
@ -1,47 +1,170 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { compose } from 'redux';
|
||||
import { connect } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import { types } from '../../util/sdkLoader';
|
||||
import * as propTypes from '../../util/propTypes';
|
||||
import { ensureCurrentUser, ensureUser } from '../../util/data';
|
||||
import { isScrollingDisabled } from '../../ducks/UI.duck';
|
||||
import { Page } from '../../components';
|
||||
import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck';
|
||||
import {
|
||||
Page,
|
||||
LayoutSideNavigation,
|
||||
LayoutWrapperMain,
|
||||
LayoutWrapperSideNav,
|
||||
LayoutWrapperTopbar,
|
||||
LayoutWrapperFooter,
|
||||
Footer,
|
||||
AvatarLarge,
|
||||
NamedLink,
|
||||
} from '../../components';
|
||||
import { TopbarContainer } from '../../containers';
|
||||
import { showUser } from './ProfilePage.duck';
|
||||
|
||||
import css from './ProfilePage.css';
|
||||
|
||||
const { UUID } = types;
|
||||
|
||||
export const ProfilePageComponent = props => {
|
||||
const { logoutError, params, scrollingDisabled } = props;
|
||||
const {
|
||||
logoutError,
|
||||
scrollingDisabled,
|
||||
currentUser,
|
||||
user,
|
||||
userShowInProgress,
|
||||
userShowError,
|
||||
} = props;
|
||||
const ensuredCurrentUser = ensureCurrentUser(currentUser);
|
||||
const profileUser = ensureUser(user);
|
||||
const isCurrentUser =
|
||||
ensuredCurrentUser.id && profileUser.id && ensuredCurrentUser.id.uuid === profileUser.id.uuid;
|
||||
const displayName = profileUser.attributes.profile.displayName;
|
||||
const bio = profileUser.attributes.profile.bio;
|
||||
const listings = [];
|
||||
const hasBio = !!bio;
|
||||
const hasListings = listings.length > 0;
|
||||
|
||||
const editLinkMobile = isCurrentUser ? (
|
||||
<NamedLink className={css.editLinkMobile} name="ProfileSettingsPage">
|
||||
<FormattedMessage id="ProfilePage.editProfileLinkMobile" />
|
||||
</NamedLink>
|
||||
) : null;
|
||||
const editLinkDesktop = isCurrentUser ? (
|
||||
<NamedLink className={css.editLinkDesktop} name="ProfileSettingsPage">
|
||||
<FormattedMessage id="ProfilePage.editProfileLinkDesktop" />
|
||||
</NamedLink>
|
||||
) : null;
|
||||
|
||||
const asideContent = (
|
||||
<div className={css.asideContent}>
|
||||
<AvatarLarge className={css.avatar} user={user} />
|
||||
<h1 className={css.mobileHeading}>
|
||||
{displayName ? (
|
||||
<FormattedMessage id="ProfilePage.mobileHeading" values={{ name: displayName }} />
|
||||
) : null}
|
||||
</h1>
|
||||
{editLinkMobile}
|
||||
{editLinkDesktop}
|
||||
</div>
|
||||
);
|
||||
|
||||
const listingsContainerClasses = classNames(css.listingsContainer, {
|
||||
[css.withBioMissingAbove]: !hasBio,
|
||||
});
|
||||
|
||||
const mainContent = (
|
||||
<div>
|
||||
<h1 className={css.desktopHeading}>
|
||||
<FormattedMessage id="ProfilePage.desktopHeading" values={{ name: displayName }} />
|
||||
</h1>
|
||||
{hasBio ? <p className={css.bio}>{bio}</p> : null}
|
||||
{hasListings ? (
|
||||
<div className={listingsContainerClasses}>
|
||||
<h2 className={css.listingsTitle}>
|
||||
<FormattedMessage id="ProfilePage.listingsTitle" values={{ count: listings.length }} />
|
||||
</h2>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
|
||||
let content;
|
||||
|
||||
if (userShowError) {
|
||||
content = (
|
||||
<p className={css.error}>
|
||||
<FormattedMessage id="ProfilePage.loadingDataFailed" />
|
||||
</p>
|
||||
);
|
||||
} else if (userShowInProgress) {
|
||||
content = (
|
||||
<p className={css.loading}>
|
||||
<FormattedMessage id="ProfilePage.loadingData" />
|
||||
</p>
|
||||
);
|
||||
} else {
|
||||
content = mainContent;
|
||||
}
|
||||
|
||||
const schemaTitle = 'Profile page';
|
||||
return (
|
||||
<Page
|
||||
logoutError={logoutError}
|
||||
title={`Profile page with display name: ${params.displayName}`}
|
||||
scrollingDisabled={scrollingDisabled}
|
||||
>
|
||||
<TopbarContainer />
|
||||
<Page logoutError={logoutError} scrollingDisabled={scrollingDisabled} title={schemaTitle}>
|
||||
<LayoutSideNavigation>
|
||||
<LayoutWrapperTopbar>
|
||||
<TopbarContainer currentPage="ProfilePage" />
|
||||
</LayoutWrapperTopbar>
|
||||
<LayoutWrapperSideNav className={css.aside}>{asideContent}</LayoutWrapperSideNav>
|
||||
<LayoutWrapperMain>{content}</LayoutWrapperMain>
|
||||
<LayoutWrapperFooter>
|
||||
<Footer />
|
||||
</LayoutWrapperFooter>
|
||||
</LayoutSideNavigation>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
ProfilePageComponent.defaultProps = {
|
||||
logoutError: null,
|
||||
currentUser: null,
|
||||
user: null,
|
||||
userShowError: null,
|
||||
};
|
||||
|
||||
const { bool, shape, string } = PropTypes;
|
||||
const { bool } = PropTypes;
|
||||
|
||||
ProfilePageComponent.propTypes = {
|
||||
logoutError: propTypes.error,
|
||||
params: shape({ displayName: string.isRequired }).isRequired,
|
||||
scrollingDisabled: bool.isRequired,
|
||||
currentUser: propTypes.currentUser,
|
||||
user: propTypes.user,
|
||||
userShowInProgress: bool.isRequired,
|
||||
userShowError: propTypes.error,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => {
|
||||
// Page needs logoutError
|
||||
const { logoutError } = state.Auth;
|
||||
const { currentUser } = state.user;
|
||||
const { userId, userShowInProgress, userShowError } = state.ProfilePage;
|
||||
const matches = getMarketplaceEntities(state, [{ type: 'user', id: userId }]);
|
||||
const user = matches.length === 1 ? matches[0] : null;
|
||||
return {
|
||||
logoutError,
|
||||
scrollingDisabled: isScrollingDisabled(state),
|
||||
currentUser,
|
||||
user,
|
||||
userShowInProgress,
|
||||
userShowError,
|
||||
};
|
||||
};
|
||||
|
||||
const ProfilePage = compose(connect(mapStateToProps))(ProfilePageComponent);
|
||||
|
||||
ProfilePage.loadData = params => {
|
||||
const id = new UUID(params.id);
|
||||
return showUser(id);
|
||||
};
|
||||
|
||||
export default ProfilePage;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { renderShallow } from '../../util/test-helpers';
|
||||
import { createUser } from '../../util/test-data';
|
||||
import { ProfilePageComponent } from './ProfilePage';
|
||||
|
||||
const noop = () => null;
|
||||
|
|
@ -8,17 +9,9 @@ describe('ProfilePage', () => {
|
|||
it('matches snapshot', () => {
|
||||
const tree = renderShallow(
|
||||
<ProfilePageComponent
|
||||
params={{ displayName: 'most-awesome-shop' }}
|
||||
history={{ push: noop }}
|
||||
location={{ search: '' }}
|
||||
scrollingDisabled={false}
|
||||
authInProgress={false}
|
||||
currentUserHasListings={false}
|
||||
isAuthenticated={false}
|
||||
onLogout={noop}
|
||||
onManageDisableScrolling={noop}
|
||||
sendVerificationEmailInProgress={false}
|
||||
onResendVerificationEmail={noop}
|
||||
user={createUser('test-user')}
|
||||
userShowInProgress={false}
|
||||
/>
|
||||
);
|
||||
expect(tree).toMatchSnapshot();
|
||||
|
|
|
|||
|
|
@ -4,8 +4,79 @@ exports[`ProfilePage matches snapshot 1`] = `
|
|||
<Page
|
||||
logoutError={null}
|
||||
scrollingDisabled={false}
|
||||
title="Profile page with display name: most-awesome-shop"
|
||||
title="Profile page"
|
||||
>
|
||||
<withRouter(Connect(TopbarContainerComponent)) />
|
||||
<LayoutSideNavigation
|
||||
className={null}
|
||||
containerClassName={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<LayoutWrapperTopbar
|
||||
className={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<withRouter(Connect(TopbarContainerComponent))
|
||||
currentPage="ProfilePage"
|
||||
/>
|
||||
</LayoutWrapperTopbar>
|
||||
<LayoutWrapperSideNav
|
||||
className={null}
|
||||
rootClassName={null}
|
||||
tabs={null}
|
||||
>
|
||||
<div>
|
||||
<AvatarLarge
|
||||
user={
|
||||
Object {
|
||||
"attributes": Object {
|
||||
"banned": false,
|
||||
"profile": Object {
|
||||
"abbreviatedName": "test-user abbreviated name",
|
||||
"displayName": "test-user display name",
|
||||
},
|
||||
},
|
||||
"id": UUID {
|
||||
"uuid": "test-user",
|
||||
},
|
||||
"type": "user",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="ProfilePage.mobileHeading"
|
||||
values={
|
||||
Object {
|
||||
"name": "test-user display name",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
</LayoutWrapperSideNav>
|
||||
<LayoutWrapperMain
|
||||
className={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<div>
|
||||
<h1>
|
||||
<FormattedMessage
|
||||
id="ProfilePage.desktopHeading"
|
||||
values={
|
||||
Object {
|
||||
"name": "test-user display name",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</h1>
|
||||
</div>
|
||||
</LayoutWrapperMain>
|
||||
<LayoutWrapperFooter
|
||||
className={null}
|
||||
rootClassName={null}
|
||||
>
|
||||
<InjectIntl(Footer) />
|
||||
</LayoutWrapperFooter>
|
||||
</LayoutSideNavigation>
|
||||
</Page>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import OrderPage from './OrderPage/OrderPage.duck';
|
|||
import PasswordChangePage from './PasswordChangePage/PasswordChangePage.duck';
|
||||
import PasswordRecoveryPage from './PasswordRecoveryPage/PasswordRecoveryPage.duck';
|
||||
import PasswordResetPage from './PasswordResetPage/PasswordResetPage.duck';
|
||||
import ProfilePage from './ProfilePage/ProfilePage.duck';
|
||||
import ProfileSettingsPage from './ProfileSettingsPage/ProfileSettingsPage.duck';
|
||||
import SalePage from './SalePage/SalePage.duck';
|
||||
import SearchPage from './SearchPage/SearchPage.duck';
|
||||
|
|
@ -28,6 +29,7 @@ export {
|
|||
PasswordChangePage,
|
||||
PasswordRecoveryPage,
|
||||
PasswordResetPage,
|
||||
ProfilePage,
|
||||
ProfileSettingsPage,
|
||||
SalePage,
|
||||
SearchPage,
|
||||
|
|
|
|||
|
|
@ -133,9 +133,10 @@ const routeConfiguration = () => {
|
|||
component: RedirectToLandingPage,
|
||||
},
|
||||
{
|
||||
path: '/u/:displayName',
|
||||
path: '/u/:id',
|
||||
name: 'ProfilePage',
|
||||
component: props => <ProfilePage {...props} />,
|
||||
loadData: ProfilePage.loadData,
|
||||
},
|
||||
{
|
||||
path: '/profile-settings',
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@
|
|||
"CheckoutPage.initiateOrderAmountTooLow": "Unfortunately we're not able to process this payment, since the payment amount is too low. Please contact the administrators.",
|
||||
"CheckoutPage.initiateOrderError": "Payment request failed. Please go back to {listingLink} and try again. If the error persists, try refreshing the page or contacting the marketplace admin.",
|
||||
"CheckoutPage.listingNotFoundError": "Unfortunately the listing is not available anymore.",
|
||||
"CheckoutPage.loadingData": "Loading checkout data...",
|
||||
"CheckoutPage.loadingData": "Loading checkout data…",
|
||||
"CheckoutPage.paymentInfo": "You'll only be charged if your request is accepted by the provider.",
|
||||
"CheckoutPage.paymentTitle": "Payment",
|
||||
"CheckoutPage.priceBreakdownTitle": "Booking breakdown",
|
||||
|
|
@ -317,10 +317,10 @@
|
|||
"PasswordRecoveryPage.forgotPasswordTitle": "Forgot your password?",
|
||||
"PasswordRecoveryPage.resendEmailInfo": "Didn't get the email? {resendEmailLink}",
|
||||
"PasswordRecoveryPage.resendEmailLinkText": "Send another email.",
|
||||
"PasswordRecoveryPage.resendingEmailInfo": "Resending instructions...",
|
||||
"PasswordRecoveryPage.resendingEmailInfo": "Resending instructions…",
|
||||
"PasswordRecoveryPage.title": "Request a new password",
|
||||
"PasswordResetForm.passwordLabel": "Your new password",
|
||||
"PasswordResetForm.passwordPlaceholder": "Enter your new password...",
|
||||
"PasswordResetForm.passwordPlaceholder": "Enter your new password…",
|
||||
"PasswordResetForm.passwordRequired": "This field is required",
|
||||
"PasswordResetForm.passwordTooLong": "Password should be at most {maxLength} characters",
|
||||
"PasswordResetForm.passwordTooShort": "Password should be at least {minLength} characters",
|
||||
|
|
@ -387,6 +387,13 @@
|
|||
"PrivacyPolicyPage.privacyTabTitle": "Privacy Policy",
|
||||
"PrivacyPolicyPage.schemaTitle": "Privacy Policy | {siteTitle}",
|
||||
"PrivacyPolicyPage.tosTabTitle": "Terms of Service",
|
||||
"ProfilePage.desktopHeading": "Hello, I'm {name}.",
|
||||
"ProfilePage.editProfileLinkDesktop": "Edit profile",
|
||||
"ProfilePage.editProfileLinkMobile": "Edit",
|
||||
"ProfilePage.listingsTitle": "My saunas ({count})",
|
||||
"ProfilePage.loadingData": "Loading user data…",
|
||||
"ProfilePage.loadingDataFailed": "Whoopsie, something went wrong - please try again.",
|
||||
"ProfilePage.mobileHeading": "{name}",
|
||||
"ProfileSettingsForm.addYourProfilePicture": "+ Add your profile picture…",
|
||||
"ProfileSettingsForm.addYourProfilePictureMobile": "+ Add",
|
||||
"ProfileSettingsForm.changeAvatar": "Change",
|
||||
|
|
@ -445,8 +452,8 @@
|
|||
"SectionHero.subTitle": "The largest online community to rent saunas in Finland.",
|
||||
"SectionHero.title": "Book saunas everywhere.",
|
||||
"SectionLocations.listingsInLocation": "Saunas in {location}",
|
||||
"SectionLocations.title": "We have wooden saunas, electric saunas and even tent saunas.",
|
||||
"SectionLocations.subtitle": "We have more than 1000 saunas around Finland. Here are some of our most popular locations.",
|
||||
"SectionLocations.title": "We have wooden saunas, electric saunas and even tent saunas.",
|
||||
"SignupForm.emailInvalid": "A valid email address is required",
|
||||
"SignupForm.emailLabel": "Email",
|
||||
"SignupForm.emailPlaceholder": "john.doe@example.com",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue