diff --git a/package.json b/package.json index d39b05685..ee48ad82f 100644 --- a/package.json +++ b/package.json @@ -40,9 +40,9 @@ "copy-webpack-plugin": "4.5.4", "cross-env": "^5.0.1", "cypress": "^3.1.5", - "enzyme": "3.7.0", - "enzyme-adapter-react-16": "1.7.0", - "enzyme-to-json": "^3.3.4", + "enzyme": "3.10.0", + "enzyme-adapter-react-16": "1.14.0", + "enzyme-to-json": "3.4.0", "eslint": "^5.16.0", "eslint-config-react-app": "3.0.5", "eslint-plugin-chai-friendly": "^0.4.1", @@ -73,7 +73,7 @@ "@lingui/cli": "2.7.2", "@lingui/react": "2.7.2", "@material-ui/core": "3.6.1", - "@material-ui/icons": "3.0.1", + "@material-ui/icons": "4.4.1", "@sentry/browser": "4.4.2", "@sentry/node": "4.4.2", "@zeit/next-css": "1.0.1", @@ -113,6 +113,7 @@ "lscache": "1.2.0", "micro-proxy": "1.1.0", "next": "^8.1.0", + "next-images": "^1.1.2", "next-routes": "1.4.2", "next-transpile-modules": "^2.3.1", "polished": "3.0.3", @@ -129,6 +130,7 @@ "react-joyride": "2.0.5", "react-jss": "8.6.1", "react-swipeable": "^4.3.0", + "react-swipeable-views": "^0.13.3", "universal-cookie": "3.0.4", "workbox-webpack-plugin": "3.6.3" }, diff --git a/packages/admin-frontend/package.json b/packages/admin-frontend/package.json index c035a2e84..186dbff3d 100644 --- a/packages/admin-frontend/package.json +++ b/packages/admin-frontend/package.json @@ -12,7 +12,7 @@ "@emotion/core": "^10.0.14", "@emotion/styled": "^10.0.14", "@material-ui/core": "3.6.1", - "@material-ui/icons": "3.0.1", + "@material-ui/icons": "4.4.1", "cookie-parser": "^1.4.3", "downshift": "2.0.10", "express": "4.16.4", @@ -32,4 +32,4 @@ "devDependencies": { "@zeit/next-css": "1.0.1" } -} \ No newline at end of file +} diff --git a/packages/gdl-frontend/components/BookListSection/PaginationArrowView.js b/packages/gdl-frontend/components/BookListSection/PaginationArrowView.js index aab9d3900..843dda5ef 100644 --- a/packages/gdl-frontend/components/BookListSection/PaginationArrowView.js +++ b/packages/gdl-frontend/components/BookListSection/PaginationArrowView.js @@ -16,6 +16,7 @@ import colorMap from '../../style/colorMapping'; import GameLink from './GameLink'; import BookLink from './BookLink'; import PaginationScrollGrid from './PaginationScrollGrid'; +import { AMOUNT_OF_ITEMS_PER_LEVEL } from '../HomePage'; import type { Book } from './BookLink'; import type { @@ -72,7 +73,10 @@ const PaginationArrowView = ({ {items - .slice((currentIndex - 1) * 5, currentIndex * 5) + .slice( + (currentIndex - 1) * AMOUNT_OF_ITEMS_PER_LEVEL, + currentIndex * AMOUNT_OF_ITEMS_PER_LEVEL + ) .map((item: any) => (
{level === 'Games' ? ( diff --git a/packages/gdl-frontend/components/EditBookLibrary/BookGrid.js b/packages/gdl-frontend/components/EditBookLibrary/BookGrid.js new file mode 100644 index 000000000..314848f6a --- /dev/null +++ b/packages/gdl-frontend/components/EditBookLibrary/BookGrid.js @@ -0,0 +1,34 @@ +// @flow + +import * as React from 'react'; +import GridContainer from '../BookGrid/styledGridContainer'; +import BookLink, { type Book } from './BookSelectionLink'; + +type Props = { + books: $ReadOnlyArray, + selectedBooks: Array, + changeActive: () => void, + selectAll: boolean +}; + +class BookGrid extends React.Component { + render() { + const { books, selectedBooks } = this.props; + + return ( + + {books.map(book => ( + + ))} + + ); + } +} + +export default BookGrid; diff --git a/packages/gdl-frontend/components/EditBookLibrary/BookSelectionLink.js b/packages/gdl-frontend/components/EditBookLibrary/BookSelectionLink.js new file mode 100644 index 000000000..c1758ae1e --- /dev/null +++ b/packages/gdl-frontend/components/EditBookLibrary/BookSelectionLink.js @@ -0,0 +1,127 @@ +// @flow + +import * as React from 'react'; +import { CardContent, Typography } from '@material-ui/core'; +import { CheckCircle } from '@material-ui/icons'; +import styled from '@emotion/styled'; + +import CoverImage from '../CoverImage'; +import media from '../../style/media'; +import { coverWidths } from '../BookListSection/coverWidths'; + +export type Book = $ReadOnly<{ + id: string, + bookId: number, + title: string, + language: { + code: string + }, + coverImage: ?{ url: string } +}>; + +type Props = { + book: Book, + selectedBooks: Array, + changeActive: () => void, + selectAll: boolean +}; + +type State = { + active: boolean +}; + +export default class BookLink extends React.Component { + state = { + active: false + }; + + componentDidUpdate(prevProps: Props) { + if (prevProps.selectedBooks !== this.props.selectedBooks) { + this.props.selectAll + ? this.setState({ active: true }) + : this.setState({ active: false }); + } + } + + handleClick(id: string, selectedBooks: Array) { + if (!selectedBooks.some(item => id === item)) { + selectedBooks.push(id); + this.setState({ active: true }); + } else { + selectedBooks.splice(selectedBooks.indexOf(id), 1); + this.setState({ active: false }); + } + this.props.changeActive(); + } + + render() { + const { book, selectedBooks } = this.props; + const bookSelected = selectedBooks.some(item => book.id === item); + if (this.state.active && !bookSelected) { + selectedBooks.push(book.id); + } else if (!this.state.active && bookSelected) { + selectedBooks.splice(selectedBooks.indexOf(book.id), 1); + } + + return ( + this.handleClick(book.id, selectedBooks)}> + + + + + {book.title} + + {' '} + + ); + } +} + +const Card = styled('div')` + .selectBook { + color: rgb(68, 68, 68); + } + .isSelected { + color: #0277bd; + } + .active img { + transition: all 0.08s ease-in-out; + padding: 10px; + padding-bottom: 0px; + } + .active svg { + color: #0277bd; + } + .active { + background-color: #d7e2f5; + } + position: relative; + box-shadow: 0 10px 30px 0 rgba(0, 0, 0, 0.1); + :hover { + .selectBook { + color: #8a8b91; + } + } + margin-right: 15px; + width: ${coverWidths.small}px; + ${media.tablet` + width: ${coverWidths.large}px; + `}; +`; diff --git a/packages/gdl-frontend/components/EditBookLibrary/EditBooks.js b/packages/gdl-frontend/components/EditBookLibrary/EditBooks.js new file mode 100644 index 000000000..8c5553920 --- /dev/null +++ b/packages/gdl-frontend/components/EditBookLibrary/EditBooks.js @@ -0,0 +1,297 @@ +// @flow +import * as React from 'react'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import { Hidden, Typography, Button, Dialog, Tooltip } from '@material-ui/core'; +import { Delete, CheckCircle, Clear } from '@material-ui/icons'; +import { Container, Center, IconButton } from '../../elements'; +import EditBookGrid from './BookGrid'; +import { css } from '@emotion/core'; +import styled from '@emotion/styled'; +import { spacing, misc } from '../../style/theme'; +import type { intlShape } from 'react-intl'; + +type Props = { + books: Array, + onClick: () => void, + selectedBooks: Array, + onDelete: () => Promise, + dialog: () => void, + open: boolean, + selectAllBooks: () => void, + deselectAllBooks: () => void, + changeActive: () => void, + favorites: boolean, + intl: intlShape, + selectAll: boolean +}; + +const translations = defineMessages({ + books: { + id: 'books', + defaultMessage: 'books' + }, + book: { + id: 'book', + defaultMessage: 'book' + }, + favorites: { + id: 'favorites', + defaultMessage: 'favorites?' + }, + offlineLibrary: { + id: 'offline library', + defaultMessage: 'offline library?' + } +}); + +const EditBooks = ({ + books, + onClick, + selectedBooks, + onDelete, + dialog, + open, + selectAllBooks, + deselectAllBooks, + changeActive, + favorites, + intl, + selectAll +}: Props) => ( + <> + + + + } + /> + + {selectedBooks.length} books chosen + + + + + } + label={ + + } + /> + + } + label={} + /> + + + + +
+ + {' '} + {favorites ? 'favorites' : 'offline library'} + +
+ + } + > + + + } + /> + + {`${selectedBooks.length} `} + + + + + + } + label={ + + } + /> + + + } + label={} + /> + +
+
+ + + +
+ +
+ +
+

+ + {` ${selectedBooks.length} `} + {` ${ + selectedBooks.length > 1 + ? `${intl.formatMessage(translations.books)} ` + : `${intl.formatMessage(translations.book)} ` + }`} + + {`${ + favorites + ? ` ${intl.formatMessage(translations.favorites)}` + : ` ${intl.formatMessage(translations.offlineLibrary)}` + }`} +

+ + + +
+
+
+ +); + +const EditBooksBar = styled('div')` + display: flex; + height: 60px; + width: 100%; + position: fixed; + background-color: rgb(248, 248, 248); + z-index: 1; + max-width: ${misc.containers.large}px; + margin-left: auto; + margin-right: auto; + border-bottom: 1px solid lightgrey; + align-items: center; + padding: 0 20px; +`; + +const Right = styled('div')` + display: flex; + align-items: center; + justify-content: flex-end; + flex: 1; +`; + +const iconButtonStyle = css` + height: 60px; + width: fit-content; + padding: 0 12px; + span { + font-size: 0.8rem; + } +`; +const bottomIconButtonStyle = css` + height: 60px; + width: 35%; + border-radius: 10%; + span { + font-size: 0.8rem; + } +`; + +export default injectIntl(EditBooks); diff --git a/packages/gdl-frontend/components/FeaturedContentCarousel/Carousel.js b/packages/gdl-frontend/components/FeaturedContentCarousel/Carousel.js new file mode 100644 index 000000000..e60a4affc --- /dev/null +++ b/packages/gdl-frontend/components/FeaturedContentCarousel/Carousel.js @@ -0,0 +1,272 @@ +// @flow +/** + * Part of GDL gdl-frontend. + * Copyright (C) 2019 GDL + * + * See LICENSE + */ + +import * as React from 'react'; +import { Button, CardContent, Typography } from '@material-ui/core'; +import { View, Hidden } from '../../elements'; +import Pagination from '../FeaturedContentCarousel/Pagination'; +import SwipeableViews from 'react-swipeable-views'; +import { autoPlay, virtualize } from 'react-swipeable-views-utils'; +import { KeyboardArrowRight, KeyboardArrowLeft } from '@material-ui/icons/'; +import styled from '@emotion/styled'; + +import Head from '../../components/Head'; +import { + Banner, + HeroCovertitle, + HeroCardMobile, + HeroCardTablet +} from '../HomePage/index'; +import { logEvent } from '../../lib/analytics'; +import { FormattedMessage } from 'react-intl'; +import { css } from '@emotion/core'; +import type { HomeContent_featuredContent as FeaturedContent } from '../../gqlTypes'; +import { colors, misc } from '../../style/theme'; + +type State = { index: number }; +const AutoPlaySwipeableViews = autoPlay(virtualize(SwipeableViews)); + +type Props = {| + featuredContent: Array +|}; + +function mod(n, m) { + let remain = n % m; + return remain >= 0 ? remain : remain + m; +} +class Carousel extends React.Component { + state = { index: 0 }; + handleNextIndex = () => { + this.setState({ + index: this.state.index + 1 + }); + }; + + handlePrevIndex = () => { + this.setState({ + index: this.state.index - 1 + }); + }; + cardContent = (content: FeaturedContent) => { + return ( + // Specifying width here makes text in IE11 wrap + + + {content.title} + + + {content.description} + + + + ); + }; + card = (content: FeaturedContent) => { + return ( + <> + + + + + + + + + {/* Specifying width here makes text in IE11 wrap*/} + + {this.cardContent(content)} + + + + + ); + }; + slideRenderer = (params: { index: number, key: number }) => { + const { key, index } = params; + const { featuredContent } = this.props; + const indexPage = mod(index, featuredContent.length); + const content = featuredContent[indexPage]; + return ( +
+ {this.card(content)} + + + {this.cardContent(content)} + +
+ this.setState({ index })} + /> +
+
+
+
+
+ ); + }; + render() { + const { index } = this.state; + const { featuredContent } = this.props; + + return ( + + {featuredContent.length > 1 ? ( + <> + this.setState({ index })} + slideRenderer={this.slideRenderer} + /> + +
+ +
+ +
+ +
+
+ this.setState({ index })} + /> +
+
+ +
+ this.setState({ index })} + /> +
+
+ + ) : ( + <> + {this.card(featuredContent[0])} + + + {this.cardContent(featuredContent[0])} + + + )} +
+ ); + } +} + +export default Carousel; + +const Container = styled('div')` + position: relative; + max-width: ${misc.containers.small}px; + margin-left: auto; + margin-right: auto; +`; + +const fadeIn = css` + &:hover { + animation: fade-in ease 0.5s; + + @keyframes fade-in { + from { + opacity: 0; + } + to { + opacity: 1; + } + } + } +`; + +const arrowRightContainer = css` + position: absolute; + width: 9.9%; + height: 100%; + top: 0; + right: 0; + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row-reverse; + &:hover { + background-image: linear-gradient( + 90deg, + rgba(240, 240, 240, 0.1), + rgba(0, 0, 0, 0.5) + ); + cursor: pointer; + } +`; +const arrowLeftContainer = css` + position: absolute; + width: 9.9%; + height: 100%; + top: 0; + left: 0; + display: flex; + justify-content: flex-start; + align-items: center; + flex-direction: row; + &:hover { + background-image: linear-gradient( + -90deg, + rgba(240, 240, 240, 0.1), + rgba(0, 0, 0, 0.5) + ); + cursor: pointer; + } +`; +const dotsContainer = css` + position: relative; + width: 100%; + height: 100%; + top: 0; + left: 0; + display: flex; + justifycontent: center; +`; diff --git a/packages/gdl-frontend/components/FeaturedContentCarousel/Pagination.js b/packages/gdl-frontend/components/FeaturedContentCarousel/Pagination.js new file mode 100644 index 000000000..be8d05cf7 --- /dev/null +++ b/packages/gdl-frontend/components/FeaturedContentCarousel/Pagination.js @@ -0,0 +1,53 @@ +// @flow +/** + * Part of GDL gdl-frontend. + * Copyright (C) 2019 GDL + * + * See LICENSE + */ + +import React from 'react'; +import PaginationDot from './PaginationDot'; + +const styles = { + root: { + position: 'absolute', + bottom: '8px', + left: '0', + display: 'flex', + flexDirection: 'row', + width: '100%', + margin: 'auto', + justifyContent: 'center' + } +}; + +type Props = {| + dots: number, + index: number, + onChangeIndex: number => void +|}; + +class Pagination extends React.Component { + handleClick = (event: SyntheticEvent, index: number) => { + this.props.onChangeIndex(index); + }; + + render() { + const { index, dots } = this.props; + + const children = Array(dots) + .fill() + .map((e, i) => ( + + )); + + return
{children}
; + } +} +export default Pagination; diff --git a/packages/gdl-frontend/components/FeaturedContentCarousel/PaginationDot.js b/packages/gdl-frontend/components/FeaturedContentCarousel/PaginationDot.js new file mode 100644 index 000000000..fd2b6f5d2 --- /dev/null +++ b/packages/gdl-frontend/components/FeaturedContentCarousel/PaginationDot.js @@ -0,0 +1,57 @@ +// @flow +/** + * Part of GDL gdl-frontend. + * Copyright (C) 2019 GDL + * + * See LICENSE + */ + +import React from 'react'; + +const styles = { + root: { + height: 18, + width: 18, + cursor: 'pointer', + border: 0, + background: 'none', + padding: 0 + }, + dot: { + backgroundColor: '#e4e6e7', + height: 9, + width: 9, + borderRadius: 6, + margin: 3 + }, + active: { + backgroundColor: '#0277bd' + } +}; + +type Props = {| + active: boolean, + index: number, + onClick: any +|}; + +class PaginationDot extends React.Component { + handleClick = (event: SyntheticEvent) => { + this.props.onClick(event, this.props.index); + }; + + render() { + const { active } = this.props; + const styleDot = active + ? Object.assign({}, styles.dot, styles.active) + : styles.dot; + + return ( + + ); + } +} + +export default PaginationDot; diff --git a/packages/gdl-frontend/components/GamePage/index.js b/packages/gdl-frontend/components/GamePage/index.js new file mode 100644 index 000000000..957c7bfdc --- /dev/null +++ b/packages/gdl-frontend/components/GamePage/index.js @@ -0,0 +1,123 @@ +// @flow +/** + * Part of GDL gdl-frontend. + * Copyright (C) 2019 GDL + * + * See LICENSE + */ + +import * as React from 'react'; +import { css } from '@emotion/core'; +import { FormattedMessage } from 'react-intl'; + +import type { GameContent_games as Games } from '../../gqlTypes'; + +import ReadingLevelTrans from '../../components/ReadingLevelTrans'; +import Layout from '../../components/Layout'; +import Main from '../../components/Layout/Main'; +import { + Container, + View, + Hidden, + SideMenuMargin, + LoadingButton +} from '../../elements'; +import { spacing } from '../../style/theme'; +import MobileBottomBar from '../../components/Navbar/MobileBottomBar'; +import SideMenuBar from '../../components/Navbar/SideMenuBar'; +import { Typography } from '@material-ui/core'; +import GridContainer from '../BookGrid/styledGridContainer'; +import LevelHR from '../Level/LevelHR'; +import GameLink from '../BookListSection/GameLink'; + +type Props = {| + games: Games, + languageCode: string, + loading: boolean, + loadMore: () => void +|}; + +const GamePage = ({ + games: { pageInfo, results }, + loading, + loadMore, + languageCode +}: Props) => ( + + + + + +
+ + + + + {results.length > 0 ? ( + <> + {/* $FlowFixMe This is the level from the query parameter. Which doesn't really typecheck */} + + + + ) : ( + + )} + + + + {results.map(game => ( + + ))} + +
+ + + +
+
+
+
+
+ + + +
+); + +const scrollStyle = css` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + padding: ${spacing.medium} 0; + margin-top: 20px; +`; + +export default GamePage; diff --git a/packages/gdl-frontend/components/GdlI18nProvider.js b/packages/gdl-frontend/components/GdlI18nProvider.js index ea76821ec..03a7742d3 100644 --- a/packages/gdl-frontend/components/GdlI18nProvider.js +++ b/packages/gdl-frontend/components/GdlI18nProvider.js @@ -27,9 +27,13 @@ const defaultCatalog = { en: enTranslations }; -const GdlI18nContext = React.createContext(); +}>({ + language: '', + changeSiteLanguage: async () => {} +}); const GdlI18nConsumer = GdlI18nContext.Consumer; // https://github.com/yahoo/react-intl/wiki/API @@ -83,6 +87,7 @@ class GdlI18nProvider extends Component< return ( diff --git a/packages/gdl-frontend/components/GlobalMenu/SelectBookLanguage.js b/packages/gdl-frontend/components/GlobalMenu/SelectBookLanguage.js index 511538f5e..e4326d566 100644 --- a/packages/gdl-frontend/components/GlobalMenu/SelectBookLanguage.js +++ b/packages/gdl-frontend/components/GlobalMenu/SelectBookLanguage.js @@ -16,9 +16,9 @@ import LanguageList from '../LanguageList'; import { getBookLanguageCode } from '../../lib/storage'; import { GdlI18nConsumer } from '../GdlI18nProvider'; -function linkProps(language) { +function linkProps(language, category) { return { - route: 'books', + route: category, params: { lang: language.code } }; } diff --git a/packages/gdl-frontend/components/HomePage/index.js b/packages/gdl-frontend/components/HomePage/index.js index 1a8084eed..26af742c5 100644 --- a/packages/gdl-frontend/components/HomePage/index.js +++ b/packages/gdl-frontend/components/HomePage/index.js @@ -8,9 +8,8 @@ import * as React from 'react'; import { css } from '@emotion/core'; -import { FormattedMessage } from 'react-intl'; import styled from '@emotion/styled'; -import { Button, Card, CardContent, Typography } from '@material-ui/core'; +import { Card } from '@material-ui/core'; import type { Category, @@ -18,31 +17,32 @@ import type { HomeContent_featuredContent as FeaturedContent } from '../../gqlTypes'; -import { logEvent } from '../../lib/analytics'; import ReadingLevelTrans from '../../components/ReadingLevelTrans'; import Layout from '../../components/Layout'; import Main from '../../components/Layout/Main'; -import { Container, View } from '../../elements'; +import { Container, View, Hidden, SideMenuMargin } from '../../elements'; import { NavContextBar, CategoryNavigation } from '../../components/NavContextBar'; -import Head from '../../components/Head'; import PaginationSection from '../BookListSection/PaginationSection'; -import { colors, spacing } from '../../style/theme'; +import { spacing, misc } from '../../style/theme'; import media from '../../style/media'; import { flexCenter } from '../../style/flex'; -import { QueryBookList, QueryGameList } from '../../gql'; +import { QueryBookList } from '../../gql'; +import MobileBottomBar from '../../components/Navbar/MobileBottomBar'; +import SideMenuBar from '../../components/Navbar/SideMenuBar'; import type { ReadingLevel } from '../../gqlTypes'; +import Carousel from '../FeaturedContentCarousel/Carousel'; -const Banner = styled('div')` +export const Banner = styled('div')` background-image: ${p => (p.src ? `url(${p.src})` : 'none')}; background-size: cover; position: relative; display: flex; padding: 15px; - justify-content: center; + justify-content: flex; ${media.mobile` height: 210px; `} ${media.tablet` @@ -50,9 +50,15 @@ const Banner = styled('div')` padding: 20px; justify-content: flex-end; `}; + ${media.largerTablet` + max-width: ${misc.containers.small}px; + align-items: center; + margin-left: auto; + margin-right: auto; + `} `; -const HeroCovertitle = styled('div')` +export const HeroCovertitle = styled('div')` position: absolute; top: 0; left: 0; @@ -61,18 +67,20 @@ const HeroCovertitle = styled('div')` padding: 3px 12px; `; -const HeroCardMobile = styled(Card)` +export const HeroCardMobile = styled(Card)` ${flexCenter}; position: relative; margin-top: -50px; margin-left: ${spacing.large}; margin-right: ${spacing.large}; + margin-bottom: 1px; + ${media.tablet` display: none; `}; `; -const HeroCardTablet = styled(Card)` +export const HeroCardTablet = styled(Card)` ${flexCenter}; max-width: 375px; ${media.mobile` @@ -85,7 +93,7 @@ export const AMOUNT_OF_ITEMS_PER_LEVEL = 5; type Props = {| homeContent: HomeContent, languageCode: string, - featuredContent: FeaturedContent, + featuredContent: Array, categories: Array, category: Category |}; @@ -97,6 +105,7 @@ class HomePage extends React.Component { this.props.category !== nextProps.category ); } + render() { const { homeContent, @@ -106,49 +115,8 @@ class HomePage extends React.Component { languageCode } = this.props; - // Destructuring Games, otherwise apollo can't seperate it - const { Games, ...readingLevels } = homeContent; - - const cardContent = ( - // Specifying width here makes text in IE11 wrap - - - {featuredContent.title} - - - {featuredContent.description} - - - - ); - return ( - { languageCode={languageCode} /> -
- - - - - - - - {/* Specifying width here makes text in IE11 wrap*/} - {cardContent} - - - - {cardContent} - - {Object.entries(readingLevels) - // $FlowFixMe TODO: Get this properly typed. Maybe newer Flow versions understands this instead of turning into a mixed type - .filter( - ([_, data]: [ReadingLevel, any]) => - data.results && data.results.length > 0 - ) - .map(([level, data]: [ReadingLevel, any]) => ( - - - - {({ books, loadMore, goBack, loading }) => ( - } - browseLinkProps={{ - lang: languageCode, - readingLevel: level, - category: category, - route: 'browseBooks' - }} - items={books.results} - /> - )} - - - - ))} - - {Games.pageInfo.pageCount > 0 && ( - - - - {({ games, loadMore, goBack, loading }) => ( - } - items={games.results} - /> - )} - - - - )} -
+ + + + +
+ + + {Object.entries(homeContent) + // $FlowFixMe TODO: Get this properly typed. Maybe newer Flow versions understands this instead of turning into a mixed type + .filter( + ([_, data]: [ReadingLevel, any]) => + data.results && data.results.length > 0 + ) + .map(([level, data]: [ReadingLevel, any]) => ( + + + + {({ books, loadMore, goBack, loading }) => ( + } + browseLinkProps={{ + lang: languageCode, + readingLevel: level, + category: category, + route: 'browseBooks' + }} + items={books.results} + /> + )} + + + + ))} +
+
+ + +
); } @@ -255,7 +186,6 @@ const scrollStyle = css` align-items: center; justify-content: center; padding: ${spacing.medium} 0; - border-bottom: solid 1px ${colors.base.grayLight}; `; export default HomePage; diff --git a/packages/gdl-frontend/components/LanguageList.js b/packages/gdl-frontend/components/LanguageList.js index 4ba1f9ccb..2b2314482 100644 --- a/packages/gdl-frontend/components/LanguageList.js +++ b/packages/gdl-frontend/components/LanguageList.js @@ -23,8 +23,10 @@ import { Check as CheckIcon } from '@material-ui/icons'; import { Link } from '../routes'; import SrOnly from './SrOnly'; import { colors } from '../style/theme'; +import { CategoryContext } from '../context/CategoryContext'; import type { intlShape } from 'react-intl'; +import type { MainCategory } from '../types'; import type { languages_languages as Language } from '../gqlTypes'; type Props = { @@ -32,7 +34,7 @@ type Props = { selectedLanguageCode: ?string, onSelectLanguage: Language => void, languages: Array, - linkProps?: (language: Language) => {}, + linkProps?: (language: Language, category: MainCategory) => {}, intl: intlShape }; @@ -141,14 +143,19 @@ class LanguageList extends React.Component { {noResult ? ( ) : ( - filteredLanguages.map(l => ( - - )) + + {({ category }) => + filteredLanguages.map(l => ( + + )) + } + )} @@ -167,10 +174,19 @@ const NoLanguageItem = () => ( ); -const LanguageItem = ({ language, linkProps, onSelectLanguage }) => { +const LanguageItem = ({ + language, + linkProps, + onSelectLanguage, + currentCategory +}) => { if (linkProps) { return ( - + { if (!online) return null; return ( - - -
+ + + +
- -
  • - - - -
  • -
  • - - - -
  • -
  • - - + +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
  • + + + +
  • +
    + + + + - -
  • - - + + + + + -
  • -
  • - - + + -
  • -
  • - - + + -
  • - - - - - - - - - - - - - - - - - - - - - + + + + ); }; -const FooterStyle = styled('footer')` +const FooterWrapper = styled('footer')` + background-color: #f6f7f9; + border-top: solid 0.5px rgba(112, 112, 112, 0.22); + margin-top: 24px; +`; + +const FooterStyle = styled('div')` display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; margin-top: 40px; margin-bottom: 50px; + position: relative; a { text-decoration: none; color: ${colors.text.default}; @@ -137,6 +150,9 @@ const FooterStyle = styled('footer')` text-decoration: underline; } } + ${media.largerTablet` + left: ${SIDE_DRAWER_WIDTH / 2}px; + `} `; const LinkList = styled('ul')` @@ -156,6 +172,12 @@ const LinkList = styled('ul')` width: 33%; `} } + ${media.largerTablet` + li { + padding: 8px 0; + } + margin-left: 0; + `} `; const SocialMediaIcons = styled('div')` @@ -176,6 +198,10 @@ const SocialMediaIcons = styled('div')` ${media.tablet` order: 4; `}; + ${media.largerTablet` + order: 4; + margin: 14px 0 14px 36px; + `} `; const CreativeCommons = styled('div')` diff --git a/packages/gdl-frontend/components/Layout/Main.js b/packages/gdl-frontend/components/Layout/Main.js index 427463e8e..668e38091 100644 --- a/packages/gdl-frontend/components/Layout/Main.js +++ b/packages/gdl-frontend/components/Layout/Main.js @@ -11,18 +11,26 @@ import { css } from '@emotion/core'; import { Paper } from '@material-ui/core'; import { misc, colors } from '../../style/theme'; +import media from '../../style/media'; const styles = { default: css` - background: ${colors.container.background}; flex: 1 0 auto; width: 100%; max-width: ${misc.containers.large}px; margin-left: auto; margin-right: auto; + min-height: 100vh; `, white: css` background: ${colors.base.white}; + `, + container: css` + margin-left: 0; + ${media.largerTablet` + margin-left: 90px; + flex: 1 0 auto; + `} ` }; @@ -32,11 +40,11 @@ type Props = { background?: 'white' | 'gray' }; -const Main = ({ background, ...props }: Props) => ( +const Main = ({ background, ...rest }: Props) => ( ); diff --git a/packages/gdl-frontend/components/NavContextBar/NavContextBar.js b/packages/gdl-frontend/components/NavContextBar/NavContextBar.js index 658c493cf..c989bee57 100644 --- a/packages/gdl-frontend/components/NavContextBar/NavContextBar.js +++ b/packages/gdl-frontend/components/NavContextBar/NavContextBar.js @@ -10,20 +10,33 @@ import React, { type Node } from 'react'; import { Paper } from '@material-ui/core'; import Container from '../../elements/Container'; +import { SIDE_DRAWER_WIDTH } from '../../style/constants'; +import { misc } from '../../style/theme'; +import media from '../../style/media'; +import css from '@emotion/css'; + +const styles = { + paper: css` + z-index: 10; + margin-left: 0; + ${media.largerTablet` + margin-left: ${SIDE_DRAWER_WIDTH}px; + `} + ` +}; type Props = {| children: Node |}; const NavContextBar = (props: Props) => ( - + diff --git a/packages/gdl-frontend/components/Navbar/MobileBottomBar.js b/packages/gdl-frontend/components/Navbar/MobileBottomBar.js new file mode 100644 index 000000000..d3ab650ef --- /dev/null +++ b/packages/gdl-frontend/components/Navbar/MobileBottomBar.js @@ -0,0 +1,114 @@ +// @flow +import React from 'react'; +import { withStyles } from '@material-ui/core/styles'; +import BottomNavigation from '@material-ui/core/BottomNavigation'; +import BottomNavigationAction from '@material-ui/core/BottomNavigationAction'; +import { LibraryBooks, SportsEsports } from '@material-ui/icons'; +import { Link } from '../../routes'; +import { getTrigger } from './helpers'; +import { Slide } from '@material-ui/core'; +import { withRouter } from 'next/router'; +import type { NextRouter } from '../../types'; +import { CategoryContext } from '../../context/CategoryContext'; + +const styles = theme => ({ + root: { + position: 'fixed', + width: '100%', + bottom: 0, + zIndex: theme.zIndex.appBar + } +}); + +const WrappedNavButton = ({ + label, + name, + value, + params, + children, + ...rest +}) => ( + + + +); + +type Props = { + classes: Object, + lang: string, + router: NextRouter +}; + +class MobileBottomBar extends React.Component { + scrollerRef = React.createRef(); + + state = { + trigger: null + }; + + handleScroll = (event: SyntheticEvent) => + this.setState({ trigger: getTrigger(event, this.scrollerRef) }); + + componentDidMount() { + window.addEventListener('scroll', this.handleScroll); + } + + componentWillUnmount() { + window.removeEventListener('scroll', this.handleScroll); + } + + render() { + const { + classes, + lang, + router: { pathname } + } = this.props; + const { trigger } = this.state; + + return ( + + {({ setCategory }) => ( + + + setCategory('books')} + params={{ lang }} + value="/" + > + + + + setCategory('games')} + params={{ lang }} + value="/games" + > + + + + + )} + + ); + } +} + +export default withRouter(withStyles(styles)(MobileBottomBar)); diff --git a/packages/gdl-frontend/components/Navbar/SideMenuBar.js b/packages/gdl-frontend/components/Navbar/SideMenuBar.js new file mode 100644 index 000000000..d13b78911 --- /dev/null +++ b/packages/gdl-frontend/components/Navbar/SideMenuBar.js @@ -0,0 +1,113 @@ +// @flow +import React from 'react'; +import { withStyles } from '@material-ui/core/styles'; +import { + Drawer, + List, + ListItem, + ListItemIcon, + ListItemText +} from '@material-ui/core'; +import { LibraryBooks, SportsEsports, Videocam } from '@material-ui/icons'; +import { withRouter } from 'next/router'; +import { SIDE_DRAWER_WIDTH } from '../../style/constants'; +import { Link } from '../../routes'; +import { CategoryContext } from '../../context/CategoryContext'; +import type { NextRouter } from '../../types'; + +const styles = theme => ({ + root: {}, + menuButton: { + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + minHeight: 67 + }, + icon: { + marginRight: 0 + }, + drawer: { + width: SIDE_DRAWER_WIDTH, + flexShrink: 0, + whiteSpace: 'nowrap' + }, + drawerPaper: { + width: SIDE_DRAWER_WIDTH, + + overflowX: 'hidden' + } +}); + +const SideMenuBar = ({ + classes, + lang, + router: { pathname } +}: { + classes: Object, + lang: string, + router: NextRouter +}) => ( + + {({ setCategory }) => ( + + + + + + + + + + setCategory('books')} + selected={pathname === '/'} + className={classes.menuButton} + > + + + + + + + + + setCategory('games')} + selected={pathname === '/games'} + className={classes.menuButton} + > + + + + + + + + + setCategory('video')} + // selected={pathname === '/videos'} + className={classes.menuButton} + > + + + + + + + + + + )} + +); + +export default withRouter(withStyles(styles)(SideMenuBar)); diff --git a/packages/gdl-frontend/components/Navbar/helpers.js b/packages/gdl-frontend/components/Navbar/helpers.js new file mode 100644 index 000000000..869ec68bd --- /dev/null +++ b/packages/gdl-frontend/components/Navbar/helpers.js @@ -0,0 +1,28 @@ +// @flow +/** + * Since the application can't get react hooks to work, I reverse engineered: + * https://github.com/mui-org/material-ui/blob/89687f38cae750650555772ba4d821c9084d8dfc/packages/material-ui/src/useScrollTrigger/useScrollTrigger.js + */ + +// Change the trigger value when the vertical scroll strictly crosses this threshold +const THRESHOLD = 100; + +function getScrollY(ref: any): number { + return ref.pageYOffset !== undefined ? ref.pageYOffset : ref.scrollTop; +} + +export function getTrigger( + event: ?SyntheticEvent, + ref: Object +): boolean { + const previous = ref.current; + ref.current = event ? getScrollY(event.currentTarget) : previous; + + if (previous !== undefined) { + if (ref.current < previous) { + return false; + } + } + + return ref.current > THRESHOLD; +} diff --git a/packages/gdl-frontend/components/Navbar/index.js b/packages/gdl-frontend/components/Navbar/index.js index 3bc8721b5..d34b3479a 100644 --- a/packages/gdl-frontend/components/Navbar/index.js +++ b/packages/gdl-frontend/components/Navbar/index.js @@ -23,6 +23,8 @@ import { WifiOff as WifiOffIcon } from '@material-ui/icons'; import { FormattedMessage } from 'react-intl'; +import { withStyles } from '@material-ui/core/styles'; +import { withRouter } from 'next/router'; import { withOnlineStatusContext } from '../OnlineStatusContext'; import SelectBookLanguage from '../GlobalMenu/SelectBookLanguage'; @@ -35,13 +37,30 @@ import { misc } from '../../style/theme'; import SearchInput from '../Search/components/SearchInput'; import SearchDrawer from '../Search/components/SearchDrawer'; import { Hidden } from '../../elements'; +import { SIDE_DRAWER_WIDTH } from '../../style/constants'; +import { CategoryContext } from '../../context/CategoryContext'; +import { getBookLanguageCode } from '../../lib/storage'; type Props = { onMenuClick(): void, online: boolean, - homeTutorialInProgress?: boolean + homeTutorialInProgress?: boolean, + classes: Object, + router: { query: { lang: string } } }; +const styles = theme => ({ + appBar: { + zIndex: theme.zIndex.drawer + 1 + }, + toolBar: { + minHeight: 56, + [`@media (min-width: 600px)`]: { + minHeight: 67 + } + } +}); + const BrandLink = styled('a')` margin-right: auto; svg { @@ -55,127 +74,149 @@ const BrandLink = styled('a')` } `; -const Navbar = ({ onMenuClick, online, homeTutorialInProgress }: Props) => { +const Navbar = ({ + onMenuClick, + online, + homeTutorialInProgress, + classes, + router: { + query: { lang } + } +}: Props) => { const offline = !online; - - const brandLink = ( - - logEvent('Navigation', 'Home', 'Brand logo')} - > - - - - ); + const language = lang || getBookLanguageCode(); return ( - - - - - - - - - - {brandLink} - - - {/* This component is not visibile on mobile */} - {!offline && ( -
    - -
    - )} - - - {offline ? ( - - - + + {({ category }) => ( + + + + + - + - - ) : ( - <> - - {({ onShowClick }) => ( - + + logEvent('Navigation', 'Home', 'Brand logo')} > - + + + + + + + {/* This component is not visibile on mobile */} + {!offline && ( +
    + +
    + )} + + + {offline ? ( + + + - - - - )} - - - logEvent('Navigation', 'Home', 'House icon')} - > - - - - - - - - {({ onClick, loading }) => ( - - } - > + +
    + + ) : ( + <> + + {({ onShowClick }) => ( + + + + + + + )} + + + { - logEvent('Navigation', 'Language', 'Globe icon'); - onClick(); - }} + data-cy="home-button" color="inherit" + component="a" + onClick={() => + logEvent('Navigation', 'Home', 'House icon') + } > - {loading ? ( - - ) : ( - - )} + - + - - )} - - - )} -
    -
    -
    + + + + {({ onClick, loading }) => ( + + } + > + { + logEvent('Navigation', 'Language', 'Globe icon'); + onClick(); + }} + color="inherit" + > + {loading ? ( + + ) : ( + + )} + + + + + + )} + + + )} + + + + )} + ); }; @@ -189,10 +230,13 @@ const Center = styled('div')` display: flex; align-items: center; max-width: ${misc.containers.large}px; - display: flex; - align-items: center; - flex: 1 25%; + + flex: 1 50%; ${media.mobile`display: none;`}; + ${media.largerTablet` + max-width: ${misc.containers.small}px; + margin-left: ${SIDE_DRAWER_WIDTH}px; + `} `; const Right = styled('div')` @@ -202,4 +246,4 @@ const Right = styled('div')` flex: 1; `; -export default withOnlineStatusContext(Navbar); +export default withRouter(withOnlineStatusContext(withStyles(styles)(Navbar))); diff --git a/packages/gdl-frontend/components/PlayGamePage/Toolbar.js b/packages/gdl-frontend/components/PlayGamePage/Toolbar.js new file mode 100644 index 000000000..2f0aa5da4 --- /dev/null +++ b/packages/gdl-frontend/components/PlayGamePage/Toolbar.js @@ -0,0 +1,62 @@ +// @flow +/** + * Part of GDL gdl-frontend. + * Copyright (C) 2019 GDL + * + * See LICENSE + */ + +import * as React from 'react'; + +import styled from '@emotion/styled'; +import { FormattedMessage } from 'react-intl'; +import { IconButton } from '@material-ui/core'; +import { Close as CloseIcon } from '@material-ui/icons'; + +import SrOnly from '../../components/SrOnly'; +import { colors } from '../../style/theme'; +import media from '../../style/media'; +import { flexCenter } from '../../style/flex'; + +type Props = { + title: string, + onClose: () => void +}; + +const Toolbar = ({ title, onClose }: Props) => ( +
    +
    {title}
    + + + + + + + + +
    +); + +const Div = styled.div` + z-index: 2; + background: #fff; + position: sticky; + top: 0; + color: ${colors.text.subtle}; + border-bottom: 1px solid ${colors.base.grayLight}; + ${flexCenter}; + + font-size: 14px; + min-height: 48px; + ${media.tablet` + margin-bottom: 50px; + `}; +`; + +const Buttons = styled.div` + position: absolute; + right: 0; + top: 0; +`; + +export default Toolbar; diff --git a/packages/gdl-frontend/components/PlayGamePage/index.js b/packages/gdl-frontend/components/PlayGamePage/index.js new file mode 100644 index 000000000..a9557a801 --- /dev/null +++ b/packages/gdl-frontend/components/PlayGamePage/index.js @@ -0,0 +1,65 @@ +// @flow +/** + * Part of GDL gdl-frontend. + * Copyright (C) 2019 GDL + * + * See LICENSE + */ + +import * as React from 'react'; +import { Card } from '@material-ui/core'; +import styled from '@emotion/styled'; +import Toolbar from './Toolbar'; +import { Container } from '../../elements'; +import { Backdrop } from '../../components/Reader/styledReader'; +import media from '../../style/media'; + +import type { game_game as Game } from '../../gqlTypes'; + +type Props = { + game: Game, + onClose: () => void +}; + +/** + * This page is customized to work with H5P embedded games + */ +const PlayGamePage = ({ game, onClose }: Props) => ( + + + + + + +