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,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" />;

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,3 @@
# NOTES
https://ionicreacthub.com/ionic-react-travel-app-ui

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,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>
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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; });
}

View 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;

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;
}