Commit 23339084 authored by Sam Gluck's avatar Sam Gluck
Browse files

- Styleguide updates

- Menu
- Navigation
parent 23ea1c01
import ApolloClient from 'apollo-boost';
import resolvers from './resolvers';
import User from '../types/User';
import typeDefs from './typeDefs';
import { NotificationType } from '../components/elements/Notification/Notification';
export const DUMMY_USER = {
__typename: 'User',
id: 0,
username: 'MoodlerJoe',
email: 'moodlerjoe@example.com',
emojiId: '',
role: '',
location: '',
language: 'en-gb',
interests: [],
languages: [],
notifications: [
{
__typename: 'Notification',
id: 0,
when: 'Just now',
content: `
<p>
We think you might find the collection
<strong>Lenin at Finland Station</strong> interesting
</p>
`,
type: NotificationType.moodlebot
},
{
__typename: 'Notification',
id: 1,
when: '20 minutes ago',
content: `
<p>
<strong>Ibrahima</strong> commented on the collection
<strong>Hyperinflation in Weimar Germany</strong>
</p>
`,
type: NotificationType.collection
},
{
__typename: 'Notification',
id: 2,
when: '1 hour ago',
content: `
<p>
<strong>Liezel</strong> commented on your post in
<strong>Progressive European Historians</strong>
</p>
`,
type: NotificationType.community
}
]
} as User;
export default new ApolloClient({
clientState: {
typeDefs,
defaults: {
user: {
__typename: 'User',
isAuthenticated: false,
data: null
// TODO reinstate after dev
// isAuthenticated: true,
// data: null,
isAuthenticated: true,
data: DUMMY_USER
}
},
resolvers
......
export default `
type User {
id: Int!
username: String!
email: String!
emojiId: String!
role: String!
location: String!
language: String!
languages: String[]!
interests: String[]!
notifications: Notification[]!
}
type Notification {
id: Int!
when: String!
type: String!
content: String!
}
`;
```js
<Logo link={false} />
```
......@@ -4,4 +4,5 @@ import styled from '../../../themes/styled';
export default styled(ZenBody)`
flex-grow: 1;
overflow: auto;
`;
import * as React from 'react';
import { Menu as ZenMenu, Item } from '@zendeskgarden/react-menus';
import { Avatar } from '@zendeskgarden/react-avatars';
import OnClickOutside from 'react-click-outside';
import compose from 'recompose/compose';
import { graphql } from 'react-apollo';
import { withTheme } from '@zendeskgarden/react-theming';
import styled from '../../../themes/styled';
import NotificationsMenuBody from './Notifications.MenuBody';
import SearchMenuBody from './Search.MenuBody';
import UserMenuBody from './User.MenuBody';
import User from '../../../types/User';
import styled, { StyledThemeInterface } from '../../../themes/styled';
import MenuNav, { MenuItems } from './MenuNav';
import { faTimes } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
const avatar = require('../../../static/img/avatar.png');
const search = require('../../../static/img/search.png');
const notifications = require('../../../static/img/notifications.png');
const { GetUserQuery } = require('../../../graphql/GET_USER.client.graphql');
//TODO replace with some utility like lodash or polyfill `Object.values`
function values(obj) {
return Object.keys(obj).map(k => obj[k]);
}
interface MenuContainerProps {
show?: boolean;
......@@ -14,27 +26,63 @@ interface MenuContainerProps {
}
const MenuContainer = styled.div`
order: 3;
max-width: 300px;
width: ${(props: MenuContainerProps) => (props.open ? '25%' : '0%')};
width: 300px;
left: ${(props: MenuContainerProps) => (props.open ? 0 : 300)}px;
overflow: hidden;
`;
const MenuNav = styled.ul`
interface MenuBodyProps {
width: number;
open: boolean;
}
const MenuBody = styled.div<MenuBodyProps>`
width: ${props => props.width}px;
padding: 60px 10px 0 10px;
height: 100%;
overflow: auto;
z-index: 10;
position: fixed;
top: 0;
right: 0;
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
box-shadow: 0 0 10px lightgrey;
background-color: ${props => props.theme.styles.colour.base5};
border-left: 1px solid ${props => props.theme.styles.colour.base4};
right: ${props => (props.open ? 0 : -Math.max(...values(menuWidths)))}px;
transition: all 0.2s ease-in-out;
`;
const MenuNavItem = styled.li``;
const MenuBodyInner = styled.div<any>`
width: ${props => props.width}px;
padding-top: 10px;
border-top: 1px solid ${props => props.theme.styles.colour.base4};
`;
const MenuBody = styled.div``;
const MenuClose = styled.div`
cursor: pointer;
position: absolute;
top: 12px;
left: 14px;
font-size: 29px;
color: grey;
interface MenuProps {
&:active {
color: black;
}
`;
const menuWidths = {
[MenuItems.notifications]: 280, // size of a Notification + 20px for padding
[MenuItems.search]: 350,
[MenuItems.user]: 260
};
interface MenuProps extends StyledThemeInterface {
data: {
//TODO use actual User type from graphql once defined, if possible
user: {
data: any;
isAuthenticated: boolean;
};
};
show?: boolean;
}
......@@ -43,7 +91,7 @@ interface MenuState {
open: boolean;
}
export default class extends React.Component<MenuProps, MenuState> {
class Menu extends React.Component<MenuProps, MenuState> {
state = {
openMenuName: null,
open: false
......@@ -52,14 +100,20 @@ export default class extends React.Component<MenuProps, MenuState> {
constructor(props: MenuProps) {
super(props);
this.toggleMenu = this.toggleMenu.bind(this);
this.closeMenu = this.closeMenu.bind(this);
}
closeMenu() {
// don't unset openMenuName otherwise the menu body content
// will disappear as the menu closes
this.setState({
open: false
});
}
toggleMenu(menuName) {
if (this.state.openMenuName === menuName) {
this.setState({
openMenuName: null,
open: false
});
this.closeMenu();
}
this.setState({
......@@ -68,47 +122,58 @@ export default class extends React.Component<MenuProps, MenuState> {
});
}
getMenuBodyComponent() {
const activeMenu = this.state.openMenuName;
if (!activeMenu) {
return null;
}
const Component = {
[MenuItems.notifications]: NotificationsMenuBody,
[MenuItems.search]: SearchMenuBody,
[MenuItems.user]: UserMenuBody
}[activeMenu] as React.ComponentType<{ user: User }>;
return <Component user={this.props.data.user.data as User} />;
}
render() {
// we use this to set the menu container width AND the inner menu
// body width. we set the inner menu body width to prevent its content
// being fluid on resize of the container when the user navigates
// between menus that are different sizes, e.g. move from search to notifs
const menuWidth = menuWidths[String(this.state.openMenuName)] || 300;
return (
<MenuContainer show={this.props.show} open={this.state.open}>
<MenuNav>
<MenuNavItem onClick={() => this.toggleMenu('Search')}>
<img width={30} height={30} src={search} alt="Search" />
</MenuNavItem>
<MenuNavItem>
<ZenMenu
trigger={({ ref }) => (
<img
ref={ref}
width={30}
height={30}
src={notifications}
alt="Notifications"
/>
)}
<OnClickOutside style={{ width: 0 }} onClickOutside={this.closeMenu}>
<MenuContainer show={this.props.show} open={this.state.open}>
<MenuNav
fixed={true}
user={this.props.data.user}
toggleMenu={this.toggleMenu}
/>
<MenuBody width={menuWidth} open={this.state.open}>
<MenuClose
title={`Close the ${this.state.openMenuName} menu`}
onClick={this.closeMenu}
>
<Item>1</Item>
<Item>2</Item>
<Item>3</Item>
</ZenMenu>
</MenuNavItem>
<MenuNavItem>
<ZenMenu
trigger={({ ref }) => (
<Avatar innerRef={ref}>
<img src={avatar} alt="Joe Bloggs" />
</Avatar>
)}
>
<Item>Your Profile</Item>
<Item>Settings</Item>
<Item>About MoodleNet</Item>
<Item>Sign out</Item>
</ZenMenu>
</MenuNavItem>
</MenuNav>
<MenuBody>menu body</MenuBody>
</MenuContainer>
<FontAwesomeIcon icon={faTimes} />
</MenuClose>
<MenuNav
fixed={false}
user={this.props.data.user}
activeMenu={this.state.open ? this.state.openMenuName : null}
toggleMenu={this.toggleMenu}
/>
<MenuBodyInner width={menuWidth - 20}>
{this.getMenuBodyComponent()}
</MenuBodyInner>
</MenuBody>
</MenuContainer>
</OnClickOutside>
);
}
}
export default compose(
withTheme,
graphql(GetUserQuery)
)(Menu);
import * as React from 'react';
import { Tooltip } from '@zendeskgarden/react-tooltips';
import { Avatar } from '@zendeskgarden/react-avatars';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faCircle, faSearch } from '@fortawesome/free-solid-svg-icons';
import styled, { withTheme } from '../../../themes/styled';
const avatar = require('../../../static/img/avatar.png');
const MenuNav = styled.ul<any>`
position: ${props => (props.fixed ? 'fixed' : 'absolute')};
top: 0;
right: 0;
list-style-type: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: row;
font-size: 29px;
`;
const MenuNavItem = styled.li<any>`
cursor: pointer;
padding: 10px;
color: ${props =>
props.active
? props.theme.styles.colour.primary
: props.theme.styles.colour.primaryAlt};
transition: color 0.2s ease;
`;
const NotifCount = styled.div`
font-weight: bold;
position: absolute;
color: white;
top: 50%;
font-size: 13px;
line-height: 1;
left: 50%;
transform: translate(-50%, -50%);
`;
const NotifMenuNavItem = styled(MenuNavItem)`
position: relative;
`;
const NotifMenuNavItemTrigger = styled.div`
user-select: none;
&:active,
&:focus {
outline: 0;
}
`;
export enum MenuItems {
notifications = 'notifications',
search = 'search',
user = 'user'
}
export default withTheme(
({ fixed, toggleMenu, user, theme, activeMenu }: any) => {
return (
<MenuNav fixed={fixed}>
<MenuNavItem
active={activeMenu === MenuItems.search}
onClick={() => toggleMenu(MenuItems.search)}
>
<FontAwesomeIcon icon={faSearch} />
</MenuNavItem>
<NotifMenuNavItem
active={activeMenu === MenuItems.notifications}
onClick={() => toggleMenu(MenuItems.notifications)}
>
<Tooltip
placement="bottom"
trigger={
<NotifMenuNavItemTrigger>
<FontAwesomeIcon icon={faCircle} />
<NotifCount>{user.data.notifications.length}</NotifCount>
</NotifMenuNavItemTrigger>
}
>
<div style={{ textAlign: 'center', fontWeight: 'bold' }}>
{user.data.notifications.length} unread notifications
</div>
</Tooltip>
</NotifMenuNavItem>
<MenuNavItem
style={{ paddingLeft: 7 }}
active={activeMenu === MenuItems.user}
onClick={() => toggleMenu(MenuItems.user)}
>
<Avatar>
<img
style={{
position: 'relative',
top: '-2px'
}}
src={avatar}
alt={user.data.username}
title={`Hi, ${user.data.username}!`}
/>
</Avatar>
</MenuNavItem>
</MenuNav>
);
}
);
import * as React from 'react';
import Notification from '../../elements/Notification/Notification';
export default ({ user }) => {
return (
<div>
{user.notifications.map(notification => {
return (
<Notification
{...notification}
onClick={() => alert(`notif id: ${notification.id} clicked`)}
key={notification.id}
/>
);
})}
</div>
);
};
import * as React from 'react';
import styled, { withTheme } from '../../../themes/styled';
import Text from '../../inputs/Text/Text';
import H6 from '../../typography/H6/H6';
import Button from '../../elements/Button/Button';
import Tag from '../../elements/Tag/Tag';
import Link from '../../elements/Link/Link';
const SearchHeading = styled(H6)`
margin-block-start: 0.5em;
margin-block-end: 0.5em;
`;
const StyledTag = styled(Tag)`
margin: 0 5px 5px 0;
`;
//TODO get tags from the API
const words = `offer
segment
slave
duck
instant
market
degree
populate
chick
dear
enemy
reply
drink
occur
support
shell
neck`;
const links = ['The Russian Revolution', 'Joseph Stalin', 'Lenin'];
export default withTheme(({ theme }: any) => {
return (
<div>
<SearchHeading>
Search{' '}
<span style={{ color: theme.styles.colour.primary }}>MoodleNet</span>
</SearchHeading>
<form
onSubmit={e => {
e.preventDefault();
const el: HTMLInputElement | null = document.getElementById(
'searchInput'
) as HTMLInputElement;
alert('search submitted: ' + (el ? el.value : ''));
}}
>
<Text
id="searchInput"
placeholder="e.g. history lessons"
button={<Button type="submit">Search</Button>}
/>
</form>
<SearchHeading>Popular tags</SearchHeading>
<div>
{words.split('\n').map(word => (
<StyledTag key={word} onClick={() => alert(word)}>
{word}
</StyledTag>
))}
</div>
<SearchHeading>Popular search phrases</SearchHeading>
<div>
{links.map(link => (
<>
<Link to={`/search?q=${encodeURIComponent(link)}`}>{link}</Link>
<br />
</>
))}
</div>
</div>
);
});
import * as React from 'react';
import styled from '../../../themes/styled';
import Link from '../../elements/Link/Link';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
faCogs,
faPencilAlt,
faSignOutAlt
} from '@fortawesome/free-solid-svg-icons';
const UserLink = styled(Link)`
text-decoration: none;
`;
const UserMenu = styled.div`
padding-top: 50px;
text-align: center;
font-size: 20px;
a {