import * as React from 'react' ; import { timezone } from '@frogpond/constants' ; import { NoticeView , LoadingView } from '@frogpond/notice' ; import type { TopLevelViewPropsType } from '../types' ; import { FoodMenu } from '@frogpond/food-menu' ; import type { EditedBonAppMenuInfoType as MenuInfoType , EditedBonAppCafeInfoType as CafeInfoType , StationMenuType , ProcessedMealType , DayPartMenuType , MenuItemContainerType , MenuItemType , } from './types' ; import sample from 'lodash/sample' ; import mapValues from 'lodash/mapValues' ; import reduce from 'lodash/reduce' ; import toPairs from 'lodash/toPairs' ; import moment from 'moment-timezone' ; import type { Moment } from 'moment-timezone' ; import { trimStationName , trimItemLabel } from './lib/trim-names' ; import { innerTextWithSpaces , parseHtml , decode } from '@frogpond/html-lib' ; import { toLaxTitleCase } from '@frogpond/titlecase' ; import { API } from '@frogpond/api' ; import { fetch } from '@frogpond/fetch' ; const BONAPP_HTML_ERROR_CODE = 'bonapp-html' ; const DEFAULT_MENU = [ { label : 'Menu' , starttime : '0:00' , endtime : '23:59' , id : 'na' , abbreviation : 'M' , stations : [ ] , } , ] ; type Props = TopLevelViewPropsType & { cafe : string | { id : string ; } ; ignoreProvidedMenus ? : boolean ; loadingMessage : string [ ] ; name : string ; } ; type State = { cachedCafe : string | { id : string ; } ; errormsg ? : string ; loading : boolean ; refreshing : boolean ; now : Moment ; cafeInfo ? : CafeInfoType ; cafeMenu ? : MenuInfoType ; } ; export class BonAppHostedMenu extends React . PureComponent < Props , State > { state = { cachedCafe : this . props . cafe , errormsg : null , loading : true , refreshing : false , now : moment . tz ( timezone ( ) ) , cafeMenu : null , cafeInfo : null , } ; componentDidMount ( ) { this . fetchData ( this . props ) . then ( ( ) => { this . setState ( ( ) => ( { loading : false } ) ) ; } ) ; } componentDidUpdate ( ) { let { cachedCafe } = this . state ; let { cafe : newCafe } = this . props ; cachedCafe = typeof cachedCafe === 'string' ? cachedCafe : cachedCafe . id ; newCafe = typeof newCafe === 'string' ? newCafe : newCafe . id ; if ( cachedCafe !== newCafe ) { this . fetchData ( this . props ) ; } } retry = ( ) => { this . fetchData ( this . props ) . then ( ( ) => { this . setState ( ( ) => ( { loading : false , errormsg : '' } ) ) ; } ) ; } ; fetchData = async ( props , reload ? ) => { let menuUrl ; let cafeUrl ; let cafe = typeof props . cafe === 'string' ? props . cafe : props . cafe . id ; if ( typeof props . cafe === 'string' ) { menuUrl = API ( `/food/named/menu/${ cafe }` ) ; cafeUrl = API ( `/food/named/cafe/${ cafe }` ) ; } else if ( 'id' in props . cafe ) { menuUrl = API ( `/food/menu/${ cafe }` ) ; cafeUrl = API ( `/food/cafe/${ cafe }` ) ; } else { throw new Error ( 'invalid cafe passed to BonappMenu!' ) ; } try { let cafeMenuPromise = fetch ( menuUrl , { delay : reload ? 500 : 0 , } ) . json ( ) ; let cafeInfoPromise = fetch ( cafeUrl , { delay : reload ? 500 : 0 , } ) . json ( ) ; let [ cafeMenu , cafeInfo ] = await Promise . all ( [ cafeMenuPromise , cafeInfoPromise , ] ) ; this . setState ( ( ) => ( { cafeMenu : cafeMenu ? cafeMenu : null , cafeInfo : cafeInfo ? cafeInfo : null , now : moment . tz ( timezone ( ) ) , } ) ) ; } catch ( error ) { if ( error . message === "JSON Parse error: Unrecognized token '<'" ) { this . setState ( ( ) => ( { errormsg : BONAPP_HTML_ERROR_CODE } ) ) ; } else { this . setState ( ( ) => ( { errormsg : error . message } ) ) ; } } } ; refresh = async ( ) : Promise < void > => { this . setState ( ( ) => ( { refreshing : true } ) ) ; await this . fetchData ( this . props , true ) ; this . setState ( ( ) => ( { refreshing : false } ) ) ; } ; findCafeMessage ( cafeInfo , now ) { let actualCafeInfo = cafeInfo . cafe ; let todayDate = now . format ( 'YYYY-MM-DD' ) ; let todayMenu = actualCafeInfo . days . find ( ( { date } ) => date === todayDate ) ; if ( ! todayMenu ) { return 'Closed today' ; } else if ( todayMenu . status === 'closed' ) { return todayMenu . message || 'Closed today' ; } return null ; } buildCustomStationMenu ( foodItems ) { let groupByStation = ( grouped , item ) => { if ( item . station in grouped ) { grouped [ item . station ] . push ( item . id ) ; } else { grouped [ item . station ] = [ item . id ] ; } return grouped ; } ; let idsGroupedByStation = reduce ( foodItems , groupByStation , { } ) ; let paired = toPairs ( idsGroupedByStation ) ; return paired . map ( ( [ name , items ] , i ) : StationMenuType => ( { order_id : String ( i ) , id : String ( i ) , label : name , price : '' , note : '' , soup : false , items : items , } ) ) ; } prepareSingleMenu ( mealInfo , foodItems , ignoreProvidedMenus ) { let stationMenus = mealInfo ? mealInfo . stations : [ ] ; if ( ignoreProvidedMenus ) { stationMenus = this . buildCustomStationMenu ( foodItems ) ; } let titleCaseLabels = ( s ) => ( { ... s , label : toLaxTitleCase ( s . label ) } ) ; stationMenus = stationMenus . map ( titleCaseLabels ) ; return { stations : stationMenus , label : mealInfo . label || '' , starttime : mealInfo . starttime || '0:00' , endtime : mealInfo . endtime || '23:59' , } ; } getMeals ( args ) { let { cafeMenu , ignoreProvidedMenus , foodItems } = args ; let dayparts = cafeMenu . days [ 0 ] . cafe . dayparts ; let mealInfoItems = dayparts . length !== 0 && dayparts [ 0 ] . length ? dayparts [ 0 ] : DEFAULT_MENU ; let ignoreMenus = dayparts . length !== 0 && dayparts [ 0 ] . length ? ignoreProvidedMenus : true ; return mealInfoItems . map ( ( mealInfo ) => this . prepareSingleMenu ( mealInfo , foodItems , ignoreMenus ) ) ; } prepareFood ( cafeMenu ) { return mapValues ( cafeMenu . items , ( item ) => ( { ... item , station : decode ( toLaxTitleCase ( trimStationName ( item . station ) ) ) , label : decode ( trimItemLabel ( item . label ) ) , description : innerTextWithSpaces ( parseHtml ( item . description || '' ) ) , } ) ) ; } render ( ) { if ( this . state . loading ) { return < LoadingView text = { sample ( this . props . loadingMessage ) } />; } if ( this . state . errormsg ) { let msg = `Error: ${ this . state . errormsg }` ; if ( this . state . errormsg === BONAPP_HTML_ERROR_CODE ) { msg = 'Something between you and BonApp is having problems. Try again in a minute or two?' ; } return < NoticeView buttonText = "Again!" onPress = { this . retry } text = { msg } / > ; } let cafe = typeof this . props . cafe === 'string' ? this . props . cafe : this . props . cafe . id ; if ( ! this . state . cafeMenu || ! this . state . cafeInfo ) { let msg = 'Something went wrong. Email allaboutolaf@frogpond.tech to let them know?' ; return < NoticeView text = { msg } />; } let { ignoreProvidedMenus = false } = this . props ; let { now , cafeMenu , cafeInfo } = this . state ; if ( Array . isArray ( cafeInfo . cafe ) ) { let msg = `There is no cafe with id #${ cafe }` ; return < NoticeView text = { msg } />; } let specialMessage = this . findCafeMessage ( cafeInfo , now ) ; let foodItems = this . prepareFood ( cafeMenu ) ; let meals = this . getMeals ( { foodItems , ignoreProvidedMenus , cafeMenu , } ) ; return ( < FoodMenu cafeMessage = { specialMessage } foodItems = { foodItems } meals = { meals } menuCorIcons = { cafeMenu . cor_icons } name = { this . props . name } navigation = { this . props . navigation } now = { now } onRefresh = { this . refresh } refreshing = { this . state . refreshing } / > ) ; } }