diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/AppPages/Tab1.jsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/AppPages/Tab1.tsx similarity index 98% rename from 03_source/mobile/src/pages/DemoReactTabsMenusCustom/AppPages/Tab1.jsx rename to 03_source/mobile/src/pages/DemoReactTabsMenusCustom/AppPages/Tab1.tsx index a24d76a..d0c5dc7 100644 --- a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/AppPages/Tab1.jsx +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/AppPages/Tab1.tsx @@ -18,7 +18,7 @@ import { SkeletonDashboard } from '../TestComponents/SkeletonDashboard'; import { chevronBackOutline, refreshOutline } from 'ionicons/icons'; import { CurrentWeather } from '../TestComponents/CurrentWeather'; -function Tab1() { +function Tab1(): React.JSX.Element { const router = useIonRouter(); const [currentWeather, setCurrentWeather] = useState(false); diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/AppPages/Tab2.jsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/AppPages/Tab2.tsx similarity index 98% rename from 03_source/mobile/src/pages/DemoReactTabsMenusCustom/AppPages/Tab2.jsx rename to 03_source/mobile/src/pages/DemoReactTabsMenusCustom/AppPages/Tab2.tsx index c258179..1b41aed 100644 --- a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/AppPages/Tab2.jsx +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/AppPages/Tab2.tsx @@ -12,7 +12,7 @@ import { import { useState } from 'react'; import { CurrentWeather } from '../TestComponents/CurrentWeather'; -function Tab2() { +function Tab2(): React.JSX.Element { const [search, setSearch] = useState(''); const [currentWeather, setCurrentWeather] = useState(false); diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/TestComponents/CurrentWeather/WeatherProperty.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/TestComponents/CurrentWeather/WeatherProperty.tsx index 52949af..029315d 100644 --- a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/TestComponents/CurrentWeather/WeatherProperty.tsx +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/TestComponents/CurrentWeather/WeatherProperty.tsx @@ -2,8 +2,31 @@ 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); +interface WeatherPropertyProps { + type: 'wind' | 'feelsLike' | 'indexUV' | 'pressure'; + currentWeather: { + current: { + wind_mph: number; + feelslike_c: number; + uv: number; + pressure_mb: number; + }; + }; +} + +interface PropertyType { + isIcon: boolean; + icon: string; + alt: string; + label: string; + value: string; +} + +export const WeatherProperty = ({ + type, + currentWeather, +}: WeatherPropertyProps): React.JSX.Element => { + const [property, setProperty] = useState(false); const properties = { wind: { diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/TestComponents/CurrentWeather/index.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/TestComponents/CurrentWeather/index.tsx index ceb4332..f710f29 100644 --- a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/TestComponents/CurrentWeather/index.tsx +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/TestComponents/CurrentWeather/index.tsx @@ -1,7 +1,24 @@ import { IonCard, IonCardContent, IonGrid, IonRow, IonText, IonCardTitle } from '@ionic/react'; import { WeatherProperty } from './WeatherProperty'; -export const CurrentWeather = ({ currentWeather }: { currentWeather: any }) => ( +interface CurrentWeatherProps { + currentWeather: { + location: { + region: string; + country: string; + localtime: string; + }; + current: { + condition: { + icon: string; + text: string; + }; + temp_c: number; + }; + }; +} + +export const CurrentWeather = ({ currentWeather }: CurrentWeatherProps): React.JSX.Element => ( diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/components/Modal.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/components/Modal.tsx new file mode 100644 index 0000000..af4dad4 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/components/Modal.tsx @@ -0,0 +1,59 @@ +import { + IonGrid, + IonRow, + IonCol, + IonModal, + IonButtons, + IonButton, + IonIcon, + IonContent, + IonHeader, + IonToolbar, + IonTitle, +} from '@ionic/react'; +import { chevronBack } from 'ionicons/icons'; + +interface ModalProps { + showModal: boolean; + close: (value: boolean) => void; + modalOptions: { + text?: string; + name?: string; + icon?: string; + }; +} + +export const Modal = (props: ModalProps): React.JSX.Element => ( + + + + + {props.modalOptions.text ? props.modalOptions.text : props.modalOptions.name} + + + props.close(false)}> + + + + + + + + + + + + + + + {props.modalOptions.name && ( + + +

{props.modalOptions.name}

+
+
+ )} +
+
+
+); diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/components/PageHeader.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/components/PageHeader.tsx new file mode 100644 index 0000000..258ee41 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/components/PageHeader.tsx @@ -0,0 +1,17 @@ +import { IonRow, IonCol, IonCardSubtitle, IonCardTitle } from '@ionic/react'; + +interface PageHeaderProps { + pageName: string; + count: number; +} + +export const PageHeader = (props: PageHeaderProps): React.JSX.Element => ( + + + Tab Menu with Side Menu + + {props.pageName} page with {props.count} side menu options + + + +); diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/index.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/index.tsx index 59ce634..9ff86d4 100644 --- a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/index.tsx +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/index.tsx @@ -19,7 +19,11 @@ function DemoReactTabsMenusCustom() { - + {/* */} diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/CustomPage.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/CustomPage.tsx new file mode 100644 index 0000000..a406d9b --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/CustomPage.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { IonHeader, IonContent, IonToolbar, IonTitle, IonButtons, IonMenuButton, IonBackButton, IonIcon, IonSearchbar } from '@ionic/react'; +import { chevronBack } from 'ionicons/icons'; + +const CustomPage = (props) => { + + const mainContent = props.children; + const { + name, + sideMenu = false, + sideMenuPosition = "end", + backButton = false, + backButtonIcon = chevronBack, + backButtonText = " ", + backButtonPath, + actionButton = false, + actionButtonPosition, + actionButtonIcon, + actionButtonIconSize, + actionButtonClickEvent, + contentClass, + searchbar = false, + searchbarEvent, + showLargeHeader = true + } = props; + + return ( + <> + + + { name } + + { backButton && + + + + } + + { (actionButton && actionButtonIcon) && + + + + } + + { sideMenu && + + + + } + + + + + + { showLargeHeader && + + + { name } + { searchbar && searchbarEvent(e) } onChange={ e => searchbarEvent(e) } /> } + + + } + { mainContent } + + + ); +} + +export default CustomPage; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/PageSideMenus.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/PageSideMenus.tsx new file mode 100644 index 0000000..f0bc900 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/PageSideMenus.tsx @@ -0,0 +1,34 @@ +import { archiveOutline, beerOutline, cogOutline, eyeOutline, golfOutline, logOutOutline, mailOutline, mailUnreadOutline, mapOutline, personOutline, pulseOutline, refreshOutline, restaurantOutline, settingsOutline } from "ionicons/icons"; +import { buildSideMenuObject } from "./Utils"; + +export const tab1SideMenu = [ + + buildSideMenuObject(false, "Inbox", "Navigates to Inbox page", mailOutline, "/tabs/tab2"), + buildSideMenuObject(false, "Places", "Navigates to Places page", mapOutline, "/tabs/tab3"), + buildSideMenuObject(true), + buildSideMenuObject(false, "Account Settings", null, settingsOutline, null), + buildSideMenuObject(false, "Settings Sub Page", "Opens settings sub page", cogOutline, "/settings"), + buildSideMenuObject(false, "Privacy", null, eyeOutline, null), + buildSideMenuObject(false, "Logout", null, logOutOutline, null) +]; + +export const tab2SideMenu = [ + + buildSideMenuObject(false, "Profile", "Navigates to Profile page", personOutline, "/tabs/tab1"), + buildSideMenuObject(false, "Places", "Navigates to Places page", mapOutline, "/tabs/tab3"), + buildSideMenuObject(true), + buildSideMenuObject(false, "Unread", null, mailUnreadOutline, null), + buildSideMenuObject(false, "Archived", null, archiveOutline, null), + buildSideMenuObject(false, "Timestamp style", "Changes the style of the timestamp", refreshOutline, null) +]; + +export const tab3SideMenu = [ + + buildSideMenuObject(false, "Profile", "Navigates to Profile page", personOutline, "/tabs/tab1"), + buildSideMenuObject(false, "Inbox", "Navigates to Inbox page", mailOutline, "/tabs/tab2"), + buildSideMenuObject(true), + buildSideMenuObject(false, "Pubs", null, beerOutline, null), + buildSideMenuObject(false, "Restaurants", null, restaurantOutline, null), + buildSideMenuObject(false, "Golf Courses", null, golfOutline, null), + buildSideMenuObject(false, "Hospitals", null, pulseOutline, null) +]; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/SideMenu.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/SideMenu.tsx new file mode 100644 index 0000000..577b5ac --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/SideMenu.tsx @@ -0,0 +1,63 @@ +import { IonHeader, IonContent, IonToolbar, IonTitle, IonMenuToggle, IonItem, IonIcon, IonMenu, IonLabel, IonList, IonListHeader } from '@ionic/react'; +import { useSideMenu } from "../main/SideMenuProvider"; + +import "../theme/SideMenu.css"; + +const SideMenu = (props) => { + + const { type = "overlay" } = props; + const mainContent = props.children; + const menuOptions = useSideMenu(); + + return ( + + + + Menu + + + + + + { mainContent } + + { menuOptions.pageName } + + { menuOptions !== null && + + { menuOptions && menuOptions.options.map((menuOption, i) => { + + if (menuOption.url === null) { + + return ( + + + + + { menuOption.text } + + + ); + } else { + + if (menuOption.url !== null) { + return ( + + + + + { menuOption.text } + + + ); + } + } + })} + + } + + + ); +} + +export default SideMenu; diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/SideMenuProvider.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/SideMenuProvider.tsx new file mode 100644 index 0000000..df8705e --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/SideMenuProvider.tsx @@ -0,0 +1,31 @@ +import React, { useContext, useState } from "react"; + +const SideMenuContext = React.createContext(); +const SideMenuUpdateContext = React.createContext(); + +export function useSideMenu() { + return useContext(SideMenuContext); +} + +export function useSideMenuUpdate() { + + return useContext(SideMenuUpdateContext); +} + +export function SideMenuProvider({ children }) { + + const [ sideMenuOptions, setSideMenuOptions ] = useState({ options: [], side: "", pageName: "" }); + + const setSideMenu = (menuOptions) => { + + setSideMenuOptions(menuOptions); + } + + return ( + + + {children} + + + ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/TabMenu.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/TabMenu.tsx new file mode 100644 index 0000000..9a2f6ce --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/TabMenu.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { IonIcon, IonLabel, IonTabBar, IonTabButton, IonTabs, IonRouterOutlet } from "@ionic/react"; +import { Redirect, Route } from "react-router-dom"; + +const TabMenu = (props) => { + + return ( + + + + { props.tabs.map((tab, i) => { + + const TabComponent = tab.component; + + if (tab.isTab) { + return } exact={ true }/>; + } else { + + return } exact={ false } />; + } + })} + + + + + { props.tabs.map((tab, i) => { + + if (tab.isTab) { + + return ( + + + { tab.label && { tab.label } } + + ); + } + })} + + + ); +} + +export default TabMenu; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/Utils.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/Utils.tsx new file mode 100644 index 0000000..71fd84d --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/Utils.tsx @@ -0,0 +1,173 @@ +import { beerOutline, golfOutline, pulseOutline, restaurantOutline } from "ionicons/icons"; + +export const getInboxItems = () => { + + return [ + + { + id: 1, + sender: "Github", + subject: "Host your code here", + message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + time: "3 mins ago", + unread: true + }, + { + id: 2, + sender: "Ionic", + subject: "Amazing cross platform apps on the web", + message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + time: "2 hrs ago", + unread: false + }, + { + id: 3, + sender: "Capacitor", + subject: "This is why capacitor is awesome", + message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + time: "Yesterday", + unread: false + }, + { + id: 4, + sender: "ReactJS", + subject: "Get ready for React 2021", + message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + time: "Yesterday", + unread: true + }, + { + id: 5, + sender: "ContextAPI", + subject: "Global state management!", + message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + time: "2 days ago", + unread: true + }, + { + id: 6, + sender: "Javascript", + subject: "The best language", + message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + time: "3 days ago", + unread: false + }, + { + id: 7, + sender: "Mobile app development", + subject: "Bring your solutions to mobile", + message: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + time: "4 days ago", + unread: false + } + ]; +} + +export const getInboxItemByID = ID => { + + const inboxItems = getInboxItems(); + const inboxItem = inboxItems.filter(i => parseInt(i.id) === parseInt(ID))[0]; + return inboxItem; +} + +export const getPlaceItems = () => { + + const places = [ + { + name: "Rusty Tavern", + rating: 8, + type: "pub", + icon: beerOutline + }, + { + name: "Meat Mall", + rating: 5, + type: "restaurant", + icon: restaurantOutline + }, + { + name: "Lousy Lager", + rating: 10, + type: "pub", + icon: beerOutline + }, + { + name: "Hole in one", + rating: 4, + type: "golf", + icon: golfOutline + }, + { + name: "Relief center", + rating: 9, + type: "hospital", + icon: pulseOutline + }, + { + name: "Yummy yams", + rating: 2, + type: "restaurant", + icon: restaurantOutline + }, + { + name: "Under power of others", + rating: 7, + type: "golf", + icon: golfOutline + }, + { + name: "Belfast General", + rating: 10, + type: "hospital", + icon: pulseOutline + }, + ]; + + return places; +} + +/** + * + * @param {Boolean} spacer Renders a space between above and below item + * @param {String} text The text or "label" to show + * @param {String} description The description to show under the text + * @param {*} icon The icon to show - This should be an imported Ion icon + * @param {String} url The url to navigate to e.g. "/tabs/tab2" + * @param {Function} clickEvent A click event to perform instead of url, leave blank and set in component if it's specific (Should be written like () => function()) + * @returns A side menu object + */ +export const buildSideMenuObject = (spacer = false, text = "", description = "", icon = false, url = null, clickEvent = null) => { + + const title = text; + + if (description !== "" && description !== null) { + + text = getInformativeSideMenuItem(text, description); + } + + return spacer ? {} : { + + title, + text, + icon, + url, + clickEvent + }; +} + +/** + * + * @param {*} text Text of a side menu object + * @param {*} description Description of a side menu object + * @returns A span and h6 holding the text and description + */ +const getInformativeSideMenuItem = (text, description) => { + + return ( + <> + { text } +
+
{ description }
+ + ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/nav/AllRoutes.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/nav/AllRoutes.tsx new file mode 100644 index 0000000..88d47a0 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/nav/AllRoutes.tsx @@ -0,0 +1,74 @@ +// Main Tabs +import Tab1 from "../../pages/Tab1" +import Tab2 from "../../pages/Tab2"; +import Tab3 from "../../pages/Tab3"; + +// Side Menus +import { tab1SideMenu, tab2SideMenu, tab3SideMenu } from "../PageSideMenus"; + +// Main tab children +import Settings from "../../pages/Settings"; + +// Sub pages +import InboxItem from "../../pages/InboxItem"; + +// Tab icons +import { personOutline, mailOutline, mapOutline } from "ionicons/icons"; + +// Import custom tab menu +import TabMenu from "../TabMenu"; +import SubRoutes from "./SubRoutes"; + +// Array of objects representing tab pages +// These will be the main tabs across the app + +// * PARAMS per tab object * +// isTab = true will make the tab appear +// default = the default tab page to open and be redirected to at "/" +// NOTE: there should only be one default tab (default: true) +// label = the label to show with the tab +// component = the component related to this tab page +// icon = icon to show on the tab bar menu +// path = the path which the tab is accessible +export const tabRoutes = [ + + { label: "Profile", component: Tab1, icon: personOutline, path: "/tabs/tab1", default: true, isTab: true, sideMenu: true, sideMenuOptions: tab1SideMenu }, + { label: "Inbox", component: Tab2, icon: mailOutline, path: "/tabs/tab2", default: false, isTab: true, sideMenu: true, sideMenuOptions: tab2SideMenu }, + { label: "Places", component: Tab3, icon: mapOutline, path: "/tabs/tab3", default: false, isTab: true, sideMenu: true, sideMenuOptions: tab3SideMenu } +]; + +// Array of objects representing children pages of tabs + +// * PARAMS per tab object * +// isTab = should always be set to false for these +// component = the component related to this tab page +// path = the path which the tab is accessible + +// These pages should be related to tab pages and be held within the same path +// E.g. /tabs/tab1/child +const tabChildrenRoutes = [ + + { component: InboxItem, path: "/tabs/tab2/:id", isTab: false }, +]; + +// Array of objects representing sub pages + +// * PARAMS per tab object * +// component = the component related to this sub page +// path = the path which the sub page is accessible + +// This array should be sub pages which are not directly related to a tab page +// E.g. /child +const subPageRoutes = [ + + { component: Settings, path: "/settings" }, +]; + +// Let's combine these together as they need to be controlled within the same IonRouterOutlet +const tabsAndChildrenRoutes = [ ...tabRoutes, ...tabChildrenRoutes ]; + +// Render sub routes +export const SubPages = () => ( ); + +// Render tab menu +export const Tabs = () => ( ); \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/nav/NavRoutes.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/nav/NavRoutes.tsx new file mode 100644 index 0000000..83bfae8 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/nav/NavRoutes.tsx @@ -0,0 +1,28 @@ +import { IonRouterOutlet, IonSplitPane } from "@ionic/react"; +import { IonReactRouter } from "@ionic/react-router"; +import { Redirect, Route } from "react-router-dom"; +import SideMenu from "../SideMenu"; +import { SubPages, Tabs, tabRoutes } from "./AllRoutes"; + +const NavRoutes = () => { + + return ( + + + + + + + + } /> + + + t.default)[0].component } exact={ true } /> + t.default)[0].path.toString() }/> + + + + ); +} + +export default NavRoutes; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/nav/SubRoutes.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/nav/SubRoutes.tsx new file mode 100644 index 0000000..221cb3d --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/main/nav/SubRoutes.tsx @@ -0,0 +1,17 @@ +import { Route } from "react-router-dom"; + +const SubRoutes = (props) => { + + return ( + <> + { props.routes.map((route, i) => { + + const RouteComponent = route.component; + + return } exact={ false } />; + })} + + ); +} + +export default SubRoutes; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/InboxItem.css b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/InboxItem.css new file mode 100644 index 0000000..a08b727 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/InboxItem.css @@ -0,0 +1,40 @@ +#view-inbox-item ion-item { + --inner-padding-end: 0; + --background: transparent; +} + +#view-inbox-item ion-label { + margin-top: 12px; + margin-bottom: 12px; +} + +#view-inbox-item ion-item h2 { + font-weight: 600; +} + +#view-inbox-item ion-item .date { + float: right; + align-items: center; + display: flex; +} + +#view-inbox-item ion-item ion-icon { + font-size: 42px; + margin-right: 8px; +} + +#view-inbox-item ion-item ion-note { + font-size: 15px; + margin-right: 12px; + font-weight: normal; +} + +#view-inbox-item h1 { + margin: 0; + font-weight: bold; + font-size: 22px; +} + +#view-inbox-item p { + line-height: 22px; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/InboxItem.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/InboxItem.tsx new file mode 100644 index 0000000..778c0ac --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/InboxItem.tsx @@ -0,0 +1,60 @@ +import { useEffect, useState } from "react"; +import { personCircle } from 'ionicons/icons'; + +import './Tab2.css'; +import CustomPage from "../main/CustomPage"; + +import { IonIcon, IonItem, IonLabel, IonNote, IonPage, useIonViewWillEnter } from '@ionic/react'; +import { useParams } from "react-router"; +import { getInboxItemByID } from "../main/Utils"; + +import "./InboxItem.css"; + +const InboxItem = props => { + + const pageName = "Inbox"; + const params = useParams(); + + const [ inboxItem, setInboxItem ] = useState({}); + + useIonViewWillEnter(() => { + + const inboxItemID = params.id; + const tempInboxItem = getInboxItemByID(inboxItemID); + setInboxItem(tempInboxItem); + }); + + return ( + + + { inboxItem ? ( + <> + + + +

+ { inboxItem.sender } + + { inboxItem.time } + +

+

+ To: Me +

+
+
+ +
+

{ inboxItem.subject }

+

{ inboxItem.message }

+
+ + ) : ( +
Message not found
+ )} +
+
+ ); +} + +export default InboxItem; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Settings.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Settings.tsx new file mode 100644 index 0000000..1d06d79 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Settings.tsx @@ -0,0 +1,27 @@ +import { IonCol, IonGrid, IonPage, IonRow } from '@ionic/react'; +import { addOutline } from 'ionicons/icons'; + +import './Tab1.css'; +import CustomPage from "../main/CustomPage"; + +const Settings = props => { + + const pageName = "Settings"; + + return ( + + + + + + +

Sub page

+
+
+
+
+
+ ); +} + +export default Settings; diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab1.css b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab1.css new file mode 100644 index 0000000..2271539 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab1.css @@ -0,0 +1,5 @@ +.role { + float: right; + align-items: center; + display: flex; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab1.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab1.tsx new file mode 100644 index 0000000..1703402 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab1.tsx @@ -0,0 +1,94 @@ +import { useEffect, useState } from "react"; +import { IonAvatar, IonBadge, IonButton, IonCol, IonGrid, IonIcon, IonImg, IonItem, IonLabel, IonNote, IonPage, IonRow, IonText } from '@ionic/react'; +import { cogOutline, eyeOutline, logOutOutline, mailOutline, mapOutline, settingsOutline } from 'ionicons/icons'; + +import './Tab1.css'; +import CustomPage from "../main/CustomPage"; + +import { PageHeader } from "../components/PageHeader"; +import { Modal } from "../components/Modal"; +import { useSideMenuUpdate, useSideMenu } from "../main/SideMenuProvider"; +import { Link } from "react-router-dom"; +import { tab1SideMenu } from "../main/PageSideMenus"; + +const Tab1 = props => { + + const pageName = "Profile"; + const { sideMenuOptions } = props; + const setSideMenu = useSideMenuUpdate(); + + const [ showModal, setShowModal ] = useState(false); + const [ modalOptions, setModalOptions ] = useState(false); + + const handleModal = async (index) => { + + await setModalOptions(tab1SideMenu[index]); + setShowModal(true); + } + + // Access other side menu options here + const sideMenu = useSideMenu(); + + useEffect(() => { + + if (props.location.pathname === "/tabs/tab1") { + + setSideMenu({ options: sideMenuOptions, side: "start", pageName: pageName }); + } + }, [ props.location ]); + + return ( + + + + + + + + + + +

Author

+

+ Alan Montgomery + + Mobile Team Lead + +

+

+ Hey there, I'm Alan! Hopefully you can take something away from this little sample app. Or even if it's to have a poke around and see how I personally like to do things, that's OK too 👏🏻. Check out each page, side menu and have a look at how things work. +

+
+
+ + + + +

Contact me on twitter if you need anything else :)

+ Tweet to @93alan +
+
+
+ + + + +

Check out Mobile DevCast

+

A podcast dedicated to mobile app development and web native technology like ionic & capacitor!

+ + https://mobiledevcast.com + +
+
+
+
+ + { (showModal && modalOptions) && + setShowModal(false) } /> + } +
+
+ ); +} + +export default Tab1; diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab2.css b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab2.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab2.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab2.tsx new file mode 100644 index 0000000..014d206 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab2.tsx @@ -0,0 +1,85 @@ +import { useEffect, useState } from "react"; +import { archiveOutline, checkmarkOutline, mailOutline, mailUnreadOutline, mapOutline, personOutline, refreshOutline, settingsSharp } from 'ionicons/icons'; +import { useSideMenuUpdate, useSideMenu } from "../main/SideMenuProvider"; + +import './Tab2.css'; +import CustomPage from "../main/CustomPage"; + +import { PageHeader } from "../components/PageHeader"; +import { Modal } from "../components/Modal"; +import { IonBadge, IonChip, IonGrid, IonItem, IonLabel, IonList, IonNote, IonPage } from '@ionic/react'; +import { getInboxItems } from "../main/Utils"; + +const Tab2 = props => { + + const pageName = "Inbox"; + var { sideMenuOptions } = props; + const setSideMenu = useSideMenuUpdate(); + + const [ Badge, setBadge ] = useState(true); + const [ showModal, setShowModal ] = useState(false); + const [ modalOptions, setModalOptions ] = useState(false); + + const inboxItems = getInboxItems(); + + const handleModal = async (index) => { + + await setModalOptions(sideMenuOptions[index]); + setShowModal(true); + } + + // Access other side menu options here + const sideMenu = useSideMenu(); + + useEffect(() => { + + if (props.location.pathname === "/tabs/tab2") { + + setSideMenu({ options: sideMenuOptions, side: "start", pageName: pageName }); + + sideMenuOptions = sideMenuOptions.filter(m => m.title === "Timestamp style")[0].clickEvent = () => setBadge(Badge => !Badge); + } + }, [ props.location ]); + + return ( + + + + + + + { inboxItems.map((item, index) => { + + return ( + + +

{ item.sender }

+

{ item.subject }

+

{ item.message }

+
+ { Badge && + + { item.time } + + } + + { !Badge && + + { item.time } + + } +
+ ); + })} +
+
+ + { (showModal && modalOptions) && + setShowModal(false) } /> + } +
+
+ ); +} + +export default Tab2; diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab3.css b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab3.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab3.tsx b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab3.tsx new file mode 100644 index 0000000..82155da --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/pages/Tab3.tsx @@ -0,0 +1,97 @@ +import { useEffect, useState } from "react"; +import { useSideMenuUpdate, useSideMenu } from "../main/SideMenuProvider"; + +import './Tab3.css'; +import CustomPage from "../main/CustomPage"; + +import { PageHeader } from "../components/PageHeader"; +import { Modal } from "../components/Modal"; +import { IonPage, IonGrid, IonList, IonItem, IonLabel, IonAvatar, IonIcon, IonBadge } from '@ionic/react'; + +import { getPlaceItems } from "../main/Utils"; + +const Tab3 = props => { + + const pageName = "Places"; + const { sideMenuOptions } = props; + const setSideMenu = useSideMenuUpdate(); + + const initialPlaceItems = getPlaceItems(); + const [ showModal, setShowModal ] = useState(false); + const [ modalOptions, setModalOptions ] = useState(false); + const [ placeItems, setPlaceItems ] = useState(initialPlaceItems); + + const handleClick = async (item) => { + + await setModalOptions(item); + setShowModal(true); + } + + const search = (e) => { + + const searchVal = e.target.value; + setPlaceItems(initialPlaceItems); + + if (searchVal !== "") { + + const newItems = initialPlaceItems.filter((item, index) => { + if (item.name.toLowerCase().includes(searchVal.toLowerCase())) { + + item.originalIndex = index; + return true; + } + }); + + setPlaceItems(newItems); + } else { + + setPlaceItems(initialPlaceItems); + } + } + + // Access other side menu options here + const sideMenu = useSideMenu(); + + useEffect(() => { + + if (props.location.pathname === "/tabs/tab3") { + + setSideMenu({ options: sideMenuOptions, side: "start", pageName: pageName }); + } + }, [ props.location ]); + + return ( + + + + + + + { placeItems.map((item, index) => { + + return ( + handleClick(item) } key={ `placeItem_${ index }`} detail={ true } lines="full"> + + + + +

{ item.name }

+
+ + { item.rating } / 10 + +
+ ); + })} +
+
+ + { (showModal && modalOptions) && + setShowModal(false) } /> + } +
+
+ ); +} + +export default Tab3; diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/theme/SideMenu.css b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/theme/SideMenu.css new file mode 100644 index 0000000..57ae101 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/theme/SideMenu.css @@ -0,0 +1,24 @@ +ion-menu ion-content { + + --padding-top: 1.5rem; + --padding-bottom: 1.5rem; +} + +ion-menu ion-item { + + --padding-start: 1rem; + --min-height: 3.5rem; + font-weight: 500; +} + +ion-menu ion-item ion-icon { + + font-size: 1.6rem; + color: #757575; +} + +ion-menu ion-item .sub-menu-title { + + color: var(--ion-color-primary) !important; + font-size: 0.75rem !important; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTabsMenusCustom/theme/variables.css b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/theme/variables.css new file mode 100644 index 0000000..8252431 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTabsMenusCustom/theme/variables.css @@ -0,0 +1,236 @@ +/* Ionic Variables and Theming. For more info, please see: +http://ionicframework.com/docs/theming/ */ + +/** Ionic CSS Variables **/ +:root { + /** primary **/ + --ion-color-primary: #3880ff; + --ion-color-primary-rgb: 56, 128, 255; + --ion-color-primary-contrast: #ffffff; + --ion-color-primary-contrast-rgb: 255, 255, 255; + --ion-color-primary-shade: #3171e0; + --ion-color-primary-tint: #4c8dff; + + /** secondary **/ + --ion-color-secondary: #3dc2ff; + --ion-color-secondary-rgb: 61, 194, 255; + --ion-color-secondary-contrast: #ffffff; + --ion-color-secondary-contrast-rgb: 255, 255, 255; + --ion-color-secondary-shade: #36abe0; + --ion-color-secondary-tint: #50c8ff; + + /** tertiary **/ + --ion-color-tertiary: #5260ff; + --ion-color-tertiary-rgb: 82, 96, 255; + --ion-color-tertiary-contrast: #ffffff; + --ion-color-tertiary-contrast-rgb: 255, 255, 255; + --ion-color-tertiary-shade: #4854e0; + --ion-color-tertiary-tint: #6370ff; + + /** success **/ + --ion-color-success: #2dd36f; + --ion-color-success-rgb: 45, 211, 111; + --ion-color-success-contrast: #ffffff; + --ion-color-success-contrast-rgb: 255, 255, 255; + --ion-color-success-shade: #28ba62; + --ion-color-success-tint: #42d77d; + + /** warning **/ + --ion-color-warning: #ffc409; + --ion-color-warning-rgb: 255, 196, 9; + --ion-color-warning-contrast: #000000; + --ion-color-warning-contrast-rgb: 0, 0, 0; + --ion-color-warning-shade: #e0ac08; + --ion-color-warning-tint: #ffca22; + + /** danger **/ + --ion-color-danger: #eb445a; + --ion-color-danger-rgb: 235, 68, 90; + --ion-color-danger-contrast: #ffffff; + --ion-color-danger-contrast-rgb: 255, 255, 255; + --ion-color-danger-shade: #cf3c4f; + --ion-color-danger-tint: #ed576b; + + /** dark **/ + --ion-color-dark: #222428; + --ion-color-dark-rgb: 34, 36, 40; + --ion-color-dark-contrast: #ffffff; + --ion-color-dark-contrast-rgb: 255, 255, 255; + --ion-color-dark-shade: #1e2023; + --ion-color-dark-tint: #383a3e; + + /** medium **/ + --ion-color-medium: #92949c; + --ion-color-medium-rgb: 146, 148, 156; + --ion-color-medium-contrast: #ffffff; + --ion-color-medium-contrast-rgb: 255, 255, 255; + --ion-color-medium-shade: #808289; + --ion-color-medium-tint: #9d9fa6; + + /** light **/ + --ion-color-light: #f4f5f8; + --ion-color-light-rgb: 244, 245, 248; + --ion-color-light-contrast: #000000; + --ion-color-light-contrast-rgb: 0, 0, 0; + --ion-color-light-shade: #d7d8da; + --ion-color-light-tint: #f5f6f9; +} + +@media (prefers-color-scheme: dark) { + /* + * Dark Colors + * ------------------------------------------- + */ + + body { + --ion-color-primary: #428cff; + --ion-color-primary-rgb: 66,140,255; + --ion-color-primary-contrast: #ffffff; + --ion-color-primary-contrast-rgb: 255,255,255; + --ion-color-primary-shade: #3a7be0; + --ion-color-primary-tint: #5598ff; + + --ion-color-secondary: #50c8ff; + --ion-color-secondary-rgb: 80,200,255; + --ion-color-secondary-contrast: #ffffff; + --ion-color-secondary-contrast-rgb: 255,255,255; + --ion-color-secondary-shade: #46b0e0; + --ion-color-secondary-tint: #62ceff; + + --ion-color-tertiary: #6a64ff; + --ion-color-tertiary-rgb: 106,100,255; + --ion-color-tertiary-contrast: #ffffff; + --ion-color-tertiary-contrast-rgb: 255,255,255; + --ion-color-tertiary-shade: #5d58e0; + --ion-color-tertiary-tint: #7974ff; + + --ion-color-success: #2fdf75; + --ion-color-success-rgb: 47,223,117; + --ion-color-success-contrast: #000000; + --ion-color-success-contrast-rgb: 0,0,0; + --ion-color-success-shade: #29c467; + --ion-color-success-tint: #44e283; + + --ion-color-warning: #ffd534; + --ion-color-warning-rgb: 255,213,52; + --ion-color-warning-contrast: #000000; + --ion-color-warning-contrast-rgb: 0,0,0; + --ion-color-warning-shade: #e0bb2e; + --ion-color-warning-tint: #ffd948; + + --ion-color-danger: #ff4961; + --ion-color-danger-rgb: 255,73,97; + --ion-color-danger-contrast: #ffffff; + --ion-color-danger-contrast-rgb: 255,255,255; + --ion-color-danger-shade: #e04055; + --ion-color-danger-tint: #ff5b71; + + --ion-color-dark: #f4f5f8; + --ion-color-dark-rgb: 244,245,248; + --ion-color-dark-contrast: #000000; + --ion-color-dark-contrast-rgb: 0,0,0; + --ion-color-dark-shade: #d7d8da; + --ion-color-dark-tint: #f5f6f9; + + --ion-color-medium: #989aa2; + --ion-color-medium-rgb: 152,154,162; + --ion-color-medium-contrast: #000000; + --ion-color-medium-contrast-rgb: 0,0,0; + --ion-color-medium-shade: #86888f; + --ion-color-medium-tint: #a2a4ab; + + --ion-color-light: #222428; + --ion-color-light-rgb: 34,36,40; + --ion-color-light-contrast: #ffffff; + --ion-color-light-contrast-rgb: 255,255,255; + --ion-color-light-shade: #1e2023; + --ion-color-light-tint: #383a3e; + } + + /* + * iOS Dark Theme + * ------------------------------------------- + */ + + .ios body { + --ion-background-color: #000000; + --ion-background-color-rgb: 0,0,0; + + --ion-text-color: #ffffff; + --ion-text-color-rgb: 255,255,255; + + --ion-color-step-50: #0d0d0d; + --ion-color-step-100: #1a1a1a; + --ion-color-step-150: #262626; + --ion-color-step-200: #333333; + --ion-color-step-250: #404040; + --ion-color-step-300: #4d4d4d; + --ion-color-step-350: #595959; + --ion-color-step-400: #666666; + --ion-color-step-450: #737373; + --ion-color-step-500: #808080; + --ion-color-step-550: #8c8c8c; + --ion-color-step-600: #999999; + --ion-color-step-650: #a6a6a6; + --ion-color-step-700: #b3b3b3; + --ion-color-step-750: #bfbfbf; + --ion-color-step-800: #cccccc; + --ion-color-step-850: #d9d9d9; + --ion-color-step-900: #e6e6e6; + --ion-color-step-950: #f2f2f2; + + --ion-item-background: #000000; + + --ion-card-background: #1c1c1d; + } + + .ios ion-modal { + --ion-background-color: #000000; + --ion-toolbar-background: #000000; + --ion-toolbar-border-color: var(--ion-color-step-150); + } + + + /* + * Material Design Dark Theme + * ------------------------------------------- + */ + + .md body { + --ion-background-color: #121212; + --ion-background-color-rgb: 18,18,18; + + --ion-text-color: #ffffff; + --ion-text-color-rgb: 255,255,255; + + --ion-border-color: #222222; + + --ion-color-step-50: #1e1e1e; + --ion-color-step-100: #2a2a2a; + --ion-color-step-150: #363636; + --ion-color-step-200: #414141; + --ion-color-step-250: #4d4d4d; + --ion-color-step-300: #595959; + --ion-color-step-350: #656565; + --ion-color-step-400: #717171; + --ion-color-step-450: #7d7d7d; + --ion-color-step-500: #898989; + --ion-color-step-550: #949494; + --ion-color-step-600: #a0a0a0; + --ion-color-step-650: #acacac; + --ion-color-step-700: #b8b8b8; + --ion-color-step-750: #c4c4c4; + --ion-color-step-800: #d0d0d0; + --ion-color-step-850: #dbdbdb; + --ion-color-step-900: #e7e7e7; + --ion-color-step-950: #f3f3f3; + + --ion-item-background: #1e1e1e; + + --ion-toolbar-background: #1f1f1f; + + --ion-tab-bar-background: #1f1f1f; + + --ion-card-background: #1e1e1e; + } +}