Skip to content

Commit

Permalink
Merge pull request #8040 from vanilla/feature/kb-user-dropdown
Browse files Browse the repository at this point in the history
New MeBox User drop down
  • Loading branch information
Stephane LaFleche committed Nov 19, 2018
2 parents a161095 + e8fc42b commit 3bd428e
Show file tree
Hide file tree
Showing 17 changed files with 343 additions and 34 deletions.
42 changes: 42 additions & 0 deletions library/src/scripts/components/NumberFormatted.tsx
@@ -0,0 +1,42 @@
/**
* @author Stéphane LaFlèche <stephane.l@vanillaforums.com>
* @copyright 2009-2018 Vanilla Forums Inc.
* @license GPL-2.0-only
*/

import * as React from "react";
import classNames from "classnames";
import numeral from "numeral";

interface IProps {
value: number;
className?: string;
}

/**
* A component to format numbers. The react number format supports localization. The way we'll pass in the options are TBD.
*/
export default class NumberFormatted extends React.Component<IProps> {
public render() {
numeral.localeData("en"); //
const { className } = this.props;
const value = numeral(this.props.value);
const compactValue = this.stripTrailingZeros(value.format("0a.0"));
const fullValue = value.format();

const Tag = fullValue === compactValue ? `span` : `abbr`;
return (
<Tag title={fullValue} className={classNames("number", className)}>
{compactValue}
</Tag>
);
}

private stripTrailingZeros(value: string) {
if (isNaN(Number(value))) {
return value;
} else {
return Number(value).toString();
}
}
}
12 changes: 8 additions & 4 deletions library/src/scripts/components/VanillaHeader.tsx
Expand Up @@ -6,15 +6,15 @@

import * as React from "react";
import ReactDOM from "react-dom";
import { MeBox } from "@library/components/mebox/MeBox";
import MeBox from "@library/components/mebox/MeBox";
import { dummyLogoData } from "./mebox/state/dummyLogoData";
import { dummyNotificationsData } from "@library/components/mebox/state/dummyNotificationsData";
import { dummyMessagesData } from "@library/components/mebox/state/dummyMessagesData";
import { dummyNavigationData } from "./mebox/state/dummyNavigationData";
import { dummyUserDropDownData } from "@library/components/mebox/state/dummyUserDropDownData";
import { dummyGuestNavigationData, dummyNavigationData } from "./mebox/state/dummyNavigationData";
import { IDeviceProps } from "@library/components/DeviceChecker";
import { withDevice } from "@library/contexts/DeviceContext";
import { dummyOtherLanguagesData } from "@library/state/dummyOtherLanguages";
import { dummyUserDropDownData } from "@library/components/mebox/state/dummyUserDropDownData";

interface IProps extends IDeviceProps {
container?: Element; // Element containing header. Should be the default most if not all of the time.
Expand All @@ -36,13 +36,17 @@ export class VanillaHeader extends React.Component<IProps> {
logoProps={dummyLogoData}
notificationsProps={dummyNotificationsData as any}
navigationProps={{ children: dummyNavigationData.children, className: "vanillaHeader-nav" }}
guestNavigationProps={{
children: dummyGuestNavigationData.children,
className: "vanillaHeader-nav vanillaHeader-guestNav",
}}
languagesProps={{
...dummyOtherLanguagesData,
className: "vanillaHeader-locales",
buttonClassName: "vanillaHeader-localesToggle",
}}
messagesProps={dummyMessagesData as any}
userDropDownProps={dummyUserDropDownData}
counts={dummyUserDropDownData}
device={this.props.device}
headerStyles={{}} // Defaults for now
/>,
Expand Down
Expand Up @@ -10,6 +10,7 @@ import classNames from "classnames";
import { LocationDescriptor } from "history";
import DropDownItem from "./DropDownItem";
import { ModalLink } from "@library/components/modal";
import SmartLink from "@library/components/navigation/SmartLink";

export interface IDropDownItemLink {
to: LocationDescriptor;
Expand All @@ -27,15 +28,15 @@ export default class DropDownItemLink extends React.Component<IDropDownItemLink>
public render() {
const { children, name, isModalLink, className, to } = this.props;
const linkContents = children ? children : name;
const LinkComponent = isModalLink ? ModalLink : NavLink;
const LinkComponent = isModalLink ? ModalLink : SmartLink;
return (
<DropDownItem className={classNames("dropDown-linkItem", className)}>
<LinkComponent
to={to}
title={name}
lang={this.props.lang}
className="dropDownItem-link"
activeClassName="isCurrent"
activeClassName={isModalLink ? null : "isCurrent"}
>
{linkContents}
</LinkComponent>
Expand Down
@@ -0,0 +1,41 @@
/*
* @author Stéphane LaFlèche <stephane.l@vanillaforums.com>
* @copyright 2009-2018 Vanilla Forums Inc.
* @license GPL-2.0-only
*/

import * as React from "react";
import DropDownItemLink, { IDropDownItemLink } from "@library/components/dropdown/items/DropDownItemLink";
import NumberFormatted from "@library/components/NumberFormatted";
import classNames from "classnames";

interface IProps extends IDropDownItemLink {
count?: number;
hideCountWhenZero?: boolean;
className?: string;
}

/**
* Implements link type of item with count for DropDown menu
*/
export default class DropDownItemLinkWithCount extends React.Component<IProps> {
public static defaultProps = {
hideCountWhenZero: true,
};
public render() {
const { name, children, count } = this.props;
const linkContents = children ? children : name;
const showCount = !!count && !(this.props.hideCountWhenZero && this.props.count === 0);
return (
<DropDownItemLink {...this.props}>
<span className="dropDownItem-text">{linkContents}</span>
{showCount && (
<NumberFormatted
className={classNames("dropDownItem-count", this.props.className)}
value={count!}
/>
)}
</DropDownItemLink>
);
}
}
35 changes: 35 additions & 0 deletions library/src/scripts/components/dropdown/items/DropDownSection.tsx
@@ -0,0 +1,35 @@
/*
* @author Stéphane LaFlèche <stephane.l@vanillaforums.com>
* @copyright 2009-2018 Vanilla Forums Inc.
* @license GPL-2.0-only
*/

import * as React from "react";
import classNames from "classnames";
import DropDownItem from "./DropDownItem";
import Heading from "@library/components/Heading";
import DropDownItemSeparator from "@library/components/dropdown/items/DropDownItemSeparator";

interface IProps {
title: string;
className?: string;
level?: 2 | 3;
children: React.ReactNode;
}

/**
* Implements DropDownSection component. It add a heading to a group of elements in a DropDown menu
*/
export default class DropDownSection extends React.Component<IProps> {
public render() {
return (
<React.Fragment>
<DropDownItemSeparator />
<DropDownItem className={classNames("dropDown-section", this.props.className)}>
<Heading title={this.props.title} className="dropDown-sectionHeading" />
<ul className="dropDown-sectionContents">{this.props.children}</ul>
</DropDownItem>
</React.Fragment>
);
}
}
44 changes: 44 additions & 0 deletions library/src/scripts/components/dropdown/items/DropDownUserCard.tsx
@@ -0,0 +1,44 @@
/*
* @author Stéphane LaFlèche <stephane.l@vanillaforums.com>
* @copyright 2009-2018 Vanilla Forums Inc.
* @license GPL-2.0-only
*/

import * as React from "react";
import classNames from "classnames";
import { UserPhoto, UserPhotoSize } from "@library/components/mebox/pieces/UserPhoto";
import { IInjectableUserState } from "@library/users/UsersModel";
import { connect } from "react-redux";
import UsersModel from "@library/users/UsersModel";
import SmartLink from "@library/components/navigation/SmartLink";

export interface IProps extends IInjectableUserState {
className?: string;
photoSize?: UserPhotoSize;
}

/**
* Implements DropDownUserCard component for DropDown menus.
*/
export class DropDownUserCard extends React.Component<IProps> {
public render() {
const currentUser = this.props.currentUser.data!;
const profileLink = `${window.location.origin}/profile/${currentUser.name}`;
return (
<li className={classNames("dropDown-userCard", this.props.className)}>
<SmartLink to={profileLink} className="userDropDown-userCardPhotoLink">
<UserPhoto
className="userDropDown-userCardPhoto"
userInfo={currentUser}
size={this.props.photoSize || UserPhotoSize.LARGE}
/>
</SmartLink>
<SmartLink to={profileLink} className="userDropDown-userCardName" tabIndex={-1}>
{currentUser.name}
</SmartLink>
</li>
);
}
}
const withRedux = connect(UsersModel.mapStateToProps);
export default withRedux(DropDownUserCard);
2 changes: 1 addition & 1 deletion library/src/scripts/components/frame/FrameBody.tsx
Expand Up @@ -9,7 +9,7 @@ import classNames from "classnames";

export interface IFrameBodyProps {
className?: string;
children: JSX.Element;
children: React.ReactNode;
}

/**
Expand Down
50 changes: 37 additions & 13 deletions library/src/scripts/components/mebox/MeBox.tsx
Expand Up @@ -20,23 +20,28 @@ import LanguagesDropDown, { ILanguageDropDownProps } from "@library/components/L
import { PanelWidgetHorizontalPadding } from "@library/components/layouts/PanelLayout";
import FlexSpacer from "@library/components/FlexSpacer";
import { ButtonBaseClass } from "@library/components/forms/Button";
import UserDropdown from "@library/components/mebox/pieces/UserDropdown";
import UserDropdown, { UserDropDown } from "./pieces/UserDropdown";
import { IInjectableUserState } from "@library/users/UsersModel";
import UsersModel from "@library/users/UsersModel";
import { connect } from "react-redux";
import get from "lodash/get";

export interface IHeaderStyles {
bgColor?: string;
fgColor?: string;
notificationColor?: string;
}

export interface IMeBoxProps extends IDeviceProps {
export interface IMeBoxProps extends IDeviceProps, IInjectableUserState {
homePage: boolean;
className?: string;
logoProps: IHeaderLogo;
navigationProps: IVanillaHeaderNavProps;
guestNavigationProps: IVanillaHeaderNavProps;
languagesProps: ILanguageDropDownProps;
notificationsProps: INotificationsDropDownProps;
messagesProps: IMessagesDropDownProps;
userDropDownProps: any;
counts: any;
headerStyles: IHeaderStyles;
}

Expand All @@ -56,7 +61,13 @@ export class MeBox extends React.Component<IMeBoxProps, IState> {
}
public render() {
const isMobile = this.props.device === Devices.MOBILE;
const hideNonSearchElements = this.state.openSearch && isMobile;
const showNonSearchItems = !this.state.openSearch && !isMobile;
const currentUser = get(this.props, "currentUser.data", {
name: null,
userID: null,
photoUrl: null,
});
const isGuest = currentUser && UsersModel && currentUser.userID === UsersModel.GUEST_ID;
const styles = {
fg: this.props.headerStyles && this.props.headerStyles.fgColor ? this.props.headerStyles.fgColor : "#fff",
bg:
Expand All @@ -82,7 +93,7 @@ export class MeBox extends React.Component<IMeBoxProps, IState> {
content = (
<React.Fragment>
<div className="vanillaHeader-bar">
{!hideNonSearchElements && (
{showNonSearchItems && (
<React.Fragment>
<HeaderLogo
{...this.props.logoProps}
Expand Down Expand Up @@ -112,13 +123,25 @@ export class MeBox extends React.Component<IMeBoxProps, IState> {
onCloseSearch={this.closeSearch}
cancelButtonClassName="meBox-searchCancel"
/>
{!hideNonSearchElements && (
<React.Fragment>
<NotificationsDropdown {...this.props.notificationsProps} countClass="meBox-count" />
<MessagesDropDown {...this.props.messagesProps} countClass="meBox-count" />
<UserDropdown {...this.props.userDropDownProps} />
</React.Fragment>
)}
{showNonSearchItems &&
!isGuest && (
<React.Fragment>
<NotificationsDropdown
{...this.props.notificationsProps}
countClass="meBox-count"
/>
<MessagesDropDown {...this.props.messagesProps} countClass="meBox-count" />
<UserDropdown counts={this.props.counts} className="meBox-userDropdown" />
</React.Fragment>
)}
{showNonSearchItems &&
isGuest && (
<VanillaHeaderNav
{...this.props.guestNavigationProps}
linkClassName="meBox-navLink"
linkContentClassName="meBox-navLinkContent"
/>
)}
</div>
</React.Fragment>
);
Expand Down Expand Up @@ -155,4 +178,5 @@ export class MeBox extends React.Component<IMeBoxProps, IState> {
};
}

export default withDevice(MeBox);
const withRedux = connect(UsersModel.mapStateToProps);
export default withRedux(MeBox);

0 comments on commit 3bd428e

Please sign in to comment.