diff --git a/packages/gdl-frontend/components/GlobalMenu/index.js b/packages/gdl-frontend/components/GlobalMenu/index.js index 6aa6eea4b..163194289 100644 --- a/packages/gdl-frontend/components/GlobalMenu/index.js +++ b/packages/gdl-frontend/components/GlobalMenu/index.js @@ -33,6 +33,7 @@ import OnlineStatusContext from '../OnlineStatusContext'; import SelectBookLanguage from './SelectBookLanguage'; import CategoriesMenu from './CategoriesMenu'; import offlineLibrary from '../../lib/offlineLibrary'; +import WelcomeTutorial from '../WelcomeTutorial/WelcomeTutorial'; type Props = {| onClose(): void, @@ -71,7 +72,7 @@ class GlobalMenu extends React.Component { onOpen={() => {}} PaperProps={{ style: { - // By setting variant="temporary" a borde right is applied. Which is why it setting it to "inherit" removes it + // By setting variant="temporary" a border right is applied. Which is why it setting it to "inherit" removes it borderRight: 'inherit' } }} @@ -163,6 +164,13 @@ class GlobalMenu extends React.Component { )} +
+ +
{online && ( <> @@ -182,6 +190,7 @@ class GlobalMenu extends React.Component { + {({ isAdmin }) => isAdmin && ( diff --git a/packages/gdl-frontend/components/WelcomeTutorial/DialogContent.js b/packages/gdl-frontend/components/WelcomeTutorial/DialogContent.js new file mode 100644 index 000000000..4bafd104d --- /dev/null +++ b/packages/gdl-frontend/components/WelcomeTutorial/DialogContent.js @@ -0,0 +1,95 @@ +//@flow +import React from 'react'; +import { Card, CardContent, DialogContent } from '@material-ui/core'; + +type Props = { + screenshotUrlM: string, + screenshotUrl: string, + graceImgUrl: string, + message: string, + fullscreen: boolean +}; + +class GetDialogContent extends React.Component { + render() { + return ( + +
+ screenshot from the page +
+
+ Grace + + +

+ {this.props.message} +

+
+
+
+
+ ); + } +} + +export default GetDialogContent; diff --git a/packages/gdl-frontend/components/WelcomeTutorial/WelcomeTutorial.js b/packages/gdl-frontend/components/WelcomeTutorial/WelcomeTutorial.js new file mode 100644 index 000000000..56e39b69e --- /dev/null +++ b/packages/gdl-frontend/components/WelcomeTutorial/WelcomeTutorial.js @@ -0,0 +1,406 @@ +//@flow +import React from 'react'; +import { injectIntl, defineMessages } from 'react-intl'; +import { + Button, + Dialog, + DialogActions, + MobileStepper, + Slide, + ListItem, + ListItemText, + ListItemIcon +} from '@material-ui/core'; +import { Help as HelpIcon } from '@material-ui/icons'; +/** @jsx jsx */ +import { css, jsx } from '@emotion/core'; +import GetDialogContent from './DialogContent'; +import { + TABLET_BREAKPOINT, + LARGER_TABLET_BREAKPOINT +} from '../../style/theme/misc'; +import type { intlShape } from 'react-intl'; + +const grace1 = '/static/img/grace1.png'; +const grace2 = '/static/img/grace2.png'; +const grace3 = '/static/img/grace3.png'; +const selectLanguage = '/static/img/selectLanguage.png'; +const saveOffline = '/static/img/saveOffline.png'; +const menu = '/static/img/menu.png'; +const start = '/static/img/startempty.png'; +const saveOfflineM = '/static/img/saveOfflineM.png'; //for mobile +const menuM = '/static/img/menuM.png'; //for mobile +const startM = '/static/img/startM.png'; //for mobile + +type State = { + open: boolean, + width: number, + height: number, + activeStep: number, + direction: 'left' | 'right', + left: number, + originalOffset: number, + velocity: number, + timeOfLastDragEvent: number, + touchStartX: number, + prevTouchX: number, + beingTouched: boolean, + intervalId: string | null +}; +type Props = { + shouldOpen: boolean, + listButton: boolean, + swipeable: ?() => void, + intl: intlShape +}; +type TargetTouches = { + clientX: number +}; +type touchMoveEventType = { + targetTouches: TargetTouches +}; +type touchStartEventType = { + targetTouches: Array +}; + +const styles = { + dialogWindow: { + backgroundColor: '#2B8DC8', + height: '100%', + display: 'flex', + flexDirection: 'column', + justifyContent: 'space-between', + overflow: 'hidden' + }, + dialogContent: { + backgroundColor: '#2B8DC8', + height: '100%', + width: '100%' + }, + nextButton: { + color: 'white', + backgroundColor: '#8FE346', + width: 44, + padding: 0, + textShadow: '0px 0px 3px #383838' + }, + skipButton: { + color: 'white', + backgroundColor: 'rgba(255, 255, 255, 0.16)', + width: '100%', + textShadow: '0px 0px 3px #383838' + } +}; + +const translations = defineMessages({ + languageTip: { + id: 'languagetip', + defaultMessage: + 'If you want to change the language, click on the globe at the top-right corner of the screen!' + }, + tutorialStartText: { + id: 'TutorialStartText', + defaultMessage: + 'Hi there! This is a quick tutorial on some of the core functionality of the website. Click next for more information.' + }, + menuTip: { + id: 'menuTip', + defaultMessage: + 'The menu is in the top-left corner. From the menu you can access Categories, Favorites and more.' + }, + offlineTip: { + id: 'offlineTip', + defaultMessage: + 'You can save books for later and read them offline by clicking the Save offline icon!' + } +}); + +let NUMBER_OF_PAGES: number; + +class WelcomeTutorial extends React.Component { + constructor(props: Props) { + super(props); + this.state = { + open: this.props.shouldOpen, + width: 0, + height: 0, + activeStep: 0, + direction: 'left', + left: 0, + originalOffset: 0, + velocity: 0, + timeOfLastDragEvent: 0, + touchStartX: 0, + prevTouchX: 0, + beingTouched: false, + intervalId: null + }; + + NUMBER_OF_PAGES = this.props.shouldOpen ? 3 : 4; + } + handleTouchStart(touchStartEvent: touchStartEventType, mobile: boolean) { + if (this.state.intervalId !== null && mobile) { + window.clearInterval(this.state.intervalId); + } + this.setState({ + originalOffset: this.state.left, + velocity: 0, + timeOfLastDragEvent: Date.now(), + touchStartX: touchStartEvent.targetTouches[0].clientX, + beingTouched: true, + intervalId: null + }); + } + + animateSlidingToZero() { + let { left, velocity, beingTouched } = this.state; + + if (!beingTouched) { + left = 0; + velocity = 0; + window.clearInterval(this.state.intervalId); + this.setState({ left, velocity, intervalId: null, originalOffset: 0 }); + } + } + handleClose = () => { + this.setState( + { + open: false + }, + function() { + if (this.props.swipeable) { + this.props.swipeable(); + } + } + ); + }; + + handleNext = () => { + this.state.activeStep === NUMBER_OF_PAGES - 1 + ? this.handleClose() + : this.setState({ + activeStep: this.state.activeStep + 1, + direction: 'left' + }); + }; + + handleMove(touchMoveEvent: touchMoveEventType, mobile: boolean) { + const clientX = touchMoveEvent.targetTouches[0].clientX; + if (this.state.beingTouched && mobile) { + const currTime = Date.now(); + const elapsed = currTime - this.state.timeOfLastDragEvent; + const velocity = (20 * (clientX - this.state.prevTouchX)) / elapsed; + let deltaX = clientX - this.state.touchStartX + this.state.originalOffset; + if (deltaX < -200) { + this.handleNext(); + deltaX = 0; + this.setState({ beingTouched: false }); + } else if (deltaX > 200) { + if (this.state.activeStep > 0) this.handleBack(); + deltaX = 0; + this.setState({ beingTouched: false }); + } + this.setState({ + left: deltaX, + velocity, + timeOfLastDragEvent: currTime, + prevTouchX: clientX + }); + } + } + handleTouchEnd(mobile: boolean) { + if (mobile) { + this.setState({ + velocity: this.state.velocity, + touchStartX: 0, + beingTouched: false, + intervalId: window.setInterval(this.animateSlidingToZero.bind(this), 33) + }); + } + } + + componentDidMount() { + this.updateWindowDimensions(); + window.addEventListener('resize', this.updateWindowDimensions.bind(this)); + } + + updateWindowDimensions() { + this.setState({ width: window.innerWidth, height: window.innerHeight }); + } + + handleClickOpen = () => { + this.setState({ + open: true, + activeStep: 0 + }); + }; + + handleBack = () => { + if (this.state.activeStep !== 0) { + this.setState({ + direction: 'right', + activeStep: this.state.activeStep - 1 + }); + } + }; + getListButton() { + if (this.props.listButton) { + return ( + + + + + Tutorial + + ); + } + } + + render() { + const isTablet: boolean = this.state.width <= LARGER_TABLET_BREAKPOINT; + const { intl } = this.props; + + const dialogContent = [ + { + screenshotUrlM: startM, + screenshotUrl: start, + graceImgUrl: grace1, + message: intl.formatMessage(translations.tutorialStartText) + }, + { + screenshotUrlM: selectLanguage, + screenshotUrl: selectLanguage, + graceImgUrl: grace3, + message: intl.formatMessage(translations.languageTip) + }, + { + screenshotUrlM: menuM, + screenshotUrl: menu, + graceImgUrl: grace2, + message: intl.formatMessage(translations.menuTip) + }, + { + screenshotUrlM: saveOfflineM, + screenshotUrl: saveOffline, + graceImgUrl: grace2, + message: intl.formatMessage(translations.offlineTip) + } + ]; + + return ( + <> + {this.getListButton()} + + +
+ this.handleTouchStart(touchStartEvent, isTablet) + } + onTouchMove={touchMoveEvent => + this.handleMove(touchMoveEvent, isTablet) + } + onTouchEnd={() => { + this.handleTouchEnd(isTablet); + }} + > + {dialogContent.map((content, index) => ( + +
+ +
+
+ ))} + + + {this.state.activeStep === NUMBER_OF_PAGES - 1 + ? 'Finish!' + : 'Next'} + + } + backButton={ + + } + /> + +
+ +
+
+
+ + ); + } +} + +export default injectIntl(WelcomeTutorial); diff --git a/packages/gdl-frontend/lib/storage.js b/packages/gdl-frontend/lib/storage.js index fe3b491ed..e136dc1d2 100644 --- a/packages/gdl-frontend/lib/storage.js +++ b/packages/gdl-frontend/lib/storage.js @@ -20,6 +20,7 @@ const { const BOOK_LANGUAGE_KEY = 'bookLanguage'; const BOOK_CATEGORY_KEY = 'bookCategory'; const SITE_LANGUAGE_KEY = 'siteLanguage'; +const VISITED_BEFORE_KEY = 'visitedBefore'; const oneMonthsInSeconds = 60 * 60 * 24 * 30; // approximately @@ -30,6 +31,11 @@ const ONE_MONTH_OPTIONS = { path: '/' }; +const FIVE_YEARS_OPTION = { + maxAge: 60 * 60 * 24 * 365 * 5, //5 years in seconds ish + path: '/' +}; + /** * Set book language and category */ @@ -50,11 +56,24 @@ export function setBookLanguageAndCategory( } } +export function setVisited(res?: $Response) { + if (res) { + res.cookie(VISITED_BEFORE_KEY, 'true', FIVE_YEARS_OPTION); + } else { + cookies().set(VISITED_BEFORE_KEY, true, FIVE_YEARS_OPTION); + } +} + export function getBookCategory(req?: $Request) { return req ? req.cookies[BOOK_CATEGORY_KEY] : cookies().get(BOOK_CATEGORY_KEY, { doNotParse: true }); } +export function getVisited(req?: $Request) { + return req + ? req.cookies[VISITED_BEFORE_KEY] + : cookies().get(VISITED_BEFORE_KEY, { doNotParse: true }); +} /** * Get the language object from the cookies. Returns fallback language if not found diff --git a/packages/gdl-frontend/locale/en/en.json b/packages/gdl-frontend/locale/en/en.json index e94388938..ea8306d15 100644 --- a/packages/gdl-frontend/locale/en/en.json +++ b/packages/gdl-frontend/locale/en/en.json @@ -1,97 +1,101 @@ { "To": "To:", - "Blog": "Blog", "From": "From", + "Sync": "Sync", "Home": "Home", + "Blog": "Blog", + "from": "from", "More": "More", "Next": "Next", - "Sync": "Sync", - "from": "from", - "About": "About", - "Oh no": "Oh no", "Share": "Share", - "Author": "Author", - "Log in": "Log in", + "Oh no": "Oh no", + "About": "About", "Oh no!": "Oh no!", + "Log in": "Log in", + "Author": "Author", "Search": "Search", - "Authors": "Authors", - "Level 1": "Level 1", - "Level 2": "Level 2", - "Level 3": "Level 3", - "Level 4": "Level 4", - "License": "License", + "menuTip": "The menu is in the top-left corner. From the menu you can access Categories, Favorites and more.", "Log out": "Log out", - "Similar": "Similar", "results": "results for", + "License": "License", + "Level 4": "Level 4", + "Level 3": "Level 3", + "Level 2": "Level 2", + "Level 1": "Level 1", + "Authors": "Authors", + "Similar": "Similar", + "Featured": "Featured", "Copy URL": "Copy URL", "Download": "Download", - "Favorite": "Favorite", - "Featured": "Featured", "Previous": "Previous", "Selected": "Selected:", - "Decodable": "Decodable", - "Favorites": "Favorites", - "GDL Admin": "GDL Admin", - "Open menu": "Open menu", + "Favorite": "Favorite", "Read book": "Read book", "Translate": "Translate", - "Categories": "Categories", - "Close book": "Close book", + "Open menu": "Open menu", + "GDL Admin": "GDL Admin", + "Decodable": "Decodable", + "Favorites": "Favorites", + "offlineTip": "You can save books for later and read them offline by clicking the Save offline icon!", + "Translator": "Translator", "Go to game": "Go to game", - "More books": "More books", "Read aloud": "Read aloud", - "Translator": "Translator", + "Categories": "Categories", + "More books": "More books", + "Close book": "Close book", "Illustrator": "Illustrator", + "languagetip": "If you want to change the language, click on the globe at the top-right corner of the screen!", "Translators": "Translators", "Illustrators": "Illustrators", "New arrivals": "New arrivals", - "Photographer": "Photographer", "Save offline": "Save offline", - "Take me home": "Take me home", "Translate to": "Translate to", - "Book language": "Book language", - "Cookie policy": "Cookie policy", + "Take me home": "Take me home", + "Photographer": "Photographer", "E-book (EPUB)": "E-book (EPUB)", + "Cookie policy": "Cookie policy", "Library books": "Library books", - "Photographers": "Photographers", "Report issues": "Report issues", + "Photographers": "Photographers", + "Book language": "Book language", + "privacy policy": "privacy policy", + "Translate from": "Translate from", + "Translate book": "Translate book", "No books found": "No books found", "Privacy policy": "Privacy policy", - "Translate book": "Translate book", - "Translate from": "Translate from", - "privacy policy": "privacy policy", - "Classroom books": "Classroom books", - "Games (Android)": "Games (Android)", - "My translations": "My translations", "Offline library": "Offline library", - "Select language": "Select language", "Sign in problem": "Oops, there was a problem signing you in.", - "Add to favorites": "Add to favorites", + "Select language": "Select language", + "My translations": "My translations", + "Classroom books": "Classroom books", + "Games (Android)": "Games (Android)", + "Search for books": "Search for books", "Remove all books": "Remove all books", + "Add to favorites": "Add to favorites", "Report a problem": "Report a problem", - "Search for books": "Search for books", + "TutorialStartText": "Hi there! This is a quick tutorial on some of the core functionality of the website. Click next for more information.", "No language found": "No language found", "Start translation": "Start translation", - "Add favorite books": "Add books to your favorites so you can easily find them later.", - "Error loading data": "Error loading data.", "Mobile translation": "Mobile translation", + "Error loading data": "Error loading data.", + "Add favorite books": "Add books to your favorites so you can easily find them later.", "Clear all favorites": "Clear all favorites", "Licensing and reuse": "Licensing and reuse", - "Prepare translation": "Prepare translation", "Sign in to continue": "Sign in to continue", + "Prepare translation": "Prepare translation", "Translate this book": "Translate this book", - "Choose book language": "Choose book language", - "No books offline yet": "No books offline yet", "Printable book (PDF)": "Printable book (PDF)", - "Sign in report error": "The error has been reported. Please feel free to try signing in again.", "Sign in using Google": "Sign in using Google", + "Sign in report error": "The error has been reported. Please feel free to try signing in again.", + "No books offline yet": "No books offline yet", + "Choose book language": "Choose book language", "Translate in context": "Translate in context", "An error has occurred": "An error has occurred. Please try again.", "No results for search": "No results for", "Remove from favorites": "Remove from favorites", - "Additional information": "Additional information", - "Find something to read": "Find something to read", "Sign in using Facebook": "Sign in using Facebook", + "Find something to read": "Find something to read", + "Additional information": "Additional information", "Prepare translation time": "Please wait while we’re preparing the book for translation. This could take some time.", "Prepare translation error": "Something went wrong while preparing the translation. Please try again.", "View your offline library": "View your offline library.", diff --git a/packages/gdl-frontend/pages/index.js b/packages/gdl-frontend/pages/index.js index d3bd1f6d0..ab926b488 100644 --- a/packages/gdl-frontend/pages/index.js +++ b/packages/gdl-frontend/pages/index.js @@ -22,11 +22,14 @@ import type { import { withErrorPage } from '../hocs'; import HomePage, { AMOUNT_OF_ITEMS_PER_LEVEL } from '../components/HomePage'; +import WelcomeTutorial from '../components/WelcomeTutorial/WelcomeTutorial'; import { setBookLanguageAndCategory, getBookLanguageCode, getBookCategory, - getSiteLanguage + getSiteLanguage, + setVisited, + getVisited } from '../lib/storage'; const { @@ -34,7 +37,7 @@ const { }: ConfigShape = getConfig(); type Props = {| - homeTutorialStatus: boolean, + visitedBefore: boolean, category: Category, categories: Array, languageCode: string, @@ -90,6 +93,7 @@ class IndexPage extends React.Component { } const categories = categoriesRes.data.categories; const categoryInCookie = getBookCategory(req); + const visitedBeforeCookie = getVisited(req); let category: string; if (asPath.includes('/classroom')) { @@ -104,6 +108,12 @@ class IndexPage extends React.Component { // Default to Library category = categories.includes('Library') ? 'Library' : categories[0]; } + let visitedBefore: boolean; + if (visitedBeforeCookie === 'true') { + visitedBefore = true; + } else { + visitedBefore = false; + } const homeContentResult: { data: HomeContent @@ -131,7 +141,8 @@ class IndexPage extends React.Component { featuredContent: featuredContent[0], homeContent, // site languge from cookie - siteLanguage + siteLanguage, + visitedBefore }; } catch (error) { /* @@ -165,7 +176,8 @@ class IndexPage extends React.Component { category, featuredContent, categories, - languageCode + languageCode, + visitedBefore } = this.props; let categoryTypeForUrl; @@ -196,6 +208,12 @@ class IndexPage extends React.Component { languageCode={languageCode} featuredContent={featuredContent} /> + + {setVisited()} ); } diff --git a/packages/gdl-frontend/static/img/grace1.png b/packages/gdl-frontend/static/img/grace1.png new file mode 100644 index 000000000..4a0af43b1 Binary files /dev/null and b/packages/gdl-frontend/static/img/grace1.png differ diff --git a/packages/gdl-frontend/static/img/grace2.png b/packages/gdl-frontend/static/img/grace2.png new file mode 100644 index 000000000..5c1b68efd Binary files /dev/null and b/packages/gdl-frontend/static/img/grace2.png differ diff --git a/packages/gdl-frontend/static/img/grace3.png b/packages/gdl-frontend/static/img/grace3.png new file mode 100644 index 000000000..b1293beb9 Binary files /dev/null and b/packages/gdl-frontend/static/img/grace3.png differ diff --git a/packages/gdl-frontend/static/img/menu.png b/packages/gdl-frontend/static/img/menu.png new file mode 100644 index 000000000..6bcd86e94 Binary files /dev/null and b/packages/gdl-frontend/static/img/menu.png differ diff --git a/packages/gdl-frontend/static/img/menuM.png b/packages/gdl-frontend/static/img/menuM.png new file mode 100644 index 000000000..c7df68ba4 Binary files /dev/null and b/packages/gdl-frontend/static/img/menuM.png differ diff --git a/packages/gdl-frontend/static/img/readBook.png b/packages/gdl-frontend/static/img/readBook.png new file mode 100644 index 000000000..c45c85237 Binary files /dev/null and b/packages/gdl-frontend/static/img/readBook.png differ diff --git a/packages/gdl-frontend/static/img/saveOffline.png b/packages/gdl-frontend/static/img/saveOffline.png new file mode 100644 index 000000000..508a1ff2e Binary files /dev/null and b/packages/gdl-frontend/static/img/saveOffline.png differ diff --git a/packages/gdl-frontend/static/img/saveOfflineM.png b/packages/gdl-frontend/static/img/saveOfflineM.png new file mode 100644 index 000000000..6ebcde661 Binary files /dev/null and b/packages/gdl-frontend/static/img/saveOfflineM.png differ diff --git a/packages/gdl-frontend/static/img/selectLanguage.png b/packages/gdl-frontend/static/img/selectLanguage.png new file mode 100644 index 000000000..0f42bbd05 Binary files /dev/null and b/packages/gdl-frontend/static/img/selectLanguage.png differ diff --git a/packages/gdl-frontend/static/img/start.png b/packages/gdl-frontend/static/img/start.png new file mode 100644 index 000000000..56ecaca08 Binary files /dev/null and b/packages/gdl-frontend/static/img/start.png differ diff --git a/packages/gdl-frontend/static/img/startM.png b/packages/gdl-frontend/static/img/startM.png new file mode 100644 index 000000000..a97009fcc Binary files /dev/null and b/packages/gdl-frontend/static/img/startM.png differ diff --git a/packages/gdl-frontend/static/img/startarrow.png b/packages/gdl-frontend/static/img/startarrow.png new file mode 100644 index 000000000..64bfb4a89 Binary files /dev/null and b/packages/gdl-frontend/static/img/startarrow.png differ diff --git a/packages/gdl-frontend/static/img/startempty.png b/packages/gdl-frontend/static/img/startempty.png new file mode 100644 index 000000000..5d8e6e088 Binary files /dev/null and b/packages/gdl-frontend/static/img/startempty.png differ