From 34b4e1172458bc6f319b4f3f507f14517956a076 Mon Sep 17 00:00:00 2001 From: David Duong Date: Fri, 3 Jan 2020 22:08:11 +0100 Subject: [PATCH] Polish before initial release --- src/BusBar/BusBar.js | 4 +- src/NTUMap/NTUMap.js | 26 ++- src/PlaceSheet/PlaceSheet.js | 375 +++++++++++++++++++++++------------ src/SearchBar/SearchBar.js | 248 +++++++++++++---------- 4 files changed, 409 insertions(+), 244 deletions(-) diff --git a/src/BusBar/BusBar.js b/src/BusBar/BusBar.js index c8310a9..0786352 100644 --- a/src/BusBar/BusBar.js +++ b/src/BusBar/BusBar.js @@ -71,12 +71,12 @@ export default ({ setRoute, setProgress, progress, route }) => { onPress={() => setRoute(route && route.value === line.value ? null : line)} /> ))} - +{/* setProgress(!progress)} - /> + /> */} ) diff --git a/src/NTUMap/NTUMap.js b/src/NTUMap/NTUMap.js index 5c50d96..8f81746 100644 --- a/src/NTUMap/NTUMap.js +++ b/src/NTUMap/NTUMap.js @@ -32,6 +32,7 @@ export default ({ location, route, progress }) => { const [busIcons, setBusIcons] = useState({}) const shotRef = useRef() + const cameraRef = useRef() useLayoutEffect(() => { (async () => { @@ -43,6 +44,13 @@ export default ({ location, route, progress }) => { })() }, []) + useLayoutEffect(() => { + if (location && cameraRef.current) { + const { lat, lng } = location + cameraRef.current.flyTo([lng, lat], 1000) + } + }, [location]) + return ( @@ -50,10 +58,11 @@ export default ({ location, route, progress }) => { {mapReady ? ( + minZoomLevel={1}> { centerCoordinate: [103.68450164794922, 1.3484472784360202], zoomLevel: 14.5, }} + maxBounds={{ + ne: [103.6820359, 1.1304753], + sw: [104.0120359, 1.4504753], + }} + ref={cameraRef} /> - + - + ) : ( @@ -77,5 +91,5 @@ export default ({ location, route, progress }) => { )} - ) + ); } diff --git a/src/PlaceSheet/PlaceSheet.js b/src/PlaceSheet/PlaceSheet.js index 7f36cb9..458847f 100644 --- a/src/PlaceSheet/PlaceSheet.js +++ b/src/PlaceSheet/PlaceSheet.js @@ -1,24 +1,40 @@ -import React, { useLayoutEffect, useState, useCallback, useRef, Fragment } from 'react' -import cheerio from 'react-native-cheerio' -import BottomSheet from 'reanimated-bottom-sheet' -import BusBar, { BAR_HEIGHT } from '../BusBar/BusBar' +import React, { + useLayoutEffect, + useState, + useCallback, + useRef, + Fragment, +} from 'react'; +import cheerio from 'react-native-cheerio'; +import BottomSheet from 'reanimated-bottom-sheet'; +import BusBar, {BAR_HEIGHT} from '../BusBar/BusBar'; -import { ActivityIndicator, StyleSheet, View, Text, Dimensions, Linking } from 'react-native' -import { TouchableNativeFeedback } from 'react-native-gesture-handler' -import Animated from 'react-native-reanimated' -import Icon from 'react-native-vector-icons/MaterialIcons' -import HTML from 'react-native-render-html' +import { + ActivityIndicator, + StyleSheet, + View, + Text, + Dimensions, + Linking, +} from 'react-native'; +import {TouchableNativeFeedback} from 'react-native-gesture-handler'; +import Animated from 'react-native-reanimated'; +import Icon from 'react-native-vector-icons/MaterialIcons'; +import HTML from 'react-native-render-html'; -export const SHEET_HEIGHT = 175.2 +export const SHEET_HEIGHT = 175.2; const styles = StyleSheet.create({ sheet: { position: 'absolute', - top: 0, left: 0, right: 0, bottom: 0, + top: 0, + left: 0, + right: 0, + bottom: 0, zIndex: 999, }, loading: { - display: "flex", + display: 'flex', flexDirection: 'row', marginTop: 5, }, @@ -38,7 +54,7 @@ const styles = StyleSheet.create({ html: { paddingTop: 0, paddingBottom: 20, - maxHeight: Dimensions.get("window").height / 2, + maxHeight: Dimensions.get('window').height / 2, }, header: { backgroundColor: '#fff', @@ -54,30 +70,31 @@ const styles = StyleSheet.create({ paddingRight: 20, }, buttonPress: { - elevation: 9, + elevation: 2, marginRight: 10, + overflow: 'hidden', + borderRadius: 21, + backgroundColor: '#D71440', }, buttonText: { color: '#fff', }, button: { - display: "flex", - flexDirection: "row", - alignItems: "center", - justifyContent: "flex-start", + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', flexShrink: 0, height: 42, - borderRadius: 21, + paddingLeft: 12, paddingRight: 12, - backgroundColor: '#D71440', - }, actions: { - display: "flex", - flexDirection: "row", - alignItems: "center", - justifyContent: "flex-start", + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', height: 42, marginTop: 10, }, @@ -85,90 +102,146 @@ const styles = StyleSheet.create({ display: 'flex', flexDirection: 'row', alignItems: 'center', - justifyContent: 'flex-start' - } -}) + justifyContent: 'flex-start', + }, +}); + +const requestData = async ({name, lat, lng}, callback) => { + const url = `http://maps.ntu.edu.sg/a/search?q=${encodeURIComponent( + name, + )}&ll=${encodeURIComponent([lat, lng].join(','))}`; + const data = await fetch(url).then(a => a.json()); + const html = data && data.where && data.where.html; -const requestData = async ({ name, lat, lng }, callback) => { - const url = `http://maps.ntu.edu.sg/a/search?q=${encodeURIComponent(name)}&ll=${encodeURIComponent([lat, lng].join(','))}` - const data = await (fetch(url).then(a => a.json())) - const html = data && data.where && data.where.html - - let title = null - let rest = null - let floorplan = null + let title = null; + let rest = null; + let code = null; + let floorplan = null; if (data.what && data.what.businesses && data.what.businesses.length === 1) { - const business = data.what.businesses[0] - - title = business.name - rest = [...((business.location && business.location.formatted_address) || '').split('|'), business.unit_number].filter(Boolean).map(t => t.trim()).join('\n') + const business = data.what.businesses[0]; + code = business.unit_number - floorplan = (business.more_info && business.more_info.floorplan) || floorplan + title = business.name; + rest = [ + ...( + (business.location && business.location.formatted_address) || + '' + ).split('|'), + business.unit_number, + ] + .filter(Boolean) + .map(t => t.trim()) + .join('\n'); + + floorplan = + (business.more_info && business.more_info.floorplan) || floorplan; } else { - const $ = cheerio.load(html) - title = $(".locf a").text().trim() - $(".locf").find('br').replaceWith('\n') - $(".locf").find('strong').remove() - rest = $(".locf").text().trim().split('\n').map(i => i.trim()).filter(Boolean).join('\n') + const $ = cheerio.load(html); + title = $('.locf a') + .text() + .trim(); + $('.locf') + .find('br') + .replaceWith('\n'); + $('.locf') + .find('strong') + .remove(); + rest = $('.locf') + .text() + .trim() + .split('\n') + .map(i => i.trim()) + .filter(Boolean) + .join('\n'); } - callback({ name, html, title, rest, floorplan }) -} + callback({name, html, title, rest, code, floorplan}); +}; + +const filteredClasses = ['bar', 'place']; + +const CodeExplanation = ({ code }) => { -const filteredClasses = ['bar', 'place'] + if (!code) return null + const [block, floor] = code.split('-') + + return ( + + {code} + = + + Building: {block} + Floor: {floor} + + + ); +} -export default ({ location, onClose, setRoute, setProgress, progress, route }) => { - const [loading, setLoading] = useState(false) - const [details, setDetails] = useState(null) - const [headerHeight, setHeaderHeight] = useState(SHEET_HEIGHT) - const [callbackNode] = useState(new Animated.Value(1)) +export default ({ + location, + onClose, + setRoute, + setProgress, + progress, + route, +}) => { + const [loading, setLoading] = useState(false); + const [details, setDetails] = useState(null); + const [headerHeight, setHeaderHeight] = useState(SHEET_HEIGHT); + const [callbackNode] = useState(new Animated.Value(1)); - const sheetRef = useRef() + const sheetRef = useRef(); const opacityNode = Animated.interpolate(callbackNode, { inputRange: [0, 1], outputRange: [BAR_HEIGHT, 0], - }) + }); const radiusNode = Animated.interpolate(callbackNode, { inputRange: [0, 1], - outputRange: [0, 10] - }) + outputRange: [0, 10], + }); // https://github.com/osdnk/react-native-reanimated-bottom-sheet/issues/51 useLayoutEffect(() => { - sheetRef.current && sheetRef.current.snapTo(0) - }, []) + sheetRef.current && sheetRef.current.snapTo(0); + }, []); useLayoutEffect(() => { if (location) { - sheetRef.current && sheetRef.current.snapTo(0) - setDetails(null) - setLoading(true) - - requestData(location, (details) => { - setLoading(false) - setDetails(details) - }) + sheetRef.current && sheetRef.current.snapTo(0); + setDetails(null); + setLoading(true); + + requestData(location, details => { + setLoading(false); + setDetails(details); + }); } - }, [location]) + }, [location]); const handleClose = useCallback(() => { - setLoading(false) - onClose() + setLoading(false); + onClose(); - sheetRef.current && sheetRef.current.snapTo(0) - }) - - const handleLayout = useCallback((e) => { - if (headerHeight !== e.nativeEvent.layout.height) { - setHeaderHeight(e.nativeEvent.layout.height) - sheetRef.current && sheetRef.current.snapTo(0) - } - }, [headerHeight]) + sheetRef.current && sheetRef.current.snapTo(0); + }); - const { name, lat, lng } = location || {} - const snapPoints = [BAR_HEIGHT + ((location || loading) ? headerHeight : 0), Dimensions.get("window").height + BAR_HEIGHT - 22] + const handleLayout = useCallback( + e => { + if (headerHeight !== e.nativeEvent.layout.height) { + setHeaderHeight(e.nativeEvent.layout.height); + sheetRef.current && sheetRef.current.snapTo(0); + } + }, + [headerHeight], + ); + + const {name, lat, lng} = location || {}; + const snapPoints = [ + BAR_HEIGHT + (location || loading ? headerHeight : 0), + Dimensions.get('window').height + BAR_HEIGHT - 22, + ]; return ( @@ -182,7 +255,7 @@ export default ({ location, onClose, setRoute, setProgress, progress, route }) = renderHeader={() => { return ( - + - + onLayout={handleLayout}> + {(details && details.title) || name} @@ -210,20 +280,15 @@ export default ({ location, onClose, setRoute, setProgress, progress, route }) = style={{ overflow: 'hidden', borderRadius: 100, - opacity: location ? 1 : 0 - }} - > + opacity: location ? 1 : 0, + }}> + }}> - + @@ -238,90 +303,136 @@ export default ({ location, onClose, setRoute, setProgress, progress, route }) = {details.rest} - {!!details.floorplan && ( - Linking.openURL(`http://maps.ntu.edu.sg/static/floorplans/${encodeURIComponent(details.floorplan)}.gif`)}> + + Linking.openURL( + `http://maps.ntu.edu.sg/static/floorplans/${encodeURIComponent( + details.floorplan, + )}.gif`, + ) + }> - Show Floorplan + + Show Floorplan + )} - Linking.openURL(`https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent([lat,lng].join(','))}&travelmode=walking`)}> + + Linking.openURL( + `https://www.google.com/maps/dir/?api=1&destination=${encodeURIComponent( + [lat, lng].join(','), + )}&travelmode=walking`, + ) + }> - Open in Google Maps + + Navigate with Google Maps + )} - - ) + ); }} renderContent={() => { return ( {!!details && ( + { - const href = attr.targetHref || '/' - if (href !== '/') return Linking.openURL(href) - if (attr.class && attr.class.includes("fp") && attr.uid) { - Linking.openURL(`http://maps.ntu.edu.sg/static/floorplans/${encodeURIComponent(attr.uid)}.gif`) + onLinkPress={(e, base, attr) => { + const href = attr.targetHref || '/'; + console.log('link press', href); + if (href !== '/') return Linking.openURL(href); + if (attr.class && attr.class.includes('fp') && attr.uid) { + Linking.openURL( + `http://maps.ntu.edu.sg/static/floorplans/${encodeURIComponent( + attr.uid, + )}.gif`, + ); } }} tagsStyles={{ - h4: { marginTop: 10, marginBottom: 10 }, - h5: { marginTop: 5, marginBottom: 5 }, - h6: { margin: 0 }, - h7: { margin: 0 }, - p: { margin: 0 }, + h4: {marginTop: 10, marginBottom: 10}, + h5: {marginTop: 5, marginBottom: 5}, + h6: {margin: 0}, + h7: {margin: 0}, + p: {margin: 0}, + a: { + padding: 5, + display: 'flex', + textDecorationLine: 'none', + color: "black", + }, }} classesStyles={{ - 'amenitiesheader': { + amenitiesheader: { display: 'flex', - flexDirection: 'row' + flexDirection: 'row', }, - 'amenitieslist': { + amenitieslist: { marginLeft: 30, marginBottom: 20, }, - 'cat': { + cat: { marginLeft: 10, marginBottom: 5, }, }} - ignoreNodesFunction={(node) => { - if (node.name === 'div' && node.attribs && node.attribs.class && node.attribs.class.split(" ").find(i => filteredClasses.includes(i))) return true - return false + ignoreNodesFunction={node => { + if ( + node.name === 'div' && + node.attribs && + node.attribs.class && + node.attribs.class + .split(' ') + .find(i => filteredClasses.includes(i)) + ) + return true; + return false; }} - alterNode={(node) => { + alterNode={node => { if (node.name === 'img') { - node.attribs = { ...(node.attribs || {}), src: `http://maps.ntu.edu.sg/${node.attribs.src}` } - return node + node.attribs = { + ...(node.attribs || {}), + src: `http://maps.ntu.edu.sg/${node.attribs.src}`, + }; + return node; } - + if (node.name === 'a') { - if (node.attribs.cat) node.name = 'div' + if (node.attribs.cat) { + node.name = 'div'; - node.attribs = { ...(node.attribs || {}), href: '/', targetHref: node.attribs.href, 'class': `cat ${node.attribs.class || ''}` } - return node + node.attribs = { + ...(node.attribs || {}), + href: '/', + targetHref: node.attribs.href, + class: `cat ${node.attribs.class || ''}`, + }; + } + return node; } }} /> )} - ) + ); }} /> - ) -} \ No newline at end of file + ); +}; diff --git a/src/SearchBar/SearchBar.js b/src/SearchBar/SearchBar.js index 2dd2b38..c808099 100644 --- a/src/SearchBar/SearchBar.js +++ b/src/SearchBar/SearchBar.js @@ -1,26 +1,54 @@ -import React, { useState, useCallback, useRef } from 'react' -import { View, StyleSheet, Text, TextInput, TouchableNativeFeedback, FlatList, ActivityIndicator } from 'react-native' +import React, {useState, useCallback, useRef} from 'react'; +import { + View, + StyleSheet, + Text, + TextInput, + FlatList, + ActivityIndicator, +} from 'react-native'; +import { TouchableNativeFeedback } from 'react-native-gesture-handler'; -import Icon from 'react-native-vector-icons/MaterialIcons' +import Icon from 'react-native-vector-icons/MaterialIcons'; const styles = StyleSheet.create({ container: { - display: "flex", - flexDirection: "column", + display: 'flex', + flexDirection: 'column', position: 'absolute', - top: 20, + top: 0, bottom: 180, - left: 20, - right: 20, + left: 0, + right: 0, zIndex: 0, }, + activeContainer: { + elevation: 5, + backgroundColor: '#fff', + borderBottomLeftRadius: 10, + borderBottomRightRadius: 10, + overflow: "hidden", + }, searchContainer: { - borderRadius: 8, - elevation: 9, + borderRadius: 30, + elevation: 5, backgroundColor: '#fff', - display: "flex", + display: 'flex', flexDirection: 'row', - marginBottom: 10, + margin: 20, + marginBottom: 9, + }, + searchList: { + flexGrow: 1, + position: 'relative', + overflow: 'hidden', + }, + searchListInner: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, }, text: { margin: 0, @@ -30,89 +58,96 @@ const styles = StyleSheet.create({ icon: { padding: 12, }, - list: { - borderRadius: 8, - overflow: "hidden", - elevation: 9, - backgroundColor: "#fff", - }, item: { - backgroundColor: '#fff', + // backgroundColor: '#fff', + borderBottomWidth: 1, + borderBottomColor: 'rgba(0,0,0,0.08)', padding: 12, - } -}) + paddingStart: 20, + paddingEnd: 20, + }, +}); -const isQueryValid = text => Boolean(text && text.trim()) +const isQueryValid = text => Boolean(text && text.trim()); const requestData = async (text, callback) => { - if (!isQueryValid(text)) return callback({ text, result: [] }) - const data = await (fetch(`http://maps.ntu.edu.sg/a/search?q=${encodeURIComponent(text)}`).then(a => a.json())) - let result = [] + if (!isQueryValid(text)) return callback({text, result: []}); + const data = await fetch( + `http://maps.ntu.edu.sg/a/search?q=${encodeURIComponent(text)}`, + ).then(a => a.json()); + let result = []; if (data && data.what) { - const business = (data.what.businesses || []).map(i => [i.name, i.location.geometry.location]) - const markers = (data.what.markers || []).map(i => [i.tooltip, i.latlng]) + const business = (data.what.businesses || []).map(i => [ + i.name, + i.location.geometry.location, + ]); + const markers = (data.what.markers || []).map(i => [i.tooltip, i.latlng]); - result = result - .concat(business) - .concat(markers) + result = result.concat(business).concat(markers); } + + result.sort(([a], [b]) => a.localeCompare(b)); + callback({text, result}); +}; + +export default ({onLocationSelect}) => { + const [query, setQuery] = useState(null); + const queryRef = useRef(query); + const inputRef = useRef(null); + + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + + const resultReceiver = useCallback( + ({text, result}) => { + if (text === queryRef.current) { + setResults(result); + setLoading(false); + } + }, + [queryRef], + ); + + const handleText = useCallback( + text => { + setQuery(text); + + // need to get a reference to text, not sure if there is a better way, bodged this one + queryRef.current = text; + + setLoading(isQueryValid(text)); + requestData(text, resultReceiver); + }, + [resultReceiver], + ); + + const handleRenderItem = useCallback( + ({item: [name, [lat, lng]], index}) => { + return ( + { + handleText(null); + inputRef.current && inputRef.current.blur(); + onLocationSelect({name, lat, lng}); + }}> + + {name} + + + ); + }, + [handleText, onLocationSelect], + ); - result.sort(([a], [b]) => a.localeCompare(b)) - callback({ text, result }) -} - -export default ({ onLocationSelect }) => { - const [query, setQuery] = useState(null) - const queryRef = useRef(query) - const inputRef = useRef(null) - - const [results, setResults] = useState([]) - const [loading, setLoading] = useState(false) - - const resultReceiver = useCallback(({ text, result }) => { - if (text === queryRef.current) { - setResults(result) - setLoading(false) - } - }, [queryRef]) - - const handleText = useCallback((text) => { - setQuery(text) - - // need to get a reference to text, not sure if there is a better way, bodged this one - queryRef.current = text + const hasText = !!(query && query.trim()) + const activeList = (results || []).length > 0 - setLoading(isQueryValid(text)) - requestData(text, resultReceiver) - }, [resultReceiver]) - - const handleRenderItem = useCallback(({ item: [name, [lat, lng]], index }) => { - return ( - { - handleText(null) - inputRef.current && inputRef.current.blur() - onLocationSelect({ name, lat, lng }) - }}> - - {name} - - - ) - }, [handleText, onLocationSelect]) - return ( - - + + - + { onChangeText={handleText} style={styles.text} ref={inputRef} - /> + /> {loading && ( - + + )} + {hasText && ( + + handleText(null)}> + + + )} - handleText(null)} - > - - - {(results || []).length >= 0 && ( - - [name, ...loc].join()} - data={results || []} - renderItem={handleRenderItem} - /> + {activeList && ( + + + [name, ...loc].join()} + data={results || []} + renderItem={handleRenderItem} + /> + )} - ) -} - + ); +};