update,
This commit is contained in:
88
03_source/mobile/src/pages/DemoReactTravelApp/AllRoutes.jsx
Normal file
88
03_source/mobile/src/pages/DemoReactTravelApp/AllRoutes.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
// Main Tabs
|
||||
import Tab1 from '../pages/Tab1';
|
||||
import Tab2 from '../pages/Tab2';
|
||||
import Tab3 from '../pages/Tab3';
|
||||
|
||||
// Main tab children
|
||||
import Place from '../pages/Place';
|
||||
|
||||
// Sub pages
|
||||
// import InboxItem from "../../pages/InboxItem";
|
||||
|
||||
// Tab icons
|
||||
// If using ionicons, import here and pass as ref to tabRoutes
|
||||
|
||||
// Import custom tab menu
|
||||
import Tabs from '../components/Tabs';
|
||||
import SubPages from '../components/SubPages';
|
||||
|
||||
// Array of objects representing tab pages
|
||||
// These will be the main tabs across the app
|
||||
|
||||
// * PARAMS per tab object *
|
||||
// isTab = true will make the tab appear
|
||||
// default = the default tab page to open and be redirected to at "/"
|
||||
// NOTE: there should only be one default tab (default: true)
|
||||
// label = the label to show with the tab
|
||||
// component = the component related to this tab page
|
||||
// icon = icon to show on the tab bar menu
|
||||
// path = the path which the tab is accessible
|
||||
export const tabRoutes = [
|
||||
{
|
||||
label: 'Home',
|
||||
component: Tab1,
|
||||
icon: 'Home',
|
||||
path: '/tabs/home',
|
||||
default: true,
|
||||
isTab: true,
|
||||
},
|
||||
{
|
||||
label: 'Places',
|
||||
component: Tab2,
|
||||
icon: 'Location',
|
||||
path: '/tabs/places',
|
||||
default: false,
|
||||
isTab: true,
|
||||
},
|
||||
{
|
||||
label: 'Favourites',
|
||||
component: Tab3,
|
||||
icon: 'Heart',
|
||||
path: '/tabs/favourites',
|
||||
default: false,
|
||||
isTab: true,
|
||||
},
|
||||
// { label: "Profile", component: Tab3, icon: "User", path: "/tabs/profile", default: false, isTab: true },
|
||||
];
|
||||
|
||||
// Array of objects representing children pages of tabs
|
||||
|
||||
// * PARAMS per tab object *
|
||||
// isTab = should always be set to false for these
|
||||
// component = the component related to this tab page
|
||||
// path = the path which the tab is accessible
|
||||
|
||||
// These pages should be related to tab pages and be held within the same path
|
||||
// E.g. /tabs/tab1/child
|
||||
const tabChildrenRoutes = [
|
||||
// { component: InboxItem, path: "/tabs/tab2/:id", isTab: false },
|
||||
];
|
||||
|
||||
// Array of objects representing sub pages
|
||||
|
||||
// * PARAMS per tab object *
|
||||
// component = the component related to this sub page
|
||||
// path = the path which the sub page is accessible
|
||||
|
||||
// This array should be sub pages which are not directly related to a tab page
|
||||
// E.g. /child
|
||||
const subPageRoutes = [{ component: Place, path: '/view-place/:place_id' }];
|
||||
|
||||
// Let's combine these together as they need to be controlled within the same IonRouterOutlet
|
||||
const tabsAndChildrenRoutes = [...tabRoutes, ...tabChildrenRoutes];
|
||||
|
||||
// Render sub routes
|
||||
export const AllSubPages = () => <SubPages routes={subPageRoutes} />;
|
||||
|
||||
// Render tab menu
|
||||
export const AllTabs = () => <Tabs tabs={tabsAndChildrenRoutes} position="bottom" />;
|
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonIcon,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
useIonRouter,
|
||||
} from '@ionic/react';
|
||||
|
||||
import { Geolocation } from '@capacitor/geolocation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { SkeletonDashboard } from '../components/SkeletonDashboard';
|
||||
import { chevronBackOutline, refreshOutline } from 'ionicons/icons';
|
||||
import { CurrentWeather } from '../components/CurrentWeather';
|
||||
|
||||
function Tab1() {
|
||||
const router = useIonRouter();
|
||||
|
||||
const [currentWeather, setCurrentWeather] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getCurrentPosition();
|
||||
}, []);
|
||||
|
||||
const getCurrentPosition = async () => {
|
||||
setCurrentWeather(false);
|
||||
const coordinates = await Geolocation.getCurrentPosition();
|
||||
getAddress(coordinates.coords);
|
||||
};
|
||||
|
||||
const getAddress = async (coords) => {
|
||||
const query = `${coords.latitude},${coords.longitude}`;
|
||||
const response = await fetch(
|
||||
`https://api.weatherapi.com/v1/current.json?key=f93eb660b2424258bf5155016210712&q=${query}`
|
||||
);
|
||||
|
||||
const data = await response.json();
|
||||
console.log(data);
|
||||
setCurrentWeather(data);
|
||||
};
|
||||
|
||||
function handleBackClick() {
|
||||
router.goBack();
|
||||
}
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>My Weather</IonTitle>
|
||||
|
||||
<IonButtons slot="end">
|
||||
<IonButton onClick={() => getCurrentPosition()}>
|
||||
<IonIcon icon={refreshOutline} color="primary" />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
|
||||
<IonButtons slot="start">
|
||||
<IonButton onClick={() => handleBackClick()}>
|
||||
<IonIcon icon={chevronBackOutline} color="primary" />
|
||||
</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Dashboard</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonRow className="ion-margin-start ion-margin-end ion-justify-content-center ion-text-center">
|
||||
<IonCol size="12">
|
||||
<h4>Here's your location based weather</h4>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<div style={{ marginTop: '-1.5rem' }}>
|
||||
{currentWeather ? (
|
||||
<CurrentWeather currentWeather={currentWeather} />
|
||||
) : (
|
||||
<SkeletonDashboard />
|
||||
)}
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tab1;
|
@@ -0,0 +1,81 @@
|
||||
import {
|
||||
IonButton,
|
||||
IonCol,
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonRow,
|
||||
IonSearchbar,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { useState } from 'react';
|
||||
import { CurrentWeather } from '../components/CurrentWeather';
|
||||
|
||||
function Tab2() {
|
||||
const [search, setSearch] = useState('');
|
||||
const [currentWeather, setCurrentWeather] = useState(false);
|
||||
|
||||
const performSearch = async () => {
|
||||
getAddress(search);
|
||||
};
|
||||
|
||||
const getAddress = async (city) => {
|
||||
const response = await fetch(
|
||||
`https://api.weatherapi.com/v1/current.json?key=f93eb660b2424258bf5155016210712&q=${city}&aqi=no`
|
||||
);
|
||||
const data = await response.json();
|
||||
|
||||
if (data && data.current && data.location) {
|
||||
setCurrentWeather(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<IonPage>
|
||||
<IonHeader>
|
||||
<IonToolbar>
|
||||
<IonTitle>Search</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen>
|
||||
<IonHeader collapse="condense">
|
||||
<IonToolbar>
|
||||
<IonTitle size="large">Search</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
|
||||
<IonRow className="ion-justify-content-center ion-margin-top ion-align-items-center">
|
||||
<IonCol size="7">
|
||||
<IonSearchbar
|
||||
placeholder="Try 'London'"
|
||||
animated
|
||||
value={search}
|
||||
onIonChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</IonCol>
|
||||
|
||||
<IonCol size="5">
|
||||
<IonButton
|
||||
expand="block"
|
||||
className="ion-margin-start ion-margin-end"
|
||||
onClick={performSearch}
|
||||
>
|
||||
Search
|
||||
</IonButton>
|
||||
</IonCol>
|
||||
</IonRow>
|
||||
|
||||
<div style={{ marginTop: '-0.8rem' }}>
|
||||
{currentWeather ? (
|
||||
<CurrentWeather currentWeather={currentWeather} />
|
||||
) : (
|
||||
<h3 className="ion-text-center">Your search result will appear here</h3>
|
||||
)}
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tab2;
|
3
03_source/mobile/src/pages/DemoReactTravelApp/NOTES.md
Normal file
3
03_source/mobile/src/pages/DemoReactTravelApp/NOTES.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# NOTES
|
||||
|
||||
https://ionicreacthub.com/ionic-react-travel-app-ui
|
@@ -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;
|
||||
}
|
@@ -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;
|
@@ -0,0 +1,16 @@
|
||||
import { IonCard, IonCardTitle, IonNote } from "@ionic/react";
|
||||
import styles from "../styles/Home.module.scss";
|
||||
|
||||
export const LongPlaceCard = ({ place = false }) => (
|
||||
|
||||
<IonCard className={ `${ styles.longSlide } animate__animated animate__fadeIn animate__faster` } routerLink={ `/view-place/${ place.id }` }>
|
||||
<div className={ styles.imageHeader }>
|
||||
<img src={ place.image } />
|
||||
</div>
|
||||
|
||||
<div className={ styles.details }>
|
||||
<IonCardTitle>{ place.name }</IonCardTitle>
|
||||
<IonNote>{ place.destination }</IonNote>
|
||||
</div>
|
||||
</IonCard>
|
||||
);
|
@@ -0,0 +1,99 @@
|
||||
import { IonCard, IonCardHeader, IonCardTitle, IonNote, useIonToast, CreateAnimation, IonIcon } from "@ionic/react";
|
||||
import { useState } from "react";
|
||||
import { useRef } from "react";
|
||||
import { Iconly } from "react-iconly";
|
||||
import { heart, trashBin } from "ionicons/icons";
|
||||
import { addFavourite } from "../store/PlacesStore";
|
||||
|
||||
import styles from "../styles/Home.module.scss";
|
||||
|
||||
const PlaceCard = ({ place = false, fromFavourites = false }) => {
|
||||
|
||||
const animationRef = useRef();
|
||||
const cardRef = useRef();
|
||||
const [ presentToast ] = useIonToast();
|
||||
const [ hideAnimatedIcon, setHideAnimatedIcon ] = useState(true);
|
||||
|
||||
const floatStyle = {
|
||||
|
||||
display: hideAnimatedIcon ? "none" : "",
|
||||
position: "absolute",
|
||||
zIndex: "10"
|
||||
};
|
||||
|
||||
const floatGrowAnimation = {
|
||||
|
||||
property: "transform",
|
||||
fromValue: "translateY(0) scale(1)",
|
||||
toValue: "translateY(-20px) scale(2)"
|
||||
};
|
||||
|
||||
const mainAnimation = {
|
||||
|
||||
duration: 600,
|
||||
iterations: "1",
|
||||
fromTo: [ floatGrowAnimation ],
|
||||
easing: "cubic-bezier(0.25, 0.7, 0.25, 0.7)"
|
||||
};
|
||||
|
||||
const handleAddFavourite = async (e, place) => {
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
|
||||
if (fromFavourites) {
|
||||
|
||||
// Add a fadeOut animation before removing
|
||||
cardRef.current.classList.add("animate__fadeOut");
|
||||
|
||||
setTimeout(() => {
|
||||
addFavourite(place, fromFavourites);
|
||||
}, 500);
|
||||
} else {
|
||||
|
||||
addFavourite(place, fromFavourites);
|
||||
}
|
||||
|
||||
presentToast({
|
||||
|
||||
header: `Favourite ${ fromFavourites ? "removed" : "added" }!`,
|
||||
buttons: [
|
||||
{
|
||||
text: "♡",
|
||||
}
|
||||
],
|
||||
message: `${ place.name } has been ${ fromFavourites ? "removed from" : "added to" } your favourites.`,
|
||||
duration: 1500,
|
||||
color: "success"
|
||||
});
|
||||
|
||||
setHideAnimatedIcon(false);
|
||||
await animationRef.current.animation.play();
|
||||
setHideAnimatedIcon(true);
|
||||
}
|
||||
|
||||
return (
|
||||
<IonCard ref={ cardRef } className={ `${ styles.slide } animate__animated animate__fadeIn animate__faster` } routerLink={ `/view-place/${ place.id }` }>
|
||||
<div className={ styles.imageHeader }>
|
||||
<img src={ place ? place.image : "/assets/nonefound.png" } />
|
||||
{ place &&
|
||||
<div className="favouriteButton" onClick={ e => handleAddFavourite(e, place) }>
|
||||
|
||||
<Iconly set="bold" name={ fromFavourites ? "Delete" : "Heart" } color="red" />
|
||||
|
||||
<CreateAnimation ref={ animationRef } { ...mainAnimation }>
|
||||
<IonIcon icon={ fromFavourites ? trashBin : heart } style={ floatStyle } color="danger" />
|
||||
</CreateAnimation>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<IonCardHeader>
|
||||
<IonCardTitle>{ place ? place.name : "Sorry" }</IonCardTitle>
|
||||
<IonNote>{ place ? place.destination : "No results found" }</IonNote>
|
||||
</IonCardHeader>
|
||||
</IonCard>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlaceCard;
|
@@ -0,0 +1,17 @@
|
||||
import { Route } from "react-router-dom";
|
||||
|
||||
const SubPages = (props) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{ props.routes.map((route, i) => {
|
||||
|
||||
const RouteComponent = route.component;
|
||||
|
||||
return <Route key={ i } path={ route.path } render={ (props) => <RouteComponent { ...props } /> } exact={ false } />;
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default SubPages;
|
@@ -0,0 +1,48 @@
|
||||
import { IonTabBar, IonTabButton, IonTabs, IonRouterOutlet } from "@ionic/react";
|
||||
import { useState } from "react";
|
||||
import { Iconly } from "react-iconly";
|
||||
import { Route } from "react-router-dom";
|
||||
|
||||
const Tabs = (props) => {
|
||||
|
||||
const [ selected, setSelected ] = useState("tab_1");
|
||||
|
||||
return (
|
||||
<IonTabs onIonTabsWillChange={ e => setSelected(e.detail.tab) }>
|
||||
<IonRouterOutlet>
|
||||
|
||||
{ props.tabs.map((tab, i) => {
|
||||
|
||||
const TabComponent = tab.component;
|
||||
|
||||
if (tab.isTab) {
|
||||
return <Route key={ `tab_route_${ i }` } path={ tab.path } render={ (props) => <TabComponent { ...props } /> } exact={ true }/>;
|
||||
} else {
|
||||
|
||||
return <Route key={ `child_tab_route_${ i }` } path={ tab.path } render={ (props) => <TabComponent {...props} /> } exact={ false } />;
|
||||
}
|
||||
})}
|
||||
</IonRouterOutlet>
|
||||
|
||||
<IonTabBar slot={ props.position }>
|
||||
|
||||
{ props.tabs.map((tab, i) => {
|
||||
|
||||
const isSelected = selected === `tab_${ i + 1 }`;
|
||||
|
||||
if (tab.isTab) {
|
||||
|
||||
return (
|
||||
<IonTabButton key={ `tab_button_${ i + 1 }` } tab={ `tab_${ i + 1 }` } href={ tab.path }>
|
||||
<Iconly set="light" name={ tab.icon } />
|
||||
{ isSelected && <div className="tab-dot" /> }
|
||||
</IonTabButton>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</IonTabBar>
|
||||
</IonTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export default Tabs;
|
@@ -0,0 +1,21 @@
|
||||
import { PlacesStore } from "../store";
|
||||
|
||||
export const fetchData = async () => {
|
||||
|
||||
const response = await fetch("/data.json");
|
||||
const data = await response.json();
|
||||
|
||||
await data.forEach((place, i) => {
|
||||
|
||||
delete place.desc;
|
||||
|
||||
const placeName = place.name;
|
||||
const placeNameParts = placeName.split(",");
|
||||
|
||||
place.id = i + 1;
|
||||
place.name = placeNameParts[0].trim();
|
||||
place.destination = placeNameParts[1].trim();
|
||||
});
|
||||
|
||||
PlacesStore.update(s => { s.places = data; });
|
||||
}
|
29
03_source/mobile/src/pages/DemoReactTravelApp/index.tsx
Normal file
29
03_source/mobile/src/pages/DemoReactTravelApp/index.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
|
||||
|
||||
import { cloudOutline, searchOutline } from 'ionicons/icons';
|
||||
import { Route, Redirect } from 'react-router';
|
||||
|
||||
// import { AllSubPages, AllTabs, tabRoutes } from './AllRoutes';
|
||||
|
||||
import './style.scss';
|
||||
|
||||
function DemoReactTravelApp() {
|
||||
return <>on hold</>;
|
||||
|
||||
// NOTE: i temporary make it constant to let the program keep develop
|
||||
// the below requires fixing, the AllRoutes is not found and it is a
|
||||
// jsx file, i want it tsx
|
||||
const hello_this_should_be_the_return = (
|
||||
<IonTabs>
|
||||
<IonRouterOutlet>
|
||||
<Route path="/tabs" render={() => <AllTabs />} />
|
||||
<AllSubPages />
|
||||
|
||||
<Route path="/" component={tabRoutes.filter((t) => t.default)[0].component} exact={true} />
|
||||
<Redirect exact from="/" to={tabRoutes.filter((t) => t.default)[0].path.toString()} />
|
||||
</IonRouterOutlet>
|
||||
</IonTabs>
|
||||
);
|
||||
}
|
||||
|
||||
export default DemoReactTravelApp;
|
103
03_source/mobile/src/pages/DemoReactTravelApp/style.scss
Normal file
103
03_source/mobile/src/pages/DemoReactTravelApp/style.scss
Normal 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;
|
||||
}
|
Reference in New Issue
Block a user