diff --git a/03_source/mobile/android/app/capacitor.build.gradle b/03_source/mobile/android/app/capacitor.build.gradle index adabd97..df803be 100644 --- a/03_source/mobile/android/app/capacitor.build.gradle +++ b/03_source/mobile/android/app/capacitor.build.gradle @@ -9,6 +9,7 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { + implementation project(':capacitor-geolocation') implementation project(':capacitor-preferences') } diff --git a/03_source/mobile/android/capacitor.settings.gradle b/03_source/mobile/android/capacitor.settings.gradle index a1c665a..7b7ef65 100644 --- a/03_source/mobile/android/capacitor.settings.gradle +++ b/03_source/mobile/android/capacitor.settings.gradle @@ -2,5 +2,8 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') +include ':capacitor-geolocation' +project(':capacitor-geolocation').projectDir = new File('../node_modules/@capacitor/geolocation/android') + include ':capacitor-preferences' project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android') diff --git a/03_source/mobile/ios/App/Podfile b/03_source/mobile/ios/App/Podfile index 0451e2c..0a75ba4 100644 --- a/03_source/mobile/ios/App/Podfile +++ b/03_source/mobile/ios/App/Podfile @@ -11,6 +11,7 @@ install! 'cocoapods', :disable_input_output_paths => true def capacitor_pods pod 'Capacitor', :path => '../../node_modules/@capacitor/ios' pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios' + pod 'CapacitorGeolocation', :path => '../../node_modules/@capacitor/geolocation' pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences' end diff --git a/03_source/mobile/package-lock.json b/03_source/mobile/package-lock.json index b208ffd..214adf2 100644 --- a/03_source/mobile/package-lock.json +++ b/03_source/mobile/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@capacitor/android": "7.0.1", "@capacitor/core": "^7.0.0", + "@capacitor/geolocation": "^7.1.2", "@capacitor/ios": "7.0.1", "@capacitor/preferences": "^7.0.0", "@hookform/resolvers": "^4.1.3", @@ -432,6 +433,18 @@ "tslib": "^2.1.0" } }, + "node_modules/@capacitor/geolocation": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-7.1.2.tgz", + "integrity": "sha512-J++OuOpn6Bjweo7SZ+jwI/dhVF9DNw6Wx0UHgQ4qfrATqmNKGNZ/BUljGhXiKJceSx2GIhfrYS7BYqo3uWibgQ==", + "license": "MIT", + "dependencies": { + "@capacitor/synapse": "^1.0.1" + }, + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/@capacitor/ios": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/@capacitor/ios/-/ios-7.0.1.tgz", @@ -450,6 +463,12 @@ "@capacitor/core": ">=7.0.0" } }, + "node_modules/@capacitor/synapse": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.2.tgz", + "integrity": "sha512-ynq39s4D2rhk+aVLWKfKfMCz9SHPKijL9tq8aFL5dG7ik7/+PvBHmg9cPHbqdvFEUSMmaGzL6cIjzkOruW7vGA==", + "license": "ISC" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", diff --git a/03_source/mobile/package.json b/03_source/mobile/package.json index 4b631e9..9c68a2e 100644 --- a/03_source/mobile/package.json +++ b/03_source/mobile/package.json @@ -8,6 +8,7 @@ "dependencies": { "@capacitor/android": "7.0.1", "@capacitor/core": "^7.0.0", + "@capacitor/geolocation": "^7.1.2", "@capacitor/ios": "7.0.1", "@capacitor/preferences": "^7.0.0", "@hookform/resolvers": "^4.1.3", @@ -20,6 +21,7 @@ "date-fns": "^2.25.0", "ionicons": "^7.1.2", "leaflet": "^1.9.4", + "pullstate": "^2.0.0-pre.0", "react": "19.0.0", "react-dom": "19.0.0", "react-hook-form": "^7.55.0", diff --git a/03_source/mobile/public/assets/WeatherDemo/icon/favicon.png b/03_source/mobile/public/assets/WeatherDemo/icon/favicon.png new file mode 100644 index 0000000..51888a7 Binary files /dev/null and b/03_source/mobile/public/assets/WeatherDemo/icon/favicon.png differ diff --git a/03_source/mobile/public/assets/WeatherDemo/icon/icon.png b/03_source/mobile/public/assets/WeatherDemo/icon/icon.png new file mode 100644 index 0000000..a7f6374 Binary files /dev/null and b/03_source/mobile/public/assets/WeatherDemo/icon/icon.png differ diff --git a/03_source/mobile/public/assets/WeatherDemo/map.png b/03_source/mobile/public/assets/WeatherDemo/map.png new file mode 100644 index 0000000..585e1b7 Binary files /dev/null and b/03_source/mobile/public/assets/WeatherDemo/map.png differ diff --git a/03_source/mobile/public/assets/WeatherDemo/shapes.svg b/03_source/mobile/public/assets/WeatherDemo/shapes.svg new file mode 100644 index 0000000..d370b4d --- /dev/null +++ b/03_source/mobile/public/assets/WeatherDemo/shapes.svg @@ -0,0 +1 @@ + diff --git a/03_source/mobile/public/assets/WeatherDemo/temp.png b/03_source/mobile/public/assets/WeatherDemo/temp.png new file mode 100644 index 0000000..981b728 Binary files /dev/null and b/03_source/mobile/public/assets/WeatherDemo/temp.png differ diff --git a/03_source/mobile/public/assets/WeatherDemo/temp2.png b/03_source/mobile/public/assets/WeatherDemo/temp2.png new file mode 100644 index 0000000..47a19c0 Binary files /dev/null and b/03_source/mobile/public/assets/WeatherDemo/temp2.png differ diff --git a/03_source/mobile/public/assets/WeatherDemo/wind.png b/03_source/mobile/public/assets/WeatherDemo/wind.png new file mode 100644 index 0000000..9bfa6ee Binary files /dev/null and b/03_source/mobile/public/assets/WeatherDemo/wind.png differ diff --git a/03_source/mobile/public/assets/react-shop/beds.jpeg b/03_source/mobile/public/assets/react-shop/beds.jpeg new file mode 100644 index 0000000..8582d80 Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/beds.jpeg differ diff --git a/03_source/mobile/public/assets/react-shop/coats3.jpeg b/03_source/mobile/public/assets/react-shop/coats3.jpeg new file mode 100644 index 0000000..edff4bf Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/coats3.jpeg differ diff --git a/03_source/mobile/public/assets/react-shop/coffee_table.jpeg b/03_source/mobile/public/assets/react-shop/coffee_table.jpeg new file mode 100644 index 0000000..21e9601 Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/coffee_table.jpeg differ diff --git a/03_source/mobile/public/assets/react-shop/dresses3.jpeg b/03_source/mobile/public/assets/react-shop/dresses3.jpeg new file mode 100644 index 0000000..7a34676 Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/dresses3.jpeg differ diff --git a/03_source/mobile/public/assets/react-shop/formal_shirts2.jpeg b/03_source/mobile/public/assets/react-shop/formal_shirts2.jpeg new file mode 100644 index 0000000..59cd44f Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/formal_shirts2.jpeg differ diff --git a/03_source/mobile/public/assets/react-shop/home.jpeg b/03_source/mobile/public/assets/react-shop/home.jpeg new file mode 100644 index 0000000..75f42a3 Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/home.jpeg differ diff --git a/03_source/mobile/public/assets/react-shop/icon/favicon.png b/03_source/mobile/public/assets/react-shop/icon/favicon.png new file mode 100644 index 0000000..51888a7 Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/icon/favicon.png differ diff --git a/03_source/mobile/public/assets/react-shop/icon/icon.png b/03_source/mobile/public/assets/react-shop/icon/icon.png new file mode 100644 index 0000000..a7f6374 Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/icon/icon.png differ diff --git a/03_source/mobile/public/assets/react-shop/jeans.jpeg b/03_source/mobile/public/assets/react-shop/jeans.jpeg new file mode 100644 index 0000000..b90bfd1 Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/jeans.jpeg differ diff --git a/03_source/mobile/public/assets/react-shop/makeup2.jpeg b/03_source/mobile/public/assets/react-shop/makeup2.jpeg new file mode 100644 index 0000000..e291108 Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/makeup2.jpeg differ diff --git a/03_source/mobile/public/assets/react-shop/men.jpeg b/03_source/mobile/public/assets/react-shop/men.jpeg new file mode 100644 index 0000000..23bf901 Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/men.jpeg differ diff --git a/03_source/mobile/public/assets/react-shop/office.jpeg b/03_source/mobile/public/assets/react-shop/office.jpeg new file mode 100644 index 0000000..486ec39 Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/office.jpeg differ diff --git a/03_source/mobile/public/assets/react-shop/shapes.svg b/03_source/mobile/public/assets/react-shop/shapes.svg new file mode 100644 index 0000000..d370b4d --- /dev/null +++ b/03_source/mobile/public/assets/react-shop/shapes.svg @@ -0,0 +1 @@ + diff --git a/03_source/mobile/public/assets/react-shop/sportswear2.jpeg b/03_source/mobile/public/assets/react-shop/sportswear2.jpeg new file mode 100644 index 0000000..285614c Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/sportswear2.jpeg differ diff --git a/03_source/mobile/public/assets/react-shop/women.jpeg b/03_source/mobile/public/assets/react-shop/women.jpeg new file mode 100644 index 0000000..0ad70ef Binary files /dev/null and b/03_source/mobile/public/assets/react-shop/women.jpeg differ diff --git a/03_source/mobile/src/App.tsx b/03_source/mobile/src/App.tsx index fa050bf..f501a4c 100644 --- a/03_source/mobile/src/App.tsx +++ b/03_source/mobile/src/App.tsx @@ -64,6 +64,8 @@ import ServiceAgreement from './pages/ServiceAgreement'; import paths from './paths'; import PrivacyAgreement from './pages/PrivacyAgreement'; import AppRoute from './AppRoute'; +import DemoReactShop from './pages/DemoReactShop'; +import DemoWeatherApp from './pages/WeatherDemo'; setupIonicReact(); @@ -121,6 +123,8 @@ const IonicApp: React.FC = ({ {/* */} } /> + } /> + } /> {/* */} diff --git a/03_source/mobile/src/AppRoute.tsx b/03_source/mobile/src/AppRoute.tsx index bdeb3c9..1f5d9f8 100644 --- a/03_source/mobile/src/AppRoute.tsx +++ b/03_source/mobile/src/AppRoute.tsx @@ -7,14 +7,12 @@ import NotImplemented from './pages/NotImplemented'; import EventDetail from './pages/EventDetail'; import MemberProfile from './pages/MemberProfile'; import paths from './paths'; -import Helloworld from './pages/Helloworld'; import Settings from './pages/Settings'; import ChangeLanguage from './pages/ChangeLanguage'; import ServiceAgreement from './pages/ServiceAgreement'; import PrivacyAgreement from './pages/PrivacyAgreement'; // import OrderDetails from './pages/OrderDetail'; import OrderDetail from './pages/OrderDetail'; -import SpeakerDetail from './pages/SpeakerDetail'; const AppRoute: React.FC = () => { return ( @@ -34,6 +32,9 @@ const AppRoute: React.FC = () => { + + {/* TODO: review DemoReactShop to fix */} + {/* } exact={true} /> */} ); }; diff --git a/03_source/mobile/src/TabAppRoute.tsx b/03_source/mobile/src/TabAppRoute.tsx index b66681a..46fc50a 100644 --- a/03_source/mobile/src/TabAppRoute.tsx +++ b/03_source/mobile/src/TabAppRoute.tsx @@ -10,6 +10,9 @@ import Favourites from './pages/Favourites'; import MyProfile from './pages/MyProfile'; import EventList from './pages/EventList'; import Helloworld from './pages/Helloworld'; +// import WeatherDemo from './pages/WeatherDemo/Tab1'; +import DemoList from './pages/DemoList'; +// import DemoReactShop from './pages/DemoReactShop'; const TabAppRoute: React.FC = () => { return ( @@ -33,6 +36,12 @@ const TabAppRoute: React.FC = () => { {/* */} } exact={true} /> + + {/* */} + } exact={true} /> + + {/* */} + } exact={true} /> ); }; diff --git a/03_source/mobile/src/pages/DemoList/TestContent.tsx b/03_source/mobile/src/pages/DemoList/TestContent.tsx new file mode 100644 index 0000000..39fd43b --- /dev/null +++ b/03_source/mobile/src/pages/DemoList/TestContent.tsx @@ -0,0 +1,13 @@ +import { format } from 'date-fns'; + +export const TestContent = { + eventDate: format(new Date(), 'yyyy-MM-dd'), + title: 'helloworld', + price: 123, + currency: 'HKD', + duration_m: 480, + ageBottom: 12, + ageTop: 48, + location: 'Hong Kong Island', + avatar: 'https://www.ionics.io/img/ionic-logo.png', +}; diff --git a/03_source/mobile/src/pages/DemoList/index.tsx b/03_source/mobile/src/pages/DemoList/index.tsx new file mode 100644 index 0000000..cac8056 --- /dev/null +++ b/03_source/mobile/src/pages/DemoList/index.tsx @@ -0,0 +1,271 @@ +// REQ0054/user-setting +// +// PURPOSE: +// - Provides functionality view user profile +// +// RULES: +// - T.B.A. +// +import React, { useEffect, useRef, useState } from 'react'; +import { + IonHeader, + IonToolbar, + IonTitle, + IonContent, + IonPage, + IonButtons, + IonMenuButton, + IonGrid, + IonRow, + IonCol, + useIonRouter, + IonButton, + IonIcon, + IonPopover, + IonAvatar, + IonImg, + IonItem, + IonLabel, + IonList, + IonModal, + IonSearchbar, + useIonModal, + IonInput, + IonNote, + IonText, +} from '@ionic/react'; +import SpeakerItem from '../../components/SpeakerItem'; +import { Speaker } from '../../models/Speaker'; +import { Session } from '../../models/Schedule'; +import { connect } from '../../data/connect'; +import * as selectors from '../../data/selectors'; +import '../SpeakerList.scss'; +import { getEvents } from '../../api/getEvents'; +import { format } from 'date-fns'; +import { Event } from './types'; +import { + alertCircleOutline, + alertOutline, + cart, + chatbubbleOutline, + chevronBackOutline, + chevronForward, + chevronForwardOutline, + createOutline, + documentTextOutline, + gift, + giftOutline, + heart, + languageOutline, + listCircle, + menuOutline, + settingsOutline, + shareSocialOutline, + sunny, + trashOutline, +} from 'ionicons/icons'; +import AboutPopover from '../../components/AboutPopover'; +import { OverlayEventDetail } from '@ionic/react/dist/types/components/react-component-lib/interfaces'; +import paths from '../../paths'; +import { logoutUser, setAccessToken, setIsLoggedIn } from '../../data/user/user.actions'; + +interface OwnProps {} + +interface StateProps { + speakers: Speaker[]; + speakerSessions: { [key: string]: Session[] }; +} + +interface DispatchProps { + logoutUser: typeof logoutUser; + setAccessToken: typeof setAccessToken; + setIsLoggedIn: typeof setIsLoggedIn; +} + +interface SettingsProps extends OwnProps, StateProps, DispatchProps {} + +const SettingsPage: React.FC = ({ + speakers, + speakerSessions, + logoutUser, + setAccessToken, + setIsLoggedIn, +}) => { + const [events, setEvents] = useState([]); + const [showPopover, setShowPopover] = useState(false); + const [popoverEvent, setPopoverEvent] = useState(); + const modal = useRef(null); + + const router = useIonRouter(); + + useEffect(() => { + getEvents().then(({ data }) => { + console.log({ data }); + setEvents(data); + }); + }, []); + + function handleBackButtonClick() { + router.goBack(); + } + + function handleLanguageClick() { + router.push(paths.CHANGE_LANGUAGE); + } + + function handleNotImplementedClick() { + router.push(paths.NOT_IMPLEMENTED); + } + + function handleDemoPageClick() { + router.push(paths.DEMO_PAGE); + } + + function handleServiceAgreementClick() { + router.push(paths.SERVICE_AGREEMENT); + } + + function handlePrivacyAgreementClick() { + router.push(paths.PRIVACY_AGREEMENT); + } + + const [showLogoutConfirmModal, setShowLogoutConfirmModal] = useState(false); + function handleConfirmLogoutClick() { + setShowLogoutConfirmModal(true); + } + + function handleLogoutClick() { + setAccessToken(); + setIsLoggedIn(false); + + router.push('/tabs', 'forward', 'replace'); + + setShowLogoutConfirmModal(false); + } + function handleLogoutCancel() { + setShowLogoutConfirmModal(false); + } + + function handleDemoWeatherApp() { + router.push(paths.DEMO_WEATHER_APP); + } + + function handleDemoReactShopClick() { + router.push(paths.DEMO_REACT_SHOP); + } + + return ( + + + + + {/* */} + handleBackButtonClick()}> + + + + +
+ + Setting +
+
+
+ + + + + Setting + + + + + handleDemoWeatherApp()}> + + Weather App + + + + + + handleDemoReactShopClick()}> + + Demo React Shop + + + + + + {/* REQ0058/logout */} + + +
+
+ +
+
+ Logout +
+
+ Unable to receive notifications after logging out +
+ +
+ + Cancel + + + Logout + +
+
+
+
+
+ ); +}; + +export default connect({ + mapStateToProps: (state) => ({ + speakers: selectors.getSpeakers(state), + speakerSessions: selectors.getSpeakerSessions(state), + }), + mapDispatchToProps: { + logoutUser, + setAccessToken, + setIsLoggedIn, + }, + component: React.memo(SettingsPage), +}); diff --git a/03_source/mobile/src/pages/DemoList/style.scss b/03_source/mobile/src/pages/DemoList/style.scss new file mode 100644 index 0000000..5fae6e3 --- /dev/null +++ b/03_source/mobile/src/pages/DemoList/style.scss @@ -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/img/about/madison.jpg'); + } + + .about-header .austin { + background-image: url('/assets/img/about/austin.jpg'); + } + + .about-header .chicago { + background-image: url('/assets/img/about/chicago.jpg'); + } + + .about-header .seattle { + background-image: url('/assets/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; +} diff --git a/03_source/mobile/src/pages/DemoList/types.ts b/03_source/mobile/src/pages/DemoList/types.ts new file mode 100644 index 0000000..2f4577f --- /dev/null +++ b/03_source/mobile/src/pages/DemoList/types.ts @@ -0,0 +1,14 @@ +export interface Event { + eventDate: Date; + joinMembers: undefined; + title: string; + price: number; + currency: string; + duration_m: number; + ageBottom: number; + ageTop: number; + location: string; + avatar: string; + // + id: string; +} diff --git a/03_source/mobile/src/pages/DemoReactShop/Categories.jsx b/03_source/mobile/src/pages/DemoReactShop/Categories.jsx new file mode 100644 index 0000000..092cfb1 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/Categories.jsx @@ -0,0 +1,89 @@ +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonIcon, + IonLabel, + IonNote, + IonPage, + IonRouterLink, + IonRow, + IonTitle, + IonToolbar, + useIonRouter, +} from '@ionic/react'; +import { + checkmarkOutline, + chevronBackOutline, + chevronDownCircleOutline, + closeOutline, + heart, + languageOutline, + menuOutline, +} from 'ionicons/icons'; +import { capitalize, productInfo } from '../utils'; + +const Categories = () => { + const categories = Object.keys(productInfo); + + const router = useIonRouter(); + function handleBackClick() { + router.goBack(); + } + + return ( + + + + + + + + + Ionic Shop + + + + + + + ionic + shop + + + + + + {categories.map((category, idx) => ( + +
+ cover +

+ {capitalize(category)} +

+
+
+ + // {capitalize(c)} + ))} +
+
+
+ ); +}; + +export default Categories; diff --git a/03_source/mobile/src/pages/DemoReactShop/Categories.module.scss b/03_source/mobile/src/pages/DemoReactShop/Categories.module.scss new file mode 100644 index 0000000..4bec6bc --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/Categories.module.scss @@ -0,0 +1,18 @@ +.categoryContainer { + display: flex; + color: white; +} + +.categoryContainer p { + display: flex; + justify-items: center; + justify-content: center; + position: absolute; + + background-color: rgba(0, 0, 0, 0.4); + width: 50%; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + margin: 0 auto; + font-size: 2rem; +} diff --git a/03_source/mobile/src/pages/DemoReactShop/Category.jsx b/03_source/mobile/src/pages/DemoReactShop/Category.jsx new file mode 100644 index 0000000..b87dc0c --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/Category.jsx @@ -0,0 +1,68 @@ +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonIcon, + IonLabel, + IonNote, + IonPage, + IonRouterLink, + IonRow, + IonTitle, + IonToolbar, + useIonRouter, +} from '@ionic/react'; +import { chevronBack } from 'ionicons/icons'; +import { useParams } from 'react-router'; +import { capitalize, productInfo } from '../utils'; + +import styles from './Categories.module.scss'; + +const Category = () => { + const router = useIonRouter(); + const { category } = useParams(); + const productTypes = Object.keys(productInfo[category].productTypes); + + return ( + + + + + router.goBack()}> + + Back + + + {category} + + + + + + + shop + {category} + + + + + + {productTypes.map((product) => ( + +
+ cover +

{capitalize(product)}

+
+
+ ))} +
+
+
+ ); +}; + +export default Category; diff --git a/03_source/mobile/src/pages/DemoReactShop/Favourites.jsx b/03_source/mobile/src/pages/DemoReactShop/Favourites.jsx new file mode 100644 index 0000000..ef53bd7 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/Favourites.jsx @@ -0,0 +1,102 @@ +import { + IonCol, + IonContent, + IonGrid, + IonHeader, + IonIcon, + IonImg, + IonLabel, + IonPage, + IonRow, + IonText, + IonTitle, + IonToolbar, + useIonModal, +} from '@ionic/react'; +import { heartOutline } from 'ionicons/icons'; +import { useStoreState } from 'pullstate'; +import { useState } from 'react'; +import { ProductModal } from '../components/ProductModal'; +import { FavouritesStore } from '../store'; +import { getFavourites } from '../store/Selectors'; + +const Favourites = () => { + const favourites = useStoreState(FavouritesStore, getFavourites); + + const [selectedProduct, setSelectedProduct] = useState([]); + const [presentProductModal, dismissProductModal] = useIonModal(ProductModal, { + dismiss: () => dismissProductModal(), + product: selectedProduct, + }); + + const handleProductModal = (product) => { + setSelectedProduct(product); + presentProductModal(); + }; + + return ( + + + + Favourites + + + + + + Favourites + + + + + + {favourites.map((product, index) => { + if ( + product.image !== null && + product.image !== '' && + !product.image.includes('Placeholder') + ) { + return ( + handleProductModal(product)} + key={index} + size="6" + sizeXs="6" + sizeSm="3" + sizeMd="3" + sizeXl="2" + > + + +

{product.title}

+

{product.price}

+
+
+ ); + } else return null; + })} +
+ + {favourites.length === 0 && ( + + + +

No favourites yet

+
+ + +

+ Add some by clicking the icon on + a product +

+
+
+
+ )} +
+
+
+ ); +}; + +export default Favourites; diff --git a/03_source/mobile/src/pages/DemoReactShop/Helloworld.tsx b/03_source/mobile/src/pages/DemoReactShop/Helloworld.tsx new file mode 100644 index 0000000..87413ac --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/Helloworld.tsx @@ -0,0 +1,37 @@ +// REQ0041/home_discover_event_tab + +import { + IonPage, + IonHeader, + IonToolbar, + IonButtons, + IonButton, + IonIcon, + IonTitle, + IonContent, +} from '@ionic/react'; +import { menuOutline } from 'ionicons/icons'; +import React, { useEffect, useRef, useState } from 'react'; +import './style.scss'; + +const Helloworld: React.FC = () => { + return ( + + + + + {/* */} + + + + + Discover Events + + + + Helloworld + + ); +}; + +export default Helloworld; diff --git a/03_source/mobile/src/pages/DemoReactShop/MainTabs.tsx b/03_source/mobile/src/pages/DemoReactShop/MainTabs.tsx new file mode 100644 index 0000000..1ebadec --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/MainTabs.tsx @@ -0,0 +1,81 @@ +// REQ0116/main-tab + +import React from 'react'; +import { IonTabs, IonRouterOutlet, IonTabBar, IonTabButton, IonIcon, IonLabel } from '@ionic/react'; +import { Route, Redirect } from 'react-router'; +import { calendar, location, informationCircle, people } from 'ionicons/icons'; +import SchedulePage from '../SchedulePage'; +import SpeakerList from '../SpeakerList'; +import SpeakerDetail from '../SpeakerDetail'; +import SessionDetail from '../SessionDetail'; +import MapView from '../MapView'; +import About from '../About'; +import EventList from '../EventList'; +import MembersNearByList from '../MembersNearByList'; +import OrderList from '../OrderList'; +import MyProfile from '../MyProfile'; +import MessageList from '../MessageList'; +import paths from '../../paths'; +import Favourites from '../Favourites'; +import TabAppRoute from '../../TabAppRoute'; + +interface MainTabsProps {} + +const MainTabs: React.FC = () => { + return ( + + + {/* REQ0117/default-route */} + + {/* + Using the render method prop cuts down the number of renders your components will have due to route changes. + Use the component prop when your component depends on the RouterComponentProps passed in automatically. + */} + } exact={true} /> + } exact={true} /> + + + + + } exact={true} /> + + } exact={true} /> + + {/* */} + + + {/* */} + + {/* + + + Speakers + + */} + + + + Event + + + + Nearby + + + + Order + + + + Message + + + + Profile + + + + ); +}; + +export default MainTabs; diff --git a/03_source/mobile/src/pages/DemoReactShop/ProductType.jsx b/03_source/mobile/src/pages/DemoReactShop/ProductType.jsx new file mode 100644 index 0000000..8921dab --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/ProductType.jsx @@ -0,0 +1,211 @@ +import { + IonBreadcrumb, + IonBreadcrumbs, + IonButton, + IonButtons, + IonCol, + IonContent, + IonGrid, + IonHeader, + IonIcon, + IonImg, + IonLabel, + IonNote, + IonPage, + IonRow, + IonSearchbar, + IonTitle, + IonToolbar, + useIonModal, + useIonRouter, +} from '@ionic/react'; +import { chevronBack, filter } from 'ionicons/icons'; +import { useRef } from 'react'; +import { useEffect, useState } from 'react'; +import { useParams } from 'react-router'; +import { FilterModal } from '../components/FilterModal'; +import { ProductModal } from '../components/ProductModal'; +import { capitalize, productInfo } from '../utils'; + +const ProductType = () => { + const router = useIonRouter(); + const { category, type } = useParams(); + const productsRef = useRef(); + + const [products, setProducts] = useState([]); + const [filteredProducts, setFilteredProducts] = useState([]); + const [filterCriteria, setFilterCriteria] = useState('None'); + + const filters = productInfo[category].productTypes[type].filters; + const searchPlaceholder = productInfo[category].productTypes[type].searchPlaceholder; + + const [selectedProduct, setSelectedProduct] = useState([]); + const [presentProductModal, dismissProductModal] = useIonModal(ProductModal, { + dismiss: () => dismissProductModal(), + category, + type, + product: selectedProduct, + }); + + const handleProductModal = (product) => { + setSelectedProduct(product); + presentProductModal(); + }; + + const [present, dismiss] = useIonModal(FilterModal, { + dismiss: () => dismiss(), + + filterCriteria, + setFilterCriteria, + productsRef, + filters, + }); + + useEffect(() => { + const getProducts = async () => { + const response = await fetch(`/data/${category}/${type}.json`); + const data = await response.json(); + setProducts(data); + setFilteredProducts(data); + }; + + getProducts(); + }, [category, type]); + + const openModal = () => { + present({ + breakpoints: [0, 0.25], + initialBreakpoint: 0.25, + backdropBreakpoint: 0, + }); + }; + + const performSearch = (e) => { + const searchCriteria = e.target.value.toLowerCase(); + let tempFilteredProducts = [...products]; + + if (searchCriteria !== '') { + tempFilteredProducts = tempFilteredProducts.filter((product) => + product.title.toLowerCase().includes(searchCriteria) + ); + setFilteredProducts(tempFilteredProducts); + } else { + setFilteredProducts(products); + } + }; + + return ( + + + + + router.goBack()}> + + Back + + + {capitalize(type)} + + + + + + + shop + {category} + + + + + + + + + {capitalize(category)} + + + {capitalize(type)} + + {filterCriteria !== 'None' && ( + + + {filterCriteria} + + )} + + + + +
+ +  Filter +
+
+
+ + performSearch(e)} + /> + + + + {filteredProducts.map((product, index) => { + if ( + product.image !== null && + product.image !== '' && + !product.image.includes('Placeholder') + ) { + return ( + handleProductModal(product)} + key={index} + size="6" + sizeXs="6" + sizeSm="3" + sizeMd="3" + sizeXl="2" + style={{ + display: + (filterCriteria !== 'None' && + product.title.toLowerCase().includes(filterCriteria.toLowerCase())) || + filterCriteria === 'None' + ? 'block' + : 'none', + }} + > + + +

{product.title}

+

{product.price}

+
+
+ ); + } else return null; + })} +
+
+
+
+ ); +}; + +export default ProductType; diff --git a/03_source/mobile/src/pages/DemoReactShop/components/AddToCartButton.jsx b/03_source/mobile/src/pages/DemoReactShop/components/AddToCartButton.jsx new file mode 100644 index 0000000..33810a5 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/components/AddToCartButton.jsx @@ -0,0 +1,58 @@ +import { CreateAnimation, IonButton, IonIcon } from "@ionic/react"; +import { cartOutline } from "ionicons/icons"; +import { useRef, useState } from "react"; +import { addToCart } from "../store/CartStore"; + +export const AddToCartButton = ({product}) => { + + const animationRef = useRef(); + const [hidden, setHidden] = useState(true); + + const floatStyle = { + + display: hidden ? "none" : "", + position: "absolute" + }; + + const floatGrowAnimation = { + + property: "transform", + fromValue: "translateY(0) scale(1)", + toValue: "translateY(-55px) scale(1.5)" + }; + + const colorAnimation = { + + property: "color", + fromValue: "green", + toValue: "green" + }; + + const mainAnimation = { + + duration: 1500, + iterations: "1", + fromTo: [ floatGrowAnimation, colorAnimation ], + easing: "cubic-bezier(0.25, 0.7, 0.25, 0.7)" + }; + + const handleAddToCart = async product => { + + setHidden(false); + await animationRef.current.animation.play(); + setHidden(true); + addToCart(product); + } + + return ( + + handleAddToCart(product)}> +   + Add to Cart + + + + + + ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactShop/components/Breadcrumbs.jsx b/03_source/mobile/src/pages/DemoReactShop/components/Breadcrumbs.jsx new file mode 100644 index 0000000..a5b3263 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/components/Breadcrumbs.jsx @@ -0,0 +1,23 @@ +import { IonBreadcrumb, IonBreadcrumbs, IonIcon } from "@ionic/react"; +import { fastFoodOutline } from "ionicons/icons"; +import { useState } from "react"; + +export const Breadcrumbs = () => { + + const [maxItems, setMaxItems] = useState(2); + + const handleClick = () => { + + setMaxItems(undefined); + } + + return ( + + + Page 1 + Page 2 + Page 3 + Page 4 + + ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactShop/components/CartModal.jsx b/03_source/mobile/src/pages/DemoReactShop/components/CartModal.jsx new file mode 100644 index 0000000..7aa1178 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/components/CartModal.jsx @@ -0,0 +1,109 @@ +import { useStoreState } from 'pullstate'; +import { useEffect, useState } from 'react'; +import { CartStore } from '../store'; +import { addToCart } from '../store/CartStore'; +import { getCart } from '../store/Selectors'; + +const { + IonPage, + IonHeader, + IonToolbar, + IonTitle, + IonButtons, + IonIcon, + IonContent, + IonGrid, + IonRow, + IonItem, + IonLabel, + IonText, + IonThumbnail, + IonFooter, + IonCol, + IonButton, + IonItemSliding, + IonItemOptions, + IonItemOption, +} = require('@ionic/react'); +const { close } = require('ionicons/icons'); + +export const CartModal = (props) => { + const cart = useStoreState(CartStore, getCart); + const [totalPrice, setTotalPrice] = useState(0); + + useEffect(() => { + let total = 0; + cart.forEach((item) => (total += parseInt(item.price.replace('£', '')))); + setTotalPrice(total); + }, [cart]); + + return ( + + + + Cart + + + + + + + + + + +

{cart.length} products in your cart

+ +

Review products and checkout

+
+
+
+
+
+ + {cart.map((item, index) => ( + + + + item + + +

{item.title}

+

{item.price}

+
+
+ + + addToCart(item)}> + Remove + + +
+ ))} +
+ + + + +

Total

+
+ + +

£{totalPrice.toFixed(2)}

+
+
+ + Checkout → + +
+
+ ); +}; diff --git a/03_source/mobile/src/pages/DemoReactShop/components/FilterModal.jsx b/03_source/mobile/src/pages/DemoReactShop/components/FilterModal.jsx new file mode 100644 index 0000000..067aac7 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/components/FilterModal.jsx @@ -0,0 +1,36 @@ +import { IonButton, IonCol, IonContent, IonGrid, IonHeader, IonRow, IonTitle, IonToolbar } from "@ionic/react"; + +export const FilterModal = ({productsRef, filterCriteria, setFilterCriteria, dismiss, filters}) => { + + const filterProducts = async filter => { + + await productsRef.current.classList.add("animate__fadeOutLeft"); + + setTimeout(() => { + productsRef.current.classList.remove("animate__fadeOutLeft"); + productsRef.current.classList.add("animate__fadeInRight"); + setFilterCriteria(filter); + }, 500); + dismiss(); + } + + return ( + + + + + Filter + + + + + {filters.map(f => ( + + filterProducts(f)}>{f} + + ))} + + + + ); + } \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactShop/components/ProductModal.css b/03_source/mobile/src/pages/DemoReactShop/components/ProductModal.css new file mode 100644 index 0000000..8884088 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/components/ProductModal.css @@ -0,0 +1,88 @@ +ion-card { + margin: 0; + /* margin-top: var(--ion-safe-area-top); */ + z-index: -1; + + border-radius: 0px; + + background-size: cover; + background-position: center; + background-repeat: no-repeat; + box-shadow: none; + aspect-ratio: 1 / 1; +} + +@supports not (aspect-ratio: 1 / 1) { + ion-card::before { + float: left; + padding-top: 100%; + content: ''; + } + + ion-card::after { + display: block; + content: ''; + clear: both; + } +} + +ion-card-header { + position: absolute; + bottom: 0; + width: 100%; + + /* background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.7) 100%); */ + background: rgba(0, 0, 0, 0.5) +} + +ion-card-title, +ion-card-subtitle { + color: white; +} + +ion-card-header ion-card-title { + + margin: 0 0 6px 0; + font-size: 22px; +} + +ion-card-header ion-card-subtitle { + + text-transform: none; + font-weight: 500; + font-size: 16px; +} + +ion-card-content { + height: calc(60px + var(--ion-safe-area-top)); + background: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%); +} + +#close-button { + position: fixed; + + top: max(var(--ion-safe-area-top), 16px); + right: 8px; +} + +#fave-button { + position: fixed; + + top: max(var(--ion-safe-area-top), 16px); + left: 8px; +} + +#product-view-buttons { + + z-index: 10; + background: linear-gradient(360deg, rgba(0, 0, 0, 0) 0%, rgba(82, 82, 82, 0.9) 100%) !important; + position: absolute; + width: 100%; + height: 4rem; +} + +.sticky-bottom { + + position: fixed; + bottom: 0; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactShop/components/ProductModal.jsx b/03_source/mobile/src/pages/DemoReactShop/components/ProductModal.jsx new file mode 100644 index 0000000..a519b87 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/components/ProductModal.jsx @@ -0,0 +1,76 @@ +import { IonButton, IonButtons, IonCard, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCol, IonContent, IonFooter, IonIcon, IonLabel, IonNote, IonRow, IonText, IonToolbar } from "@ionic/react"; +import { closeCircle, heart, heartOutline } from "ionicons/icons"; +import { useStoreState } from "pullstate"; +import { useRef } from "react"; + +import { checkFavourites } from "../store/Selectors"; +import { addToFavourites } from "../store/FavouritesStore"; +import { FavouritesStore } from "../store"; + +import "./ProductModal.css"; +import { ProductReviews } from "./ProductReviews"; +import { ProductSpecificationsAccordion } from "./ProductSpecificationsAccordion"; +import { AddToCartButton } from "./AddToCartButton"; + +export const ProductModal = props => { + + const { dismiss, category = false, product } = props; + const isFavourite = useStoreState(FavouritesStore, checkFavourites(product)); + const contentRef = useRef(null); + + return ( + <> + + + + + + + addToFavourites(product, category)} id="fave-button"> + + + + + + + {product.title} + {product.price} + + + +
+ + + + + shop + {category ? category : "Favourite"} + + + + + +

Product Description

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam elit felis, molestie id venenatis at, commodo ac tortor. Pellentesque tempus aliquet purus, sed vulputate elit tempus ut. + +

Product Specifications

+ +
+
+ + + + + + {product.price} + + + + + + + + + + ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactShop/components/ProductReviews.jsx b/03_source/mobile/src/pages/DemoReactShop/components/ProductReviews.jsx new file mode 100644 index 0000000..d564773 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/components/ProductReviews.jsx @@ -0,0 +1,23 @@ +import { IonCol, IonIcon, IonNote } from "@ionic/react"; +import { star } from "ionicons/icons"; +import { useEffect, useState } from "react"; +import { randomCount } from "../utils"; + +export const ProductReviews = () => { + + // This count could come from the product (if real data was fed) + const [reviewCount, setReviewCount] = useState(0); + + useEffect(() => { + + setReviewCount(randomCount()); + }, []); + + return ( + + +    + {reviewCount} review{reviewCount > 1 && "s"} + + ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactShop/components/ProductSpecificationsAccordion.jsx b/03_source/mobile/src/pages/DemoReactShop/components/ProductSpecificationsAccordion.jsx new file mode 100644 index 0000000..77ede79 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/components/ProductSpecificationsAccordion.jsx @@ -0,0 +1,58 @@ +import { IonAccordion, IonAccordionGroup, IonItem, IonLabel, IonList, IonNote } from "@ionic/react"; +import { useRef } from "react"; +import { productSpecs } from "../utils"; + +export const ProductSpecificationsAccordion = ({type, contentRef}) => { + + const accordionGroupRef = useRef(null); + + const log = () => { + + const selectedAccordion = accordionGroupRef.current.value; + + if (selectedAccordion) { + + setTimeout(() => contentRef.current.scrollToBottom(400), 200); + } + } + + return ( + + {Object.keys(productSpecs).map((spec, index) => { + + const {header, options, wrapText = false, noteColor = false} = productSpecs[spec]; + + return ( + + + + {header} + + + + + {options.map((option, index2) => { + + const {label, value} = option; + + return ( + + + +

{label}

+
+ + + {noteColor ? (value ? "In stock" : "Out of stock") : value} + + +
+ ); + })} +
+
+ ); + })} +
+ ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactShop/index.tsx b/03_source/mobile/src/pages/DemoReactShop/index.tsx new file mode 100644 index 0000000..de5618a --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/index.tsx @@ -0,0 +1,129 @@ +// REQ0116/main-tab + +import React, { useRef, useState } from 'react'; +import { + IonTabs, + IonRouterOutlet, + IonTabBar, + IonTabButton, + IonIcon, + IonLabel, + IonModal, +} from '@ionic/react'; +import { Route, Redirect } from 'react-router'; +import { calendar, location, informationCircle, people } from 'ionicons/icons'; +import SchedulePage from '../SchedulePage'; +import SpeakerList from '../SpeakerList'; +import SpeakerDetail from '../SpeakerDetail'; +import SessionDetail from '../SessionDetail'; +import MapView from '../MapView'; +import About from '../About'; +import paths from '../../paths'; +import TabAppRoute from '../../TabAppRoute'; +import { CartStore } from './store'; +import { getCartCount } from './store/Selectors'; +import { CartModal } from './components/CartModal'; + +interface MainTabsProps {} + +const DemoReactShop: React.FC = () => { + const cartCount = useStoreState(CartStore, getCartCount); + const [selected, setSelected] = useState('tab0'); + const [open, setOpen] = useState(false); + const ref = useRef(null); + + const handleClick = (tab) => { + tab === 'tabCart' ? setOpen(true) : setSelected(tab); + }; + + return ( + + + {ReactShopPages.map((page, index) => ( + + ))} + + {/* + + p.default)[0].href} /> + + */} + + + {/* */} + + + {ReactShopPages.map((page, index) => { + const isSelected = selected === `tab${index}`; + + if (page.isTab) { + return ( + + + {isSelected &&
} + + ); + } else return null; + })} + + + +
{cartCount}
+
+ + + {/* setOpen(false)}> */} + {/* setOpen(false)} /> */} + {/* */} + + ); +}; + +export default DemoReactShop; + +import { cartOutline, heartOutline, homeOutline, shirtOutline } from 'ionicons/icons'; + +import Categories from './Categories'; +import Favourites from './Favourites'; +import ProductType from './ProductType'; +import Category from './Category'; +import { useStoreState } from 'pullstate'; + +export const ReactShopPages = [ + { + href: '/categories', + icon: shirtOutline, + component: Categories, + default: true, + isTab: true, + }, + { + href: '/categories/:category/:type', + component: ProductType, + default: false, + isTab: false, + }, + { + href: '/categories/:category', + icon: shirtOutline, + component: Category, + default: true, + isTab: false, + }, + { + href: '/favourites', + icon: heartOutline, + component: Favourites, + default: false, + isTab: true, + }, +]; diff --git a/03_source/mobile/src/pages/DemoReactShop/store/CartStore.js b/03_source/mobile/src/pages/DemoReactShop/store/CartStore.js new file mode 100644 index 0000000..fb9f018 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/store/CartStore.js @@ -0,0 +1,27 @@ +import { Store } from "pullstate"; + +const CartStore = new Store({ + + cart: [] +}); + +export default CartStore; + +export const addToCart = product => { + + const currentCart = CartStore.getRawState().cart; + const added = !currentCart.includes(product); + + CartStore.update(s => { + + if (currentCart.includes(product)) { + + s.cart = currentCart.filter(current => current !== product); + } else { + + s.cart = [ ...s.cart, product ]; + } + }); + + return added; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactShop/store/FavouritesStore.js b/03_source/mobile/src/pages/DemoReactShop/store/FavouritesStore.js new file mode 100644 index 0000000..6ea363d --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/store/FavouritesStore.js @@ -0,0 +1,35 @@ +import { Store } from "pullstate"; + +const FavouritesStore = new Store({ + + favourites: [] +}); + +export default FavouritesStore; + +export const checkIfFavourite = product => { + + const currentFavourites = FavouritesStore.getRawState().favourites; + const isFavourite = currentFavourites.includes(product); + + return isFavourite; +} + +export const addToFavourites = (product, category) => { + + const currentFavourites = FavouritesStore.getRawState().favourites; + const added = !currentFavourites.includes(product); + + FavouritesStore.update(s => { + + if (!added) { + + s.favourites = currentFavourites.filter(current => current !== product); + } else { + + s.favourites = [ ...s.favourites, product ]; + } + }); + + return added; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactShop/store/Selectors.js b/03_source/mobile/src/pages/DemoReactShop/store/Selectors.js new file mode 100644 index 0000000..c5073f7 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/store/Selectors.js @@ -0,0 +1,10 @@ +import { createSelector } from 'reselect'; + +const getState = (state) => state; + +// General getters +export const getFavourites = createSelector(getState, (state) => state.favourites); +export const checkFavourites = (product) => + createSelector(getState, (state) => state.favourites.includes(product)); +export const getCart = createSelector(getState, (state) => state.cart); +export const getCartCount = createSelector(getState, (state) => state.cart.length); diff --git a/03_source/mobile/src/pages/DemoReactShop/store/index.js b/03_source/mobile/src/pages/DemoReactShop/store/index.js new file mode 100644 index 0000000..bbd2f01 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/store/index.js @@ -0,0 +1,2 @@ +export { default as FavouritesStore } from './FavouritesStore'; +export { default as CartStore } from './CartStore'; diff --git a/03_source/mobile/src/pages/DemoReactShop/style.scss b/03_source/mobile/src/pages/DemoReactShop/style.scss new file mode 100644 index 0000000..5fae6e3 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactShop/style.scss @@ -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/img/about/madison.jpg'); + } + + .about-header .austin { + background-image: url('/assets/img/about/austin.jpg'); + } + + .about-header .chicago { + background-image: url('/assets/img/about/chicago.jpg'); + } + + .about-header .seattle { + background-image: url('/assets/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; +} diff --git a/03_source/mobile/src/pages/Helloworld/index.tsx b/03_source/mobile/src/pages/Helloworld/index.tsx index b8e7438..87413ac 100644 --- a/03_source/mobile/src/pages/Helloworld/index.tsx +++ b/03_source/mobile/src/pages/Helloworld/index.tsx @@ -1,8 +1,18 @@ // REQ0041/home_discover_event_tab -import { IonPage, IonHeader, IonToolbar, IonButtons, IonButton, IonIcon, IonTitle, IonContent } from '@ionic/react'; +import { + IonPage, + IonHeader, + IonToolbar, + IonButtons, + IonButton, + IonIcon, + IonTitle, + IonContent, +} from '@ionic/react'; import { menuOutline } from 'ionicons/icons'; import React, { useEffect, useRef, useState } from 'react'; +import './style.scss'; const Helloworld: React.FC = () => { return ( diff --git a/03_source/mobile/src/pages/Helloworld/style.scss b/03_source/mobile/src/pages/Helloworld/style.scss new file mode 100644 index 0000000..5fae6e3 --- /dev/null +++ b/03_source/mobile/src/pages/Helloworld/style.scss @@ -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/img/about/madison.jpg'); + } + + .about-header .austin { + background-image: url('/assets/img/about/austin.jpg'); + } + + .about-header .chicago { + background-image: url('/assets/img/about/chicago.jpg'); + } + + .about-header .seattle { + background-image: url('/assets/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; +} diff --git a/03_source/mobile/src/pages/MainTabs.tsx b/03_source/mobile/src/pages/MainTabs.tsx index bf2d8dd..c7d659b 100644 --- a/03_source/mobile/src/pages/MainTabs.tsx +++ b/03_source/mobile/src/pages/MainTabs.tsx @@ -38,6 +38,7 @@ const MainTabs: React.FC = () => { } exact={true} /> + } exact={true} /> {/* */} diff --git a/03_source/mobile/src/pages/Settings/index.tsx b/03_source/mobile/src/pages/Settings/index.tsx index 934df4d..d984aa7 100644 --- a/03_source/mobile/src/pages/Settings/index.tsx +++ b/03_source/mobile/src/pages/Settings/index.tsx @@ -52,6 +52,8 @@ import { chevronForwardOutline, createOutline, documentTextOutline, + gift, + giftOutline, heart, languageOutline, listCircle, @@ -78,9 +80,9 @@ interface DispatchProps { setIsLoggedIn: typeof setIsLoggedIn; } -interface SpeakerListProps extends OwnProps, StateProps, DispatchProps {} +interface SettingsProps extends OwnProps, StateProps, DispatchProps {} -const EventList: React.FC = ({ +const SettingsPage: React.FC = ({ speakers, speakerSessions, logoutUser, @@ -113,6 +115,10 @@ const EventList: React.FC = ({ router.push(paths.NOT_IMPLEMENTED); } + function handleDemoPageClick() { + router.push(paths.DEMO_PAGE); + } + function handleServiceAgreementClick() { router.push(paths.SERVICE_AGREEMENT); } @@ -196,6 +202,12 @@ const EventList: React.FC = ({ Delete Account + + + + Demo pages + +
@@ -251,7 +263,13 @@ const EventList: React.FC = ({ Unable to receive notifications after logging out
-
+
Cancel @@ -276,5 +294,5 @@ export default connect({ setAccessToken, setIsLoggedIn, }, - component: React.memo(EventList), + component: React.memo(SettingsPage), }); diff --git a/03_source/mobile/src/pages/WeatherDemo/AppPages/Tab1.jsx b/03_source/mobile/src/pages/WeatherDemo/AppPages/Tab1.jsx new file mode 100644 index 0000000..a6f4101 --- /dev/null +++ b/03_source/mobile/src/pages/WeatherDemo/AppPages/Tab1.jsx @@ -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 ( + + + + My Weather + + + getCurrentPosition()}> + + + + + + handleBackClick()}> + + + + + + + + + Dashboard + + + + + +

Here's your location based weather

+
+
+ +
+ {currentWeather ? ( + + ) : ( + + )} +
+
+
+ ); +} + +export default Tab1; diff --git a/03_source/mobile/src/pages/WeatherDemo/AppPages/Tab2.jsx b/03_source/mobile/src/pages/WeatherDemo/AppPages/Tab2.jsx new file mode 100644 index 0000000..216544f --- /dev/null +++ b/03_source/mobile/src/pages/WeatherDemo/AppPages/Tab2.jsx @@ -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 ( + + + + Search + + + + + + Search + + + + + + setSearch(e.target.value)} + /> + + + + + Search + + + + +
+ {currentWeather ? ( + + ) : ( +

Your search result will appear here

+ )} +
+
+
+ ); +} + +export default Tab2; diff --git a/03_source/mobile/src/pages/WeatherDemo/components/CurrentWeather/WeatherProperty.tsx b/03_source/mobile/src/pages/WeatherDemo/components/CurrentWeather/WeatherProperty.tsx new file mode 100644 index 0000000..52949af --- /dev/null +++ b/03_source/mobile/src/pages/WeatherDemo/components/CurrentWeather/WeatherProperty.tsx @@ -0,0 +1,62 @@ +import { IonCardSubtitle, IonCol, IonIcon, IonNote, IonRow } from '@ionic/react'; +import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons'; +import { useEffect, useState } from 'react'; + +export const WeatherProperty = ({ type, currentWeather }: { type: any; currentWeather: any }) => { + const [property, setProperty] = useState(false); + + const properties = { + wind: { + isIcon: false, + icon: '/assets/WeatherDemo/wind.png', + alt: 'wind', + label: 'Wind', + value: `${currentWeather.current.wind_mph}mph`, + }, + feelsLike: { + isIcon: true, + icon: thermometerOutline, + alt: 'feels like', + label: 'Feels like', + value: `${currentWeather.current.feelslike_c}°C`, + }, + indexUV: { + isIcon: true, + icon: sunnyOutline, + alt: 'index uv', + label: 'Index UV', + value: currentWeather.current.uv, + }, + pressure: { + isIcon: true, + icon: pulseOutline, + alt: 'pressure', + label: 'Pressure', + value: `${currentWeather.current.pressure_mb} mbar`, + }, + }; + + useEffect(() => { + setProperty(properties[type]); + }, [type]); + + return ( + + + + {!property.isIcon && ( + {property.alt} + )} + {property.isIcon && ( + + )} + + + + {property.label} + {property.value} + + + + ); +}; diff --git a/03_source/mobile/src/pages/WeatherDemo/components/CurrentWeather/index.tsx b/03_source/mobile/src/pages/WeatherDemo/components/CurrentWeather/index.tsx new file mode 100644 index 0000000..ceb4332 --- /dev/null +++ b/03_source/mobile/src/pages/WeatherDemo/components/CurrentWeather/index.tsx @@ -0,0 +1,48 @@ +import { IonCard, IonCardContent, IonGrid, IonRow, IonText, IonCardTitle } from '@ionic/react'; +import { WeatherProperty } from './WeatherProperty'; + +export const CurrentWeather = ({ currentWeather }: { currentWeather: any }) => ( + + + + +

+ {currentWeather.location.region},{' '} + {currentWeather.location.country} +

+
+ +
+ condition + + +

{currentWeather.current.condition.text}

+
+ + +

{new Date(currentWeather.location.localtime).toDateString()}

+
+
+ + + {currentWeather.current.temp_c}℃ + + + + + + + + + + + + + +
+
+
+); diff --git a/03_source/mobile/src/pages/WeatherDemo/components/SkeletonDashboard/index.tsx b/03_source/mobile/src/pages/WeatherDemo/components/SkeletonDashboard/index.tsx new file mode 100644 index 0000000..234fb9b --- /dev/null +++ b/03_source/mobile/src/pages/WeatherDemo/components/SkeletonDashboard/index.tsx @@ -0,0 +1,117 @@ +import { + IonCard, + IonCardContent, + IonCardSubtitle, + IonCardTitle, + IonCol, + IonGrid, + IonIcon, + IonNote, + IonRow, + IonSkeletonText, + IonText, + IonThumbnail, +} from '@ionic/react'; +import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons'; + +export const SkeletonDashboard = () => ( + + + + +

+ +

+
+ +
+ + + + + +

+ +

+
+ + +

+ +

+
+
+ + + + + + + + + + + wind + + + + Wind + + + + + + + + + + + + + + + Feels like + + + + + + + + + + + + + + + + + Index UV + + + + + + + + + + + + + + + Pressure + + + + + + + + +
+
+
+); diff --git a/03_source/mobile/src/pages/WeatherDemo/index.tsx b/03_source/mobile/src/pages/WeatherDemo/index.tsx new file mode 100644 index 0000000..25df523 --- /dev/null +++ b/03_source/mobile/src/pages/WeatherDemo/index.tsx @@ -0,0 +1,59 @@ +import { + IonButton, + IonButtons, + IonCol, + IonContent, + IonHeader, + IonIcon, + IonLabel, + IonPage, + IonRouterOutlet, + IonRow, + IonTabBar, + IonTabButton, + IonTabs, + IonTitle, + IonToolbar, + useIonRouter, +} from '@ionic/react'; + +import { Geolocation } from '@capacitor/geolocation'; +import { useEffect, useState } from 'react'; +import { SkeletonDashboard } from './components/SkeletonDashboard'; +import { chevronBack, cloudOutline, refreshOutline, searchOutline } from 'ionicons/icons'; +import { CurrentWeather } from './components/CurrentWeather'; +import { IonReactRouter } from '@ionic/react-router'; +import { Route, Redirect } from 'react-router'; +import Tab1 from './AppPages/Tab1'; +import Tab2 from './AppPages/Tab2'; + +const DemoWeatherApp = () => { + return ( + + + + + + + + + + + + + {/* */} + + + + Dashboard + + + + Search + + + + ); +}; + +export default DemoWeatherApp; diff --git a/03_source/mobile/src/pages/WeatherDemo/style.scss b/03_source/mobile/src/pages/WeatherDemo/style.scss new file mode 100644 index 0000000..37c1e1a --- /dev/null +++ b/03_source/mobile/src/pages/WeatherDemo/style.scss @@ -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; +} diff --git a/03_source/mobile/src/pages/components/AddToCartButton.jsx b/03_source/mobile/src/pages/components/AddToCartButton.jsx new file mode 100644 index 0000000..33810a5 --- /dev/null +++ b/03_source/mobile/src/pages/components/AddToCartButton.jsx @@ -0,0 +1,58 @@ +import { CreateAnimation, IonButton, IonIcon } from "@ionic/react"; +import { cartOutline } from "ionicons/icons"; +import { useRef, useState } from "react"; +import { addToCart } from "../store/CartStore"; + +export const AddToCartButton = ({product}) => { + + const animationRef = useRef(); + const [hidden, setHidden] = useState(true); + + const floatStyle = { + + display: hidden ? "none" : "", + position: "absolute" + }; + + const floatGrowAnimation = { + + property: "transform", + fromValue: "translateY(0) scale(1)", + toValue: "translateY(-55px) scale(1.5)" + }; + + const colorAnimation = { + + property: "color", + fromValue: "green", + toValue: "green" + }; + + const mainAnimation = { + + duration: 1500, + iterations: "1", + fromTo: [ floatGrowAnimation, colorAnimation ], + easing: "cubic-bezier(0.25, 0.7, 0.25, 0.7)" + }; + + const handleAddToCart = async product => { + + setHidden(false); + await animationRef.current.animation.play(); + setHidden(true); + addToCart(product); + } + + return ( + + handleAddToCart(product)}> +   + Add to Cart + + + + + + ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/components/Breadcrumbs.jsx b/03_source/mobile/src/pages/components/Breadcrumbs.jsx new file mode 100644 index 0000000..a5b3263 --- /dev/null +++ b/03_source/mobile/src/pages/components/Breadcrumbs.jsx @@ -0,0 +1,23 @@ +import { IonBreadcrumb, IonBreadcrumbs, IonIcon } from "@ionic/react"; +import { fastFoodOutline } from "ionicons/icons"; +import { useState } from "react"; + +export const Breadcrumbs = () => { + + const [maxItems, setMaxItems] = useState(2); + + const handleClick = () => { + + setMaxItems(undefined); + } + + return ( + + + Page 1 + Page 2 + Page 3 + Page 4 + + ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/components/CartModal.jsx b/03_source/mobile/src/pages/components/CartModal.jsx new file mode 100644 index 0000000..ce7c030 --- /dev/null +++ b/03_source/mobile/src/pages/components/CartModal.jsx @@ -0,0 +1,82 @@ +import { useStoreState } from "pullstate"; +import { useEffect, useState } from "react"; +import { CartStore } from "../store"; +import { addToCart } from "../store/CartStore"; +import { getCart } from "../store/Selectors"; + +const { IonPage, IonHeader, IonToolbar, IonTitle, IonButtons, IonIcon, IonContent, IonGrid, IonRow, IonItem, IonLabel, IonText, IonThumbnail, IonFooter, IonCol, IonButton, IonItemSliding, IonItemOptions, IonItemOption } = require("@ionic/react"); +const { close } = require("ionicons/icons"); + +export const CartModal = props => { + + const cart = useStoreState(CartStore, getCart); + const [totalPrice, setTotalPrice] = useState(0); + + useEffect(() => { + + let total = 0; + cart.forEach(item => total += parseInt(item.price.replace("£", ""))); + setTotalPrice(total); + }, [cart]); + + return ( + + + + Cart + + + + + + + + + + + +

{cart.length} products in your cart

+ +

Review products and checkout

+
+
+
+
+
+ + {cart.map((item, index) => ( + + + + item + + +

{item.title}

+

{item.price}

+
+
+ + + addToCart(item)}> + Remove + + +
+ ))} +
+ + + + +

Total

+
+ + +

£{totalPrice.toFixed(2)}

+
+
+ Checkout → +
+
+ ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/components/FilterModal.jsx b/03_source/mobile/src/pages/components/FilterModal.jsx new file mode 100644 index 0000000..067aac7 --- /dev/null +++ b/03_source/mobile/src/pages/components/FilterModal.jsx @@ -0,0 +1,36 @@ +import { IonButton, IonCol, IonContent, IonGrid, IonHeader, IonRow, IonTitle, IonToolbar } from "@ionic/react"; + +export const FilterModal = ({productsRef, filterCriteria, setFilterCriteria, dismiss, filters}) => { + + const filterProducts = async filter => { + + await productsRef.current.classList.add("animate__fadeOutLeft"); + + setTimeout(() => { + productsRef.current.classList.remove("animate__fadeOutLeft"); + productsRef.current.classList.add("animate__fadeInRight"); + setFilterCriteria(filter); + }, 500); + dismiss(); + } + + return ( + + + + + Filter + + + + + {filters.map(f => ( + + filterProducts(f)}>{f} + + ))} + + + + ); + } \ No newline at end of file diff --git a/03_source/mobile/src/pages/components/ProductModal.css b/03_source/mobile/src/pages/components/ProductModal.css new file mode 100644 index 0000000..8884088 --- /dev/null +++ b/03_source/mobile/src/pages/components/ProductModal.css @@ -0,0 +1,88 @@ +ion-card { + margin: 0; + /* margin-top: var(--ion-safe-area-top); */ + z-index: -1; + + border-radius: 0px; + + background-size: cover; + background-position: center; + background-repeat: no-repeat; + box-shadow: none; + aspect-ratio: 1 / 1; +} + +@supports not (aspect-ratio: 1 / 1) { + ion-card::before { + float: left; + padding-top: 100%; + content: ''; + } + + ion-card::after { + display: block; + content: ''; + clear: both; + } +} + +ion-card-header { + position: absolute; + bottom: 0; + width: 100%; + + /* background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.7) 100%); */ + background: rgba(0, 0, 0, 0.5) +} + +ion-card-title, +ion-card-subtitle { + color: white; +} + +ion-card-header ion-card-title { + + margin: 0 0 6px 0; + font-size: 22px; +} + +ion-card-header ion-card-subtitle { + + text-transform: none; + font-weight: 500; + font-size: 16px; +} + +ion-card-content { + height: calc(60px + var(--ion-safe-area-top)); + background: linear-gradient(0deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.5) 100%); +} + +#close-button { + position: fixed; + + top: max(var(--ion-safe-area-top), 16px); + right: 8px; +} + +#fave-button { + position: fixed; + + top: max(var(--ion-safe-area-top), 16px); + left: 8px; +} + +#product-view-buttons { + + z-index: 10; + background: linear-gradient(360deg, rgba(0, 0, 0, 0) 0%, rgba(82, 82, 82, 0.9) 100%) !important; + position: absolute; + width: 100%; + height: 4rem; +} + +.sticky-bottom { + + position: fixed; + bottom: 0; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/components/ProductModal.jsx b/03_source/mobile/src/pages/components/ProductModal.jsx new file mode 100644 index 0000000..a519b87 --- /dev/null +++ b/03_source/mobile/src/pages/components/ProductModal.jsx @@ -0,0 +1,76 @@ +import { IonButton, IonButtons, IonCard, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCol, IonContent, IonFooter, IonIcon, IonLabel, IonNote, IonRow, IonText, IonToolbar } from "@ionic/react"; +import { closeCircle, heart, heartOutline } from "ionicons/icons"; +import { useStoreState } from "pullstate"; +import { useRef } from "react"; + +import { checkFavourites } from "../store/Selectors"; +import { addToFavourites } from "../store/FavouritesStore"; +import { FavouritesStore } from "../store"; + +import "./ProductModal.css"; +import { ProductReviews } from "./ProductReviews"; +import { ProductSpecificationsAccordion } from "./ProductSpecificationsAccordion"; +import { AddToCartButton } from "./AddToCartButton"; + +export const ProductModal = props => { + + const { dismiss, category = false, product } = props; + const isFavourite = useStoreState(FavouritesStore, checkFavourites(product)); + const contentRef = useRef(null); + + return ( + <> + + + + + + + addToFavourites(product, category)} id="fave-button"> + + + + + + + {product.title} + {product.price} + + + +
+ + + + + shop + {category ? category : "Favourite"} + + + + + +

Product Description

+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam elit felis, molestie id venenatis at, commodo ac tortor. Pellentesque tempus aliquet purus, sed vulputate elit tempus ut. + +

Product Specifications

+ +
+
+ + + + + + {product.price} + + + + + + + + + + ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/components/ProductReviews.jsx b/03_source/mobile/src/pages/components/ProductReviews.jsx new file mode 100644 index 0000000..d564773 --- /dev/null +++ b/03_source/mobile/src/pages/components/ProductReviews.jsx @@ -0,0 +1,23 @@ +import { IonCol, IonIcon, IonNote } from "@ionic/react"; +import { star } from "ionicons/icons"; +import { useEffect, useState } from "react"; +import { randomCount } from "../utils"; + +export const ProductReviews = () => { + + // This count could come from the product (if real data was fed) + const [reviewCount, setReviewCount] = useState(0); + + useEffect(() => { + + setReviewCount(randomCount()); + }, []); + + return ( + + +    + {reviewCount} review{reviewCount > 1 && "s"} + + ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/components/ProductSpecificationsAccordion.jsx b/03_source/mobile/src/pages/components/ProductSpecificationsAccordion.jsx new file mode 100644 index 0000000..77ede79 --- /dev/null +++ b/03_source/mobile/src/pages/components/ProductSpecificationsAccordion.jsx @@ -0,0 +1,58 @@ +import { IonAccordion, IonAccordionGroup, IonItem, IonLabel, IonList, IonNote } from "@ionic/react"; +import { useRef } from "react"; +import { productSpecs } from "../utils"; + +export const ProductSpecificationsAccordion = ({type, contentRef}) => { + + const accordionGroupRef = useRef(null); + + const log = () => { + + const selectedAccordion = accordionGroupRef.current.value; + + if (selectedAccordion) { + + setTimeout(() => contentRef.current.scrollToBottom(400), 200); + } + } + + return ( + + {Object.keys(productSpecs).map((spec, index) => { + + const {header, options, wrapText = false, noteColor = false} = productSpecs[spec]; + + return ( + + + + {header} + + + + + {options.map((option, index2) => { + + const {label, value} = option; + + return ( + + + +

{label}

+
+ + + {noteColor ? (value ? "In stock" : "Out of stock") : value} + + +
+ ); + })} +
+
+ ); + })} +
+ ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/store/CartStore.js b/03_source/mobile/src/pages/store/CartStore.js new file mode 100644 index 0000000..fb9f018 --- /dev/null +++ b/03_source/mobile/src/pages/store/CartStore.js @@ -0,0 +1,27 @@ +import { Store } from "pullstate"; + +const CartStore = new Store({ + + cart: [] +}); + +export default CartStore; + +export const addToCart = product => { + + const currentCart = CartStore.getRawState().cart; + const added = !currentCart.includes(product); + + CartStore.update(s => { + + if (currentCart.includes(product)) { + + s.cart = currentCart.filter(current => current !== product); + } else { + + s.cart = [ ...s.cart, product ]; + } + }); + + return added; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/store/FavouritesStore.js b/03_source/mobile/src/pages/store/FavouritesStore.js new file mode 100644 index 0000000..6ea363d --- /dev/null +++ b/03_source/mobile/src/pages/store/FavouritesStore.js @@ -0,0 +1,35 @@ +import { Store } from "pullstate"; + +const FavouritesStore = new Store({ + + favourites: [] +}); + +export default FavouritesStore; + +export const checkIfFavourite = product => { + + const currentFavourites = FavouritesStore.getRawState().favourites; + const isFavourite = currentFavourites.includes(product); + + return isFavourite; +} + +export const addToFavourites = (product, category) => { + + const currentFavourites = FavouritesStore.getRawState().favourites; + const added = !currentFavourites.includes(product); + + FavouritesStore.update(s => { + + if (!added) { + + s.favourites = currentFavourites.filter(current => current !== product); + } else { + + s.favourites = [ ...s.favourites, product ]; + } + }); + + return added; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/store/Selectors.js b/03_source/mobile/src/pages/store/Selectors.js new file mode 100644 index 0000000..960d7df --- /dev/null +++ b/03_source/mobile/src/pages/store/Selectors.js @@ -0,0 +1,9 @@ +import { createSelector } from 'reselect'; + +const getState = state => state; + +// General getters +export const getFavourites = createSelector(getState, state => state.favourites); +export const checkFavourites = product => createSelector(getState, state => state.favourites.includes(product)); +export const getCart = createSelector(getState, state => state.cart); +export const getCartCount = createSelector(getState, state => state.cart.length); \ No newline at end of file diff --git a/03_source/mobile/src/pages/store/index.js b/03_source/mobile/src/pages/store/index.js new file mode 100644 index 0000000..0be7fa7 --- /dev/null +++ b/03_source/mobile/src/pages/store/index.js @@ -0,0 +1,2 @@ +export { default as FavouritesStore } from "./FavouritesStore"; +export { default as CartStore } from "./CartStore"; diff --git a/03_source/mobile/src/pages/utils/index.js b/03_source/mobile/src/pages/utils/index.js new file mode 100644 index 0000000..9303337 --- /dev/null +++ b/03_source/mobile/src/pages/utils/index.js @@ -0,0 +1,143 @@ +export const capitalize = (s) => s && (s[0].toUpperCase() + s.slice(1)).replaceAll('_', ' '); + +export const productInfo = { + men: { + coverImage: '/assets/react-shop/men.jpeg', + productTypes: { + formal_shirts: { + coverImage: '/assets/react-shop/formal_shirts2.jpeg', + filters: ['None', 'Regular', 'Slim', 'Stretch'], + searchPlaceholder: 'Single Cuff', + }, + sportswear: { + coverImage: '/assets/react-shop/sportswear2.jpeg', + filters: ['None', 'Trainers', 'Joggers', 'Shorts', 'Hoodie'], + searchPlaceholder: 'Nike', + }, + coats: { + coverImage: '/assets/react-shop/coats3.jpeg', + filters: ['None', 'Funnel', 'Hooded', 'Barbour', 'Collar'], + searchPlaceholder: 'Bomber', + }, + }, + }, + women: { + coverImage: '/assets/react-shop/women.jpeg', + productTypes: { + jeans: { + coverImage: '/assets/react-shop/jeans.jpeg', + filters: ['None', 'Skinny', 'Slim', 'Boot Cut', 'Flare'], + searchPlaceholder: 'Skinny', + }, + dresses: { + coverImage: '/assets/react-shop/dresses3.jpeg', + filters: ['None', 'Short', 'Maxi', 'Long', 'Regular'], + searchPlaceholder: 'Long Sleeve', + }, + makeup: { + coverImage: '/assets/react-shop/makeup2.jpeg', + filters: ['None', 'Mascara', 'Lip Gloss', 'Foundation', 'Blush'], + searchPlaceholder: 'Brush Set', + }, + }, + }, + home: { + coverImage: '/assets/react-shop/home.jpeg', + productTypes: { + beds: { + coverImage: '/assets/react-shop/beds.jpeg', + filters: ['None', 'Metal', 'Ottoman', 'Storage', 'Wooden'], + searchPlaceholder: 'Upholstered', + }, + office: { + coverImage: '/assets/react-shop/office.jpeg', + filters: ['None', 'Desk', 'Chair', 'Lamp', 'Shelf'], + searchPlaceholder: 'Space Saving', + }, + coffee_tables: { + coverImage: '/assets/react-shop/coffee_table.jpeg', + filters: ['None', 'Wood', 'Glass', 'Round', 'Storage'], + searchPlaceholder: 'Oak Effect', + }, + }, + }, +}; + +export const productSpecs = { + dimensions: { + header: 'Dimensions', + options: [ + { + label: 'Height', + value: '100cm', + }, + { + label: 'Width', + value: '130cm', + }, + { + label: 'Depth', + value: '150cm', + }, + ], + }, + shipping: { + header: 'Shipping', + options: [ + { + label: 'UK', + value: '£4.99', + }, + { + label: 'USA', + value: '£6.99', + }, + { + label: 'Gloal', + value: '£9.99', + }, + ], + }, + colors: { + header: 'Colors', + noteColor: true, + options: [ + { + label: 'Red', + value: true, + }, + { + label: 'Blue', + value: false, + }, + { + label: 'Brown', + value: true, + }, + ], + }, + sizes: { + header: 'Sizes', + wrapText: true, + options: [ + { + label: 'Large', + value: 'Check size guide for details', + }, + { + label: 'Width', + value: 'Check size guide for details', + }, + { + label: 'Depth', + value: 'Check size guide for details', + }, + ], + }, +}; + +export const randomCount = () => { + const max = 273; + const min = 23; + return Math.floor(Math.random() * (max - min) + min).toFixed(0); +}; diff --git a/03_source/mobile/src/paths.ts b/03_source/mobile/src/paths.ts index 4ae423b..31ba1b2 100644 --- a/03_source/mobile/src/paths.ts +++ b/03_source/mobile/src/paths.ts @@ -21,5 +21,9 @@ const paths = { PROFILE: '/tabs/my_profile', // SIGN_IN: '/mylogin', + // + DEMO_PAGE: '/tabs/demo-list', + DEMO_WEATHER_APP: '/demo-weather-app', + DEMO_REACT_SHOP: '/demo-react-shop', }; export default paths; diff --git a/03_source/mobile/tsconfig.json b/03_source/mobile/tsconfig.json index 3be20a0..cd4eb38 100644 --- a/03_source/mobile/tsconfig.json +++ b/03_source/mobile/tsconfig.json @@ -21,7 +21,8 @@ "useDefineForClassFields": true }, "include": [ - "src" + "src", + "src/pages/Settings/index.tsx" ], "exclude": [ "node_modules", diff --git a/03_source/mobile/yarn.lock b/03_source/mobile/yarn.lock index dcc335f..df8873f 100644 --- a/03_source/mobile/yarn.lock +++ b/03_source/mobile/yarn.lock @@ -24,7 +24,7 @@ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz" integrity sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ== -"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.26.10": +"@babel/core@^7.26.10": version "7.27.1" resolved "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz" integrity sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ== @@ -203,13 +203,20 @@ tslib "^2.8.1" xml2js "^0.6.2" -"@capacitor/core@^7.0.0", "@capacitor/core@>=7.0.0": +"@capacitor/core@^7.0.0": version "7.2.0" resolved "https://registry.npmjs.org/@capacitor/core/-/core-7.2.0.tgz" integrity sha512-2zCnA6RJeZ9ec4470o8QMZEQTWpekw9FNoqm5TLc10jeCrhvHVI8MPgxdZVc3mOdFlyieYu4AS1fNxSqbS57Pw== dependencies: tslib "^2.1.0" +"@capacitor/geolocation@^7.1.2": + version "7.1.2" + resolved "https://registry.npmjs.org/@capacitor/geolocation/-/geolocation-7.1.2.tgz" + integrity sha512-J++OuOpn6Bjweo7SZ+jwI/dhVF9DNw6Wx0UHgQ4qfrATqmNKGNZ/BUljGhXiKJceSx2GIhfrYS7BYqo3uWibgQ== + dependencies: + "@capacitor/synapse" "^1.0.1" + "@capacitor/ios@7.0.1": version "7.0.1" resolved "https://registry.npmjs.org/@capacitor/ios/-/ios-7.0.1.tgz" @@ -220,11 +227,136 @@ resolved "https://registry.npmjs.org/@capacitor/preferences/-/preferences-7.0.1.tgz" integrity sha512-XF9jOHzvoIBZLwZr/EX6aVaUO1d8Mx7TwBLQS33pYHOliCW5knT5KUkFOXNNYxh9qqODYesee9xuQIKNJpQBag== +"@capacitor/synapse@^1.0.1": + version "1.0.2" + resolved "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.2.tgz" + integrity sha512-ynq39s4D2rhk+aVLWKfKfMCz9SHPKijL9tq8aFL5dG7ik7/+PvBHmg9cPHbqdvFEUSMmaGzL6cIjzkOruW7vGA== + +"@esbuild/aix-ppc64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz#830d6476cbbca0c005136af07303646b419f1162" + integrity sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q== + +"@esbuild/android-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz#d11d4fc299224e729e2190cacadbcc00e7a9fd67" + integrity sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A== + +"@esbuild/android-arm@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.4.tgz#5660bd25080553dd2a28438f2a401a29959bd9b1" + integrity sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ== + +"@esbuild/android-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.4.tgz#18ddde705bf984e8cd9efec54e199ac18bc7bee1" + integrity sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ== + +"@esbuild/darwin-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz#b0b7fb55db8fc6f5de5a0207ae986eb9c4766e67" + integrity sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g== + +"@esbuild/darwin-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz#e6813fdeba0bba356cb350a4b80543fbe66bf26f" + integrity sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A== + +"@esbuild/freebsd-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz#dc11a73d3ccdc308567b908b43c6698e850759be" + integrity sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ== + +"@esbuild/freebsd-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz#91da08db8bd1bff5f31924c57a81dab26e93a143" + integrity sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ== + +"@esbuild/linux-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz#efc15e45c945a082708f9a9f73bfa8d4db49728a" + integrity sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ== + +"@esbuild/linux-arm@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz#9b93c3e54ac49a2ede6f906e705d5d906f6db9e8" + integrity sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ== + +"@esbuild/linux-ia32@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz#be8ef2c3e1d99fca2d25c416b297d00360623596" + integrity sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ== + +"@esbuild/linux-loong64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz#b0840a2707c3fc02eec288d3f9defa3827cd7a87" + integrity sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA== + +"@esbuild/linux-mips64el@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz#2a198e5a458c9f0e75881a4e63d26ba0cf9df39f" + integrity sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg== + +"@esbuild/linux-ppc64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz#64f4ae0b923d7dd72fb860b9b22edb42007cf8f5" + integrity sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag== + +"@esbuild/linux-riscv64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz#fb2844b11fdddd39e29d291c7cf80f99b0d5158d" + integrity sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA== + +"@esbuild/linux-s390x@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz#1466876e0aa3560c7673e63fdebc8278707bc750" + integrity sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g== + "@esbuild/linux-x64@0.25.4": version "0.25.4" resolved "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz" integrity sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA== +"@esbuild/netbsd-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz#02e483fbcbe3f18f0b02612a941b77be76c111a4" + integrity sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ== + +"@esbuild/netbsd-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz#ec401fb0b1ed0ac01d978564c5fc8634ed1dc2ed" + integrity sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw== + +"@esbuild/openbsd-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz#f272c2f41cfea1d91b93d487a51b5c5ca7a8c8c4" + integrity sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A== + +"@esbuild/openbsd-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz#2e25950bc10fa9db1e5c868e3d50c44f7c150fd7" + integrity sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw== + +"@esbuild/sunos-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz#cd596fa65a67b3b7adc5ecd52d9f5733832e1abd" + integrity sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q== + +"@esbuild/win32-arm64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz#b4dbcb57b21eeaf8331e424c3999b89d8951dc88" + integrity sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ== + +"@esbuild/win32-ia32@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz#410842e5d66d4ece1757634e297a87635eb82f7a" + integrity sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg== + +"@esbuild/win32-x64@0.25.4": + version "0.25.4" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz#0b17ec8a70b2385827d52314c1253160a0b9bacc" + integrity sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ== + "@hookform/resolvers@^4.1.3": version "4.1.3" resolved "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz" @@ -258,7 +390,7 @@ "@ionic/react" "8.5.7" tslib "*" -"@ionic/react@^8.5.0", "@ionic/react@8.5.7": +"@ionic/react@8.5.7", "@ionic/react@^8.5.0": version "8.5.7" resolved "https://registry.npmjs.org/@ionic/react/-/react-8.5.7.tgz" integrity sha512-AgX4iu6SfuBhNgYr0H+K3oGsp7ESkCsnaqZdHRO2+GtKTmo4akMrFPihGj4LrZB/IaYwcvYQR/bPWHuZGJYsnw== @@ -275,7 +407,7 @@ debug "^4.0.0" tslib "^2.0.1" -"@ionic/utils-fs@^3.1.7", "@ionic/utils-fs@3.1.7": +"@ionic/utils-fs@3.1.7", "@ionic/utils-fs@^3.1.7": version "3.1.7" resolved "https://registry.npmjs.org/@ionic/utils-fs/-/utils-fs-3.1.7.tgz" integrity sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA== @@ -327,7 +459,7 @@ debug "^4.0.0" tslib "^2.0.1" -"@ionic/utils-terminal@^2.3.4", "@ionic/utils-terminal@^2.3.5", "@ionic/utils-terminal@2.3.5": +"@ionic/utils-terminal@2.3.5", "@ionic/utils-terminal@^2.3.4", "@ionic/utils-terminal@^2.3.5": version "2.3.5" resolved "https://registry.npmjs.org/@ionic/utils-terminal/-/utils-terminal-2.3.5.tgz" integrity sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A== @@ -412,6 +544,46 @@ dependencies: "@types/mdx" "^2.0.0" +"@parcel/watcher-android-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz#507f836d7e2042f798c7d07ad19c3546f9848ac1" + integrity sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA== + +"@parcel/watcher-darwin-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz#3d26dce38de6590ef79c47ec2c55793c06ad4f67" + integrity sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw== + +"@parcel/watcher-darwin-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz#99f3af3869069ccf774e4ddfccf7e64fd2311ef8" + integrity sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg== + +"@parcel/watcher-freebsd-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz#14d6857741a9f51dfe51d5b08b7c8afdbc73ad9b" + integrity sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ== + +"@parcel/watcher-linux-arm-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz#43c3246d6892381db473bb4f663229ad20b609a1" + integrity sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA== + +"@parcel/watcher-linux-arm-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz#663750f7090bb6278d2210de643eb8a3f780d08e" + integrity sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q== + +"@parcel/watcher-linux-arm64-glibc@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz#ba60e1f56977f7e47cd7e31ad65d15fdcbd07e30" + integrity sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w== + +"@parcel/watcher-linux-arm64-musl@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz#f7fbcdff2f04c526f96eac01f97419a6a99855d2" + integrity sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg== + "@parcel/watcher-linux-x64-glibc@2.5.1": version "2.5.1" resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz" @@ -422,6 +594,21 @@ resolved "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz" integrity sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg== +"@parcel/watcher-win32-arm64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz#7e9e02a26784d47503de1d10e8eab6cceb524243" + integrity sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw== + +"@parcel/watcher-win32-ia32@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz#2d0f94fa59a873cdc584bf7f6b1dc628ddf976e6" + integrity sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ== + +"@parcel/watcher-win32-x64@2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz#ae52693259664ba6f2228fa61d7ee44b64ea0947" + integrity sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA== + "@parcel/watcher@^2.4.1": version "2.5.1" resolved "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz" @@ -456,6 +643,81 @@ resolved "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz" integrity sha512-e9MeMtVWo186sgvFFJOPGy7/d2j2mZhLJIdVW0C/xDluuOvymEATqz6zKsP0ZmXGzQtqlyjz5sC1sYQUoJG98w== +"@rollup/rollup-android-arm-eabi@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.1.tgz#f39f09f60d4a562de727c960d7b202a2cf797424" + integrity sha512-NELNvyEWZ6R9QMkiytB4/L4zSEaBC03KIXEghptLGLZWJ6VPrL63ooZQCOnlx36aQPGhzuOMwDerC1Eb2VmrLw== + +"@rollup/rollup-android-arm64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.1.tgz#d19af7e23760717f1d879d4ca3d2cd247742dff2" + integrity sha512-DXdQe1BJ6TK47ukAoZLehRHhfKnKg9BjnQYUu9gzhI8Mwa1d2fzxA1aw2JixHVl403bwp1+/o/NhhHtxWJBgEA== + +"@rollup/rollup-darwin-arm64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.1.tgz#1c3a2fbf205d80641728e05f4a56c909e95218b7" + integrity sha512-5afxvwszzdulsU2w8JKWwY8/sJOLPzf0e1bFuvcW5h9zsEg+RQAojdW0ux2zyYAz7R8HvvzKCjLNJhVq965U7w== + +"@rollup/rollup-darwin-x64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.1.tgz#aa66d2ba1a25e609500e13bef06dc0e71cc0c0d4" + integrity sha512-egpJACny8QOdHNNMZKf8xY0Is6gIMz+tuqXlusxquWu3F833DcMwmGM7WlvCO9sB3OsPjdC4U0wHw5FabzCGZg== + +"@rollup/rollup-freebsd-arm64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.1.tgz#df10a7b6316a0ef1028c6ca71a081124c537e30d" + integrity sha512-DBVMZH5vbjgRk3r0OzgjS38z+atlupJ7xfKIDJdZZL6sM6wjfDNo64aowcLPKIx7LMQi8vybB56uh1Ftck/Atg== + +"@rollup/rollup-freebsd-x64@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.1.tgz#a3fdce8a05e95b068cbcb46e4df5185e407d0c35" + integrity sha512-3FkydeohozEskBxNWEIbPfOE0aqQgB6ttTkJ159uWOFn42VLyfAiyD9UK5mhu+ItWzft60DycIN1Xdgiy8o/SA== + +"@rollup/rollup-linux-arm-gnueabihf@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.1.tgz#49f766c55383bd0498014a9d76924348c2f3890c" + integrity sha512-wC53ZNDgt0pqx5xCAgNunkTzFE8GTgdZ9EwYGVcg+jEjJdZGtq9xPjDnFgfFozQI/Xm1mh+D9YlYtl+ueswNEg== + +"@rollup/rollup-linux-arm-musleabihf@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.1.tgz#1d4d7d32fc557e17d52e1857817381ea365e2959" + integrity sha512-jwKCca1gbZkZLhLRtsrka5N8sFAaxrGz/7wRJ8Wwvq3jug7toO21vWlViihG85ei7uJTpzbXZRcORotE+xyrLA== + +"@rollup/rollup-linux-arm64-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.1.tgz#f4fc317268441e9589edad3be8f62b6c03009bc1" + integrity sha512-g0UBcNknsmmNQ8V2d/zD2P7WWfJKU0F1nu0k5pW4rvdb+BIqMm8ToluW/eeRmxCared5dD76lS04uL4UaNgpNA== + +"@rollup/rollup-linux-arm64-musl@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.1.tgz#63a1f1b0671cb17822dabae827fef0e443aebeb7" + integrity sha512-XZpeGB5TKEZWzIrj7sXr+BEaSgo/ma/kCgrZgL0oo5qdB1JlTzIYQKel/RmhT6vMAvOdM2teYlAaOGJpJ9lahg== + +"@rollup/rollup-linux-loongarch64-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.1.tgz#c659b01cc6c0730b547571fc3973e1e955369f98" + integrity sha512-bkCfDJ4qzWfFRCNt5RVV4DOw6KEgFTUZi2r2RuYhGWC8WhCA8lCAJhDeAmrM/fdiAH54m0mA0Vk2FGRPyzI+tw== + +"@rollup/rollup-linux-powerpc64le-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.1.tgz#612e746f9ad7e58480f964d65e0d6c3f4aae69a8" + integrity sha512-3mr3Xm+gvMX+/8EKogIZSIEF0WUu0HL9di+YWlJpO8CQBnoLAEL/roTCxuLncEdgcfJcvA4UMOf+2dnjl4Ut1A== + +"@rollup/rollup-linux-riscv64-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.1.tgz#4610dbd1dcfbbae32fbc10c20ae7387acb31110c" + integrity sha512-3rwCIh6MQ1LGrvKJitQjZFuQnT2wxfU+ivhNBzmxXTXPllewOF7JR1s2vMX/tWtUYFgphygxjqMl76q4aMotGw== + +"@rollup/rollup-linux-riscv64-musl@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.1.tgz#054911fab40dc83fafc21e470193c058108f19d8" + integrity sha512-LdIUOb3gvfmpkgFZuccNa2uYiqtgZAz3PTzjuM5bH3nvuy9ty6RGc/Q0+HDFrHrizJGVpjnTZ1yS5TNNjFlklw== + +"@rollup/rollup-linux-s390x-gnu@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.1.tgz#98896eca8012547c7f04bd07eaa6896825f9e1a5" + integrity sha512-oIE6M8WC9ma6xYqjvPhzZYk6NbobIURvP/lEbh7FWplcMO6gn7MM2yHKA1eC/GvYwzNKK/1LYgqzdkZ8YFxR8g== + "@rollup/rollup-linux-x64-gnu@4.41.1": version "4.41.1" resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.1.tgz" @@ -466,6 +728,21 @@ resolved "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.1.tgz" integrity sha512-y5CbN44M+pUCdGDlZFzGGBSKCA4A/J2ZH4edTYSSxFg7ce1Xt3GtydbVKWLlzL+INfFIZAEg1ZV6hh9+QQf9YQ== +"@rollup/rollup-win32-arm64-msvc@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.1.tgz#7eeada98444e580674de6989284e4baacd48ea65" + integrity sha512-lZkCxIrjlJlMt1dLO/FbpZbzt6J/A8p4DnqzSa4PWqPEUUUnzXLeki/iyPLfV0BmHItlYgHUqJe+3KiyydmiNQ== + +"@rollup/rollup-win32-ia32-msvc@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.1.tgz#516c4b54f80587b4a390aaf4940b40870271d35d" + integrity sha512-+psFT9+pIh2iuGsxFYYa/LhS5MFKmuivRsx9iPJWNSGbh2XVEjk90fmpUEjCnILPEPJnikAU6SFDiEUyOv90Pg== + +"@rollup/rollup-win32-x64-msvc@4.41.1": + version "4.41.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.1.tgz#848f99b0d9936d92221bb6070baeff4db6947a30" + integrity sha512-Wq2zpapRYLfi4aKxf2Xff0tN+7slj2d4R87WEzqw7ZLsVvO5zwYCIuEGSZYiK41+GlwUo1HiR+GdkLEJnCKTCw== + "@sheerun/mutationobserver-shim@^0.3.2": version "0.3.3" resolved "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz" @@ -476,7 +753,7 @@ resolved "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz" integrity sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g== -"@stencil/core@^4.0.3", "@stencil/core@4.20.0": +"@stencil/core@4.20.0", "@stencil/core@^4.0.3": version "4.20.0" resolved "https://registry.npmjs.org/@stencil/core/-/core-4.20.0.tgz" integrity sha512-WPrTHFngvN081RY+dJPneKQLwnOFD60OMCOQGmmSHfCW0f4ujPMzzhwWU1gcSwXPWXz5O+8cBiiCaxAbJU7kAg== @@ -550,7 +827,7 @@ dependencies: "@types/estree" "*" -"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@1.0.7": +"@types/estree@*", "@types/estree@1.0.7", "@types/estree@^1.0.0": version "1.0.7" resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz" integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== @@ -648,7 +925,7 @@ resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz" integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA== -"@types/node@*", "@types/node@^18.0.0 || ^20.0.0 || >=22.0.0": +"@types/node@*": version "22.15.21" resolved "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz" integrity sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ== @@ -687,7 +964,7 @@ "@types/history" "^4.7.11" "@types/react" "*" -"@types/react@*", "@types/react@^18.2.25 || ^19", "@types/react@^19.0.0", "@types/react@>=16", "@types/react@>=18", "@types/react@19.0.10": +"@types/react@*", "@types/react@19.0.10": version "19.0.10" resolved "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz" integrity sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g== @@ -890,7 +1167,7 @@ braces@^3.0.2: dependencies: fill-range "^7.1.1" -browserslist@^4.24.0, "browserslist@>= 4.21.0": +browserslist@^4.24.0: version "4.24.5" resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz" integrity sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw== @@ -923,6 +1200,11 @@ ccount@^2.0.0: resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz" integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== +chalk@5.3.0: + version "5.3.0" + resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" + integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== + chalk@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz" @@ -931,11 +1213,6 @@ chalk@^3.0.0: ansi-styles "^4.1.0" supports-color "^7.1.0" -chalk@5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz" - integrity sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w== - character-entities-html4@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz" @@ -997,16 +1274,16 @@ color-convert@^2.0.1: dependencies: color-name "~1.1.4" -color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - color-name@1.1.3: version "1.1.3" resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz" integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + colorette@^2.0.20: version "2.0.20" resolved "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz" @@ -1024,16 +1301,16 @@ comma-separated-tokens@^2.0.0: resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz" integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== -commander@^12.1.0: - version "12.1.0" - resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz" - integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== - commander@11.0.0: version "11.0.0" resolved "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz" integrity sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ== +commander@^12.1.0: + version "12.1.0" + resolved "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz" + integrity sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA== + convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz" @@ -1087,13 +1364,6 @@ date-fns@^2.25.0: dependencies: "@babel/runtime" "^7.21.0" -debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.4, debug@^4.4.0: - version "4.4.1" - resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz" - integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== - dependencies: - ms "^2.1.3" - debug@4.3.4: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" @@ -1101,6 +1371,13 @@ debug@4.3.4: dependencies: ms "2.1.2" +debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.4, debug@^4.4.0: + version "4.4.1" + resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz" + integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ== + dependencies: + ms "^2.1.3" + decode-named-character-reference@^1.0.0: version "1.1.0" resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz" @@ -1364,6 +1641,11 @@ fs-minipass@^2.0.0: dependencies: minipass "^3.0.0" +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + function-bind@^1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" @@ -1516,6 +1798,11 @@ hyphenate-style-name@^1.0.3: resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz" integrity sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw== +immer@^9.0.16: + version "9.0.21" + resolved "https://registry.yarnpkg.com/immer/-/immer-9.0.21.tgz#1e025ea31a40f24fb064f1fef23e931496330176" + integrity sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== + immutable@^5.0.2: version "5.1.2" resolved "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz" @@ -1678,7 +1965,7 @@ kleur@^4.1.5: resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz" integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== -leaflet@^1.9.0, leaflet@^1.9.4: +leaflet@^1.9.4: version "1.9.4" resolved "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz" integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA== @@ -2065,7 +2352,7 @@ micromark@^4.0.0: micromark-util-symbol "^2.0.0" micromark-util-types "^2.0.0" -micromatch@^4.0.5, micromatch@4.0.5: +micromatch@4.0.5, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -2132,16 +2419,16 @@ mkdirp@^1.0.3: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -ms@^2.1.3: - version "2.1.3" - resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + nano-css@^5.6.2: version "5.6.2" resolved "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz" @@ -2281,7 +2568,7 @@ picomatch@^2.3.1: resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== -"picomatch@^3 || ^4", picomatch@^4.0.2: +picomatch@^4.0.2: version "4.0.2" resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz" integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== @@ -2361,14 +2648,22 @@ proxy-from-env@^1.1.0: resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz" integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== -react-dom@*, "react-dom@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", react-dom@^19.0.0, react-dom@>=16.8.6, react-dom@19.0.0: +pullstate@^2.0.0-pre.0: + version "2.0.0-pre.0" + resolved "https://registry.yarnpkg.com/pullstate/-/pullstate-2.0.0-pre.0.tgz#30dd108112b520ded57c35bdb10929314466da40" + integrity sha512-lvMKp36NYcl1fVsm7v36CRokEEJUW2j8IlwvOMqbYeFe/shIscDoT+boW1CXFMy08EG8KO1X9pyOFCxXjMh4jw== + dependencies: + fast-deep-equal "^3.1.3" + immer "^9.0.16" + +react-dom@19.0.0: version "19.0.0" resolved "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz" integrity sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ== dependencies: scheduler "^0.25.0" -react-hook-form@^7.0.0, react-hook-form@^7.55.0: +react-hook-form@^7.55.0: version "7.57.0" resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.57.0.tgz" integrity sha512-RbEks3+cbvTP84l/VXGUZ+JMrKOS8ykQCRYdm5aYsxnDquL0vspsyNhGRO7pcH6hsZqWlPOjLye7rJqdtdAmlg== @@ -2415,7 +2710,7 @@ react-refresh@^0.17.0: resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz" integrity sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ== -react-router-dom@^5.0.1, react-router-dom@^5.3.4: +react-router-dom@^5.3.4: version "5.3.4" resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz" integrity sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ== @@ -2428,7 +2723,7 @@ react-router-dom@^5.0.1, react-router-dom@^5.3.4: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-router@^5.0.1, react-router@^5.3.4, react-router@5.3.4: +react-router@5.3.4, react-router@^5.3.4: version "5.3.4" resolved "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz" integrity sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA== @@ -2473,7 +2768,7 @@ react-use@^17.6.0: ts-easing "^0.2.0" tslib "^2.1.0" -react@*, "react@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^18.0 || ^19", react@^19.0.0, react@>=15, react@>=16, react@>=16.8.6, react@>=18, react@19.0.0: +react@19.0.0: version "19.0.0" resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz" integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== @@ -2499,11 +2794,6 @@ redux@^4.0.0: dependencies: "@babel/runtime" "^7.9.2" -redux@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz" - integrity sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w== - remark-parse@^11.0.0: version "11.0.0" resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz" @@ -2602,7 +2892,7 @@ safe-buffer@~5.2.0: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== -sass@*, sass@^1.85.1: +sass@^1.85.1: version "1.89.0" resolved "https://registry.npmjs.org/sass/-/sass-1.89.0.tgz" integrity sha512-ld+kQU8YTdGNjOLfRWBzewJpU5cwEv/h5yyqlSeJcj6Yh8U4TDA9UA5FPicqDz/xgRPWRSYIQNiFks21TbA9KQ== @@ -2613,7 +2903,7 @@ sass@*, sass@^1.85.1: optionalDependencies: "@parcel/watcher" "^2.4.1" -sax@>=0.6.0, sax@1.1.4: +sax@1.1.4, sax@>=0.6.0: version "1.1.4" resolved "https://registry.npmjs.org/sax/-/sax-1.1.4.tgz" integrity sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg== @@ -2687,21 +2977,21 @@ slice-ansi@^5.0.0: ansi-styles "^6.0.0" is-fullwidth-code-point "^4.0.0" -source-map-js@^1.2.1, "source-map-js@>=0.6.2 <2.0.0": +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== -source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - source-map@0.5.6: version "0.5.6" resolved "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz" integrity sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA== +source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + space-separated-tokens@^2.0.0: version "2.0.2" resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz" @@ -2746,13 +3036,6 @@ stacktrace-js@^2.0.2: stack-generator "^2.0.5" stacktrace-gps "^3.0.4" -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - string-argv@0.3.2: version "0.3.2" resolved "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz" @@ -2776,7 +3059,7 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.1" -string-width@^5.0.0: +string-width@^5.0.0, string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== @@ -2785,14 +3068,12 @@ string-width@^5.0.0: emoji-regex "^9.2.2" strip-ansi "^7.0.1" -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" + safe-buffer "~5.2.0" stringify-entities@^4.0.0: version "4.0.4" @@ -3055,7 +3336,7 @@ vfile@^6.0.0: "@types/unist" "^3.0.0" vfile-message "^4.0.0" -"vite@^4.2.0 || ^5.0.0 || ^6.0.0", vite@^6.2.0: +vite@^6.2.0: version "6.3.5" resolved "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz" integrity sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ== @@ -3099,16 +3380,7 @@ wrap-ansi@^7.0.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^8.0.1: - version "8.1.0" - resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - -wrap-ansi@^8.1.0: +wrap-ansi@^8.0.1, wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz" integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== @@ -3145,11 +3417,6 @@ yallist@^4.0.0: resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== -yaml@^2.4.2: - version "2.8.0" - resolved "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz" - integrity sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ== - yaml@2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/yaml/-/yaml-2.3.1.tgz"