Introduce and use "ProfilePage" component

pull/10616/head
Alex P 2018-09-26 10:57:39 -07:00
parent 10bd12ccdd
commit 7bba3905b5
10 changed files with 299 additions and 5 deletions

View File

@ -106,7 +106,7 @@ class Root extends PureComponent<{}, State> {
<Route path="manage-sources/new" component={SourcePage} />
<Route path="manage-sources/:id/edit" component={SourcePage} />
<Route path="delorean" component={FluxPage} />
<Route path="user" component={UserPage} />
<Route path="user/:tab" component={UserPage} />
</Route>
</Route>
<Route path="*" component={NotFound} />

View File

@ -91,7 +91,7 @@ class SideNav extends PureComponent<Props> {
{
type: NavItemType.Avatar,
title: 'My Profile',
link: `/user`,
link: `/user/settings/${this.sourceParam}`,
image: LeroyJenkins.avatar,
location: location.pathname,
highlightWhen: ['user'],

View File

@ -0,0 +1,81 @@
/*
Styles for Profile Page
----------------------------------------------------------------------------
*/
.profile {
display: flex;
align-items: stretch;
background-color: rgba($g3-castle, 0.5);
}
.profile-nav {
display: flex;
flex-direction: column;
align-items: stretch;
flex: 0 1 0;
padding-left: $ix-marg-d;
}
.profile-nav--header {
@include no-user-select();
margin: $ix-marg-d;
margin-left: 0;
display: flex;
flex-direction: column;
align-items: center;
text-align: center
}
.profile-nav--name {
font-size: 19px;
line-height: 22px;
color: $g15-platinum;
margin: 0;
margin-top: 16px;
}
.profile-nav--description {
font-weight: 600;
font-size: 13px;
line-height: 15px;
margin: 0;
margin-top: $ix-marg-a + $ix-marg-b;
color: $g11-sidewalk;
}
.profile-nav--tabs {
@include no-user-select();
display: flex;
flex-direction: column;
align-items: stretch;
}
.profile-nav--tab {
border-radius: $radius 0 0 $radius;
font-size: 17px;
height: $nav-size;
line-height: $nav-size;
padding: 0 17px;
color: $g11-sidewalk;
transition: background-color 0.25s ease, color 0.25s ease;
&:hover {
background-color: rgba($g3-castle, 0.5);
color: $g16-pearl;
cursor: pointer;
}
&.active {
background-color: $g3-castle;
color: $g18-cloud;
}
}
.profile-content {
flex: 1 0 0;
background-color: $g3-castle;
border-radius: $radius;
min-height: 500px;
padding: $ix-marg-d;
}

View File

@ -0,0 +1,111 @@
// Libraries
import React, {Component, ReactElement, ReactNode} from 'react'
import {withRouter, InjectedRouter} from 'react-router'
// Components
import ProfilePageSection from 'src/shared/components/profile_page/ProfilePageSection'
import ProfilePageTab from 'src/shared/components/profile_page/ProfilePageTab'
import Avatar from 'src/shared/components/avatar/Avatar'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
name: string
avatar: string
description?: string
children: ReactNode[]
activeTabUrl: string
router: InjectedRouter
parentUrl: string
}
@ErrorHandling
class ProfilePage extends Component<Props> {
public static Section = ProfilePageSection
constructor(props) {
super(props)
}
public render() {
this.validateChildTypes()
return (
<div className="profile">
<div className="profile-nav">
{this.profileNavHeader}
{this.profileNavTabs}
</div>
<div className="profile-content">{this.activeSectionComponent}</div>
</div>
)
}
private get profileNavTabs(): JSX.Element {
const {children, activeTabUrl} = this.props
return (
<div className="profile-nav--tabs">
{React.Children.map(children, (child: JSX.Element) => (
<ProfilePageTab
title={child.props.title}
key={child.props.id}
id={child.props.id}
url={child.props.url}
active={child.props.url === activeTabUrl}
onClick={this.handleTabClick}
/>
))}
</div>
)
}
private get profileNavHeader(): JSX.Element {
const {avatar, name, description} = this.props
return (
<div className="profile-nav--header">
<Avatar
imageURI={avatar}
diameterPixels={160}
customClass="profile-nav--avatar"
/>
<h3 className="profile-nav--name">{name}</h3>
{description && (
<p className="profile-nav--description">{description}</p>
)}
</div>
)
}
private get activeSectionComponent(): JSX.Element[] {
const {children, activeTabUrl} = this.props
// Using ReactElement as type to ensure children have props
return React.Children.map(children, (child: ReactElement<any>) => {
if (child.props.url === activeTabUrl) {
return child.props.children
}
})
}
public handleTabClick = (url: string) => (): void => {
const {router, parentUrl} = this.props
router.push(`${parentUrl}/${url}/`)
}
private validateChildTypes = (): void => {
const {children} = this.props
React.Children.forEach(children, (child: JSX.Element) => {
if (child.type !== ProfilePageSection) {
throw new Error(
'<ProfilePage> expected children of type <ProfilePage.Section />'
)
}
})
}
}
export default withRouter(ProfilePage)

View File

@ -0,0 +1,21 @@
// Libraries
import React, {Component} from 'react'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
id: string
title: string
url: string
children: JSX.Element
}
@ErrorHandling
class ProfilePageSection extends Component<Props> {
public render() {
return <div>{this.props.children}</div>
}
}
export default ProfilePageSection

View File

@ -0,0 +1,22 @@
// Libraries
import React, {SFC} from 'react'
import classnames from 'classnames'
interface Props {
id: string
title: string
active: boolean
url: string
onClick: (url: string) => () => void
}
const ProfilePageTab: SFC<Props> = ({title, active, url, onClick}) => (
<div
className={classnames('profile-nav--tab', {active})}
onClick={onClick(url)}
>
{title}
</div>
)
export default ProfilePageTab

View File

@ -22,6 +22,7 @@
// Components
// TODO: Import these styles into their respective components instead of this stylesheet
@import 'src/page_layout/PageLayout';
@import 'src/shared/components/profile_page/ProfilePage';
@import 'src/shared/components/avatar/Avatar';
@import 'src/shared/components/fancy_scrollbar/FancyScrollbar';
@import 'src/shared/components/notifications/Notifications';

View File

@ -0,0 +1,14 @@
// Libraries
import React, {Component} from 'react'
interface Props {
token: string
}
class TokenManager extends Component<Props> {
public render() {
return <div>{this.props.token}</div>
}
}
export default TokenManager

View File

@ -0,0 +1,14 @@
// Libraries
import React, {Component} from 'react'
interface Props {
blargh: string
}
class UserSettings extends Component<Props> {
public render() {
return <div>{this.props.blargh}</div>
}
}
export default UserSettings

View File

@ -3,6 +3,9 @@ import React, {PureComponent} from 'react'
/// Components
import {Page} from 'src/page_layout'
import ProfilePage from 'src/shared/components/profile_page/ProfilePage'
import UserSettings from 'src/user/components/UserSettings'
import TokenManager from 'src/user/components/TokenManager'
// Decorators
import {ErrorHandling} from 'src/shared/decorators/errors'
@ -26,15 +29,20 @@ interface User {
interface Props {
user?: User
params: {
tab: string
}
}
@ErrorHandling
export class FluxPage extends PureComponent<Props> {
export class UserPage extends PureComponent<Props> {
public static defaultProps: Partial<Props> = {
user: LeroyJenkins,
}
public render() {
const {user, params} = this.props
return (
<Page>
<Page.Header fullWidth={false}>
@ -44,11 +52,33 @@ export class FluxPage extends PureComponent<Props> {
<Page.Header.Right />
</Page.Header>
<Page.Contents fullWidth={false} scrollable={true}>
<div>fsfsfsdfs</div>
<div className="col-xs-12">
<ProfilePage
name={user.name}
avatar={user.avatar}
parentUrl="/user"
activeTabUrl={params.tab}
>
<ProfilePage.Section
id="user-profile-tab--settings"
url="settings"
title="Settings"
>
<UserSettings blargh="User Settings" />
</ProfilePage.Section>
<ProfilePage.Section
id="user-profile-tab--tokens"
url="tokens"
title="Tokens"
>
<TokenManager token="Token Manager" />
</ProfilePage.Section>
</ProfilePage>
</div>
</Page.Contents>
</Page>
)
}
}
export default FluxPage
export default UserPage