This commit is contained in:
louiscklaw
2025-06-05 11:29:42 +08:00
parent 8c46a93e61
commit d909805283
207 changed files with 10412 additions and 46 deletions

View File

@@ -0,0 +1,192 @@
import { IonContent, IonFab, IonFabButton, IonHeader, IonIcon, IonModal, IonPage, IonSearchbar, IonToolbar, isPlatform, useIonViewWillEnter } from '@ionic/react';
import { useEffect, useState } from 'react';
import { getRecords } from '../main/yelp';
import styles from "../styles/Map.module.scss";
import { Map, Marker, Overlay } from "pigeon-maps";
import { maptiler } from 'pigeon-maps/providers';
import { MapOverlay } from "../components/MapOverlay";
import { CurrentPointOverlay } from "../components/CurrentPointOverlay";
import { flashOffOutline, flashOutline, list } from 'ionicons/icons';
import RecordsStore from '../store/RecordsStore';
import { fetchRecords } from '../store/Selectors';
import { getLocation } from '../main/utils';
import { ListModal } from '../components/ListModal';
const maptilerProvider = maptiler('d5JQJPLLuap8TkJJlTdJ', 'streets');
const Tab1 = () => {
const web = isPlatform("web" || "pwa" || "mobileweb" || "");
// UNCOMMENT THESE TO USE CURRENT LOCATION.
// const [ currentPoint, setCurrentPoint ] = useState(false);
// useEffect(() => {
// const getCurrentLocation = async () => {
// const fetchedLocation = await getLocation();
// setCurrentPoint(fetchedLocation.currentLocation);
// }
// getCurrentLocation();
// }, []);
useIonViewWillEnter(() => {
getRecords(currentPoint);
});
const [ currentPoint, setCurrentPoint ] = useState({ latitude: 40.8264691, longitude: -73.9549618 });
const [ showCurrentPointInfo, setShowCurrentPointInfo ] = useState(false);
const records = RecordsStore.useState(fetchRecords);
const center = RecordsStore.useState(s => s.center);
const [ results, setResults ] = useState(false);
const [ zoom, setZoom ] = useState(14);
const [ searchTerm, setSearchTerm ] = useState("");
const [ moveMode, setMoveMode ] = useState(false);
const [ showListModal, setShowListModal ] = useState(false);
useEffect(() => {
const getData = async () => {
await getRecords(currentPoint);
}
getData();
}, [ currentPoint ]);
useEffect(() => {
setResults([...records]);
}, [ records ]);
useEffect(() => {
const search = searchTerm.toLowerCase();
var searchResults = [];
if (searchTerm !== "") {
records.forEach(record => {
if (record.name.toLowerCase().includes(search)) {
searchResults.push(record);
}
});
setResults(searchResults);
} else {
setResults([...records]);
}
}, [ searchTerm ]);
const showMarkerInfo = (e, index) => {
const tempRecords = JSON.parse(JSON.stringify(results));
// Hide all current marker infos
setShowCurrentPointInfo(false);
!tempRecords[index].showInfo && tempRecords.forEach(tempRecord => tempRecord.showInfo = false);
tempRecords[index].showInfo = !tempRecords[index].showInfo;
setResults(tempRecords);
}
const hideMarkers = () => {
const tempRecords = JSON.parse(JSON.stringify(results));
tempRecords.forEach(tempRecord => tempRecord.showInfo = false);
setResults(tempRecords);
setShowCurrentPointInfo(false);
}
const handleMapClick = e => {
const clickedPoint = e.latLng;
setCurrentPoint({ latitude: clickedPoint[0], longitude: clickedPoint[1] });
setMoveMode(false);
}
const handleShowCurrentPointInfo = () => {
hideMarkers();
setShowCurrentPointInfo(!showCurrentPointInfo);
}
return (
<IonPage>
<IonContent fullscreen>
{ (center && center.latitude && center.longitude) && results &&
<>
<div className={ styles.overlaySearch } style={{ marginTop: web ? "0.5rem" : "3.5rem" }}>
<IonSearchbar placeholder="Search plotted points" animated={ true } value={ searchTerm } onIonChange={ e => setSearchTerm(e.target.value) } />
</div>
<Map onClick={ e => moveMode ? handleMapClick(e) : hideMarkers(e) } defaultCenter={ [center.latitude, center.longitude] } defaultZoom={ zoom } provider={ maptilerProvider } touchEvents={ true }>
<Marker onClick={ handleShowCurrentPointInfo } color="red" width={ 50 } anchor={ currentPoint ? [ currentPoint.latitude, currentPoint.longitude] : [ center.latitude, center.longitude] } />
{ results.map((record, index) => {
return <Marker onClick={ e => showMarkerInfo(e, index) } key={ index } color="#3578e5" width={ 50 } anchor={ [ record.latitude, record.longitude ] } />
})}
{ results.map((record, index) => {
if (record.showInfo) {
return (
<Overlay key={ index } anchor={ [ record.latitude, record.longitude ] } offset={[95, 304]}>
<MapOverlay record={ record } />
</Overlay>
);
}
})}
{ showCurrentPointInfo &&
<Overlay anchor={ [ currentPoint.latitude, currentPoint.longitude ] } offset={[95, 153]}>
<CurrentPointOverlay />
</Overlay>
}
</Map>
<IonFab vertical="bottom" horizontal="end" slot="fixed" onClick={ () => setMoveMode(!moveMode) }>
<IonFabButton>
<IonIcon icon={ moveMode ? flashOffOutline : flashOutline } />
</IonFabButton>
</IonFab>
<IonFab vertical="bottom" horizontal="start" slot="fixed" onClick={ () => setShowListModal(!showListModal) }>
<IonFabButton>
<IonIcon icon={ list } />
</IonFabButton>
</IonFab>
<IonModal isOpen={ showListModal } onDidDismiss={ () => setShowListModal(false) } swipeToClose={ true } initialBreakpoint={ 0.6 } breakpoints={ [0, 0.6, 1] } backdropBreakpoint={ 0.6 }>
<ListModal hideModal={ () => setShowListModal(false) } searchTerm={ searchTerm } search={ setSearchTerm } records={ results } />
</IonModal>
</>
}
</IonContent>
</IonPage>
);
};
export default Tab1;

View File

@@ -0,0 +1,66 @@
import { IonButton, IonCard, IonCardHeader, IonCardSubtitle, IonContent, IonHeader, IonIcon, IonNote, IonPage, IonRow, IonText, IonTitle, IonToolbar } from '@ionic/react';
import { arrowForward, navigateOutline } from 'ionicons/icons';
import { RatingStar } from '../components/RatingStar';
import RecordsStore from '../store/RecordsStore';
import { fetchRecords } from '../store/Selectors';
import styles from "../styles/ViewAll.module.scss";
const Tab2 = () => {
const records = RecordsStore.useState(fetchRecords);
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>All places in your location</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Feeling hungry?</IonTitle>
</IonToolbar>
</IonHeader>
{ records.map((record, index) => {
const imageURL = record.imageURL ? record.imageURL : "/placeholder.jpeg";
const rating = Math.floor(record.rating).toFixed(0);
return (
<IonCard key={ `record_${ index }` } className={ `${ styles.viewCard } animate__animated animate__faster animate__fadeIn` } routerLink={ `/list/${ record.id }` }>
<div className={ styles.cardImage } style={{ backgroundImage: `url(${ imageURL })` }} />
<IonCardHeader>
{ Array.apply(null, { length: 5 }).map((e, i) => (
<RatingStar key={ i } rated={ rating > i } />
))}
<IonCardSubtitle>{ record.name }</IonCardSubtitle>
<IonNote color="medium">{ record.displayAddress }</IonNote>
<IonRow className="ion-justify-content-between ion-align-items-center">
<IonText color="primary">
<p>
<IonIcon icon={ navigateOutline } />
{ record.distance } miles away
</p>
</IonText>
<IonButton size="small" color="primary">
<IonIcon icon={ arrowForward } />
</IonButton>
</IonRow>
</IonCardHeader>
</IonCard>
);
})}
</IonContent>
</IonPage>
);
};
export default Tab2;

View File

@@ -0,0 +1,25 @@
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import ExploreContainer from '../components/ExploreContainer';
import './Tab3.css';
const Tab3 = () => {
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Tab 3</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Tab 3</IonTitle>
</IonToolbar>
</IonHeader>
<ExploreContainer name="Tab 3 page" />
</IonContent>
</IonPage>
);
};
export default Tab3;

View File

@@ -0,0 +1,232 @@
import {
IonAvatar,
IonBackButton,
IonBadge,
IonButton,
IonButtons,
IonCardSubtitle,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonNote,
IonPage,
IonRow,
IonTitle,
IonToolbar,
useIonLoading,
useIonModal,
useIonViewWillEnter,
} from '@ionic/react';
import { callOutline } from 'ionicons/icons';
import { Map, Marker } from 'pigeon-maps';
import { useState } from 'react';
import { useParams } from 'react-router';
import { RatingStar } from '../components/RatingStar';
import { getRecord } from '../main/yelp';
import { RecordsStore } from '../store';
import { fetchRecord } from '../store/Selectors';
import styles from '../styles/ViewPlace.module.scss';
import { maptiler } from 'pigeon-maps/providers';
import { useRef } from 'react';
const maptilerProvider = maptiler('d5JQJPLLuap8TkJJlTdJ', 'streets');
const ViewPlace = ({}) => {
const pageRef = useRef();
const [present, dismiss] = useIonLoading();
const { id } = useParams();
const record = RecordsStore.useState(fetchRecord(id));
const [extendedRecord, setExtendedRecord] = useState(false);
const MapView = () => (
<IonContent className="ion-text-center">
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonButton onClick={dismissModal}>Close</IonButton>
</IonButtons>
<IonTitle>Map View</IonTitle>
</IonToolbar>
</IonHeader>
<Map
defaultCenter={[extendedRecord.coordinates.latitude, extendedRecord.coordinates.longitude]}
defaultZoom={13}
provider={maptilerProvider}
touchEvents={true}
>
<Marker
color="red"
width={50}
anchor={[extendedRecord.coordinates.latitude, extendedRecord.coordinates.longitude]}
/>
</Map>
</IonContent>
);
useIonViewWillEnter(() => {
const getData = async () => {
const extendedData = await getRecord(id);
setExtendedRecord(extendedData);
dismiss();
};
present('Fetching extended details...');
getData();
});
const imageURL = record.imageURL ? record.imageURL : '/placeholder.jpeg';
const rating = Math.floor(record.rating).toFixed(0);
const [presentModal, dismissModal] = useIonModal(MapView);
return (
<IonPage className={styles.page} ref={pageRef}>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton text="All places" />
</IonButtons>
<IonTitle>{record.distance} miles away</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<div className={styles.cardImage} style={{ backgroundImage: `url(${imageURL})` }}>
<IonRow>
<IonCol size="8">
<IonCardSubtitle>
{record.name}
<div>
{Array.apply(null, { length: 5 }).map((e, i) => (
<RatingStar key={i} rated={rating > i} small={true} />
))}
</div>
<IonNote>{record.distance} miles away</IonNote>
</IonCardSubtitle>
</IonCol>
</IonRow>
</div>
</IonToolbar>
</IonHeader>
<IonGrid>
<IonRow className={styles.categoryContainer}>
<IonCol size="8">
{extendedRecord.categories &&
extendedRecord.categories.length > 0 &&
extendedRecord.categories.map((category, index) => {
return (
<IonCol key={index} size="1">
<IonBadge key={`category_${index}`} color="primary">
{category.title}
</IonBadge>
</IonCol>
);
})}
</IonCol>
<IonCol size="4" className="ion-justify-content-between">
<a href={record.url} target="_blank" rel="noreferrer">
View on Yelp &rarr;
</a>
</IonCol>
</IonRow>
<IonRow className="ion-margin-top">
<IonCol size="6">
<a href={`tel:${record.phone}`}>
<IonButton color="primary" expand="block">
<IonIcon icon={callOutline} />
</IonButton>
</a>
</IonCol>
<IonCol size="6">
<IonButton
color="primary"
expand="block"
fill="outline"
onClick={() =>
presentModal({
swipetoClose: true,
presentingElement: pageRef.current,
})
}
>
View on map
</IonButton>
</IonCol>
</IonRow>
{extendedRecord.photos && extendedRecord.photos.length > 0 && (
<IonRow className="ion-margin-top">
<IonCol size="12">
<IonCardSubtitle>Photos ({extendedRecord.photos.length})</IonCardSubtitle>
</IonCol>
</IonRow>
)}
<IonRow>
{extendedRecord.photos &&
extendedRecord.photos.length > 0 &&
extendedRecord.photos.map((photo, index) => {
if (index < 3) {
return (
<IonCol key={index} size="4">
<div
className={styles.cardImage}
style={{ backgroundImage: `url(${photo})` }}
/>
</IonCol>
);
}
})}
</IonRow>
{extendedRecord.reviews && extendedRecord.reviews.length > 0 && (
<IonRow className="ion-margin-top">
<IonCol size="12">
<IonCardSubtitle>Reviews ({extendedRecord.reviews.length})</IonCardSubtitle>
</IonCol>
</IonRow>
)}
<IonRow>
{extendedRecord.reviews &&
extendedRecord.reviews.length > 0 &&
extendedRecord.reviews.map((review, index) => {
return (
<IonCol key={`review_${index}`} size="12">
<IonItem lines="full">
<IonLabel className="ion-text-wrap">
<IonRow className="ion-align-items-center ion-justify-content-between">
<IonAvatar>
<img src={review.user.image_url} />
</IonAvatar>
<IonCardSubtitle>{review.user.name}</IonCardSubtitle>
<IonButton color="primary">Full review on Yelp &rarr;</IonButton>
</IonRow>
<p className="ion-padding-top">{review.text}</p>
</IonLabel>
</IonItem>
</IonCol>
);
})}
</IonRow>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default ViewPlace;

View File

@@ -0,0 +1,5 @@
# notes
## TODO
need server for map showing

View File

@@ -0,0 +1,11 @@
import { IonCardSubtitle, IonNote } from "@ionic/react";
import styles from "../styles/MapOverlay.module.scss";
export const CurrentPointOverlay = () => (
<div className={ styles.overlayContainer }>
<IonCardSubtitle>Current Location</IonCardSubtitle>
<IonNote color="medium">Click on the lightning button then choose a new point on the map to view places around that point.</IonNote>
</div>
)

View File

@@ -0,0 +1,24 @@
.container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
.container strong {
font-size: 20px;
line-height: 26px;
}
.container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
.container a {
text-decoration: none;
}

View File

@@ -0,0 +1,12 @@
import './ExploreContainer.css';
const ExploreContainer = ({ name }) => {
return (
<div className="container">
<strong>{name}</strong>
<p>Explore <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
</div>
);
};
export default ExploreContainer;

View File

@@ -0,0 +1,57 @@
import { IonButton, IonCard, IonCardHeader, IonCardSubtitle, IonCol, IonGrid, IonIcon, IonNote, IonRow, IonSearchbar, IonText } from "@ionic/react";
import { RatingStar } from "./RatingStar";
import styles from "../styles/ViewAll.module.scss";
import { arrowForward, navigateOutline } from "ionicons/icons";
export const ListModal = ({ records, searchTerm, search, hideModal }) => {
return (
<IonGrid>
<IonRow>
<IonCol size="12">
<IonSearchbar placeholder="Search for a place..." value={ searchTerm } onIonChange={ e => search(e.target.value) } />
</IonCol>
</IonRow>
<IonRow>
<div className={ styles.viewCardContainerModal }>
{ records.map((record, index) => {
const rating = Math.floor(record.rating).toFixed(0);
return (
<IonCard key={ `record_${ index }` } className={ `${ styles.viewCardModal } animate__animated animate__faster animate__fadeIn` } routerLink={ `/list/${ record.id }` } onClick={ hideModal }>
<IonCardHeader>
{ Array.apply(null, { length: 5 }).map((e, i) => (
<RatingStar key={ i } rated={ rating > i } />
))}
<IonCardSubtitle>{ record.name }</IonCardSubtitle>
<IonNote color="medium">{ record.displayAddress }</IonNote>
<IonRow className="ion-justify-content-between ion-align-items-center">
<IonText color="primary">
<p>
<IonIcon icon={ navigateOutline } />
{ record.distance } miles away
</p>
</IonText>
<IonButton size="small" color="primary">
<IonIcon icon={ arrowForward } />
</IonButton>
</IonRow>
</IonCardHeader>
</IonCard>
);
})}
</div>
</IonRow>
</IonGrid>
);
}

View File

@@ -0,0 +1,50 @@
import { IonBadge, IonButton, IonCardSubtitle, IonCol, IonIcon, IonNote, IonRow } from "@ionic/react";
import { arrowForward, call, callOutline, navigateOutline, phoneLandscapeOutline, phonePortraitOutline, pricetag, pricetags, pricetagsOutline } from "ionicons/icons";
import styles from "../styles/MapOverlay.module.scss";
export const MapOverlay = ({ record }) => (
<div className={ styles.overlayContainer }>
<IonCardSubtitle>{ record.name }</IonCardSubtitle>
<IonNote color="medium">{ record.displayAddress }</IonNote>
<IonBadge color="dark">{ record.rating } star rating</IonBadge>
<p>
<IonIcon icon={ navigateOutline } />
&nbsp;{ record.distance } miles away
</p>
{ record.phone &&
<p>
<IonIcon icon={ call } />
&nbsp;{ record.phone }
</p>
}
<IonRow className="ion-no-padding ion-no-margin ion-margin-top">
<IonCol size="12" className="ion-no-padding ion-no-margin">
<IonButton color="primary" fill="solid" size="small" expand="block" routerLink={ `/list/${ record.id }` }>
View &rarr;
</IonButton>
</IonCol>
</IonRow>
<IonRow className="ion-no-padding ion-no-margin">
{ record.phone &&
<IonCol size="6" className="ion-no-padding ion-no-margin">
<IonButton color="primary" fill="outline" size="small" expand="block">
<IonIcon icon={ callOutline } />
</IonButton>
</IonCol>
}
<IonCol size={ record.phone ? "6" : "12" } className="ion-no-padding ion-no-margin">
<IonButton color="primary" fill="outline" size="small" expand="block">
<IonIcon icon={ navigateOutline } />
</IonButton>
</IonCol>
</IonRow>
</div>
)

View File

@@ -0,0 +1,8 @@
import { IonIcon } from "@ionic/react";
import { star, starOutline } from "ionicons/icons";
import styles from "../styles/RatingStar.module.scss";
export const RatingStar = ({ rated = false, small = false }) => (
<IonIcon className={ rated ? styles.star : styles.outlineStar } icon={ star } style={ small ? { fontSize: "0.6rem" } : {} } />
);

View File

@@ -0,0 +1,45 @@
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
import { cloudOutline, listOutline, mapOutline, searchOutline } from 'ionicons/icons';
import { Route, Redirect } from 'react-router';
import Tab1 from './AppPages/Tab1';
import Tab2 from './AppPages/Tab2';
import ViewPlace from './AppPages/ViewPlace';
import './style.scss';
function DemoRestaurantFinder() {
return (
<IonTabs>
<IonRouterOutlet>
<Route exact path="/demo-restaurant-finder/map">
<Tab1 />
</Route>
<Route exact path="/demo-restaurant-finder/list">
<Tab2 />
</Route>
<Route exact path="/demo-restaurant-finder/list/:id">
<ViewPlace />
</Route>
<Redirect exact path="/demo-restaurant-finder" to="/demo-restaurant-finder/map" />
</IonRouterOutlet>
{/* */}
<IonTabBar slot="bottom">
<IonTabBar slot="bottom">
<IonTabButton tab="tab1" href="/map">
<IonIcon icon={mapOutline} />
</IonTabButton>
<IonTabButton tab="tab2" href="/list">
<IonIcon icon={listOutline} />
</IonTabButton>
</IonTabBar>
</IonTabBar>
</IonTabs>
);
}
export default DemoRestaurantFinder;

View File

@@ -0,0 +1,40 @@
import { Geolocation } from '@capacitor/geolocation';
import { Capacitor } from '@capacitor/core';
const platform = Capacitor.getPlatform();
export const getLocation = async () => {
const permission = await Geolocation.checkPermissions();
var coordinates;
if (permission.location === "granted") {
var options = {
enableHighAccuracy: true,
timeout: 10000,
maximumAge: Infinity
};
coordinates = await Geolocation.getCurrentPosition(options);
} else {
if (platform === "web") {
console.log("Permission Denied.");
} else {
await Geolocation.requestPermissions();
coordinates = await Geolocation.getCurrentPosition(options);
}
}
return {
currentLocation: {
latitude: coordinates.coords.latitude,
longitude: coordinates.coords.longitude
}
}
}

View File

@@ -0,0 +1,32 @@
import { setStore } from "../store/RecordsStore";
export const getRecords = async (currentPoint) => {
// Replace lat/long with values from get current location.
// Allow choosing of radius?
// Offset could = amount loaded in an infinite scroll?
var latitude = currentPoint.latitude, longitude = currentPoint.longitude, radius = 1000, offset = 0;
const response = await fetch(`http://localhost:4000/get-records?latitude=${ latitude }&longitude=${ longitude }&radius=${ radius }&offset=${ offset }`);
const data = await response.json();
setStore(data);
}
export const getRecord = async recordId => {
const response = await fetch(`http://localhost:4000/get-record?id=${ recordId }`);
const data = await response.json();
const response2 = await fetch(`http://localhost:4000/get-reviews?id=${ recordId }`);
const data2 = await response2.json();
data.reviews = data2.reviews;
return data;
}
export const getCategories = async () => {
const response = await fetch(`http://localhost:4000/get-categories`);
const data = await response.json();
return data;
}

View File

@@ -0,0 +1,152 @@
const axios = require('axios');
const express = require('express');
const app = express();
var session = require('express-session');
var cors = require('cors');
// Use session
app.use(
session({
secret: 'Ionic Rocks!',
cookie: { maxAge: 86400000 },
resave: true,
saveUninitialized: true,
})
);
app.use(cors({ origin: '*' }));
app.listen(process.env.PORT || 4000, function () {
console.log('server is running...');
});
// DON'T LEAVE THIS API KEY IN YOUR PRODUCTION APPS
// This is a test account of mine, so i've left this in for demo purposes.
// Secure your nodejs server and API key when building real things!
let API_KEY =
'd02MG5N6GCJ0Y6GN5OHYCIW7XBHCbuu0O0w6sxtZmHMuhn-tgvOK1NaFIgST-4r8E3CQp6APMNMjKs0sZV3UHtQO-e32ysCBY-3nGqxJGsvjTCZ_eEM5jE14H-XuYHYx';
// REST API for Yelp
let yelpAPI = axios.create({
baseURL: 'https://api.yelp.com/v3/',
headers: {
Authorization: `Bearer ${API_KEY}`,
'Content-type': 'application/json',
},
});
app.get('/get-record', function (req, res) {
const { id } = req.query;
yelpAPI(`/businesses/${id}`).then(({ data }) => {
res.send(JSON.stringify(data));
});
});
app.get('/get-reviews', function (req, res) {
const { id } = req.query;
yelpAPI(`/businesses/${id}/reviews`).then(({ data }) => {
res.send(JSON.stringify(data));
});
});
app.get('/get-categories', function (req, res) {
yelpAPI('/categories').then(({ data }) => {
res.send(JSON.stringify(data));
});
});
app.get('/get-records', function (req, res) {
const { latitude, longitude, radius } = req.query;
const categories = 'restaurant,takeaway';
const params = {
latitude,
longitude,
radius,
categories,
};
yelpAPI('/businesses/search', { params: params }).then(({ data }) => {
const allRecords = parseDetails(data);
res.send(JSON.stringify({ allRecords, center: data.region.center }));
});
});
const parseDetails = (info) => {
console.log('Parsing details...');
var records = [];
var parsedInfo = info;
var businesses = parsedInfo.businesses;
var total = parsedInfo.total;
var distance = 0;
var distanceMiles = 0;
for (var i = 0; i < businesses.length; i++) {
var id = businesses[i].id;
var url = businesses[i].url;
var imageURL = businesses[i].image_url;
var name = businesses[i].name;
var alias = businesses[i].alias;
var phone = businesses[i].display_phone;
var price = businesses[i].price;
var rating = businesses[i].rating;
var isClosed = businesses[i].is_closed;
var isOpen = isClosed == true ? false : true;
var coordinates = businesses[i].coordinates;
var latitude = coordinates.latitude;
var longitude = coordinates.longitude;
var displayAddress = '';
if (businesses[i].location) {
var addressDetails = businesses[i].location;
if (addressDetails.display_address) {
var displayAddressArr = addressDetails.display_address;
if (Array.isArray(displayAddressArr)) {
for (var j = 0; j < displayAddressArr.length; j++) {
var displayAddressPart = displayAddressArr[j];
displayAddress = displayAddress + displayAddressPart;
if (j != displayAddressArr.length - 1) {
displayAddress = displayAddress + ', ';
}
}
} else {
displayAddress = displayAddressArr;
}
}
}
if (businesses[i].distance) {
var distance = businesses[i].distance;
var distanceMiles = (distance * 0.000621371192).toFixed(2);
}
if (isClosed != true) {
records.push({
id,
alias,
url,
imageURL,
name,
phone,
price,
rating,
latitude,
longitude,
displayAddress,
isOpen,
distance: distanceMiles,
});
}
}
return records;
};

View File

@@ -0,0 +1,15 @@
import { Store } from 'pullstate';
const RecordsStore = new Store({
records: [],
center : []
});
export default RecordsStore;
export const setStore = records => {
RecordsStore.update(state => { state.records = records.allRecords });
RecordsStore.update(state => { state.center = records.center });
}

View File

@@ -0,0 +1,15 @@
import { createSelector } from 'reselect';
const getState = state => state;
// General getters
export const fetchRecords = createSelector(getState, state => state.records);
// More specific getters
export const fetchRecord = recordId => createSelector(getState, state => {
return state.records.filter(record => record.id === recordId)[0];
});
// export const getPoll = pollId => createSelector(getState, state => state.polls.filter(poll => poll.id === parseInt(pollId))[0]);
// export const getChat = contactId => createSelector(getState, state => state.chats.filter(c => parseInt(c.contact_id) === parseInt(contactId))[0].chats);
// export const getContact = contactId => createSelector(getState, state => state.contacts.filter(c => parseInt(c.id) === parseInt(contactId))[0]);

View File

@@ -0,0 +1 @@
export { default as RecordsStore } from "./RecordsStore";

View File

@@ -0,0 +1,103 @@
#about-page {
ion-toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
--background: transparent;
--color: white;
}
ion-toolbar ion-back-button,
ion-toolbar ion-button,
ion-toolbar ion-menu-button {
--color: white;
}
.about-header {
position: relative;
width: 100%;
height: 30%;
}
.about-header .about-image {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
opacity: 0;
transition: opacity 500ms ease-in-out;
}
.about-header .madison {
background-image: url('/assets/WeatherDemo/img/about/madison.jpg');
}
.about-header .austin {
background-image: url('/assets/WeatherDemo/img/about/austin.jpg');
}
.about-header .chicago {
background-image: url('/assets/WeatherDemo/img/about/chicago.jpg');
}
.about-header .seattle {
background-image: url('/assets/WeatherDemo/img/about/seattle.jpg');
}
.about-info {
position: relative;
margin-top: -10px;
border-radius: 10px;
background: var(--ion-background-color, #fff);
z-index: 2; // display rounded border above header image
}
.about-info h3 {
margin-top: 0;
}
.about-info ion-list {
padding-top: 0;
}
.about-info p {
line-height: 130%;
color: var(--ion-color-dark);
}
.about-info ion-icon {
margin-inline-end: 32px;
}
/*
* iOS Only
*/
.ios .about-info {
--ion-padding: 19px;
}
.ios .about-info h3 {
font-weight: 700;
}
}
#date-input-popover {
--offset-y: -var(--ion-safe-area-bottom);
--max-width: 90%;
--width: 336px;
}

View File

@@ -0,0 +1,19 @@
.overlaySearch {
position: absolute;
z-index: 2;
background-color: white;
width: 70%;
height: 3rem;
margin: 0 auto;
margin-left: 14%;
border-radius: 10px;
box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
align-items: center;
}

View File

@@ -0,0 +1,60 @@
.overlayContainer {
display: flex;
flex-direction: column;
// align-items: center;
// align-content: center;
padding: 1rem;
width: 12rem;
height: fit-content;
background-color: white;
border-radius: 5px;
box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
ion-card-subtitle {
font-size: 0.7rem;
color: black;
}
ion-note {
font-size: 0.6rem;
margin-bottom: 0.5rem;
}
ion-badge {
margin-bottom: 1.5rem;
}
p {
padding: 0;
margin: 0;
margin-bottom: 0.3rem;
font-size: 0.6rem;
display: flex;
flex-direction: row;
align-items: center;
align-content: center;
}
}
.overlayContainer:after {
content:'';
position: absolute;
top: 100%;
left: 70%;
margin-left: -50px;
width: 0;
height: 0;
border-top: solid 10px white;
border-left: solid 10px transparent;
border-right: solid 10px transparent;
}

View File

@@ -0,0 +1,17 @@
.star {
background-color: var(--ion-color-primary);
padding: 0.2rem;
margin-right: 0.1rem;
border-radius: 4px;
color: white;
}
.outlineStar {
background-color: rgb(216, 216, 216);
color: white;
padding: 0.2rem;
margin-right: 0.1rem;
border-radius: 4px;
}

View File

@@ -0,0 +1,54 @@
.viewCard {
p {
display: flex;
flex-direction: row;
align-content: center;
align-items: center;
ion-icon {
margin-right: 0.5rem;
}
}
.cardImage {
height: 10rem;
width: 100%;
background-position: top center;
background-size: cover;
}
}
.viewCardModal {
p {
display: flex;
flex-direction: row;
align-content: center;
align-items: center;
ion-icon {
margin-right: 0.5rem;
}
}
.cardImage {
height: 10rem;
width: 100%;
background-position: top center;
background-size: cover;
}
}
.viewCardContainerModal {
height: 90vh;
width: 100%;
overflow: scroll;
}

View File

@@ -0,0 +1,81 @@
.page {
ion-content {
ion-toolbar,
ion-header {
--border-style: none;
--border-color: none;
}
}
.cardImage {
height: 10rem;
width: 100%;
background-position: top center;
background-size: cover;
border-radius: 5px;
ion-row {
--color: white;
background-color: rgba(0, 0, 0, 0.4);
height: 100%;
display: flex;
flex-direction: row;
align-content: center;
align-items: center;
justify-content: center;
ion-card-subtitle {
--color: white;
background-color: rgba(0, 0, 0, 0.6);
padding: 1rem;
text-align: center;
border-radius: 5px;
font-size: 1.2rem;
div {
margin-bottom: -0.6rem;
}
ion-note {
font-size: 0.6rem;
padding: 0;
margin: 0;
--color: white;
text-transform: lowercase;
font-weight: 600;
}
}
}
}
a {
text-decoration: none;
font-size: 0.9rem;
}
.categoryContainer {
padding-top: 1rem;
padding-bottom: 1rem;
border-bottom: 2px solid rgb(241, 241, 241);
}
.placePhoto {
height: 4rem;
}
.reviewUser {
}
}