diff --git a/03_source/mobile/android/app/capacitor.build.gradle b/03_source/mobile/android/app/capacitor.build.gradle index 90339c8..ea6fc3b 100644 --- a/03_source/mobile/android/app/capacitor.build.gradle +++ b/03_source/mobile/android/app/capacitor.build.gradle @@ -13,6 +13,7 @@ dependencies { implementation project(':capacitor-clipboard') implementation project(':capacitor-geolocation') implementation project(':capacitor-preferences') + implementation project(':capacitor-share') } diff --git a/03_source/mobile/android/capacitor.settings.gradle b/03_source/mobile/android/capacitor.settings.gradle index a9bf201..fd55fca 100644 --- a/03_source/mobile/android/capacitor.settings.gradle +++ b/03_source/mobile/android/capacitor.settings.gradle @@ -13,3 +13,6 @@ project(':capacitor-geolocation').projectDir = new File('../node_modules/@capaci include ':capacitor-preferences' project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android') + +include ':capacitor-share' +project(':capacitor-share').projectDir = new File('../node_modules/@capacitor/share/android') diff --git a/03_source/mobile/ios/App/Podfile b/03_source/mobile/ios/App/Podfile index 9e8cc13..fd27c63 100644 --- a/03_source/mobile/ios/App/Podfile +++ b/03_source/mobile/ios/App/Podfile @@ -15,6 +15,7 @@ def capacitor_pods pod 'CapacitorClipboard', :path => '../../node_modules/@capacitor/clipboard' pod 'CapacitorGeolocation', :path => '../../node_modules/@capacitor/geolocation' pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences' + pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' end target 'App' do diff --git a/03_source/mobile/package.json b/03_source/mobile/package.json index 9eb4e98..5954c78 100644 --- a/03_source/mobile/package.json +++ b/03_source/mobile/package.json @@ -13,6 +13,7 @@ "@capacitor/geolocation": "^7.1.2", "@capacitor/ios": "7.0.1", "@capacitor/preferences": "^7.0.0", + "@capacitor/share": "^7.0.1", "@hookform/resolvers": "^4.1.3", "@ionic/react": "^8.5.0", "@ionic/react-router": "^8.5.0", @@ -27,6 +28,7 @@ "pigeon-maps": "^0.22.1", "pullstate": "^1", "react": "19.0.0", + "react-color": "^2.19.3", "react-confetti": "^6.4.0", "react-dom": "19.0.0", "react-hook-form": "^7.55.0", diff --git a/03_source/mobile/public/assets/DemoQuizApp/icon/favicon.png b/03_source/mobile/public/assets/DemoQuizApp/icon/favicon.png new file mode 100644 index 0000000..51888a7 Binary files /dev/null and b/03_source/mobile/public/assets/DemoQuizApp/icon/favicon.png differ diff --git a/03_source/mobile/public/assets/DemoQuizApp/icon/icon.png b/03_source/mobile/public/assets/DemoQuizApp/icon/icon.png new file mode 100644 index 0000000..a7f6374 Binary files /dev/null and b/03_source/mobile/public/assets/DemoQuizApp/icon/icon.png differ diff --git a/03_source/mobile/public/assets/DemoQuizApp/main.png b/03_source/mobile/public/assets/DemoQuizApp/main.png new file mode 100644 index 0000000..42f8f63 Binary files /dev/null and b/03_source/mobile/public/assets/DemoQuizApp/main.png differ diff --git a/03_source/mobile/public/assets/DemoQuizApp/shapes.svg b/03_source/mobile/public/assets/DemoQuizApp/shapes.svg new file mode 100644 index 0000000..d370b4d --- /dev/null +++ b/03_source/mobile/public/assets/DemoQuizApp/shapes.svg @@ -0,0 +1 @@ + diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/autumn.png b/03_source/mobile/public/assets/DemoSlidingProfile/autumn.png new file mode 100644 index 0000000..3725b34 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/autumn.png differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/avatar.jpeg b/03_source/mobile/public/assets/DemoSlidingProfile/avatar.jpeg new file mode 100644 index 0000000..9a30b74 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/avatar.jpeg differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/avatar1.png b/03_source/mobile/public/assets/DemoSlidingProfile/avatar1.png new file mode 100644 index 0000000..094634d Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/avatar1.png differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/avatar2.png b/03_source/mobile/public/assets/DemoSlidingProfile/avatar2.png new file mode 100644 index 0000000..a787854 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/avatar2.png differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/avatar3.png b/03_source/mobile/public/assets/DemoSlidingProfile/avatar3.png new file mode 100644 index 0000000..084d895 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/avatar3.png differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/avatar4.png b/03_source/mobile/public/assets/DemoSlidingProfile/avatar4.png new file mode 100644 index 0000000..c0c8738 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/avatar4.png differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/avatar5.png b/03_source/mobile/public/assets/DemoSlidingProfile/avatar5.png new file mode 100644 index 0000000..050e908 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/avatar5.png differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/avatar6.png b/03_source/mobile/public/assets/DemoSlidingProfile/avatar6.png new file mode 100644 index 0000000..8114df3 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/avatar6.png differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/cover1.jpeg b/03_source/mobile/public/assets/DemoSlidingProfile/cover1.jpeg new file mode 100644 index 0000000..eeb888c Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/cover1.jpeg differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/cover2.jpeg b/03_source/mobile/public/assets/DemoSlidingProfile/cover2.jpeg new file mode 100644 index 0000000..2e5074e Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/cover2.jpeg differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/cover4.jpeg b/03_source/mobile/public/assets/DemoSlidingProfile/cover4.jpeg new file mode 100644 index 0000000..f31d1f9 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/cover4.jpeg differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/cover5.jpeg b/03_source/mobile/public/assets/DemoSlidingProfile/cover5.jpeg new file mode 100644 index 0000000..1e1fea9 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/cover5.jpeg differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/cover6.jpeg b/03_source/mobile/public/assets/DemoSlidingProfile/cover6.jpeg new file mode 100644 index 0000000..e28dd31 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/cover6.jpeg differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/flower.jpeg b/03_source/mobile/public/assets/DemoSlidingProfile/flower.jpeg new file mode 100644 index 0000000..3f56d8c Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/flower.jpeg differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/h.jpeg b/03_source/mobile/public/assets/DemoSlidingProfile/h.jpeg new file mode 100644 index 0000000..a5be47e Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/h.jpeg differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/icon/favicon.png b/03_source/mobile/public/assets/DemoSlidingProfile/icon/favicon.png new file mode 100644 index 0000000..51888a7 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/icon/favicon.png differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/icon/icon.png b/03_source/mobile/public/assets/DemoSlidingProfile/icon/icon.png new file mode 100644 index 0000000..a7f6374 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/icon/icon.png differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/ocean.jpeg b/03_source/mobile/public/assets/DemoSlidingProfile/ocean.jpeg new file mode 100644 index 0000000..22892f1 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/ocean.jpeg differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/shapes.svg b/03_source/mobile/public/assets/DemoSlidingProfile/shapes.svg new file mode 100644 index 0000000..d370b4d --- /dev/null +++ b/03_source/mobile/public/assets/DemoSlidingProfile/shapes.svg @@ -0,0 +1 @@ + diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/spring.png b/03_source/mobile/public/assets/DemoSlidingProfile/spring.png new file mode 100644 index 0000000..2726d11 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/spring.png differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/summer.png b/03_source/mobile/public/assets/DemoSlidingProfile/summer.png new file mode 100644 index 0000000..ae4def9 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/summer.png differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/van.jpeg b/03_source/mobile/public/assets/DemoSlidingProfile/van.jpeg new file mode 100644 index 0000000..f73f338 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/van.jpeg differ diff --git a/03_source/mobile/public/assets/DemoSlidingProfile/winter.png b/03_source/mobile/public/assets/DemoSlidingProfile/winter.png new file mode 100644 index 0000000..81694c9 Binary files /dev/null and b/03_source/mobile/public/assets/DemoSlidingProfile/winter.png differ diff --git a/03_source/mobile/src/App.tsx b/03_source/mobile/src/App.tsx index 79cc364..b016ce1 100644 --- a/03_source/mobile/src/App.tsx +++ b/03_source/mobile/src/App.tsx @@ -78,6 +78,20 @@ import DemoDictionaryApp from './pages/DemoDictionaryApp'; // demo-recipe-app import DemoRecipeApp from './pages/DemoRecipeApp'; +// DemoSlidingProfile +import DemoSlidingProfile from './pages/DemoSlidingProfile'; + +// DemoQuizApp +import DemoQuizApp from './pages/DemoQuizApp'; +import DemoBlogPostUi from './pages/DemoBlogPostUi'; +import DemoReactTravelApp from './pages/DemoReactTravelApp'; +import DemoPinterestFloatingTabBar from './pages/DemoPinterestFloatingTabBar'; +import DemoRestaurantFinder from './pages/DemoRestaurantFinder'; +import DemoReactOverlayHooks from './pages/DemoReactOverlayHooks'; +import DemoReactSwitchTabs from './pages/DemoReactSwitchTabs'; +import DemoReactPollApp from './pages/DemoReactPollApp'; +import DemoReactWhatsAppClone from './pages/DemoReactWhatsAppClone'; + setupIonicReact(); const App: React.FC = () => { @@ -102,14 +116,7 @@ interface DispatchProps { interface IonicAppProps extends StateProps, DispatchProps {} -const IonicApp: React.FC = ({ - darkMode, - schedule, - setIsLoggedIn, - setUsername, - loadConfData, - loadUserData, -}) => { +const IonicApp: React.FC = ({ darkMode, schedule, setIsLoggedIn, setUsername, loadConfData, loadUserData }) => { useEffect(() => { loadUserData(); loadConfData(); @@ -132,19 +139,34 @@ const IonicApp: React.FC = ({ - {/* */} } /> - } /> } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* */} + {/* */} + {/* */} + {/* */} + } /> + + } /> + + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + @@ -153,7 +175,6 @@ const IonicApp: React.FC = ({ - {/* */} { diff --git a/03_source/mobile/src/pages/DemoBlogPostUi/AppPages/BlogPost.css b/03_source/mobile/src/pages/DemoBlogPostUi/AppPages/BlogPost.css new file mode 100644 index 0000000..e754dfd --- /dev/null +++ b/03_source/mobile/src/pages/DemoBlogPostUi/AppPages/BlogPost.css @@ -0,0 +1,7 @@ +.view-post-footer { + + background-color: white; + padding-left: 1rem; + padding-right: 1rem; + padding-bottom: 1rem; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoBlogPostUi/AppPages/BlogPost.jsx b/03_source/mobile/src/pages/DemoBlogPostUi/AppPages/BlogPost.jsx new file mode 100644 index 0000000..3c295ba --- /dev/null +++ b/03_source/mobile/src/pages/DemoBlogPostUi/AppPages/BlogPost.jsx @@ -0,0 +1,90 @@ +import { + IonBackButton, + IonBadge, + IonButton, + IonButtons, + IonCardSubtitle, + IonCardTitle, + IonCol, + IonContent, + IonFooter, + IonGrid, + IonHeader, + IonIcon, + IonNote, + IonPage, + IonRow, + IonText, + IonTitle, + IonToolbar, +} from '@ionic/react'; +import { bookmarkOutline, shareOutline } from 'ionicons/icons'; +import { useParams } from 'react-router'; +import { blogPosts } from '../localData'; +import './BlogPost.css'; + +const BlogPost = () => { + const { id } = useParams(); + const post = blogPosts.filter((post) => parseInt(post.id) === parseInt(id))[0]; + + return ( + + + + Blog + + + + + + + post header + + + + + post author + + {post.author} + + + {post.date} + + + + + {post.title} + + + + + + {post.content} + + + + + + + +
+ + + + + + +
+ +
+ + {post.category} + +
+
+
+
+ ); +}; + +export default BlogPost; diff --git a/03_source/mobile/src/pages/DemoBlogPostUi/AppPages/Home.jsx b/03_source/mobile/src/pages/DemoBlogPostUi/AppPages/Home.jsx new file mode 100644 index 0000000..7fff026 --- /dev/null +++ b/03_source/mobile/src/pages/DemoBlogPostUi/AppPages/Home.jsx @@ -0,0 +1,51 @@ +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonIcon, + IonPage, + IonTitle, + IonToolbar, + useIonRouter, +} from '@ionic/react'; +import { Post } from '../components/Post'; +import { blogPosts } from '../localData'; +import { chevronBackOutline } from 'ionicons/icons'; + +const Home = () => { + const router = useIonRouter(); + + function handleBackClick() { + router.goBack(); + } + + return ( + + + + Ionic Blog + + + handleBackClick()}> + + + + + + + + + Ionic Blog + + + + {blogPosts.map((post, index) => ( + + ))} + + + ); +}; + +export default Home; diff --git a/03_source/mobile/src/pages/DemoBlogPostUi/components/Post.css b/03_source/mobile/src/pages/DemoBlogPostUi/components/Post.css new file mode 100644 index 0000000..4b6c13d --- /dev/null +++ b/03_source/mobile/src/pages/DemoBlogPostUi/components/Post.css @@ -0,0 +1,37 @@ +.post-author-avatar { + height: 2rem; + width: 2rem; + border-radius: 500px; +} + +.post-title { + font-size: 1.4rem; + margin-top: 0.75rem; +} + +.post-content { + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; +} + +.post-footer { + display: flex; + flex-direction: row; + justify-content: space-between; + align-content: center; + width: 100%; + border-top: 2px solid rgb(245, 245, 245); + margin-top: 2rem; + padding-top: 1rem; +} + +.post-category { + margin-top: 1.1rem; +} + +.post-image { + width: 100%; +} diff --git a/03_source/mobile/src/pages/DemoBlogPostUi/components/Post.jsx b/03_source/mobile/src/pages/DemoBlogPostUi/components/Post.jsx new file mode 100644 index 0000000..96437dd --- /dev/null +++ b/03_source/mobile/src/pages/DemoBlogPostUi/components/Post.jsx @@ -0,0 +1,57 @@ +import { + IonBadge, + IonButton, + IonCard, + IonCardContent, + IonCardHeader, + IonCardSubtitle, + IonCardTitle, + IonIcon, + IonNote, + IonRow, +} from '@ionic/react'; +import { bookmarkOutline, shareOutline } from 'ionicons/icons'; + +import './Post.css'; + +export const Post = ({ post }) => { + return ( + + main post + + + + + post author + + {post.author} + + + {post.date} + + {post.title} + + + +

{post.content}

+ + +
+ + + + + + +
+ +
+ + {post.category} + +
+
+
+
+ ); +}; diff --git a/03_source/mobile/src/pages/DemoBlogPostUi/index.tsx b/03_source/mobile/src/pages/DemoBlogPostUi/index.tsx new file mode 100644 index 0000000..c924f7d --- /dev/null +++ b/03_source/mobile/src/pages/DemoBlogPostUi/index.tsx @@ -0,0 +1,27 @@ +import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react'; + +import { cloudOutline, searchOutline } from 'ionicons/icons'; +import { Route, Redirect } from 'react-router'; + +import Home from './AppPages/Home'; +import BlogPost from './AppPages/BlogPost'; + +import './style.scss'; + +function DemoBlogPostUi() { + return ( + + + + + + + + + + + + ); +} + +export default DemoBlogPostUi; diff --git a/03_source/mobile/src/pages/DemoBlogPostUi/localData/index.js b/03_source/mobile/src/pages/DemoBlogPostUi/localData/index.js new file mode 100644 index 0000000..f6aa8b1 --- /dev/null +++ b/03_source/mobile/src/pages/DemoBlogPostUi/localData/index.js @@ -0,0 +1,98 @@ +export const blogPosts = [ + { + "id": 1, + "title": "How to Convince Your Boss to Choose Ionic", + "title_link": "https://ionicframework.com/blog/convince-boss-choose-ionic-app-development/", + "date": "August 3, 2021", + "author": "By Kim Maida", + "authorImage": "https://ionicframework.com/blog/wp-content/uploads/2021/07/kim-maida-150x150.jpg", + "category": "ANNOUNCEMENTS", + "category_link": "https://ionicframework.com/blog//blog/category/announcements", + "image": "https://ionicframework.com/blog/wp-content/uploads/2021/07/how-to-convince-your-boss_image_1aug2021.png", + "content": "Greetings, friend! You’re a web developer, team lead, or engineering manager who has discovered that Ionic products are awesome. They have helped you build cross-platform applications quickly, made the app development process enjoyable, and solved important mobile development problems. You can see that Ionic would be extremely beneficial in your daily job, but are wondering how to convince your boss to endorse the adoption of new software. In a nutshell:" + }, + { + "id": 2, + "title": "Ioniconf 2021 Conference Recap", + "title_link": "https://ionicframework.com/blog/ioniconf-2021-conference-recap/", + "date": "July 29, 2021", + "author": "By Mike Hartington", + "authorImage": "https://ionicframework.com/blog/wp-content/uploads/2018/08/mike-headshot-2-smaller-150x150.png", + "category": "ANNOUNCEMENTS", + "category_link": "https://ionicframework.com/blog//blog/category/announcements", + "image": "https://ionicframework.com/blog/wp-content/uploads/2021/06/og-imgx2.png", + "content": "And with that, Ioniconf 2021 has concluded! Ioniconf, our online conference for Ionic developers and the wider web development community, featured twelve expert Ionic speakers and was attended by many thousands of Ionic community members. We’re thrilled by the community’s reception to the event and are already looking forward to our next event taking place in September. Read on for a recap and links to all recorded talks." + }, + { + "id": 3, + "title": "Announcing Identity Vault 5.0", + "title_link": "https://ionicframework.com/blog/announcing-identity-vault-5-0/", + "date": "July 28, 2021", + "author": "By Dallas James", + "authorImage": "https://ionicframework.com/blog/wp-content/uploads/2021/07/dallas-james-150x150.jpg", + "category": "PRODUCT", + "category_link": "https://ionicframework.com/blog//blog/category/announcements", + "image": "https://ionicframework.com/blog/wp-content/uploads/2021/07/iv-5-feature-image.png", + "content": "Today I’m excited to announce Identity Vault 5.0, the newest version of Ionic’s mobile biometrics solution. Featuring the latest in native security best practices, Identity Vault improves frontend security in any Ionic app by making it easy to add secure biometric authentication in minutes." + }, + { + "id": 4, + "title": "Building with Stencil: Clock Component", + "title_link": "https://ionicframework.com/blog/building-with-stencil-clock-component/", + "date": "July 22, 2021", + "author": "By Kevin Hoyt", + "authorImage": "https://ionicframework.com/blog/wp-content/uploads/2021/07/2520666-150x150.jpg", + "category": "ANNOUNCEMENTS", + "category_link": "https://ionicframework.com/blog//blog/category/announcements", + "image": "https://ionicframework.com/blog/wp-content/uploads/2021/07/Image-from-iOS.png", + "content": "I have not seen a clock in a web-based user interface in a long time. This makes sense — they are pretty redundant these days. You have a clock on your watch, on your mobile device, and on your desktop, and those are just the digital versions available at a glance. Nonetheless, the process of building a clock can reveal a lot about how a platform works." + }, + { + "id": 5, + "title": "Building with Stencil: Calendar Component", + "title_link": "https://ionicframework.com/blog/building-with-stencil-calendar-component/", + "date": "July 19, 2021", + "author": "By Kevin Hoyt", + "authorImage": "https://ionicframework.com/blog/wp-content/uploads/2021/07/2520666-150x150.jpg", + "category": "TUTORIALS", + "category_link": null, + "image": "https://ionicframework.com/blog/wp-content/uploads/2021/07/ionic-blog-post-image_first-look-01.png", + "content": "Take a look at the month view of a calendar and you will see several rows of numbers. The numbers themselves, increasing in value one after the other, are arranged in columns. HTML and CSS provide us with a number of tools to display content in rows and columns. Making a calendar component should be easy, right? Right?" + }, + { + "id": 6, + "title": "Introducing the New Overlay Hooks for Ionic React", + "title_link": "https://ionicframework.com/blog/introducing-the-new-overlay-hooks-for-ionic-react/", + "date": "July 14, 2021", + "author": "By Ely Lucas", + "authorImage": "https://secure.gravatar.com/avatar/45ad19965b4bde97e9f4396ea01ed184?s=32&r=g", + "category": "ENGINEERING", + "category_link": null, + "image": "https://ionicframework.com/blog/wp-content/uploads/2021/07/react-overlay-hooks-feature-image.png", + "content": "Hello Friends! We know everyone is excited about the new features in Ionic Framework 6.0 beta, but that doesn’t mean we’re done with V5! In Ionic React 5.6, we packaged up a new set of hooks for controlling our overlay components that we think you might like. What is an overlay you ask? It’s the term we give components that display over your current content, such as alerts, modals, toasts, etc." + }, + { + "id": 7, + "title": "The Future of Stencil: Expanded Team, New Software Platform, and More", + "title_link": "https://ionicframework.com/blog/the-future-of-stencil-expanded-team-new-software-platform-and-more/", + "date": "July 7, 2021", + "author": "By Nick Hyatt", + "authorImage": "https://ionicframework.com/blog/wp-content/uploads/2018/11/Nick-Hyatt-Headshot-150x150.jpeg", + "category": "ANNOUNCEMENTS", + "category_link": null, + "image": "https://ionicframework.com/blog/wp-content/uploads/2021/07/stencil-future-feature-image.png", + "content": "Today I’m excited to share some news about Stencil, Ionic’s open source toolchain that generates small, fast, and 100% standards-based Web Components that run in every browser. As you might have noticed, we’ve been actively increasing our investments across the entire Ionic App Platform, including the recent launch of Capacitor 3.0, Ionic Portals, tons of Appflow improvements, and the upcoming Ionic Framework v6." + }, + { + "id": 8, + "title": "Announcing the Ionic Framework v6 Beta", + "title_link": "https://ionicframework.com/blog/announcing-the-ionic-framework-v6-beta/", + "date": "June 29, 2021", + "author": "By Liam DeBeasi", + "authorImage": "https://ionicframework.com/blog/wp-content/uploads/2020/01/ZNK4lRAJ_400x400-150x150.jpg", + "category": "ANNOUNCEMENTS", + "category_link": null, + "image": "https://ionicframework.com/blog/wp-content/uploads/2021/06/framework6-feature-image.png", + "content": "Earlier this week I had the privilege of giving the Ionic Framework Update at Ioniconf 2021 where we announced the Ionic Framework v6 beta. Ionic Framework has come far from its roots as an AngularJS-only UI library to a truly cross-platform framework for building Web Native applications. As we look to the future of Ionic Framework, let’s talk about some of the improvements coming in Framework v6 and how you can get access to these improvements today." + } +]; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoBlogPostUi/style.scss b/03_source/mobile/src/pages/DemoBlogPostUi/style.scss new file mode 100644 index 0000000..83c6630 --- /dev/null +++ b/03_source/mobile/src/pages/DemoBlogPostUi/style.scss @@ -0,0 +1,79 @@ +.demo-blog-post-ui { + /* Ionic Variables and Theming. For more info, please see: +http://ionicframework.com/docs/theming/ */ + + /** Ionic CSS Variables **/ + * { + /** 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; + } +} diff --git a/03_source/mobile/src/pages/DemoList/index.tsx b/03_source/mobile/src/pages/DemoList/index.tsx index 714e050..235e22e 100644 --- a/03_source/mobile/src/pages/DemoList/index.tsx +++ b/03_source/mobile/src/pages/DemoList/index.tsx @@ -48,23 +48,33 @@ import { alertOutline, apps, appsOutline, + book, car, cart, + chatbubbleEllipses, chatbubbleOutline, chevronBackOutline, chevronForward, chevronForwardOutline, createOutline, + document, documentTextOutline, gift, giftOutline, + globeSharp, heart, languageOutline, + layers, listCircle, menuOutline, + people, + person, + restaurant, settingsOutline, shareSocialOutline, + statsChart, sunny, + swapHorizontal, trashOutline, } from 'ionicons/icons'; import AboutPopover from '../../components/AboutPopover'; @@ -87,13 +97,7 @@ interface DispatchProps { interface SettingsProps extends OwnProps, StateProps, DispatchProps {} -const SettingsPage: React.FC = ({ - speakers, - speakerSessions, - logoutUser, - setAccessToken, - setIsLoggedIn, -}) => { +const SettingsPage: React.FC = ({ speakers, speakerSessions, logoutUser, setAccessToken, setIsLoggedIn }) => { const [events, setEvents] = useState([]); const [showPopover, setShowPopover] = useState(false); const [popoverEvent, setPopoverEvent] = useState(); @@ -190,6 +194,72 @@ const SettingsPage: React.FC = ({ + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + {/* */} + + router.push(paths.DEMO_REACT_WHATSAPP_CLONE, 'forward')}> + + + Demo React WhatsApp Clone (need to resolve path problem) + + + + + + + router.push(paths.DEMO_REACT_POLL_APP, 'forward')}> + + + Demo React Poll App (css temporary broken, ignored) + + + + + + + router.push(paths.DEMO_REACT_SWITCH_TABS, 'forward')}> + + + Demo React Switch Tabs (hardcoded back button) + + + + + + + router.push(paths.DEMO_REACT_OVERLAY_HOOKS, 'forward')}> + + Demo React Overlay Hooks + + + + + + router.push(paths.DEMO_PINTEREST_FLOATING_TAB_BAR, 'forward')}> + + + Demo Pinterest Floating Tab Bar (css not work well) + + + + + + + router.push(paths.DEMO_RESTAURANT_FINDER, 'forward')}> + + + Demo Restaurant Finder need server for map showing + + + + + handleDemoReactShopClick()}> @@ -227,8 +297,6 @@ const SettingsPage: React.FC = ({ - {/* */} - router.push(paths.DEMO_QUOTE_APP, 'forward')}> @@ -261,7 +329,6 @@ const SettingsPage: React.FC = ({ - {/* */} router.push(paths.DEMO_RECIPE_APP, 'forward')}> @@ -269,14 +336,44 @@ const SettingsPage: React.FC = ({ + + + router.push(paths.DEMO_SLIDING_PROFILE, 'forward')}> + + Demo Sliding Profile + + + + + + router.push(paths.DEMO_QUIZ_APP, 'forward')}> + + Demo Quiz App + + + + + + router.push(paths.DEMO_BLOG_POST_UI, 'forward')}> + + Demo Blog Post UI + + + + + + router.push(paths.DEMO_REACT_TRAVEL_APP, 'forward')}> + + + Demo React Travel App (on hold) + + + + {/* REQ0058/logout */} - + { + const router = useIonRouter(); + + function handleBackClick() { + router.goBack(); + } + + return ( + + + + Tab 1 + {/* */} + + handleBackClick()}> + + + + + + + + + Tab 1 + + + + + + ); +}; + +export default Tab1; diff --git a/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab2.css b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab2.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab2.jsx b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab2.jsx new file mode 100644 index 0000000..f09edf4 --- /dev/null +++ b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab2.jsx @@ -0,0 +1,25 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab2.css'; + +const Tab2 = () => { + return ( + + + + Tab 2 + + + + + + Tab 2 + + + + + + ); +}; + +export default Tab2; diff --git a/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab3.css b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab3.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab3.jsx b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab3.jsx new file mode 100644 index 0000000..903b1a0 --- /dev/null +++ b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab3.jsx @@ -0,0 +1,25 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab3.css'; + +const Tab3 = () => { + return ( + + + + Tab 3 + + + + + + Tab 3 + + + + + + ); +}; + +export default Tab3; diff --git a/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab4.jsx b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab4.jsx new file mode 100644 index 0000000..9cb5fa3 --- /dev/null +++ b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/AppPages/Tab4.jsx @@ -0,0 +1,25 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab3.css'; + +const Tab4 = () => { + return ( + + + + Tab 3 + + + + + + Tab 4 + + + + + + ); +}; + +export default Tab4; diff --git a/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/components/ExploreContainer.css b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/components/ExploreContainer.css new file mode 100644 index 0000000..e99f514 --- /dev/null +++ b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/components/ExploreContainer.css @@ -0,0 +1,24 @@ +.container { + text-align: center; + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +.container strong { + font-size: 20px; + line-height: 26px; +} + +.container p { + font-size: 16px; + line-height: 22px; + color: #8c8c8c; + margin: 0; +} + +.container a { + text-decoration: none; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/components/ExploreContainer.jsx b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/components/ExploreContainer.jsx new file mode 100644 index 0000000..093861b --- /dev/null +++ b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/components/ExploreContainer.jsx @@ -0,0 +1,12 @@ +import './ExploreContainer.css'; + +const ExploreContainer = ({ name }) => { + return ( +
+ {name} +

Explore UI Components

+
+ ); +}; + +export default ExploreContainer; diff --git a/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/custom-tab-bar.scss b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/custom-tab-bar.scss new file mode 100644 index 0000000..1aa74fb --- /dev/null +++ b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/custom-tab-bar.scss @@ -0,0 +1,29 @@ +.custom-tab-bar { + * { + /* --ion-background-color: white; */ + --ion-tab-bar-color: var(--tab-color); + --ion-tab-bar-color-selected: var(--tab-color-selected); + } + + ion-tab-bar { + --background: var(--tab-background); + box-shadow: 0px 1px 8px rgba(0, 0, 0, 0.4); + border-radius: 50px !important; + + height: 50px; + width: 50%; + padding-top: 5px; + padding-bottom: 5px; + padding-left: 10px; + padding-right: 10px; + + bottom: 20px; + position: relative; + margin: 0 auto !important; + border-top: none; + } + + ion-tab-button { + border-radius: 16px !important; + } +} diff --git a/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/index.tsx b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/index.tsx new file mode 100644 index 0000000..6ccb3e7 --- /dev/null +++ b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/index.tsx @@ -0,0 +1,54 @@ +import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react'; + +import { chatbubble, cloudOutline, home, person, search, searchOutline } from 'ionicons/icons'; +import { Route, Redirect } from 'react-router'; + +import Tab1 from './AppPages/Tab1'; +import Tab2 from './AppPages/Tab2'; +import Tab3 from './AppPages/Tab3'; +import Tab4 from './AppPages/Tab4'; + +import './style.scss'; +import './custom-tab-bar.scss'; + +function DemoPinterestFloatingTabBar() { + return ( + + + + + + + + + + + + + + + + + + + {/* */} + + + + + + + + + + + + + + + + + ); +} + +export default DemoPinterestFloatingTabBar; diff --git a/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/style.scss b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/style.scss new file mode 100644 index 0000000..fae1253 --- /dev/null +++ b/03_source/mobile/src/pages/DemoPinterestFloatingTabBar/style.scss @@ -0,0 +1,253 @@ +/* Ionic Variables and Theming. For more info, please see: +http://ionicframework.com/docs/theming/ */ + +/** Ionic CSS Variables **/ +.demo-pinterest-floating-tab-bar { + * { + /** 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: var(--ion-color-step-100); + --ion-toolbar-background: var(--ion-color-step-150); + --ion-toolbar-border-color: var(--ion-color-step-250); + } + + /* + * 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; + } + } + + :root { + /* Custom tab bar */ + --tab-background: rgb(251, 251, 251); + --tab-color: rgb(153, 153, 153); + --tab-color-selected: black; + } + + @media (prefers-color-scheme: dark) { + :root { + /* Custom tab bar */ + --tab-background: rgb(53, 53, 53); + --tab-color: rgb(83, 83, 83); + --tab-color-selected: white; + } + } +} diff --git a/03_source/mobile/src/pages/DemoQuizApp/AppPages/Home.jsx b/03_source/mobile/src/pages/DemoQuizApp/AppPages/Home.jsx new file mode 100644 index 0000000..75349e2 --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/AppPages/Home.jsx @@ -0,0 +1,63 @@ +import { + IonButton, + IonCol, + IonContent, + IonGrid, + IonHeader, + IonIcon, + IonPage, + IonRow, + IonTitle, + IonToolbar, + useIonActionSheet, +} from '@ionic/react'; +import styles from './Home.module.scss'; + +import { informationCircleOutline } from 'ionicons/icons'; + +const Home = () => { + const [show, hide] = useIonActionSheet(); + + return ( + + + + + + title + + + + + + + Start Playing + + + + show({ + buttons: [{ text: 'Close' }], + header: 'How to play', + subHeader: + 'Pick a category and difficulty, then proceed to answer each question. You will gain a score by getting an answer right and you will also be indicated whether your answer was correct or incorrect. Have fun!', + }) + } + > + How to play + + + + + + ); +}; + +export default Home; diff --git a/03_source/mobile/src/pages/DemoQuizApp/AppPages/Home.module.scss b/03_source/mobile/src/pages/DemoQuizApp/AppPages/Home.module.scss new file mode 100644 index 0000000..d0bd1ef --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/AppPages/Home.module.scss @@ -0,0 +1,43 @@ +.title { + + height: 10rem; + margin-top: 30%; +} + +.buttons { + + position: absolute; + bottom: 3rem; + width: 100%; +} + +.playButton { + + height: 4rem; + --border-radius: 500px; + width: fit-content; + --padding-start: 5rem; + --padding-end: 5rem; + margin: 0 auto; +} + +.helpButton { + + display: flex; + flex-direction: row; + justify-content: center; + align-content: center; + text-align: center; + width: fit-content; + margin: 0 auto; + margin-top: 3rem; + opacity: 70%; + --border-radius: 10rem !important; + --padding-end: 1.25rem; + + ion-icon { + + margin-top: 0.2rem; + margin-right: 0.5rem; + } +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoQuizApp/AppPages/Questions.jsx b/03_source/mobile/src/pages/DemoQuizApp/AppPages/Questions.jsx new file mode 100644 index 0000000..825304c --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/AppPages/Questions.jsx @@ -0,0 +1,193 @@ +import { + IonBadge, + IonButton, + IonCard, + IonCardContent, + IonCardHeader, + IonCardSubtitle, + IonCardTitle, + IonCol, + IonContent, + IonGrid, + IonHeader, + IonIcon, + IonItem, + IonLabel, + IonNote, + IonPage, + IonRow, + IonTitle, + IonToolbar, + useIonRouter, + useIonViewDidEnter, +} from '@ionic/react'; + +import styles from './Quiz.module.scss'; +import { useStoreState } from 'pullstate'; +import { SettingsStore } from '../store'; +import { + getCategories, + getChosenCategory, + getChosenDifficulty, + getDifficulties, +} from '../store/Selectors'; +import { Category, Difficulty } from '../components/Settings'; + +import { useState } from 'react'; +import { useEffect } from 'react'; +import { fetchQuestions } from '../questions'; + +// Import Swiper React components +import { Swiper, SwiperSlide } from 'swiper/react'; + +// Import Swiper styles +// import 'swiper/swiper.scss'; +import 'swiper/css'; + +import { useRef } from 'react'; +import { updateChosenCategory, updateChosenDifficulty } from '../store/SettingsStore'; +import { Answer } from '../components/Answer'; +import { CompletedCard } from '../components/CompletedCard'; +import { QuizStats } from '../components/QuizStats'; + +const Questions = () => { + const mainContainerRef = useRef(); + const completionContainerRef = useRef(); + const swiperRef = useRef(null); + + const router = useIonRouter(); + const chosenCategory = useStoreState(SettingsStore, getChosenCategory); + const chosenDifficulty = useStoreState(SettingsStore, getChosenDifficulty); + + const [currentQuestion, setCurrentQuestion] = useState(1); + const [score, setScore] = useState(0); + const [completed, setCompleted] = useState(false); + + const [questions, setQuestions] = useState(false); + const [slideSpace, setSlideSpace] = useState(0); + + useEffect(() => { + const getQuestions = async () => { + const fetchedQuestions = await fetchQuestions(chosenCategory, chosenDifficulty); + setQuestions(fetchedQuestions); + }; + + getQuestions(); + }, []); + + useIonViewDidEnter(() => { + setSlideSpace(40); + }); + + const handleAnswerClick = (event, answer, question) => { + const isCorrect = question.correct_answers[`${answer}_correct`] === 'true'; + + if (isCorrect) { + event.target.setAttribute('color', 'success'); + } else { + event.target.setAttribute('color', 'danger'); + } + + setTimeout(() => { + isCorrect && setScore((score) => score + 1); + event.target.setAttribute('color', 'light'); + swiperRef.current.swiper.slideNext(); + checkIfComplete(); + }, 1000); + }; + + const checkIfComplete = () => { + if (currentQuestion === questions.length) { + // Quiz has finished + // Hide Slides and show completion screen + mainContainerRef.current.classList.add('animate__zoomOutDown'); + + setTimeout(() => { + setCompleted(true); + completionContainerRef.current.classList.add('animate__zoomInUp'); + }, 1000); + } + }; + + return ( + + + + + logo + + + + + {!completed && ( + + + + + + + setCurrentQuestion(e.activeIndex + 1)} + > + {questions && + questions.map((question, index) => { + return ( + + + + {question.category} + {question.tags.length > 0 && ( + {question.tags[0].name} + )} + + {question.question} + + + + + {Object.keys(question.answers).map((answer, index) => { + if (question.answers[answer] !== null) { + return ( + + ); + } + })} + + + + ); + })} + + + + + + )} + + {completed && ( + + )} + + + ); +}; + +export default Questions; diff --git a/03_source/mobile/src/pages/DemoQuizApp/AppPages/Quiz.jsx b/03_source/mobile/src/pages/DemoQuizApp/AppPages/Quiz.jsx new file mode 100644 index 0000000..987de42 --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/AppPages/Quiz.jsx @@ -0,0 +1,153 @@ +import { + IonButton, + IonCard, + IonCardContent, + IonCardHeader, + IonCardSubtitle, + IonCol, + IonContent, + IonGrid, + IonHeader, + IonIcon, + IonPage, + IonRow, + IonTitle, + IonToolbar, + useIonRouter, + useIonToast, +} from '@ionic/react'; + +import styles from './Quiz.module.scss'; +import { useStoreState } from 'pullstate'; +import { SettingsStore } from '../store'; +import { + getCategories, + getChosenCategory, + getChosenDifficulty, + getDifficulties, +} from '../store/Selectors'; +import { Category, Difficulty } from '../components/Settings'; + +const Quiz = () => { + const router = useIonRouter(); + const categories = useStoreState(SettingsStore, getCategories); + const difficulties = useStoreState(SettingsStore, getDifficulties); + + const chosenCategory = useStoreState(SettingsStore, getChosenCategory); + const chosenDifficulty = useStoreState(SettingsStore, getChosenDifficulty); + + const [show, hide] = useIonToast(); + + const startQuiz = async () => { + if (chosenCategory && chosenDifficulty) { + const chosenCategoryElement = document.getElementById(`categoryButton_${chosenCategory}`); + const chosenDifficultyElement = document.getElementById( + `difficultyButton_${chosenDifficulty}` + ); + + const categoriesCardElement = document.getElementById('categoriesCard'); + const difficultiesCardElement = document.getElementById('difficultiesCard'); + + chosenCategoryElement.classList.add('ontop'); + chosenDifficultyElement.classList.add('ontop'); + + chosenCategoryElement.classList.add('animate__heartBeat'); + chosenDifficultyElement.classList.add('animate__heartBeat'); + + setTimeout(() => { + chosenCategoryElement.classList.remove('animate__heartBeat'); + chosenDifficultyElement.classList.remove('animate__heartBeat'); + chosenCategoryElement.classList.remove('ontop'); + chosenDifficultyElement.classList.remove('ontop'); + }, 1000); + + setTimeout(() => { + categoriesCardElement.classList.add('animate__slideOutRight'); + difficultiesCardElement.classList.add('animate__slideOutLeft'); + + setTimeout(() => { + categoriesCardElement.classList.remove('animate__slideOutRight'); + difficultiesCardElement.classList.remove('animate__slideOutLeft'); + }, 1000); + }, 1100); + + setTimeout(() => { + router.push('/questions'); + }, 1700); + } else { + show({ + header: 'Hang on there!', + message: 'You must choose a category and difficulty!', + duration: 3000, + color: 'warning', + }); + } + }; + + return ( + + + + + logo + + + + + + + + + + Choose a category + + + + + {categories.map((category, index) => { + const chosen = category.value === chosenCategory; + + return ; + })} + + + + + + + + + + + Choose a difficulty + + + + + {difficulties.map((difficulty, index) => { + const chosen = difficulty.value === chosenDifficulty; + + return ( + + ); + })} + + + + + + + + +
+ Start Quiz! +
+
+
+
+
+
+ ); +}; + +export default Quiz; diff --git a/03_source/mobile/src/pages/DemoQuizApp/AppPages/Quiz.module.scss b/03_source/mobile/src/pages/DemoQuizApp/AppPages/Quiz.module.scss new file mode 100644 index 0000000..babf39e --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/AppPages/Quiz.module.scss @@ -0,0 +1,46 @@ +.difficultyContainer { + + margin-top: -2rem !important; +} + +.startButton { + + background-color: #994ec1; + padding: 1.25rem; + margin: 1rem; + margin-top: -1rem; + border-radius: 5px; + text-align: center; + color: white; + border: 2px solid #632485; +} + +.questionTitle { + + font-size: 1rem; +} + +.answerButton { + + height: fit-content; + --padding-top: 1rem; + --padding-bottom: 1rem; +} + +.mainGrid { + + // margin-top: -2rem; +} + +.mainRow { + + margin-top: -2rem; +} + +.emoji { + + font-size: 4rem; + padding: 0; + margin: 0; + padding-top: 1rem; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoQuizApp/components/Answer.jsx b/03_source/mobile/src/pages/DemoQuizApp/components/Answer.jsx new file mode 100644 index 0000000..b18c318 --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/components/Answer.jsx @@ -0,0 +1,17 @@ +import { IonButton, IonCol, IonRow } from '@ionic/react'; +import styles from './Quiz.module.scss'; + +export const Answer = ({ answer, handleAnswerClick, question }) => ( + + + handleAnswerClick(e, answer, question)} + expand="block" + color="light" + className={`ion-text-wrap ${styles.answerButton}`} + > + {question.answers[answer]} + + + +); diff --git a/03_source/mobile/src/pages/DemoQuizApp/components/CompletedCard.jsx b/03_source/mobile/src/pages/DemoQuizApp/components/CompletedCard.jsx new file mode 100644 index 0000000..0be18ca --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/components/CompletedCard.jsx @@ -0,0 +1,58 @@ +import { + IonButton, + IonCard, + IonCardContent, + IonCardHeader, + IonCardSubtitle, + IonCardTitle, + IonCol, + IonGrid, + IonNote, + IonRow, + useIonRouter, +} from '@ionic/react'; +import styles from './Quiz.module.scss'; +import { updateChosenCategory, updateChosenDifficulty } from '../store/SettingsStore'; + +export const CompletedCard = ({ completionContainerRef, score, questionsLength }) => { + const router = useIonRouter(); + + const playAgain = () => { + updateChosenCategory(false); + updateChosenDifficulty(false); + router.push('/'); + }; + + return ( + + + + + + Congratulations + Quiz Complete! +

🎉

+
+ + + You scored + + + {score}/{questionsLength} + + + + Play Again! + + +
+
+
+
+ ); +}; diff --git a/03_source/mobile/src/pages/DemoQuizApp/components/Quiz.module.scss b/03_source/mobile/src/pages/DemoQuizApp/components/Quiz.module.scss new file mode 100644 index 0000000..babf39e --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/components/Quiz.module.scss @@ -0,0 +1,46 @@ +.difficultyContainer { + + margin-top: -2rem !important; +} + +.startButton { + + background-color: #994ec1; + padding: 1.25rem; + margin: 1rem; + margin-top: -1rem; + border-radius: 5px; + text-align: center; + color: white; + border: 2px solid #632485; +} + +.questionTitle { + + font-size: 1rem; +} + +.answerButton { + + height: fit-content; + --padding-top: 1rem; + --padding-bottom: 1rem; +} + +.mainGrid { + + // margin-top: -2rem; +} + +.mainRow { + + margin-top: -2rem; +} + +.emoji { + + font-size: 4rem; + padding: 0; + margin: 0; + padding-top: 1rem; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoQuizApp/components/QuizStats.jsx b/03_source/mobile/src/pages/DemoQuizApp/components/QuizStats.jsx new file mode 100644 index 0000000..137e292 --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/components/QuizStats.jsx @@ -0,0 +1,25 @@ +import { IonCard, IonCardContent, IonCardSubtitle, IonCol, IonItem, IonLabel, IonNote, IonRow } from "@ionic/react"; + +export const QuizStats = ({ chosenCategory, chosenDifficulty, currentQuestion, questionsLength, score }) => ( + + + + + + { chosenCategory } | { chosenDifficulty } + + + Question + { currentQuestion } / { questionsLength } + + + + Score + { score } + + + + + + +); \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoQuizApp/components/Settings.jsx b/03_source/mobile/src/pages/DemoQuizApp/components/Settings.jsx new file mode 100644 index 0000000..6ea398c --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/components/Settings.jsx @@ -0,0 +1,17 @@ +import { IonCol } from "@ionic/react"; +import { updateChosenCategory, updateChosenDifficulty } from "../store/SettingsStore"; +import styles from "./Settings.module.scss"; + +export const Category = ({ label, value, set, chosen }) => ( + + updateChosenCategory(value) }> +

{ label }

+
+); + +export const Difficulty = ({ label, value, set, chosen }) => ( + + updateChosenDifficulty(value) }> +

{ label }

+
+); \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoQuizApp/components/Settings.module.scss b/03_source/mobile/src/pages/DemoQuizApp/components/Settings.module.scss new file mode 100644 index 0000000..1aeadf8 --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/components/Settings.module.scss @@ -0,0 +1,20 @@ +.category { + + height: 4rem; + border: 5px solid rgb(255, 255, 255); + background-color: #994ec1; + color: white; + border-radius: 10px; + text-align: center; + justify-content: center; + align-content: center; + display: flex; + align-items: center; + font-weight: 700; +} + +.chosen { + + border: 2px solid #3a1d49; + font-weight: 700; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoQuizApp/index.tsx b/03_source/mobile/src/pages/DemoQuizApp/index.tsx new file mode 100644 index 0000000..6138886 --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/index.tsx @@ -0,0 +1,33 @@ +import { IonRouterOutlet, IonTabs } from '@ionic/react'; + +import { Route, Redirect } from 'react-router'; + +import Home from './AppPages/Home'; +import Quiz from './AppPages/Quiz'; +import Questions from './AppPages/Questions'; + +import './style.scss'; + +function DemoQuizApp() { + return ( + + + + + + + + + + + + + + + + + + ); +} + +export default DemoQuizApp; diff --git a/03_source/mobile/src/pages/DemoQuizApp/questions/index.js b/03_source/mobile/src/pages/DemoQuizApp/questions/index.js new file mode 100644 index 0000000..000eeba --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/questions/index.js @@ -0,0 +1,11 @@ +const API_URL = 'https://quizapi.io/api/v1/questions'; +const API_KEY = 'B27jnk1wmfEOQ42FtmrgBogiNTLLhOArJj29y24a'; + +export const fetchQuestions = async (category, difficulty) => { + const response = await fetch( + `${API_URL}?apiKey=${API_KEY}&category=${category}&difficulty=${difficulty}&limit=10` + ); + const questions = await response.json(); + + return questions; +}; diff --git a/03_source/mobile/src/pages/DemoQuizApp/store/Selectors.js b/03_source/mobile/src/pages/DemoQuizApp/store/Selectors.js new file mode 100644 index 0000000..1e6b3dd --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/store/Selectors.js @@ -0,0 +1,10 @@ +import { createSelector } from "reselect"; + +const getState = state => state; + +// Getters +export const getCategories = createSelector(getState, state => state.categories); +export const getDifficulties = createSelector(getState, state => state.difficulties); + +export const getChosenCategory = createSelector(getState, state => state.chosenCategory); +export const getChosenDifficulty = createSelector(getState, state => state.chosenDifficulty); \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoQuizApp/store/SettingsStore.js b/03_source/mobile/src/pages/DemoQuizApp/store/SettingsStore.js new file mode 100644 index 0000000..cb35422 --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/store/SettingsStore.js @@ -0,0 +1,60 @@ +import { Store } from "pullstate"; + +const SettingsStore = new Store({ + + categories: [ + { + label: "Code", + value: "code", + }, + { + label: "Linux", + value: "linux" + }, + { + label: "Dev Ops", + value: "devops" + }, + { + label: "Authentication", + value: "authentication" + }, + { + label: "Bash", + value: "bash" + }, + { + label: "SQL", + value: "sql" + } + ], + difficulties: [ + { + label: "Easy", + value: "easy" + }, + { + label: "Medium", + value: "medium" + }, + { + label: "Hard", + value: "hard" + } + ], + + chosenCategory: false, + chosenDifficulty: false +}); + +export default SettingsStore; + +export const updateChosenCategory = category => { + + SettingsStore.update(s => { s.chosenCategory = category }); +} + +export const updateChosenDifficulty = difficulty => { + + SettingsStore.update(s => { s.chosenDifficulty = difficulty }); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoQuizApp/store/index.js b/03_source/mobile/src/pages/DemoQuizApp/store/index.js new file mode 100644 index 0000000..b85ef88 --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/store/index.js @@ -0,0 +1 @@ +export { default as SettingsStore } from "./SettingsStore"; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoQuizApp/style.scss b/03_source/mobile/src/pages/DemoQuizApp/style.scss new file mode 100644 index 0000000..d3a45f3 --- /dev/null +++ b/03_source/mobile/src/pages/DemoQuizApp/style.scss @@ -0,0 +1,113 @@ +.demo-quiz-app { + background-color: gold; + color: cyan; + + /* Ionic Variables and Theming. For more info, please see: +http://ionicframework.com/docs/theming/ */ + + /** Ionic CSS Variables **/ + * { + /** 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; + + /* --ion-background-color: #7a27b0; */ + } + + /* ion-toolbar, +ion-header { + + --background: #7a27b0 !important; +} */ + + ion-toolbar, + ion-header { + --border: none; + --border-color: transparent; + --padding-top: 2rem; + --padding-bottom: 2rem; + } + + ion-content, + ion-toolbar, + ion-header { + background-color: #7a27b0 !important; + --background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='260' height='260' viewBox='0 0 260 260'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23994ec1' fill-opacity='0.4'%3E%3Cpath d='M24.37 16c.2.65.39 1.32.54 2H21.17l1.17 2.34.45.9-.24.11V28a5 5 0 0 1-2.23 8.94l-.02.06a8 8 0 0 1-7.75 6h-20a8 8 0 0 1-7.74-6l-.02-.06A5 5 0 0 1-17.45 28v-6.76l-.79-1.58-.44-.9.9-.44.63-.32H-20a23.01 23.01 0 0 1 44.37-2zm-36.82 2a1 1 0 0 0-.44.1l-3.1 1.56.89 1.79 1.31-.66a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .86.02l2.88-1.27a3 3 0 0 1 2.43 0l2.88 1.27a1 1 0 0 0 .85-.02l3.1-1.55-.89-1.79-1.42.71a3 3 0 0 1-2.56.06l-2.77-1.23a1 1 0 0 0-.4-.09h-.01a1 1 0 0 0-.4.09l-2.78 1.23a3 3 0 0 1-2.56-.06l-2.3-1.15a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1L.9 19.22a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01zm0-2h-4.9a21.01 21.01 0 0 1 39.61 0h-2.09l-.06-.13-.26.13h-32.31zm30.35 7.68l1.36-.68h1.3v2h-36v-1.15l.34-.17 1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0L2.26 23h2.59l1.36.68a3 3 0 0 0 2.56.06l1.67-.74h3.23l1.67.74a3 3 0 0 0 2.56-.06zM-13.82 27l16.37 4.91L18.93 27h-32.75zm-.63 2h.34l16.66 5 16.67-5h.33a3 3 0 1 1 0 6h-34a3 3 0 1 1 0-6zm1.35 8a6 6 0 0 0 5.65 4h20a6 6 0 0 0 5.66-4H-13.1z'/%3E%3Cpath id='path6_fill-copy' d='M284.37 16c.2.65.39 1.32.54 2H281.17l1.17 2.34.45.9-.24.11V28a5 5 0 0 1-2.23 8.94l-.02.06a8 8 0 0 1-7.75 6h-20a8 8 0 0 1-7.74-6l-.02-.06a5 5 0 0 1-2.24-8.94v-6.76l-.79-1.58-.44-.9.9-.44.63-.32H240a23.01 23.01 0 0 1 44.37-2zm-36.82 2a1 1 0 0 0-.44.1l-3.1 1.56.89 1.79 1.31-.66a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .9 0l2.21-1.1a3 3 0 0 1 2.69 0l2.2 1.1a1 1 0 0 0 .86.02l2.88-1.27a3 3 0 0 1 2.43 0l2.88 1.27a1 1 0 0 0 .85-.02l3.1-1.55-.89-1.79-1.42.71a3 3 0 0 1-2.56.06l-2.77-1.23a1 1 0 0 0-.4-.09h-.01a1 1 0 0 0-.4.09l-2.78 1.23a3 3 0 0 1-2.56-.06l-2.3-1.15a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01a1 1 0 0 0-.44.1l-2.21 1.11a3 3 0 0 1-2.69 0l-2.2-1.1a1 1 0 0 0-.45-.11h-.01zm0-2h-4.9a21.01 21.01 0 0 1 39.61 0h-2.09l-.06-.13-.26.13h-32.31zm30.35 7.68l1.36-.68h1.3v2h-36v-1.15l.34-.17 1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.69 0l1.36-.68h2.59l1.36.68a3 3 0 0 0 2.56.06l1.67-.74h3.23l1.67.74a3 3 0 0 0 2.56-.06zM246.18 27l16.37 4.91L278.93 27h-32.75zm-.63 2h.34l16.66 5 16.67-5h.33a3 3 0 1 1 0 6h-34a3 3 0 1 1 0-6zm1.35 8a6 6 0 0 0 5.65 4h20a6 6 0 0 0 5.66-4H246.9z'/%3E%3Cpath d='M159.5 21.02A9 9 0 0 0 151 15h-42a9 9 0 0 0-8.5 6.02 6 6 0 0 0 .02 11.96A8.99 8.99 0 0 0 109 45h42a9 9 0 0 0 8.48-12.02 6 6 0 0 0 .02-11.96zM151 17h-42a7 7 0 0 0-6.33 4h54.66a7 7 0 0 0-6.33-4zm-9.34 26a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-4.34a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-4.34a8.98 8.98 0 0 0 3.34-7h-2a7 7 0 0 1-7 7h-7a7 7 0 1 1 0-14h42a7 7 0 1 1 0 14h-9.34zM109 27a9 9 0 0 0-7.48 4H101a4 4 0 1 1 0-8h58a4 4 0 0 1 0 8h-.52a9 9 0 0 0-7.48-4h-42z'/%3E%3Cpath d='M39 115a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm6-8a6 6 0 1 1-12 0 6 6 0 0 1 12 0zm-3-29v-2h8v-6H40a4 4 0 0 0-4 4v10H22l-1.33 4-.67 2h2.19L26 130h26l3.81-40H58l-.67-2L56 84H42v-6zm-4-4v10h2V74h8v-2h-8a2 2 0 0 0-2 2zm2 12h14.56l.67 2H22.77l.67-2H40zm13.8 4H24.2l3.62 38h22.36l3.62-38z'/%3E%3Cpath d='M129 92h-6v4h-6v4h-6v14h-3l.24 2 3.76 32h36l3.76-32 .24-2h-3v-14h-6v-4h-6v-4h-8zm18 22v-12h-4v4h3v8h1zm-3 0v-6h-4v6h4zm-6 6v-16h-4v19.17c1.6-.7 2.97-1.8 4-3.17zm-6 3.8V100h-4v23.8a10.04 10.04 0 0 0 4 0zm-6-.63V104h-4v16a10.04 10.04 0 0 0 4 3.17zm-6-9.17v-6h-4v6h4zm-6 0v-8h3v-4h-4v12h1zm27-12v-4h-4v4h3v4h1v-4zm-6 0v-8h-4v4h3v4h1zm-6-4v-4h-4v8h1v-4h3zm-6 4v-4h-4v8h1v-4h3zm7 24a12 12 0 0 0 11.83-10h7.92l-3.53 30h-32.44l-3.53-30h7.92A12 12 0 0 0 130 126z'/%3E%3Cpath d='M212 86v2h-4v-2h4zm4 0h-2v2h2v-2zm-20 0v.1a5 5 0 0 0-.56 9.65l.06.25 1.12 4.48a2 2 0 0 0 1.94 1.52h.01l7.02 24.55a2 2 0 0 0 1.92 1.45h4.98a2 2 0 0 0 1.92-1.45l7.02-24.55a2 2 0 0 0 1.95-1.52L224.5 96l.06-.25a5 5 0 0 0-.56-9.65V86a14 14 0 0 0-28 0zm4 0h6v2h-9a3 3 0 1 0 0 6H223a3 3 0 1 0 0-6H220v-2h2a12 12 0 1 0-24 0h2zm-1.44 14l-1-4h24.88l-1 4h-22.88zm8.95 26l-6.86-24h18.7l-6.86 24h-4.98zM150 242a22 22 0 1 0 0-44 22 22 0 0 0 0 44zm24-22a24 24 0 1 1-48 0 24 24 0 0 1 48 0zm-28.38 17.73l2.04-.87a6 6 0 0 1 4.68 0l2.04.87a2 2 0 0 0 2.5-.82l1.14-1.9a6 6 0 0 1 3.79-2.75l2.15-.5a2 2 0 0 0 1.54-2.12l-.19-2.2a6 6 0 0 1 1.45-4.46l1.45-1.67a2 2 0 0 0 0-2.62l-1.45-1.67a6 6 0 0 1-1.45-4.46l.2-2.2a2 2 0 0 0-1.55-2.13l-2.15-.5a6 6 0 0 1-3.8-2.75l-1.13-1.9a2 2 0 0 0-2.5-.8l-2.04.86a6 6 0 0 1-4.68 0l-2.04-.87a2 2 0 0 0-2.5.82l-1.14 1.9a6 6 0 0 1-3.79 2.75l-2.15.5a2 2 0 0 0-1.54 2.12l.19 2.2a6 6 0 0 1-1.45 4.46l-1.45 1.67a2 2 0 0 0 0 2.62l1.45 1.67a6 6 0 0 1 1.45 4.46l-.2 2.2a2 2 0 0 0 1.55 2.13l2.15.5a6 6 0 0 1 3.8 2.75l1.13 1.9a2 2 0 0 0 2.5.8zm2.82.97a4 4 0 0 1 3.12 0l2.04.87a4 4 0 0 0 4.99-1.62l1.14-1.9a4 4 0 0 1 2.53-1.84l2.15-.5a4 4 0 0 0 3.09-4.24l-.2-2.2a4 4 0 0 1 .97-2.98l1.45-1.67a4 4 0 0 0 0-5.24l-1.45-1.67a4 4 0 0 1-.97-2.97l.2-2.2a4 4 0 0 0-3.09-4.25l-2.15-.5a4 4 0 0 1-2.53-1.84l-1.14-1.9a4 4 0 0 0-5-1.62l-2.03.87a4 4 0 0 1-3.12 0l-2.04-.87a4 4 0 0 0-4.99 1.62l-1.14 1.9a4 4 0 0 1-2.53 1.84l-2.15.5a4 4 0 0 0-3.09 4.24l.2 2.2a4 4 0 0 1-.97 2.98l-1.45 1.67a4 4 0 0 0 0 5.24l1.45 1.67a4 4 0 0 1 .97 2.97l-.2 2.2a4 4 0 0 0 3.09 4.25l2.15.5a4 4 0 0 1 2.53 1.84l1.14 1.9a4 4 0 0 0 5 1.62l2.03-.87zM152 207a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm6 2a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-11 1a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-6 0a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm3-5a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-8 8a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm3 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm0 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4 7a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm5-2a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm5 4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4-6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm6-4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-4-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm4-3a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-5-4a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm-24 6a1 1 0 1 1 2 0 1 1 0 0 1-2 0zm16 5a5 5 0 1 0 0-10 5 5 0 0 0 0 10zm7-5a7 7 0 1 1-14 0 7 7 0 0 1 14 0zm86-29a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm19 9a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-14 5a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-25 1a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm5 4a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm9 0a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm15 1a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm12-2a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-11-14a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-19 0a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm6 5a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-25 15c0-.47.01-.94.03-1.4a5 5 0 0 1-1.7-8 3.99 3.99 0 0 1 1.88-5.18 5 5 0 0 1 3.4-6.22 3 3 0 0 1 1.46-1.05 5 5 0 0 1 7.76-3.27A30.86 30.86 0 0 1 246 184c6.79 0 13.06 2.18 18.17 5.88a5 5 0 0 1 7.76 3.27 3 3 0 0 1 1.47 1.05 5 5 0 0 1 3.4 6.22 4 4 0 0 1 1.87 5.18 4.98 4.98 0 0 1-1.7 8c.02.46.03.93.03 1.4v1h-62v-1zm.83-7.17a30.9 30.9 0 0 0-.62 3.57 3 3 0 0 1-.61-4.2c.37.28.78.49 1.23.63zm1.49-4.61c-.36.87-.68 1.76-.96 2.68a2 2 0 0 1-.21-3.71c.33.4.73.75 1.17 1.03zm2.32-4.54c-.54.86-1.03 1.76-1.49 2.68a3 3 0 0 1-.07-4.67 3 3 0 0 0 1.56 1.99zm1.14-1.7c.35-.5.72-.98 1.1-1.46a1 1 0 1 0-1.1 1.45zm5.34-5.77c-1.03.86-2 1.79-2.9 2.77a3 3 0 0 0-1.11-.77 3 3 0 0 1 4-2zm42.66 2.77c-.9-.98-1.87-1.9-2.9-2.77a3 3 0 0 1 4.01 2 3 3 0 0 0-1.1.77zm1.34 1.54c.38.48.75.96 1.1 1.45a1 1 0 1 0-1.1-1.45zm3.73 5.84c-.46-.92-.95-1.82-1.5-2.68a3 3 0 0 0 1.57-1.99 3 3 0 0 1-.07 4.67zm1.8 4.53c-.29-.9-.6-1.8-.97-2.67.44-.28.84-.63 1.17-1.03a2 2 0 0 1-.2 3.7zm1.14 5.51c-.14-1.21-.35-2.4-.62-3.57.45-.14.86-.35 1.23-.63a2.99 2.99 0 0 1-.6 4.2zM275 214a29 29 0 0 0-57.97 0h57.96zM72.33 198.12c-.21-.32-.34-.7-.34-1.12v-12h-2v12a4.01 4.01 0 0 0 7.09 2.54c.57-.69.91-1.57.91-2.54v-12h-2v12a1.99 1.99 0 0 1-2 2 2 2 0 0 1-1.66-.88zM75 176c.38 0 .74-.04 1.1-.12a4 4 0 0 0 6.19 2.4A13.94 13.94 0 0 1 84 185v24a6 6 0 0 1-6 6h-3v9a5 5 0 1 1-10 0v-9h-3a6 6 0 0 1-6-6v-24a14 14 0 0 1 14-14 5 5 0 0 0 5 5zm-17 15v12a1.99 1.99 0 0 0 1.22 1.84 2 2 0 0 0 2.44-.72c.21-.32.34-.7.34-1.12v-12h2v12a3.98 3.98 0 0 1-5.35 3.77 3.98 3.98 0 0 1-.65-.3V209a4 4 0 0 0 4 4h16a4 4 0 0 0 4-4v-24c.01-1.53-.23-2.88-.72-4.17-.43.1-.87.16-1.28.17a6 6 0 0 1-5.2-3 7 7 0 0 1-6.47-4.88A12 12 0 0 0 58 185v6zm9 24v9a3 3 0 1 0 6 0v-9h-6z'/%3E%3Cpath d='M-17 191a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm19 9a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2H3a1 1 0 0 1-1-1zm-14 5a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm-25 1a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm5 4a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm9 0a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm15 1a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm12-2a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2H4zm-11-14a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-19 0a1 1 0 0 0 0 2h2a1 1 0 0 0 0-2h-2zm6 5a1 1 0 0 1 1-1h2a1 1 0 0 1 0 2h-2a1 1 0 0 1-1-1zm-25 15c0-.47.01-.94.03-1.4a5 5 0 0 1-1.7-8 3.99 3.99 0 0 1 1.88-5.18 5 5 0 0 1 3.4-6.22 3 3 0 0 1 1.46-1.05 5 5 0 0 1 7.76-3.27A30.86 30.86 0 0 1-14 184c6.79 0 13.06 2.18 18.17 5.88a5 5 0 0 1 7.76 3.27 3 3 0 0 1 1.47 1.05 5 5 0 0 1 3.4 6.22 4 4 0 0 1 1.87 5.18 4.98 4.98 0 0 1-1.7 8c.02.46.03.93.03 1.4v1h-62v-1zm.83-7.17a30.9 30.9 0 0 0-.62 3.57 3 3 0 0 1-.61-4.2c.37.28.78.49 1.23.63zm1.49-4.61c-.36.87-.68 1.76-.96 2.68a2 2 0 0 1-.21-3.71c.33.4.73.75 1.17 1.03zm2.32-4.54c-.54.86-1.03 1.76-1.49 2.68a3 3 0 0 1-.07-4.67 3 3 0 0 0 1.56 1.99zm1.14-1.7c.35-.5.72-.98 1.1-1.46a1 1 0 1 0-1.1 1.45zm5.34-5.77c-1.03.86-2 1.79-2.9 2.77a3 3 0 0 0-1.11-.77 3 3 0 0 1 4-2zm42.66 2.77c-.9-.98-1.87-1.9-2.9-2.77a3 3 0 0 1 4.01 2 3 3 0 0 0-1.1.77zm1.34 1.54c.38.48.75.96 1.1 1.45a1 1 0 1 0-1.1-1.45zm3.73 5.84c-.46-.92-.95-1.82-1.5-2.68a3 3 0 0 0 1.57-1.99 3 3 0 0 1-.07 4.67zm1.8 4.53c-.29-.9-.6-1.8-.97-2.67.44-.28.84-.63 1.17-1.03a2 2 0 0 1-.2 3.7zm1.14 5.51c-.14-1.21-.35-2.4-.62-3.57.45-.14.86-.35 1.23-.63a2.99 2.99 0 0 1-.6 4.2zM15 214a29 29 0 0 0-57.97 0h57.96z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); + } + + ion-col { + padding: 5px; + } + + .ontop { + z-index: 9999; + } +} diff --git a/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/ActionSheet.jsx b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/ActionSheet.jsx new file mode 100644 index 0000000..8d223d7 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/ActionSheet.jsx @@ -0,0 +1,65 @@ +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonMenuButton, + IonPage, + IonTitle, + IonToolbar, + useIonActionSheet, +} from '@ionic/react'; + +const ActionSheet = () => { + const [present, dismiss] = useIonActionSheet(); + + return ( + + + + + + + Action Sheet + + + + + + + Action Sheet + + + + + present({ + buttons: [{ text: 'Ok' }, { text: 'Cancel' }], + header: 'Action Sheet', + }) + } + > + Show ActionSheet + + present([{ text: 'Ok' }, { text: 'Cancel' }], 'Action Sheet')} + > + Show ActionSheet using params + + { + present([{ text: 'Ok' }, { text: 'Cancel' }], 'Action Sheet'); + setTimeout(dismiss, 3000); + }} + > + Show ActionSheet, hide after 3 seconds + + + + ); +}; + +export default ActionSheet; diff --git a/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Alert.jsx b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Alert.jsx new file mode 100644 index 0000000..d23d1ed --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Alert.jsx @@ -0,0 +1,56 @@ +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonMenuButton, + IonPage, + IonTitle, + IonToolbar, + useIonAlert, +} from '@ionic/react'; + +const Alert = () => { + const [present] = useIonAlert(); + + return ( + + + + + + + Alert + + + + + + + Alert + + + + + present({ + cssClass: 'my-css', + header: 'Alert', + message: 'alert from hook', + buttons: ['Cancel', { text: 'Ok', handler: (d) => console.log('ok pressed') }], + onDidDismiss: (e) => console.log('did dismiss'), + }) + } + > + Show Alert + + present('hello with params', [{ text: 'Ok' }])}> + Show Alert using params + + + + ); +}; + +export default Alert; diff --git a/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/All.jsx b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/All.jsx new file mode 100644 index 0000000..6db05e9 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/All.jsx @@ -0,0 +1,76 @@ +import { + IonButtons, + IonCard, + IonCardHeader, + IonContent, + IonHeader, + IonMenuButton, + IonPage, + IonTitle, + IonToolbar, + IonCardTitle, + IonCardSubtitle, + IonCardContent, + IonText, +} from '@ionic/react'; + +const All = () => { + return ( + + + + + + + All + + + + + + + All + + + + + + Sample usage + Overlay Hooks + + + + +

+ In Ionic React 5.6, the team packaged up a new set of hooks for controlling overlay + components that they thought we might like. What is an overlay you ask? It’s the + term that Ionic give components that display over your current content, such as + alerts, modals, toasts, etc. +

+
+
+ +

+ All of the code is taken from the Ionic Framework docs. You can find the blog post + outlining these new overlay hooks{' '} + + here. + +

+
+
+ +

Check out the samples by navigating to a respective one in the side menu.

+
+
+
+
+
+ ); +}; + +export default All; diff --git a/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Loading.jsx b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Loading.jsx new file mode 100644 index 0000000..803f134 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Loading.jsx @@ -0,0 +1,52 @@ +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonMenuButton, + IonPage, + IonTitle, + IonToolbar, + useIonLoading, +} from '@ionic/react'; + +const Loading = () => { + const [present] = useIonLoading(); + + return ( + + + + + + + Loading + + + + + + + Loading + + + + + present({ + duration: 3000, + }) + } + > + Show Loading + + present('Loading', 2000, 'dots')}> + Show Loading using params + + + + ); +}; + +export default Loading; diff --git a/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Modal.jsx b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Modal.jsx new file mode 100644 index 0000000..0533680 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Modal.jsx @@ -0,0 +1,80 @@ +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonMenuButton, + IonPage, + IonText, + IonTitle, + IonToolbar, + useIonModal, +} from '@ionic/react'; +import { useState } from 'react'; + +const Modal = () => { + const Body = ({ count, onDismiss, onIncrement }) => ( +
+ + Count: {count} + + onIncrement()}> + Increment Count + + onDismiss()}> + Close + +
+ ); + + const [count, setCount] = useState(0); + + const handleIncrement = () => { + setCount(count + 1); + }; + + const handleDismiss = () => { + dismiss(); + }; + + const [present, dismiss] = useIonModal(Body, { + count, + onDismiss: handleDismiss, + onIncrement: handleIncrement, + }); + + return ( + + + + + + + Modal + + + + + + + Modal + + + + { + present({ + cssClass: 'my-class', + }); + }} + > + Show Modal + +
Count: {count}
+
+
+ ); +}; + +export default Modal; diff --git a/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Picker.jsx b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Picker.jsx new file mode 100644 index 0000000..d036f6f --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Picker.jsx @@ -0,0 +1,104 @@ +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonMenuButton, + IonPage, + IonTitle, + IonToolbar, + useIonPicker, +} from '@ionic/react'; +import { useState } from 'react'; + +const Picker = () => { + const [present] = useIonPicker(); + const [value, setValue] = useState(''); + + return ( + + + + + + + Picker + + + + + + + Picker + + + + + present({ + buttons: [ + { + text: 'Confirm', + handler: (selected) => { + setValue(selected.animal.value); + }, + }, + ], + columns: [ + { + name: 'animal', + options: [ + { text: 'Dog', value: 'dog' }, + { text: 'Cat', value: 'cat' }, + { text: 'Bird', value: 'bird' }, + ], + }, + ], + }) + } + > + Show Picker + + + present( + [ + { + name: 'animal', + options: [ + { text: 'Dog', value: 'dog' }, + { text: 'Cat', value: 'cat' }, + { text: 'Bird', value: 'bird' }, + ], + }, + { + name: 'vehicle', + options: [ + { text: 'Car', value: 'car' }, + { text: 'Truck', value: 'truck' }, + { text: 'Bike', value: 'bike' }, + ], + }, + ], + [ + { + text: 'Confirm', + handler: (selected) => { + setValue(`${selected.animal.value}, ${selected.vehicle.value}`); + }, + }, + ] + ) + } + > + Show Picker using params + + {value &&
Selected Value: {value}
} +
+
+ ); +}; + +export default Picker; diff --git a/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Popover.jsx b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Popover.jsx new file mode 100644 index 0000000..20b54c7 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Popover.jsx @@ -0,0 +1,65 @@ +import { + IonButtons, + IonContent, + IonHeader, + IonItem, + IonListHeader, + IonMenuButton, + IonPage, + IonTitle, + IonToolbar, + IonList, + useIonPopover, + IonButton, +} from '@ionic/react'; + +const Popover = () => { + const PopoverList = ({ onHide }) => ( + + Ionic + Learn Ionic + Documentation + Showcase + GitHub Repo + + Close + + + ); + + const [present, dismiss] = useIonPopover(PopoverList, { onHide: () => dismiss() }); + + return ( + + + + + + + Popover + + + + + + + Popover + + + + + present({ + event: e.nativeEvent, + }) + } + > + Show Popover + + + + ); +}; + +export default Popover; diff --git a/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Toast.jsx b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Toast.jsx new file mode 100644 index 0000000..aabc217 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactOverlayHooks/AppPages/Toast.jsx @@ -0,0 +1,58 @@ +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonMenuButton, + IonPage, + IonTitle, + IonToolbar, + useIonToast, +} from '@ionic/react'; + +const Toast = () => { + const [present, dismiss] = useIonToast(); + + return ( + + + + + + + Toast + + + + + + + Toast + + + + + present({ + buttons: [{ text: 'hide', handler: () => dismiss() }], + message: 'toast from hook, click hide to dismiss', + onDidDismiss: () => console.log('dismissed'), + onWillDismiss: () => console.log('will dismiss'), + }) + } + > + Show Toast + + present('hello from hook', 3000)}> + Show Toast using params, closes in 3 secs + + + Hide Toast + + + + ); +}; + +export default Toast; diff --git a/03_source/mobile/src/pages/DemoReactOverlayHooks/components/Menu.css b/03_source/mobile/src/pages/DemoReactOverlayHooks/components/Menu.css new file mode 100644 index 0000000..0ca47a2 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactOverlayHooks/components/Menu.css @@ -0,0 +1,113 @@ +ion-menu ion-content { + --background: var(--ion-item-background, var(--ion-background-color, #fff)); +} + +ion-menu.md ion-content { + --padding-start: 8px; + --padding-end: 8px; + --padding-top: 20px; + --padding-bottom: 20px; +} + +ion-menu.md ion-list { + padding: 20px 0; +} + +ion-menu.md ion-note { + margin-bottom: 30px; +} + +ion-menu.md ion-list-header, ion-menu.md ion-note { + padding-left: 10px; +} + +ion-menu.md ion-list#inbox-list { + border-bottom: 1px solid var(--ion-color-step-150, #d7d8da); +} + +ion-menu.md ion-list#inbox-list ion-list-header { + font-size: 22px; + font-weight: 600; + min-height: 20px; +} + +ion-menu.md ion-list#labels-list ion-list-header { + font-size: 16px; + margin-bottom: 18px; + color: #757575; + min-height: 26px; +} + +ion-menu.md ion-item { + --padding-start: 10px; + --padding-end: 10px; + border-radius: 4px; +} + +ion-menu.md ion-item.selected { + --background: rgba(var(--ion-color-primary-rgb), 0.14); +} + +ion-menu.md ion-item.selected ion-icon { + color: var(--ion-color-primary); +} + +ion-menu.md ion-item ion-icon { + color: #616e7e; +} + +ion-menu.md ion-item ion-label { + font-weight: 500; +} + +ion-menu.ios ion-content { + --padding-bottom: 20px; +} + +ion-menu.ios ion-list { + padding: 20px 0 0 0; +} + +ion-menu.ios ion-note { + line-height: 24px; + margin-bottom: 20px; +} + +ion-menu.ios ion-item { + --padding-start: 16px; + --padding-end: 16px; + --min-height: 50px; +} + +ion-menu.ios ion-item ion-icon { + font-size: 24px; + color: #73849a; +} + +ion-menu.ios ion-item .selected ion-icon { + color: var(--ion-color-primary); +} + +ion-menu.ios ion-list#labels-list ion-list-header { + margin-bottom: 8px; +} + +ion-menu.ios ion-list-header, +ion-menu.ios ion-note { + padding-left: 16px; + padding-right: 16px; +} + +ion-menu.ios ion-note { + margin-bottom: 8px; +} + +ion-note { + display: inline-block; + font-size: 16px; + color: var(--ion-color-medium-shade); +} + +ion-item.selected { + --color: var(--ion-color-primary); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactOverlayHooks/components/Menu.jsx b/03_source/mobile/src/pages/DemoReactOverlayHooks/components/Menu.jsx new file mode 100644 index 0000000..6c33109 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactOverlayHooks/components/Menu.jsx @@ -0,0 +1,37 @@ +import { IonContent, IonIcon, IonItem, IonLabel, IonList, IonListHeader, IonMenu, IonMenuToggle, IonNote } from '@ionic/react'; + +import { useLocation } from 'react-router-dom'; +import { star, starOutline } from 'ionicons/icons'; +import './Menu.css'; + +const Menu = ({ pages }) => { + + const location = useLocation(); + + return ( + + + + Overlay Hooks + Choose one below to see a demo + + { pages.map((appPage, index) => { + + const isSelected = location.pathname === appPage.url; + + return ( + + + + { appPage.label } + + + ); + })} + + + + ); +}; + +export default Menu; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactOverlayHooks/index.tsx b/03_source/mobile/src/pages/DemoReactOverlayHooks/index.tsx new file mode 100644 index 0000000..a6faa34 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactOverlayHooks/index.tsx @@ -0,0 +1,62 @@ +import { + IonIcon, + IonLabel, + IonRouterOutlet, + IonSplitPane, + IonTabBar, + IonTabButton, + IonTabs, +} from '@ionic/react'; + +import { cloudOutline, searchOutline } from 'ionicons/icons'; +import { Route, Redirect } from 'react-router'; +import Menu from './components/Menu'; + +import All from './AppPages/All'; +import ActionSheet from './AppPages/ActionSheet'; +import Alert from './AppPages/Alert'; +import Loading from './AppPages/Loading'; +import Modal from './AppPages/Modal'; +import Picker from './AppPages/Picker'; +import Popover from './AppPages/Popover'; +import Toast from './AppPages/Toast'; +import './style.scss'; + +function DemoReactOverlayHooks() { + const pages = [ + { label: 'All', url: '/overlay/all', component: All }, + { label: 'Action Sheet', url: '/overlay/action-sheet', component: ActionSheet }, + { label: 'Alert', url: '/overlay/alert', component: Alert }, + { label: 'Loading', url: '/overlay/loading', component: Loading }, + { label: 'Modal', url: '/overlay/modal', component: Modal }, + { label: 'Picker', url: '/overlay/picker', component: Picker }, + { label: 'Popover', url: '/overlay/popover', component: Popover }, + { label: 'Toast', url: '/overlay/toast', component: Toast }, + ]; + + return ( + + + + + + + + {pages.map((page, index) => { + const pageComponent = page.component; + + return ( + + ); + })} + + + ); +} + +export default DemoReactOverlayHooks; diff --git a/03_source/mobile/src/pages/DemoReactOverlayHooks/style.scss b/03_source/mobile/src/pages/DemoReactOverlayHooks/style.scss new file mode 100644 index 0000000..37c1e1a --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactOverlayHooks/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/DemoReactPollApp/AppPages/Add.jsx b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/Add.jsx new file mode 100644 index 0000000..41c1a75 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/Add.jsx @@ -0,0 +1,198 @@ +import { IonBackButton, IonButton, IonButtons, IonCardTitle, IonCol, IonContent, IonFooter, IonGrid, IonHeader, IonIcon, IonItem, IonLabel, IonMenuButton, IonPage, IonRow, IonTextarea, IonTitle, IonToolbar, useIonRouter, useIonViewDidEnter } from '@ionic/react'; +import { useState } from 'react'; + +import { GithubPicker } from 'react-color'; +import { getColors } from '../helpers/utils'; +import { addOutline } from 'ionicons/icons'; +import { PollDuration } from '../components/PollDuration'; +import { PollAnswer } from '../components/PollAnswer'; +import { addPoll } from '../store/PollStore'; + +const Add = () => { + + const router = useIonRouter(); + const [ showPicker, setShowPicker ] = useState(false); + const [ pollQuestion, setPollQuestion ] = useState(""); + const [ pollColor, setPollColor ] = useState("#427ed8"); + const [ pollAnswers, setPollAnswers ] = useState([]); + + const [ pollDays, setPollDays ] = useState(0); + const [ pollHours, setPollHours ] = useState(0); + const [ pollMins, setPollMins ] = useState(0); + + const pickerColors = [ + + "#759dc7", + "#68bd8d", + "#bd7368", + "#8d68bd", + "#bd68ac", + "#6868bd", + "#68a8bd", + "#68bda5", + "#bd9868", + "#d84848", + "#d87c48", + "#d8bb48", + "#7c7c7c" + ]; + + const colors = pollColor && getColors(pollColor); + + useIonViewDidEnter(() => { + + setShowPicker(false); + setPollQuestion(""); + setPollColor("#427ed8"); + setPollAnswers([]); + + setPollDays(0); + setPollHours(0); + setPollMins(0); + }); + + const handleAdd = async () => { + + const timeLeftDays = pollDays !== 0 && pollDays !== "" ? `${ pollDays } days, ` : ""; + const timeLeftHours = pollHours !== 0 && pollHours !== "" ? `${ pollHours } hours, ` : ""; + const timeLeftMins = pollMins !== 0 && pollMins !== "" ? `${ pollMins } mins` : ""; + + const timeLeft = `${ timeLeftDays }${ timeLeftHours }${ timeLeftMins }`; + + const poll = { + + id: Date.now(), + question: pollQuestion, + color: pollColor, + timeLeft, + answers: pollAnswers, + totalVotes: 0, + voted: false + }; + + addPoll(poll); + router.push("/page/view"); + } + + const addAnswer = () => { + + const answer = { + + id: Date.now(), + answer: "", + votes: 0, + voted: false, + percent: 0 + }; + + setPollAnswers(prev => [...prev, answer ] ); + } + + const removeAnswer = answer => { + + const newAnswers = pollAnswers.filter((p) => p !== answer); + setPollAnswers(newAnswers); + } + + const handleChange = (e, index) => { + + const newAnswers = [ ...pollAnswers ]; + newAnswers[index].answer = e.target.value; + setPollAnswers(newAnswers); + } + + return ( + + + + + + + Add Poll + + + + + + + Add Poll + + + + + + + setShowPicker(!showPicker) } size="large" fill="solid" style={ colors.votedButtonStyle }>Poll Color + { showPicker && setPollColor(color.hex) } onChangeComplete={ () => setShowPicker(false) } /> } + + + + Poll Question + setPollQuestion(e.target.value) } placeholder="A question to ask..." /> + + + + + + + Poll Duration + + + + + + + + + + + + + + + Poll Answers + + + + + 0 }> + + + + + + { pollAnswers.length > 0 && pollAnswers.map((answer, index) => { + + return ; + })} + + + { !pollAnswers.length && + + + + +

There are currenty no answers added for this poll.

+ Add one now +
+
+
+ } +
+
+
+ + + + + + Save + + + + +
+ ); +}; + +export default Add; diff --git a/03_source/mobile/src/pages/DemoReactPollApp/AppPages/Add.module.scss b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/Add.module.scss new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoReactPollApp/AppPages/Tab1.jsx b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/Tab1.jsx new file mode 100644 index 0000000..54ab2b9 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/Tab1.jsx @@ -0,0 +1,96 @@ +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); + }; + + // const router = useIonRouter(); + 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/DemoReactPollApp/AppPages/Tab2.jsx b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/Tab2.jsx new file mode 100644 index 0000000..216544f --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/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/DemoReactPollApp/AppPages/View.jsx b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/View.jsx new file mode 100644 index 0000000..4c38d2e --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/View.jsx @@ -0,0 +1,91 @@ +import { + IonButton, + IonButtons, + IonCard, + IonCardHeader, + IonCardSubtitle, + IonCardTitle, + IonCol, + IonContent, + IonFooter, + IonHeader, + IonIcon, + IonMenuButton, + IonPage, + IonRow, + IonTitle, + IonToolbar, +} from '@ionic/react'; +import { arrowForwardOutline } from 'ionicons/icons'; +import { getCardStyle } from '../helpers/utils'; +import { PollStore } from '../store'; +import { getPolls } from '../store/Selectors'; +import styles from './View.module.scss'; + +const View = () => { + const polls = PollStore.useState(getPolls); + + return ( + + + + + + + Ionic Polls + + + + + + + Ionic Polls + + + + {polls.map((poll) => { + const colors = getCardStyle(poll.color); + + return ( + + + + + {poll.question} + {poll.timeLeft} left +

{poll.totalVotes} votes already

+

{poll.voted ? 'You have voted on this poll' : "You haven't voted on this poll"}

+
+
+ + + + View + + + +
+
+ ); + })} +
+ + + + + + Add new poll + + + + +
+ ); +}; + +export default View; diff --git a/03_source/mobile/src/pages/DemoReactPollApp/AppPages/View.module.scss b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/View.module.scss new file mode 100644 index 0000000..efa8dd9 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/View.module.scss @@ -0,0 +1,28 @@ +.pollQuestion { + + ion-card-title { + + font-size: 1rem; + } + + ion-card-subtitle { + + font-size: 0.7rem; + } + + ion-button { + + margin-left: -1.5rem; + + ion-icon { + + font-size: 1rem; + margin-left: 0.3rem; + } + } + + p { + + + } +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactPollApp/AppPages/ViewPoll.jsx b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/ViewPoll.jsx new file mode 100644 index 0000000..a1bda7b --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/ViewPoll.jsx @@ -0,0 +1,130 @@ +import { IonBackButton, IonButton, IonButtons, IonCard, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCol, IonContent, IonFooter, IonHeader, IonIcon, IonPage, IonProgressBar, IonRow, IonTitle, IonToast, IonToolbar, useIonViewWillEnter, useIonPopover } from '@ionic/react'; +import { arrowRedoOutline, colorWandOutline } from 'ionicons/icons'; +import { useState } from 'react'; +import { useParams } from 'react-router'; +import { PollStore } from '../store'; +import { addVote } from '../store/PollStore'; +import { getPoll } from '../store/Selectors'; +import styles from "./ViewPoll.module.scss"; +import { getColors } from '../helpers/utils'; +import { SharePopover } from '../components/Share'; +import { useRef } from 'react'; + +const ViewPoll = () => { + + const params = useParams(); + const poll = PollStore.useState(getPoll(params.id)); + + const answerRefs = useRef([]); + const [ colors, setColors ] = useState({}); + const [ showToast, setShowToast ] = useState(false); + const [ showVotes, setShowVotes ] = useState(false); + + const [ present, dismiss ] = useIonPopover(SharePopover, { onHide: () => dismiss(), poll, setShowToast }); + + useIonViewWillEnter(() => { + + const cardStyleColors = getColors(poll.color); + setColors(cardStyleColors); + }); + + const vote = answerId => { + + if (!poll.voted) { + + poll.answers.forEach((answer, index) => { + + const pollAnswer = answerRefs.current[index]; + pollAnswer.classList.add("animate__fadeOut"); + }); + + setTimeout(() => addVote(params.id, answerId), 550); + } + } + + const getAnswerPercentage = (totalVotes, answerVotes) => { + + const percent = Math.round((answerVotes / totalVotes) * 100); + return percent; + } + + return ( + + + + + + + Ionic Poll + + + + + + + Ionic Poll + + + + + + { poll.question } + { poll.timeLeft } left +

{ poll.totalVotes } votes already

+ +
+
+ +
+ { poll.answers.map((answer, index) => { + + const answerPercentage = getAnswerPercentage(poll.totalVotes, answer.votes); + + return ( + + + { !poll.voted && + answerRefs.current[index] = ref } expand="block" fill={ answer.voted ? "solid" : "outline" } style={ answer.voted ? colors.votedButtonStyle : colors.notVotedbuttonStyle } onClick={ () => vote(answer.id) }> + { answer.answer } + { poll.voted && ` (${ answerPercentage }%)` } + + } + + { (poll.voted) && +
+

{ answer.answer } ({ answerPercentage })%

+ { showVotes &&

{ answer.votes } of { poll.totalVotes } total votes

} + +
+ } +
+
+ ); + })} +
+ + setShowToast(false) } isOpen={ showToast } color="dark" position="bottom" /> +
+ + + + + present({ event: e.nativeEvent }) }> + +   Share + + + + + setShowVotes(!showVotes) } disabled={ !poll.voted }> + +   { showVotes ? "Hide" : "Show" } votes + + + + +
+ ); +}; + +export default ViewPoll; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactPollApp/AppPages/ViewPoll.module.scss b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/ViewPoll.module.scss new file mode 100644 index 0000000..8d3d411 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/AppPages/ViewPoll.module.scss @@ -0,0 +1,40 @@ +.pollQuestion { + + ion-card-title { + + font-size: 1rem; + } + + ion-card-subtitle { + + font-size: 0.7rem; + } + + ion-button { + + margin-left: -1.5rem; + + ion-icon { + + font-size: 1rem; + margin-left: 0.3rem; + } + } +} + +.pollAnswer { + + p { + + margin: 0; + padding: 0; + text-align: center; + font-weight: 600; + margin-bottom: 0.2rem; + } + + div { + + margin-bottom: 1rem; + } +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactPollApp/TODO.md b/03_source/mobile/src/pages/DemoReactPollApp/TODO.md new file mode 100644 index 0000000..0bf5d30 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/TODO.md @@ -0,0 +1,4 @@ +# TODO + +css temporary broken, ignored +https://ionicreacthub.com/ionic-react-poll-app diff --git a/03_source/mobile/src/pages/DemoReactPollApp/components/PollAnswer.jsx b/03_source/mobile/src/pages/DemoReactPollApp/components/PollAnswer.jsx new file mode 100644 index 0000000..4797f02 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/components/PollAnswer.jsx @@ -0,0 +1,21 @@ +import { IonButton, IonCol, IonIcon, IonInput, IonItem, IonLabel, IonRow } from "@ionic/react"; +import { trashOutline } from "ionicons/icons"; + +export const PollAnswer = ({ index, value, change, remove}) => ( + + + + + +

Option { index + 1 }

+ change(e, index) } placeholder="Enter answer..." /> +
+
+
+ + remove(value) }> + + + +
+); \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactPollApp/components/PollDuration.jsx b/03_source/mobile/src/pages/DemoReactPollApp/components/PollDuration.jsx new file mode 100644 index 0000000..3cdfb2d --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/components/PollDuration.jsx @@ -0,0 +1,13 @@ +import { IonCardSubtitle, IonCol, IonInput, IonItem } from "@ionic/react"; + +export const PollDuration = ({ label, value, setter }) => ( + + + + + { label } + setter(e.target.value) } /> + + + +); \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactPollApp/components/Share.jsx b/03_source/mobile/src/pages/DemoReactPollApp/components/Share.jsx new file mode 100644 index 0000000..e0a6be4 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/components/Share.jsx @@ -0,0 +1,43 @@ +import { IonItem, IonList, IonListHeader } from "@ionic/react"; +import { Share } from '@capacitor/share'; +import { Clipboard } from '@capacitor/clipboard'; + +export const SharePopover = ({ onHide, poll, setShowToast }) => { + + const sharePoll = async () => { + + const shareLink = `https://ionic-react-poll-app.netlify.app/page/view/${ poll.id }`; + const title = `Check out this poll - ${ poll.question } | ${ poll.totalVotes } votes already`; + + await Share.share({ + + title: title, + text: title, + url: shareLink, + dialogTitle: 'Share this poll', + }); + } + + const copyLink = async () => { + + await Clipboard.write({ + + string: `https://ionic-react-poll-app.netlify.app/page/view/${ poll.id }` + }); + + onHide(); + + setShowToast(true); + }; + + return ( + + Share Poll + Socials + Copy Link + + Close + + + ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactPollApp/helpers/utils.js b/03_source/mobile/src/pages/DemoReactPollApp/helpers/utils.js new file mode 100644 index 0000000..17f4ee5 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/helpers/utils.js @@ -0,0 +1,89 @@ +export const getColors = baseColor => { + + const textColor = shadeColor(baseColor, 200); + const subTextColor = shadeColor(baseColor, 120); + const backgroundColor = baseColor; + + return { + + textColor, + subTextColor, + backgroundColor, + notVotedbuttonStyle: { + + "--color": baseColor, + "--background-focused": baseColor, + "--background-activated": baseColor, + "--background": "white", + "--border-color": baseColor + }, + votedButtonStyle: { + + "--color": shadeColor(baseColor, 200), + "--background-focused": baseColor, + "--background-activated": baseColor, + "--background": baseColor, + "--border-color": shadeColor(baseColor, 200) + }, + percentTrack: { + + "--background": shadeColor(baseColor, 70), + "--progress-background": baseColor, + height: "1rem" + } + }; +} + +export const getCardStyle = baseColor => { + + const textColor = shadeColor(baseColor, 200); + const subTextColor = shadeColor(baseColor, 120); + const backgroundColor = baseColor; + const buttonColor = shadeColor(baseColor, 100); + const buttonTextColor = shadeColor(baseColor, 0); + + return { + + textColor, + subTextColor, + backgroundColor, + buttonStyle: { + + "--color": buttonTextColor, + "--background": buttonColor, + "--background-focused": shadeColor(baseColor, 20), + "--background-activated": shadeColor(baseColor, 20), + }, + statusBadge: { + + backgroundColor: "rgba(0, 0, 0, 0.1)", + width: "fit-content", + padding: "0.5rem", + color: subTextColor, + margin: 0, + marginTop: "1rem", + borderRadius: "5px" + } + }; +} + +export const shadeColor = (color, percent) => { + + var R = parseInt(color.substring(1,3),16); + var G = parseInt(color.substring(3,5),16); + var B = parseInt(color.substring(5,7),16); + + R = parseInt(R * (100 + percent) / 100); + G = parseInt(G * (100 + percent) / 100); + B = parseInt(B * (100 + percent) / 100); + + R = (R<255)?R:255; + G = (G<255)?G:255; + B = (B<255)?B:255; + + var RR = ((R.toString(16).length==1)?"0"+R.toString(16):R.toString(16)); + var GG = ((G.toString(16).length==1)?"0"+G.toString(16):G.toString(16)); + var BB = ((B.toString(16).length==1)?"0"+B.toString(16):B.toString(16)); + + return "#"+RR+GG+BB; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactPollApp/index.tsx b/03_source/mobile/src/pages/DemoReactPollApp/index.tsx new file mode 100644 index 0000000..6505d11 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/index.tsx @@ -0,0 +1,34 @@ +import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react'; + +import { cloudOutline, searchOutline } from 'ionicons/icons'; +import { Route, Redirect } from 'react-router'; + +import Add from './AppPages/Add'; +import View from './AppPages/View'; +import ViewPoll from './AppPages/ViewPoll'; + +import './style.scss'; + +function DemoReactPollApp() { + return ( + + + + + + + + + + + + + + + + + + ); +} + +export default DemoReactPollApp; diff --git a/03_source/mobile/src/pages/DemoReactPollApp/store/PollStore.js b/03_source/mobile/src/pages/DemoReactPollApp/store/PollStore.js new file mode 100644 index 0000000..3123749 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/store/PollStore.js @@ -0,0 +1,159 @@ +import { Store } from 'pullstate'; + +const PollStore = new Store({ + + polls: [ + { + id: 1, + question: "What is your favourite movie?", + timeLeft: "2 hours, 17 mins", + totalVotes: 137, + color: "#759dc7", + voted: false, + answers: [ + + { + id: 1, + answer: "Avengers", + votes: 76, + voted: false, + percent: 0 + }, + { + id: 2, + answer: "Taken 2", + votes: 61, + voted: false, + percent: 0 + } + ] + }, + { + id: 2, + question: "Do you prefer night or day?", + timeLeft: "1 hours, 3 mins", + totalVotes: 22, + color: "#68bd8d", + voted: false, + answers: [ + + { + id: 1, + answer: "Night", + votes: 11, + voted: false, + percent: 0 + }, + { + id: 2, + answer: "Day", + votes: 8, + voted: false, + percent: 0 + }, + { + id: 3, + answer: "In the middle", + votes: 3, + voted: false, + percent: 0 + } + ] + }, + { + id: 3, + question: "Who is the better singer?", + timeLeft: "1 day, 2 hours, 43 mins", + totalVotes: 268, + color: "#8d68bd", + voted: false, + answers: [ + + { + id: 1, + answer: "Abba", + votes: 104, + voted: false, + percent: 0 + }, + { + id: 2, + answer: "Metallica", + votes: 114, + voted: false, + percent: 0 + }, + { + id: 3, + answer: "Queen", + votes: 50, + voted: false, + percent: 0 + } + ] + }, + { + id: 4, + question: "Best type of food?", + timeLeft: "4 days, 6 hours, 19 mins", + totalVotes: 166, + color: "#7c7c7c", + voted: false, + answers: [ + + { + id: 1, + answer: "Hamburger", + votes: 76, + voted: false, + percent: 0 + }, + { + id: 2, + answer: "Hotdog", + votes: 61, + voted: false, + percent: 0 + }, + { + id: 3, + answer: "Chips", + votes: 10, + voted: false, + percent: 0 + }, + { + id: 4, + answer: "Steak", + votes: 19, + voted: false, + percent: 0 + } + ] + }, + ] +}); + +export default PollStore; + +export const addVote = (pollId, answerId) => { + + PollStore.update(state => { + + const pollIndex = state.polls.findIndex(poll => poll.id === parseInt(pollId)); + const answerIndex = state.polls[pollIndex].answers.findIndex(answer => answer.id === parseInt(answerId)); + + state.polls[pollIndex].voted = true; + state.polls[pollIndex].totalVotes = state.polls[pollIndex].totalVotes + 1; + state.polls[pollIndex].answers[answerIndex].votes = state.polls[pollIndex].answers[answerIndex].votes + 1; + state.polls[pollIndex].answers[answerIndex].voted = true; + }); +} + +export const addPoll = (poll) => { + + PollStore.update(state => { + + state.polls.unshift(poll); + }); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactPollApp/store/Selectors.js b/03_source/mobile/src/pages/DemoReactPollApp/store/Selectors.js new file mode 100644 index 0000000..d30b898 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/store/Selectors.js @@ -0,0 +1,13 @@ +import { createSelector } from 'reselect'; + +const getState = state => state; + +// General getters +export const getPolls = createSelector(getState, state => state.polls); + +// More specific getters +export const getPoll = pollId => createSelector(getState, state => state.polls.filter(poll => poll.id === parseInt(pollId))[0]); +// export const getChat = contactId => createSelector(getState, state => state.chats.filter(c => parseInt(c.contact_id) === parseInt(contactId))[0].chats); +// export const getContact = contactId => createSelector(getState, state => state.contacts.filter(c => parseInt(c.id) === parseInt(contactId))[0]); + +// export const getChatNotificationCount = contactId => createSelector(getState, state => (state.chats.filter(c => parseInt(c.contact_id) === parseInt(contactId))[0].chats).filter(chat => chat.read === false)); diff --git a/03_source/mobile/src/pages/DemoReactPollApp/store/index.js b/03_source/mobile/src/pages/DemoReactPollApp/store/index.js new file mode 100644 index 0000000..eb98b57 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/store/index.js @@ -0,0 +1,2 @@ + +export { default as PollStore } from "./PollStore"; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactPollApp/style.scss b/03_source/mobile/src/pages/DemoReactPollApp/style.scss new file mode 100644 index 0000000..97a4acb --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactPollApp/style.scss @@ -0,0 +1,84 @@ +/* Ionic Variables and Theming. For more info, please see: +http://ionicframework.com/docs/theming/ */ + +/** Ionic CSS Variables **/ +.demo-react-poll-app { + * { + /** 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; + } + + ion-footer { + border-top: 4px solid rgb(243, 243, 243); + background-color: white; + } +} diff --git a/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab1.css b/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab1.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab1.jsx b/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab1.jsx new file mode 100644 index 0000000..24873ba --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab1.jsx @@ -0,0 +1,52 @@ +import { + IonButton, + IonButtons, + IonCol, + IonContent, + IonHeader, + IonIcon, + IonPage, + IonRow, + IonTitle, + IonToolbar, + useIonRouter, +} from '@ionic/react'; + +import ExploreContainer from '../components/ExploreContainer'; +import { chevronBackOutline, refreshOutline } from 'ionicons/icons'; + +import './Tab1.css'; + +const Tab1 = () => { + const router = useIonRouter(); + + function handleBackClick() { + window.location.href = '/tabs/demo-list'; + } + + return ( + + + + Home + + + handleBackClick()}> + + + + + + + + + Home + + + + + + ); +}; + +export default Tab1; diff --git a/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab2.css b/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab2.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab2.jsx b/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab2.jsx new file mode 100644 index 0000000..492b34c --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab2.jsx @@ -0,0 +1,25 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab2.css'; + +const Tab2 = () => { + return ( + + + + Profile + + + + + + Profile + + + + + + ); +}; + +export default Tab2; diff --git a/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab3.css b/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab3.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab3.jsx b/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab3.jsx new file mode 100644 index 0000000..6ab2366 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactSwitchTabs/AppPages/Tab3.jsx @@ -0,0 +1,25 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab3.css'; + +const Tab3 = () => { + return ( + + + + Settings + + + + + + Settings + + + + + + ); +}; + +export default Tab3; diff --git a/03_source/mobile/src/pages/DemoReactSwitchTabs/NOTES.md b/03_source/mobile/src/pages/DemoReactSwitchTabs/NOTES.md new file mode 100644 index 0000000..6d2c598 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactSwitchTabs/NOTES.md @@ -0,0 +1,6 @@ +# NOTE + +## TODO + +hardcoded back button, +tab collision with the main app diff --git a/03_source/mobile/src/pages/DemoReactSwitchTabs/components/ExploreContainer.css b/03_source/mobile/src/pages/DemoReactSwitchTabs/components/ExploreContainer.css new file mode 100644 index 0000000..e99f514 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactSwitchTabs/components/ExploreContainer.css @@ -0,0 +1,24 @@ +.container { + text-align: center; + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +.container strong { + font-size: 20px; + line-height: 26px; +} + +.container p { + font-size: 16px; + line-height: 22px; + color: #8c8c8c; + margin: 0; +} + +.container a { + text-decoration: none; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactSwitchTabs/components/ExploreContainer.jsx b/03_source/mobile/src/pages/DemoReactSwitchTabs/components/ExploreContainer.jsx new file mode 100644 index 0000000..1e13540 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactSwitchTabs/components/ExploreContainer.jsx @@ -0,0 +1,11 @@ +import './ExploreContainer.css'; + +const ExploreContainer = ({ name }) => { + return ( +
+ {name} +
+ ); +}; + +export default ExploreContainer; diff --git a/03_source/mobile/src/pages/DemoReactSwitchTabs/components/SwitchTabBar.jsx b/03_source/mobile/src/pages/DemoReactSwitchTabs/components/SwitchTabBar.jsx new file mode 100644 index 0000000..6ec16cf --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactSwitchTabs/components/SwitchTabBar.jsx @@ -0,0 +1,125 @@ +import { + CreateAnimation, + IonIcon, + IonLabel, + IonRouterOutlet, + IonTabBar, + IonTabButton, + IonTabs, +} from '@ionic/react'; +import { IonReactRouter } from '@ionic/react-router'; +import { home, person, settings } from 'ionicons/icons'; +import Tab1 from '../AppPages/Tab1'; +import Tab2 from '../AppPages/Tab2'; +import Tab3 from '../AppPages/Tab3'; +import { useRef } from 'react'; +import { useEffect, useState } from 'react'; +import { Redirect, Route } from 'react-router'; + +const SwitchTabBar = () => { + const [activeTab, setActiveTab] = useState('tab0'); + const switchRefs = useRef([]); + + const tabs = [ + { + label: 'Home', + url: '/home', + icon: home, + color: '#76b140', + backgroundColor: '#ddf7c5', + component: Tab1, + }, + { + label: 'Profile', + url: '/profile', + icon: person, + color: '#e46062', + backgroundColor: '#fcddde', + component: Tab2, + }, + { + label: 'Settings', + url: '/settings', + icon: settings, + color: '#3578e5', + backgroundColor: '#e7f0ff', + component: Tab3, + }, + ]; + + const revealAnimation = { + property: 'transform', + fromValue: 'translateX(-30px)', + toValue: 'translateX(0px)', + }; + + const switchAnimation = { + duration: 200, + direction: 'normal', + iterations: '1', + fromTo: [revealAnimation], + easing: 'ease-in-out', + }; + + const getTabButtonStyle = (tab) => { + const tabStyle = { + backgroundColor: tab.backgroundColor, + color: tab.color, + transition: '0.5s all ease-in-out', + }; + + return tabStyle; + }; + + useEffect(() => { + const tabIndex = activeTab.match(/\d+/)[0]; + switchRefs.current[tabIndex].animation.play(); + }, [activeTab]); + + return ( + + + + {tabs.map((tab, index) => { + return ( + + + + ); + })} + + + + + setActiveTab(e.detail.tab)}> + {tabs.map((tab, index) => { + const tabStyle = getTabButtonStyle(tab); + const isActive = activeTab === `tab${index}`; + + return ( + + + + {isActive && ( + (switchRefs.current[index] = ref)} + {...switchAnimation} + > + {tab.label} + + )} + + ); + })} + + + + ); +}; + +export default SwitchTabBar; diff --git a/03_source/mobile/src/pages/DemoReactSwitchTabs/index.tsx b/03_source/mobile/src/pages/DemoReactSwitchTabs/index.tsx new file mode 100644 index 0000000..462916c --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactSwitchTabs/index.tsx @@ -0,0 +1,20 @@ +import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react'; + +import { cloudOutline, searchOutline } from 'ionicons/icons'; +import { Route, Redirect } from 'react-router'; + +import Tab1 from './AppPages/Tab1'; +import Tab2 from './AppPages/Tab2'; +import SwitchTabBar from './components/SwitchTabBar'; + +import './style.scss'; + +function DemoReactSwitchTabs() { + return ( + <> + + + ); +} + +export default DemoReactSwitchTabs; diff --git a/03_source/mobile/src/pages/DemoReactSwitchTabs/style.scss b/03_source/mobile/src/pages/DemoReactSwitchTabs/style.scss new file mode 100644 index 0000000..7a5a4a1 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactSwitchTabs/style.scss @@ -0,0 +1,97 @@ +.demo-react-switch-tabs { + /* 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; + } + + ion-tab-bar { + padding: 1rem; + } + + ion-tab-button { + flex-direction: row; + border-radius: 20px; + } + + ion-tab-button ion-icon { + font-size: 1.5rem; + } + + ion-tab-button ion-label { + margin-left: 1rem; + font-size: 0.8rem; + } +} diff --git a/03_source/mobile/src/pages/DemoReactTravelApp/AllRoutes.jsx b/03_source/mobile/src/pages/DemoReactTravelApp/AllRoutes.jsx new file mode 100644 index 0000000..3857cfd --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/AllRoutes.jsx @@ -0,0 +1,88 @@ +// Main Tabs +import Tab1 from '../pages/Tab1'; +import Tab2 from '../pages/Tab2'; +import Tab3 from '../pages/Tab3'; + +// Main tab children +import Place from '../pages/Place'; + +// Sub pages +// import InboxItem from "../../pages/InboxItem"; + +// Tab icons +// If using ionicons, import here and pass as ref to tabRoutes + +// Import custom tab menu +import Tabs from '../components/Tabs'; +import SubPages from '../components/SubPages'; + +// Array of objects representing tab pages +// These will be the main tabs across the app + +// * PARAMS per tab object * +// isTab = true will make the tab appear +// default = the default tab page to open and be redirected to at "/" +// NOTE: there should only be one default tab (default: true) +// label = the label to show with the tab +// component = the component related to this tab page +// icon = icon to show on the tab bar menu +// path = the path which the tab is accessible +export const tabRoutes = [ + { + label: 'Home', + component: Tab1, + icon: 'Home', + path: '/tabs/home', + default: true, + isTab: true, + }, + { + label: 'Places', + component: Tab2, + icon: 'Location', + path: '/tabs/places', + default: false, + isTab: true, + }, + { + label: 'Favourites', + component: Tab3, + icon: 'Heart', + path: '/tabs/favourites', + default: false, + isTab: true, + }, + // { label: "Profile", component: Tab3, icon: "User", path: "/tabs/profile", default: false, isTab: true }, +]; + +// Array of objects representing children pages of tabs + +// * PARAMS per tab object * +// isTab = should always be set to false for these +// component = the component related to this tab page +// path = the path which the tab is accessible + +// These pages should be related to tab pages and be held within the same path +// E.g. /tabs/tab1/child +const tabChildrenRoutes = [ + // { component: InboxItem, path: "/tabs/tab2/:id", isTab: false }, +]; + +// Array of objects representing sub pages + +// * PARAMS per tab object * +// component = the component related to this sub page +// path = the path which the sub page is accessible + +// This array should be sub pages which are not directly related to a tab page +// E.g. /child +const subPageRoutes = [{ component: Place, path: '/view-place/:place_id' }]; + +// Let's combine these together as they need to be controlled within the same IonRouterOutlet +const tabsAndChildrenRoutes = [...tabRoutes, ...tabChildrenRoutes]; + +// Render sub routes +export const AllSubPages = () => ; + +// Render tab menu +export const AllTabs = () => ; diff --git a/03_source/mobile/src/pages/DemoReactTravelApp/AppPages/Tab1.jsx b/03_source/mobile/src/pages/DemoReactTravelApp/AppPages/Tab1.jsx new file mode 100644 index 0000000..a6f4101 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/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/DemoReactTravelApp/AppPages/Tab2.jsx b/03_source/mobile/src/pages/DemoReactTravelApp/AppPages/Tab2.jsx new file mode 100644 index 0000000..216544f --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/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/DemoReactTravelApp/NOTES.md b/03_source/mobile/src/pages/DemoReactTravelApp/NOTES.md new file mode 100644 index 0000000..c079ae5 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/NOTES.md @@ -0,0 +1,3 @@ +# NOTES + +https://ionicreacthub.com/ionic-react-travel-app-ui diff --git a/03_source/mobile/src/pages/DemoReactTravelApp/components/ExploreContainer.css b/03_source/mobile/src/pages/DemoReactTravelApp/components/ExploreContainer.css new file mode 100644 index 0000000..e99f514 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/components/ExploreContainer.css @@ -0,0 +1,24 @@ +.container { + text-align: center; + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +.container strong { + font-size: 20px; + line-height: 26px; +} + +.container p { + font-size: 16px; + line-height: 22px; + color: #8c8c8c; + margin: 0; +} + +.container a { + text-decoration: none; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTravelApp/components/ExploreContainer.jsx b/03_source/mobile/src/pages/DemoReactTravelApp/components/ExploreContainer.jsx new file mode 100644 index 0000000..093861b --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/components/ExploreContainer.jsx @@ -0,0 +1,12 @@ +import './ExploreContainer.css'; + +const ExploreContainer = ({ name }) => { + return ( +
+ {name} +

Explore UI Components

+
+ ); +}; + +export default ExploreContainer; diff --git a/03_source/mobile/src/pages/DemoReactTravelApp/components/LongPlaceCard.jsx b/03_source/mobile/src/pages/DemoReactTravelApp/components/LongPlaceCard.jsx new file mode 100644 index 0000000..a05df74 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/components/LongPlaceCard.jsx @@ -0,0 +1,16 @@ +import { IonCard, IonCardTitle, IonNote } from "@ionic/react"; +import styles from "../styles/Home.module.scss"; + +export const LongPlaceCard = ({ place = false }) => ( + + +
+ +
+ +
+ { place.name } + { place.destination } +
+
+); \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTravelApp/components/PlaceCard.jsx b/03_source/mobile/src/pages/DemoReactTravelApp/components/PlaceCard.jsx new file mode 100644 index 0000000..1804633 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/components/PlaceCard.jsx @@ -0,0 +1,99 @@ +import { IonCard, IonCardHeader, IonCardTitle, IonNote, useIonToast, CreateAnimation, IonIcon } from "@ionic/react"; +import { useState } from "react"; +import { useRef } from "react"; +import { Iconly } from "react-iconly"; +import { heart, trashBin } from "ionicons/icons"; +import { addFavourite } from "../store/PlacesStore"; + +import styles from "../styles/Home.module.scss"; + +const PlaceCard = ({ place = false, fromFavourites = false }) => { + + const animationRef = useRef(); + const cardRef = useRef(); + const [ presentToast ] = useIonToast(); + const [ hideAnimatedIcon, setHideAnimatedIcon ] = useState(true); + + const floatStyle = { + + display: hideAnimatedIcon ? "none" : "", + position: "absolute", + zIndex: "10" + }; + + const floatGrowAnimation = { + + property: "transform", + fromValue: "translateY(0) scale(1)", + toValue: "translateY(-20px) scale(2)" + }; + + const mainAnimation = { + + duration: 600, + iterations: "1", + fromTo: [ floatGrowAnimation ], + easing: "cubic-bezier(0.25, 0.7, 0.25, 0.7)" + }; + + const handleAddFavourite = async (e, place) => { + + e.stopPropagation(); + e.preventDefault(); + + if (fromFavourites) { + + // Add a fadeOut animation before removing + cardRef.current.classList.add("animate__fadeOut"); + + setTimeout(() => { + addFavourite(place, fromFavourites); + }, 500); + } else { + + addFavourite(place, fromFavourites); + } + + presentToast({ + + header: `Favourite ${ fromFavourites ? "removed" : "added" }!`, + buttons: [ + { + text: "♡", + } + ], + message: `${ place.name } has been ${ fromFavourites ? "removed from" : "added to" } your favourites.`, + duration: 1500, + color: "success" + }); + + setHideAnimatedIcon(false); + await animationRef.current.animation.play(); + setHideAnimatedIcon(true); + } + + return ( + +
+ + { place && +
handleAddFavourite(e, place) }> + + + + + + +
+ } +
+ + + { place ? place.name : "Sorry" } + { place ? place.destination : "No results found" } + +
+ ); +} + +export default PlaceCard; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTravelApp/components/SubPages.jsx b/03_source/mobile/src/pages/DemoReactTravelApp/components/SubPages.jsx new file mode 100644 index 0000000..1037485 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/components/SubPages.jsx @@ -0,0 +1,17 @@ +import { Route } from "react-router-dom"; + +const SubPages = (props) => { + + return ( + <> + { props.routes.map((route, i) => { + + const RouteComponent = route.component; + + return } exact={ false } />; + })} + + ); +} + +export default SubPages; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTravelApp/components/Tabs.jsx b/03_source/mobile/src/pages/DemoReactTravelApp/components/Tabs.jsx new file mode 100644 index 0000000..65e3d56 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/components/Tabs.jsx @@ -0,0 +1,48 @@ +import { IonTabBar, IonTabButton, IonTabs, IonRouterOutlet } from "@ionic/react"; +import { useState } from "react"; +import { Iconly } from "react-iconly"; +import { Route } from "react-router-dom"; + +const Tabs = (props) => { + + const [ selected, setSelected ] = useState("tab_1"); + + return ( + setSelected(e.detail.tab) }> + + + { props.tabs.map((tab, i) => { + + const TabComponent = tab.component; + + if (tab.isTab) { + return } exact={ true }/>; + } else { + + return } exact={ false } />; + } + })} + + + + + { props.tabs.map((tab, i) => { + + const isSelected = selected === `tab_${ i + 1 }`; + + if (tab.isTab) { + + return ( + + + { isSelected &&
} + + ); + } + })} + + + ); +} + +export default Tabs; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTravelApp/data/fetcher.js b/03_source/mobile/src/pages/DemoReactTravelApp/data/fetcher.js new file mode 100644 index 0000000..c0ebd98 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/data/fetcher.js @@ -0,0 +1,21 @@ +import { PlacesStore } from "../store"; + +export const fetchData = async () => { + + const response = await fetch("/data.json"); + const data = await response.json(); + + await data.forEach((place, i) => { + + delete place.desc; + + const placeName = place.name; + const placeNameParts = placeName.split(","); + + place.id = i + 1; + place.name = placeNameParts[0].trim(); + place.destination = placeNameParts[1].trim(); + }); + + PlacesStore.update(s => { s.places = data; }); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactTravelApp/index.tsx b/03_source/mobile/src/pages/DemoReactTravelApp/index.tsx new file mode 100644 index 0000000..933ccff --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/index.tsx @@ -0,0 +1,29 @@ +import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react'; + +import { cloudOutline, searchOutline } from 'ionicons/icons'; +import { Route, Redirect } from 'react-router'; + +// import { AllSubPages, AllTabs, tabRoutes } from './AllRoutes'; + +import './style.scss'; + +function DemoReactTravelApp() { + return <>on hold; + + // NOTE: i temporary make it constant to let the program keep develop + // the below requires fixing, the AllRoutes is not found and it is a + // jsx file, i want it tsx + const hello_this_should_be_the_return = ( + + + } /> + + + t.default)[0].component} exact={true} /> + t.default)[0].path.toString()} /> + + + ); +} + +export default DemoReactTravelApp; diff --git a/03_source/mobile/src/pages/DemoReactTravelApp/style.scss b/03_source/mobile/src/pages/DemoReactTravelApp/style.scss new file mode 100644 index 0000000..37c1e1a --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactTravelApp/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/DemoReactWhatsAppClone/AppPages1/Calls.css b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Calls.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Calls.js b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Calls.js new file mode 100644 index 0000000..cd8c9b7 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Calls.js @@ -0,0 +1,23 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import './Calls.css'; + +const Calls = () => { + return ( + + + + Calls + + + + + + Calls + + + + + ); +}; + +export default Calls; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Chat.css b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Chat.css new file mode 100644 index 0000000..3382f0d --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Chat.css @@ -0,0 +1,289 @@ +.chat-page ion-header, +.chat-page ion-toolbar { + + --min-height: 3.5rem; +} + +.chat-page ion-title { + + margin-left: -3.5rem; +} + +.chat-page ion-title p { + + padding: 0; + margin: 0; +} + +.chat-contact { + + display: flex; + flex-direction: row; + align-content: center; + justify-content: center; + align-items: center; +} + +.chat-contact img { + + height: 2rem; + width: 2rem; + border-radius: 500px; +} + +.chat-contact-details { + + display: flex; + flex-direction: column; + margin-left: 0.5rem; + text-align: left; +} + +.chat-contact-details p { + + font-size: 0.9rem; +} + +.chat-contact-details ion-text { + + font-size: 0.7rem; + font-weight: 400; +} + +.chat-bubble { + + border-radius: 5px; + margin-left: 1rem; + margin-right: 1rem; + margin-top: 0.8rem; + + padding: 0.5rem; + max-width: 80%; + clear: both; + + display: flex; + flex-direction: row; + transition: 0.2s all linear; +} + +.chat-bubble:last-child { + + margin-bottom: 0.8rem; +} + +.bubble-sent { + + background-color: var(--chat-bubble-sent-color); + float: right; +} + +.bubble-received { + + background-color: var(--chat-bubble-received-color); + float: left; +} + +.chat-bubble p { + + padding: 0; + margin: 0; +} + +.chat-footer { + + background-color: rgb(22, 22, 22); + border-top: 1px solid rgb(47, 47, 47); + padding-top: 0.2rem; + padding-bottom: 1rem; +} + +.chat-footer ion-textarea { + + background-color: rgb(31, 31, 31); + border: 1px solid rgb(36, 36, 36); + color: white; + border-radius: 25px; + padding-left: 0.5rem; + caret-color: var(--ion-color-primary); +} + +.chat-footer ion-icon { + + font-size: 1.5rem; + margin-top: 0.2rem; +} + +.chat-input-container { + + width: 70%; + margin-right: 0.75rem; +} + +.chat-send-button { + + margin: 0 !important; + padding: 0 !important; + position: absolute; + right: 17px; + margin-top: -0.2rem !important; + + display: flex; + flex-direction: row; + align-content: center; + align-items: center; + justify-content: center; +} + +.chat-send-button ion-icon { + + color: white; + background-color: var(--ion-color-primary); + font-size: 1.1rem; + border-radius: 500px; + padding: 0.5rem; +} + +.chat-time { + + color: rgb(165, 165, 165); + font-size: 0.75rem; + right: 0; + bottom: 0 !important; + margin: 0; + padding: 0; + margin-top: 5px; +} + +.bubble-arrow { + + position: absolute; + float: left; + left: 6px; + margin-top: -8px; + /* top: 0px; */ +} + +.bubble-arrow.alt { + + position: relative; + bottom: 0px; + left: auto; + right: -3px; + float: right; +} + +.bubble-arrow:after { + + content: ""; + position: absolute; + border-top: 15px solid var(--chat-bubble-received-color); + border-left: 15px solid transparent; + border-radius: 4px 0 0 0px; + width: 0; + height: 0; +} + +.bubble-arrow.alt:after { + + border-top: 15px solid var(--chat-bubble-sent-color); + transform: scaleX(-1); +} + +.chat-reply-to-row { + + bottom: 70px !important; + position: absolute; + + border-left: 4px solid rgb(224, 176, 18); + width: 100%; + background-color: rgb(22, 22, 22); + border-top: 1px solid rgb(47, 47, 47); + padding: 0.5rem; + padding-bottom: 0.8rem; +} + +.chat-reply-to-container { + + display: flex; + flex-direction: column; +} + +.chat-reply-to-name { + + color: rgb(224, 176, 18); + font-weight: 500; + margin-bottom: 0.5rem; +} + +.chat-reply-to-message { + + font-size: 0.8rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.all-chats { + + +} + +.chat-bottom-details { + + display: flex; + flex-direction: row; + width: 100%; + align-content: center; + align-items: center; + justify-content: flex-end; + margin-top: 0.4rem; +} + +.chat-bottom-details ion-icon { + + font-size: 0.6rem; + color: grey; + margin-left: 0.5rem; + margin-top: 0.05rem; +} + +.chat-bottom-details span { + + margin: 0; + padding: 0; + font-size: 0.75rem; + color: rgb(190, 190, 190); +} + +.in-chat-reply-to-container { + + background-color: rgba(0, 0, 0, 0.2); + border-left: 3px solid rgb(224, 176, 18); + height: fit-content; + padding: 0.5rem; + border-radius: 5px; + margin-bottom: 0.5rem; +} + +.in-chat-reply-to-container h1 { + + margin: 0; + padding: 0; + color: rgb(224, 176, 18); + font-size: 0.8rem; +} + +.in-chat-reply-to-container p { + + color: rgb(167, 167, 167); + font-size: 0.8rem; +} + +.bottom-container { + + position: absolute; + bottom: 4.5rem; + height: 5rem; + background-color: red; + width: 100%; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Chat.js b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Chat.js new file mode 100644 index 0000000..9bbab03 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Chat.js @@ -0,0 +1,378 @@ +import { IonBackButton, IonButton, IonButtons, IonCol, IonContent, IonFooter, IonGrid, IonHeader, IonIcon, IonPage, IonRow, IonText, IonTextarea, IonTitle, IonToolbar, CreateAnimation, createGesture, useIonViewWillEnter, IonActionSheet, IonToast } from "@ionic/react"; +import { addOutline, alertOutline, callOutline, cameraOutline, micOutline, send, shareOutline, starOutline, trashOutline, videocamOutline } from "ionicons/icons"; +import { useRef } from "react"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router"; +import { ChatStore, ContactStore } from "../store"; +import { getNotificationCount, markAllAsRead, sendChatMessage, starChatMessage } from "../store/ChatStore"; +import { getChat, getChats, getContact } from "../store/Selectors"; + +import { useLongPress } from 'react-use'; +import "./Chat.css" +import ReplyTo from "../components/ReplyTo"; +import { ChatBottomDetails } from "../components/ChatBottomDetails"; +import { ChatRepliedQuote } from "../components/ChatRepliedQuote"; +import { useCamera } from "../hooks/useCamera"; +import { useGallery } from "../hooks/useGallery"; + +const Chat = () => { + + const params = useParams(); + + // Global State + const chat = ChatStore.useState(getChat(params.contact_id)); + const chats = ChatStore.useState(getChats); + const contact = ContactStore.useState(getContact(params.contact_id)); + const notificationCount = getNotificationCount(chats); + + const { takePhoto } = useCamera(); + const { prompt } = useGallery(); + + // Local state + const [ message, setMessage ] = useState(""); + const [ showSendButton, setShowSendButton ] = useState(false); + const [ replyToMessage, setReplyToMessage ] = useState(false); + const [ messageSent, setMessageSent ] = useState(false); + + const [ showActionSheet, setShowActionSheet ] = useState(false); + const [ actionMessage, setActionMessage ] = useState(false); + + const [ showToast, setShowToast ] = useState(false); + const [ toastMessage, setToastMessage ] = useState(""); + + // Refs + const contentRef = useRef(); + const swiperRefs = useRef([]); + const textareaRef = useRef(); + const sideRef = useRef(); + const sendRef = useRef(); + const replyToAnimationRef = useRef(); + + const actionSheetButtons = [ + + { + text: (actionMessage && actionMessage.starred) ? "Unstar Message" : "Star Message", + icon: starOutline, + handler: () => starChatMessage(params.contact_id, actionMessage.id) + }, + actionMessage && actionMessage.received ? + { + text: "Reply To Message", + icon: shareOutline, + handler: () => showReplyToMessage(actionMessage) + } + : + { + text: "Unsend Message", + icon: alertOutline, + handler: () => toaster("I haven't implemented unsend :) Simple store update though") + }, + { + text: "Delete Message", + icon: trashOutline, + handler: () => toaster("I haven't implemented delete :) Simple store update though"), + role: "destructive" + } + ]; + + useEffect(() => { + + !showActionSheet && setActionMessage(false); + }, [ showActionSheet ]); + + // Scroll to end of content + // Mark all chats as read if we come into a chat + // Set up our swipe events for animations and gestures + useIonViewWillEnter(() => { + + scrollToBottom(); + setupObserver(); + markAllAsRead(params.contact_id); + setSwipeEvents(); + }); + + // For displaying toast messages + const toaster = message => { + + setToastMessage(message); + setShowToast(true); + } + + // Scroll to end of content + const scrollToBottom = async () => { + + contentRef.current.scrollToBottom(); + } + + // Watch for DOM changes + // Then scroll to bottom + // This ensures that the new chat message has *actually* been rendered + // Check this: + // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver + const setupObserver = () => { + + // Mutation Observers watch for DOM changes + // This will ensure that we scroll to bottom AFTER the new chat has rendered + const observer = new MutationObserver(() => { + + scrollToBottom(); + }); + + // We observe the ion-content (or containing element of chats) + observer.observe(contentRef.current, { + + childList: true + }); + } + + // Long press callback + const onLongPress = (e) => { + + const elementID = e.target.id; + const chatMessageID = elementID.includes("chatText") ? parseInt(elementID.replace("chatText_", "")) : elementID.includes("chatTime") ? parseInt(elementID.replace("chatTime_", "")) : parseInt(elementID.replace("chatBubble_", "")); + + const chatMessage = chat.filter(message => parseInt(message.id) === parseInt(chatMessageID))[0]; + + setActionMessage(chatMessage); + setShowActionSheet(true); + }; + + const longPressEvent = useLongPress(onLongPress, { + + isPreventDefault: true, + delay: 2000, + }); + + const showReplyToMessage = async message => { + + // Activate reply-to functionality + setReplyToMessage(message); + await replyToAnimationRef.current.animation.play(); + contentRef.current.scrollToBottom(300); + } + + const checkBubble = async (bubble, message, event) => { + + if (event.deltaX >= 120) { + + // Activate reply-to functionality + bubble.style.transform = "none"; + showReplyToMessage(message); + } else { + + // Put chat bubble back to original position + bubble.style.transform = "none"; + } + } + + // Function to move a bubble with the deltaX swipe + const moveBubble = (bubble, event) => { + + if (event.velocityX > 0) { + + bubble.style.transform = `translateX(${ event.deltaX }px)`; + } + } + + const setSwipeEvents = () => { + + chat.forEach((message, index) => { + + if (!message.sent) { + + const chatBubble = swiperRefs.current[index]; + + const swipeGesture = createGesture({ + el: chatBubble, + onEnd: e => checkBubble(chatBubble, message, e), + onMove: e => moveBubble(chatBubble, e) + }); + + swipeGesture.enable(); + } + }); + } + + const widthAnimation = { + + property: "width", + fromValue: "110%", + toValue: "100%" + }; + + const fadeAnimation = { + + property: "opacity", + fromValue: "100%", + toValue: "0%" + }; + + const sideButtonsAnimation = { + + duration: 200, + direction: showSendButton ? "normal" : "reverse", + iterations: "1", + fromTo: [ fadeAnimation ], + easing: "ease-in-out" + }; + + const sendButtonAnimation = { + + duration: showSendButton ? 300 : 100, + direction: !showSendButton ? "normal" : "reverse", + iterations: "1", + fromTo: [ fadeAnimation ], + easing: "ease-in-out" + }; + + const textareaAnimation = { + + duration: 200, + direction: !showSendButton ? "normal" : "reverse", + iterations: "1", + fromTo: [ widthAnimation ], + easing: "ease-in-out" + }; + + // Set the state value when message val changes + useEffect(() => { + + setShowSendButton(message !== ""); + }, [ message ]); + + // Play the animations when the state value changes + useEffect(() => { + + textareaRef.current.animation.play(); + sideRef.current.animation.play(); + sendRef.current.animation.play(); + }, [ showSendButton ]); + + const sendMessage = (image = false, imagePath = false) => { + + if (message !== "" || image === true) { + + sendChatMessage(params.contact_id, message, replyToMessage, replyToMessage ? replyToMessage.id : false, image, imagePath); + setMessage(""); + + setMessageSent(true); + setTimeout(() => setMessageSent(false), 10); + image && setTimeout(() => scrollToBottom(), 100); + } + } + + const handlePhoto = async () => { + + const returnedFilePath = await takePhoto(); + sendMessage(true, returnedFilePath); + } + + const handlePrompt = async () => { + + const returnedFilePath = await prompt(); + sendMessage(true, returnedFilePath); + } + + const replyToProps = { + + replyToAnimationRef, + replyToMessage, + setReplyToMessage, + contact: contact.name, + messageSent + }; + + return ( + + + + + 0) ? notificationCount : "" } /> + + +
+ avatar +
+

{ contact.name }

+ last seen today at 22:10 +
+
+
+ + + toaster("As this is a UI only, video calling wouldn't work here.")}> + + + + toaster("As this is a UI only, calling wouldn't work here.")}> + + + +
+
+ + + + { chat.map((message, index) => { + + const repliedMessage = chat.filter(subMessage => parseInt(subMessage.id) === parseInt(message.replyID))[0]; + + return ( +
swiperRefs.current[index] = ref } id={ `chatBubble_${ message.id }`} key={ index } className={ `chat-bubble ${ message.sent ? "bubble-sent" : "bubble-received" }` } { ...longPressEvent }> +
+ + + + { message.preview } + { message.image && message.imagePath && chat message } + +
+ +
+
+ ); + })} + + setShowActionSheet(false) } buttons={ actionSheetButtons } /> + + setShowToast(false) } message={ toastMessage } position="bottom" duration="3000" /> +
+ + { replyToMessage && } + + + + + + + + +
+ + setMessage(e.target.value) } /> + +
+ + + + + + + + + + + + + + + + +
+
+
+
+ ); +} + +export default Chat; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Chats.css b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Chats.css new file mode 100644 index 0000000..c1c674d --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Chats.css @@ -0,0 +1,112 @@ +.chat-row { + + display: flex; + flex-direction: row; + /* justify-content: space-between; */ + align-items: center; + /* align-content: center; */ + padding-left: 1rem; +} + +.chat-row ion-item { + + width: 100%; +} + +.chat-row img { + + height: 3rem; + width: 3rem; + border-radius: 500px; + background-color: inherit; +} + +.chat-content { + + padding-bottom: 1rem; + padding-top: 1rem; + width: 100%; +} + +.chat-content h2 { + + font-size: 1rem; + font-weight: 600; +} + +.chat-content p, +.chat-content h2 { + + margin: 0; + padding: 0; +} + +.chat-content p { + + font-size: 1rem; + margin-top: 0.2rem; + color: rgb(153, 153, 153); +} + +.chat-content p ion-icon { + + margin-right: 0.4rem; +} + +.chat-name-date { + + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.chat-details .chat-date { + + color: rgb(153, 153, 153); + font-size: 0.8rem; + padding-left: 0.5rem;; +} + +.chat-details .chat-unread { + + color: var(--ion-color-primary); +} + +.chat-notification-count { + + margin: 0; + padding: 0; +} + +.chat-details { + + display: flex; + flex-direction: column; + align-content: flex-end; + justify-content: flex-end; + align-content: flex-end; + align-items: flex-end; +} + +.chat-notification { + + font-size: 0.7rem; + padding: 0.2rem; + background-color: var(--ion-color-primary); + border-radius: 500px; + display: flex; + flex-direction: column; + align-content: center; + justify-content: center; + align-content: center; + align-items: center; + height: 1rem; + width: 1rem; +} + +.chat-content-container { + + display: flex; + flex-direction: row; + justify-content: space-between; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Chats.js b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Chats.js new file mode 100644 index 0000000..e2e454f --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Chats.js @@ -0,0 +1,79 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonSearchbar, IonButtons, IonButton, IonIcon, IonItem, IonModal } from '@ionic/react'; +import { checkmarkDone, createOutline } from 'ionicons/icons'; +import './Chats.css'; + +import { ChatStore, ContactStore } from '../store'; +import { getContacts, getChats } from '../store/Selectors'; +import { useEffect, useState } from 'react'; +import ChatItem from '../components/ChatItem'; +import { useRef } from 'react'; +import ContactModal from '../components/ContactModal'; + +const Chats = () => { + + const pageRef = useRef(); + const contacts = ContactStore.useState(getContacts); + const latestChats = ChatStore.useState(getChats); + + const [ results, setResults ] = useState(latestChats); + const [ showContactModal, setShowContactModal ] = useState(false); + + useEffect(() => { + + setResults(latestChats); + }, [ latestChats ]); + + const search = e => { + + const searchTerm = e.target.value; + + if (searchTerm !== "") { + + const searchTermLower = searchTerm.toLowerCase(); + + const newResults = latestChats.filter(chat => contacts.filter(c => c.id === chat.contact_id)[0].name.toLowerCase().includes(searchTermLower)); + setResults(newResults); + } else { + + setResults(latestChats); + } + } + + return ( + + + + + Edit + + + setShowContactModal(true) }> + + + + Chats + + + + + + Chats + + search(e) } /> + + + + { results.map((chat, index) => { + + return ; + })} + + setShowContactModal(false) }> + setShowContactModal(false) } /> + + + + ); +}; + +export default Chats; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Settings.js b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Settings.js new file mode 100644 index 0000000..79873c4 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Settings.js @@ -0,0 +1,120 @@ +import { IonCardSubtitle, IonCol, IonContent, IonHeader, IonIcon, IonItem, IonList, IonPage, IonRow, IonText, IonTitle, IonToolbar } from '@ionic/react'; +import { camera, cloudUpload, cloudUploadOutline, heart, helpOutline, informationOutline, key, laptop, laptopOutline, logoWhatsapp, mailUnreadOutline, notificationsOutline, pencil, qrCodeOutline, star } from 'ionicons/icons'; +import styles from './Settings.module.scss'; + +const Settings = () => { + + const settings = [ + + [ + { + title: "Starred Messages", + url: "/starred-messages", + icon: star, + color: "rgb(255, 208, 0)" + }, + { + title: "WhatsApp Web/Desktop", + icon: laptopOutline, + color: "rgb(33, 165, 114)" + } + ], + [ + { + title: "Account", + icon: key, + color: "rgb(0, 81, 255)" + }, + { + title: "Chats", + icon: logoWhatsapp, + color: "rgb(79, 182, 96)" + }, + { + title: "Notifications", + icon: mailUnreadOutline, + color: "rgb(233, 46, 46)" + }, + { + title: "Storage and Data", + icon: cloudUploadOutline, + color: "rgb(79, 182, 96)" + } + ], + [ + { + title: "Help", + icon: informationOutline, + color: "rgb(0, 81, 255)" + }, + { + title: "Tell a Friend", + icon: heart, + color: "rgb(228, 70, 70)" + } + ] + ]; + + return ( + + + + Settings + + + + + + Settings + + + + + avatar + + + Alan Montgomery + +
+ This is my status! +
+ + + + + + +
+ + { settings.map((setting, index) => { + + return ( + + + + { setting.map((option, index) => { + + var itemStyle = { "--setting-item-color": option.color }; + + return ( + + + +

{ option.title }

+
+ ); + })} +
+ ); + })} + +
+ from + IONIC React HUB +
+
+
+ ); +}; + +export default Settings; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Settings.module.scss b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Settings.module.scss new file mode 100644 index 0000000..7388538 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Settings.module.scss @@ -0,0 +1,69 @@ +.settingsPage { + + ion-item { + + --background: rgb(27, 27, 27); + background: rgb(27, 27, 27); + border-top: 1px solid rgb(41, 41, 41); + border-bottom: 1px solid rgb(41, 41, 41); + padding: 0.5rem; + } +} + +.settingsList { + + ion-item { + + --background: rgb(27, 27, 27); + background: rgb(27, 27, 27); + // border: none !important; + border-top: 1px solid rgb(34, 34, 34); + border-bottom: 1px solid rgb(36, 36, 36); + padding: 0; + + p { + margin: 0; + } + + ion-icon { + + border-radius: 5px; + padding: 0.2rem; + font-size: 1.4rem; + margin-right: 1.2rem; + --setting-item-color: white; + background-color: var(--setting-item-color); + color: rgb(233, 46, 46); + } + } +} + +.smallText { + + font-size: 0.9rem; +} + +.statusAvatar { + + padding: 0.5rem; +} + +.statusAvatar { + + img { + + height: 3.5rem; + width: 3.5rem; + border-radius: 500px; + } +} + +.statusActions { + + ion-icon { + + padding: 0.5rem; + background-color: rgb(56, 56, 56); + border-radius: 500px; + } +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Starred.js b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Starred.js new file mode 100644 index 0000000..7512537 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Starred.js @@ -0,0 +1,87 @@ +import { IonBackButton, IonContent, IonHeader, IonIcon, IonPage, IonTitle, IonToolbar, useIonViewWillEnter } from '@ionic/react'; +import { chevronForward } from 'ionicons/icons'; +import { useState } from 'react'; +import { ChatStore, ContactStore } from '../store'; +import { getChats, getContacts } from '../store/Selectors'; + +import "./Starred.scss"; + +const Starred = () => { + + const contacts = ContactStore.useState(getContacts); + const chats = ChatStore.useState(getChats); + + const [ starredMessages, setStarredMessages ] = useState(false); + + useIonViewWillEnter(() => { + + var tempChats = [ ...chats ]; + var starred = []; + + tempChats.forEach(tempChat => { + + tempChat.chats.forEach(chat => { + + if (chat.starred) { + + starred.push({ + + contact_id: tempChat.contact_id, + ...chat + }); + } + }); + }); + + setStarredMessages(starred); + }); + + return ( + + + + + Starred Messages + + + + + { starredMessages && starredMessages.map(starredMessage => { + + const { id, contact_id, date, preview, received } = starredMessage; + const contact = contacts.filter(c => c.id === contact_id)[0]; + + return ( +
+
+ +
+ starred avatar +

{ contact.name }

+
+ +

{ date }

+ +
+
+

{ preview }

+ +
+
+ ); + })} + + { starredMessages.length < 1 && + +
+ no starred +

No Starred Messages

+

Tap and hold on any message to star it, so you can easily find it later.

+
+ } +
+
+ ); +}; + +export default Starred; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Starred.scss b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Starred.scss new file mode 100644 index 0000000..8bf228a --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Starred.scss @@ -0,0 +1,117 @@ +.starred-message { + + display: flex; + flex-direction: column; + margin-top: 1rem; +} + +.starred-header { + + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.starred-contact { + + display: flex; + flex-direction: row; + align-content: center; + align-items: center; + margin-left: 1rem; + + img { + + height: 2rem; + width: 2rem; + border-radius: 500px; + } + + p { + + margin-left: 1rem; + font-size: 0.9rem; + font-weight: 500; + } +} + +.starred-date { + + margin-right: 1.5rem; + color: rgb(138, 138, 138); + font-size: 0.8rem; +} + +.starred-content { + + display: flex; + flex-direction: row; + align-content: center; + align-items: center; + justify-content: space-between; + margin-right: 1.5rem; + margin-left: 3.2rem; + + ion-icon { + + color: rgb(138, 138, 138); + font-size: 1rem; + } + + p { + + border-radius: 10px; + max-width: 75%; + padding: 0.5rem; + margin: 0; + margin-bottom: 1rem; + } +} + +.received-starred-content { + + p { + + background-color: var(--chat-bubble-received-color); + } +} + +.sent-starred-content { + + p { + + background-color: var(--chat-bubble-sent-color); + } +} + +.starred-content:not(:first-child) { + + border-bottom: 2px solid rgb(24, 24, 24); +} + +.no-starred { + + padding: 3rem; + margin: 0 auto; + text-align: center; + margin-top: 3rem; + + img { + + border-radius: 500px; + width: 10rem; + height: 10rem; + } + + h1 { + + color: rgb(165, 165, 165); + font-size: 1.1rem; + } + + p { + + color: rgb(165, 165, 165); + font-size: 0.9rem; + } +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Status.js b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Status.js new file mode 100644 index 0000000..3b40f12 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Status.js @@ -0,0 +1,53 @@ +import { IonContent, IonCardTitle, IonIcon, IonCol, IonItem, IonHeader, IonPage, IonTitle, IonToolbar, IonButtons, IonButton, IonText, IonRow } from '@ionic/react'; +import { add, camera, pencil } from 'ionicons/icons'; +import styles from './Status.module.scss'; + +const Status = () => { + return ( + + + + + Privacy + + Status + + + + + + Status + + + + + avatar +
+ +
+ + + My Status + +
+ Add to my status +
+ + + + + + + + + + +
+ +

No recent updates to show right now.

+
+
+ ); +}; + +export default Status; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Status.module.scss b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Status.module.scss new file mode 100644 index 0000000..5962b55 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/AppPages1/Status.module.scss @@ -0,0 +1,73 @@ +.statusPage { + + ion-item { + + --background: rgb(27, 27, 27); + background: rgb(27, 27, 27); + border-top: 1px solid rgb(41, 41, 41); + border-bottom: 1px solid rgb(41, 41, 41); + padding: 0.5rem; + } +} + +.updates { + + margin-top: 2rem; + text-align: center; + background: rgb(27, 27, 27); + border-top: 1px solid rgb(41, 41, 41); + border-bottom: 1px solid rgb(41, 41, 41); + padding: 1rem; + color: rgb(144, 144, 144); + + ion-text { + + text-align: center; + } +} + +.smallText { + + font-size: 0.9rem; +} + +.statusAvatar { + + padding: 0.5rem; +} + +.statusAvatar { + + img { + + height: 3.5rem; + width: 3.5rem; + border-radius: 500px; + } +} + +.statusActions { + + ion-icon { + + padding: 0.5rem; + background-color: rgb(56, 56, 56); + border-radius: 500px; + } +} + +.imageUpload { + + display: flex; + flex-direction: row; + justify-content: center; + align-content: center; + align-items: center; + position: absolute; + background-color: var(--ion-color-primary); + border-radius: 500px; + height: 1.2rem; + width: 1.2rem; + margin-left: 2.5rem; + margin-top: 1rem; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/TODO.md b/03_source/mobile/src/pages/DemoReactWhatsAppClone/TODO.md new file mode 100644 index 0000000..78e249c --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/TODO.md @@ -0,0 +1,5 @@ +# TODO + +need to resolve path problem + +`demo-react-whatsapp-clone` sub path from the main diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ChatBottomDetails.js b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ChatBottomDetails.js new file mode 100644 index 0000000..deab9ca --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ChatBottomDetails.js @@ -0,0 +1,10 @@ +import { IonIcon } from '@ionic/react'; +import { checkmarkDone, star } from 'ionicons/icons'; + +export const ChatBottomDetails = ({ message }) => ( + + {message.date} + {message.sent && } + {message.starred && } + +); diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ChatItem.js b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ChatItem.js new file mode 100644 index 0000000..460e044 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ChatItem.js @@ -0,0 +1,38 @@ +import { IonIcon, IonItem } from '@ionic/react'; +import { checkmarkDone } from 'ionicons/icons'; +import { ContactStore } from '../store'; +import { getContacts } from '../store/Selectors'; + +const ChatItem = ({ chat }) => { + const contacts = ContactStore.useState(getContacts); + const { chats, contact_id } = chat; + const { read, date, preview, received } = chats[chats.length - 1]; + const contact = contacts.filter((c) => c.id === contact_id)[0]; + const notificationCount = chats.filter((chat) => chat.read === false).length; + + return ( +
+ avatar + + +
+
+

{contact.name}

+
+

+ {read && received && } + {preview} +

+
+ +
+

0 && 'chat-unread'}`}>{date}

+ + {notificationCount > 0 &&
{notificationCount}
} +
+
+
+ ); +}; + +export default ChatItem; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ChatRepliedQuote.js b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ChatRepliedQuote.js new file mode 100644 index 0000000..6365556 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ChatRepliedQuote.js @@ -0,0 +1,14 @@ +const Quote = ({ message, contact, repliedMessage }) => ( +
+

{contact.name}

+

{repliedMessage.preview}

+
+); + +export const ChatRepliedQuote = ({ message, contact, repliedMessage }) => { + if (message.reply && repliedMessage) { + return ; + } else { + return ''; + } +}; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ContactModal.js b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ContactModal.js new file mode 100644 index 0000000..3bbfa4e --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ContactModal.js @@ -0,0 +1,42 @@ +import { IonButton, IonButtons, IonContent, IonHeader, IonItem, IonLabel, IonList, IonTitle, IonToolbar } from '@ionic/react'; +import { ContactStore } from '../store'; +import { getContacts } from '../store/Selectors'; + +import './ContactModal.scss'; + +const ContactModal = ({ close }) => { + const contacts = ContactStore.useState(getContacts); + + return ( +
+ + + New Chat + + + Cancel + + + + + + + + {contacts.map((contact) => { + return ( + + contact avatar + +

{contact.name}

+

Available

+
+
+ ); + })} +
+
+
+ ); +}; + +export default ContactModal; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ContactModal.scss b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ContactModal.scss new file mode 100644 index 0000000..0ed4a84 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ContactModal.scss @@ -0,0 +1,14 @@ +.contact-item { + img { + border-radius: 500px; + height: 2.5rem; + width: 2.5rem; + margin-right: 1.5rem; + } + + ion-label { + h1 { + font-size: 1rem; + } + } +} diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ReplyTo.js b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ReplyTo.js new file mode 100644 index 0000000..e4b5616 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/ReplyTo.js @@ -0,0 +1,54 @@ +import { CreateAnimation, IonButton, IonCol, IonIcon, IonLabel, IonRow } from '@ionic/react'; +import { closeCircleOutline } from 'ionicons/icons'; +import { useEffect } from 'react'; +import { useState } from 'react'; + +const ReplyTo = ({ contact, replyToMessage = false, replyToAnimationRef, setReplyToMessage, messageSent }) => { + const [cancellingReplyTo, setCancellingReplyTo] = useState(false); + + useEffect(() => { + messageSent && cancelReplyTo(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [messageSent]); + + const slideAnimation = { + property: 'transform', + fromValue: 'translateY(100px)', + toValue: 'translateY(0px)', + }; + + const replyToAnimation = { + duration: 300, + direction: !cancellingReplyTo ? 'normal' : 'reverse', + iterations: '1', + fromTo: [slideAnimation], + easing: 'ease-in-out', + }; + + // Cancel the reply-to + const cancelReplyTo = async () => { + setCancellingReplyTo(true); + await replyToAnimationRef.current.animation.play(); + setCancellingReplyTo(false); + setReplyToMessage(false); + }; + + return ( + + + + {contact} + {replyToMessage.preview} + + + + + + + + + + ); +}; + +export default ReplyTo; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/SubPages.jsx b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/SubPages.jsx new file mode 100644 index 0000000..3c58900 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/SubPages.jsx @@ -0,0 +1,15 @@ +import { Route } from 'react-router-dom'; + +const SubPages = (props) => { + return ( + <> + {props.routes.map((route, i) => { + const RouteComponent = route.component; + + return } exact={false} />; + })} + + ); +}; + +export default SubPages; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/Tabs.jsx b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/Tabs.jsx new file mode 100644 index 0000000..2c0702a --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/components/Tabs.jsx @@ -0,0 +1,35 @@ +import { IonIcon, IonLabel, IonTabBar, IonTabButton, IonTabs, IonRouterOutlet } from '@ionic/react'; +import { Redirect, Route } from 'react-router-dom'; + +const Tabs = (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 Tabs; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/index.tsx b/03_source/mobile/src/pages/DemoReactWhatsAppClone/index.tsx new file mode 100644 index 0000000..8a1fa21 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/index.tsx @@ -0,0 +1,14 @@ +import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react'; + +import { cloudOutline, searchOutline } from 'ionicons/icons'; +import { Route, Redirect } from 'react-router'; + +import NavRoutes from './nav/NavRoutes.jsx'; + +import './style.scss'; + +function DemoReactWhatsAppClone() { + return ; +} + +export default DemoReactWhatsAppClone; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/nav/AllRoutes.jsx b/03_source/mobile/src/pages/DemoReactWhatsAppClone/nav/AllRoutes.jsx new file mode 100644 index 0000000..d3417b6 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/nav/AllRoutes.jsx @@ -0,0 +1,73 @@ +// Main Tabs +import Status from '../pages/Status'; +import Calls from '../pages/Calls'; +import Chats from '../pages/Chats'; +import Settings from '../pages/Settings'; + +// Main tab children +import Chat from '../pages/Chat'; +import Starred from '../pages/Starred'; + +// Sub pages +// import InboxItem from "../../pages/InboxItem"; + +// Tab icons +import { callOutline, cameraOutline, chatbubblesOutline, discOutline, settingsOutline } from 'ionicons/icons'; + +// Import custom tab menu +import Tabs from '../components/Tabs'; +import SubPages from '../components/SubPages'; + +// Array of objects representing tab pages +// These will be the main tabs across the app + +// * PARAMS per tab object * +// isTab = true will make the tab appear +// default = the default tab page to open and be redirected to at "/" +// NOTE: there should only be one default tab (default: true) +// label = the label to show with the tab +// component = the component related to this tab page +// icon = icon to show on the tab bar menu +// path = the path which the tab is accessible +export const tabRoutes = [ + { label: 'Status', component: Status, icon: discOutline, path: '/tabs/status', default: false, isTab: true }, + { label: 'Calls', component: Calls, icon: callOutline, path: '/tabs/calls', default: false, isTab: true }, + { label: 'Camera', component: Calls, icon: cameraOutline, path: '/tabs/camera', default: false, isTab: true }, + { label: 'Chats', component: Chats, icon: chatbubblesOutline, path: '/tabs/chats', default: true, isTab: true }, + { label: 'Settings', component: Settings, icon: settingsOutline, path: '/tabs/settings', default: false, isTab: true }, +]; + +// Array of objects representing children pages of tabs + +// * PARAMS per tab object * +// isTab = should always be set to false for these +// component = the component related to this tab page +// path = the path which the tab is accessible + +// These pages should be related to tab pages and be held within the same path +// E.g. /tabs/tab1/child +const tabChildrenRoutes = [ + // { component: InboxItem, path: "/tabs/tab2/:id", isTab: false }, +]; + +// Array of objects representing sub pages + +// * PARAMS per tab object * +// component = the component related to this sub page +// path = the path which the sub page is accessible + +// This array should be sub pages which are not directly related to a tab page +// E.g. /child +const subPageRoutes = [ + { component: Chat, path: '/view-chat/:contact_id' }, + { component: Starred, path: '/starred-messages' }, +]; + +// Let's combine these together as they need to be controlled within the same IonRouterOutlet +const tabsAndChildrenRoutes = [...tabRoutes, ...tabChildrenRoutes]; + +// Render sub routes +export const AllSubPages = () => ; + +// Render tab menu +export const AllTabs = () => ; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/nav/NavRoutes.jsx b/03_source/mobile/src/pages/DemoReactWhatsAppClone/nav/NavRoutes.jsx new file mode 100644 index 0000000..afafa52 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/nav/NavRoutes.jsx @@ -0,0 +1,20 @@ +import { IonRouterOutlet } from '@ionic/react'; +import { IonReactRouter } from '@ionic/react-router'; +import { Redirect, Route } from 'react-router-dom'; +import { AllSubPages, AllTabs, tabRoutes } from './AllRoutes.jsx'; + +const NavRoutes = () => { + return ( + + + } /> + + + t.default)[0].component} exact={true} /> + t.default)[0].path.toString()} /> + + + ); +}; + +export default NavRoutes; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Calls.css b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Calls.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Calls.jsx b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Calls.jsx new file mode 100644 index 0000000..cd8c9b7 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Calls.jsx @@ -0,0 +1,23 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import './Calls.css'; + +const Calls = () => { + return ( + + + + Calls + + + + + + Calls + + + + + ); +}; + +export default Calls; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Chat.css b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Chat.css new file mode 100644 index 0000000..3382f0d --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Chat.css @@ -0,0 +1,289 @@ +.chat-page ion-header, +.chat-page ion-toolbar { + + --min-height: 3.5rem; +} + +.chat-page ion-title { + + margin-left: -3.5rem; +} + +.chat-page ion-title p { + + padding: 0; + margin: 0; +} + +.chat-contact { + + display: flex; + flex-direction: row; + align-content: center; + justify-content: center; + align-items: center; +} + +.chat-contact img { + + height: 2rem; + width: 2rem; + border-radius: 500px; +} + +.chat-contact-details { + + display: flex; + flex-direction: column; + margin-left: 0.5rem; + text-align: left; +} + +.chat-contact-details p { + + font-size: 0.9rem; +} + +.chat-contact-details ion-text { + + font-size: 0.7rem; + font-weight: 400; +} + +.chat-bubble { + + border-radius: 5px; + margin-left: 1rem; + margin-right: 1rem; + margin-top: 0.8rem; + + padding: 0.5rem; + max-width: 80%; + clear: both; + + display: flex; + flex-direction: row; + transition: 0.2s all linear; +} + +.chat-bubble:last-child { + + margin-bottom: 0.8rem; +} + +.bubble-sent { + + background-color: var(--chat-bubble-sent-color); + float: right; +} + +.bubble-received { + + background-color: var(--chat-bubble-received-color); + float: left; +} + +.chat-bubble p { + + padding: 0; + margin: 0; +} + +.chat-footer { + + background-color: rgb(22, 22, 22); + border-top: 1px solid rgb(47, 47, 47); + padding-top: 0.2rem; + padding-bottom: 1rem; +} + +.chat-footer ion-textarea { + + background-color: rgb(31, 31, 31); + border: 1px solid rgb(36, 36, 36); + color: white; + border-radius: 25px; + padding-left: 0.5rem; + caret-color: var(--ion-color-primary); +} + +.chat-footer ion-icon { + + font-size: 1.5rem; + margin-top: 0.2rem; +} + +.chat-input-container { + + width: 70%; + margin-right: 0.75rem; +} + +.chat-send-button { + + margin: 0 !important; + padding: 0 !important; + position: absolute; + right: 17px; + margin-top: -0.2rem !important; + + display: flex; + flex-direction: row; + align-content: center; + align-items: center; + justify-content: center; +} + +.chat-send-button ion-icon { + + color: white; + background-color: var(--ion-color-primary); + font-size: 1.1rem; + border-radius: 500px; + padding: 0.5rem; +} + +.chat-time { + + color: rgb(165, 165, 165); + font-size: 0.75rem; + right: 0; + bottom: 0 !important; + margin: 0; + padding: 0; + margin-top: 5px; +} + +.bubble-arrow { + + position: absolute; + float: left; + left: 6px; + margin-top: -8px; + /* top: 0px; */ +} + +.bubble-arrow.alt { + + position: relative; + bottom: 0px; + left: auto; + right: -3px; + float: right; +} + +.bubble-arrow:after { + + content: ""; + position: absolute; + border-top: 15px solid var(--chat-bubble-received-color); + border-left: 15px solid transparent; + border-radius: 4px 0 0 0px; + width: 0; + height: 0; +} + +.bubble-arrow.alt:after { + + border-top: 15px solid var(--chat-bubble-sent-color); + transform: scaleX(-1); +} + +.chat-reply-to-row { + + bottom: 70px !important; + position: absolute; + + border-left: 4px solid rgb(224, 176, 18); + width: 100%; + background-color: rgb(22, 22, 22); + border-top: 1px solid rgb(47, 47, 47); + padding: 0.5rem; + padding-bottom: 0.8rem; +} + +.chat-reply-to-container { + + display: flex; + flex-direction: column; +} + +.chat-reply-to-name { + + color: rgb(224, 176, 18); + font-weight: 500; + margin-bottom: 0.5rem; +} + +.chat-reply-to-message { + + font-size: 0.8rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.all-chats { + + +} + +.chat-bottom-details { + + display: flex; + flex-direction: row; + width: 100%; + align-content: center; + align-items: center; + justify-content: flex-end; + margin-top: 0.4rem; +} + +.chat-bottom-details ion-icon { + + font-size: 0.6rem; + color: grey; + margin-left: 0.5rem; + margin-top: 0.05rem; +} + +.chat-bottom-details span { + + margin: 0; + padding: 0; + font-size: 0.75rem; + color: rgb(190, 190, 190); +} + +.in-chat-reply-to-container { + + background-color: rgba(0, 0, 0, 0.2); + border-left: 3px solid rgb(224, 176, 18); + height: fit-content; + padding: 0.5rem; + border-radius: 5px; + margin-bottom: 0.5rem; +} + +.in-chat-reply-to-container h1 { + + margin: 0; + padding: 0; + color: rgb(224, 176, 18); + font-size: 0.8rem; +} + +.in-chat-reply-to-container p { + + color: rgb(167, 167, 167); + font-size: 0.8rem; +} + +.bottom-container { + + position: absolute; + bottom: 4.5rem; + height: 5rem; + background-color: red; + width: 100%; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Chat.jsx b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Chat.jsx new file mode 100644 index 0000000..9bbab03 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Chat.jsx @@ -0,0 +1,378 @@ +import { IonBackButton, IonButton, IonButtons, IonCol, IonContent, IonFooter, IonGrid, IonHeader, IonIcon, IonPage, IonRow, IonText, IonTextarea, IonTitle, IonToolbar, CreateAnimation, createGesture, useIonViewWillEnter, IonActionSheet, IonToast } from "@ionic/react"; +import { addOutline, alertOutline, callOutline, cameraOutline, micOutline, send, shareOutline, starOutline, trashOutline, videocamOutline } from "ionicons/icons"; +import { useRef } from "react"; +import { useEffect, useState } from "react"; +import { useParams } from "react-router"; +import { ChatStore, ContactStore } from "../store"; +import { getNotificationCount, markAllAsRead, sendChatMessage, starChatMessage } from "../store/ChatStore"; +import { getChat, getChats, getContact } from "../store/Selectors"; + +import { useLongPress } from 'react-use'; +import "./Chat.css" +import ReplyTo from "../components/ReplyTo"; +import { ChatBottomDetails } from "../components/ChatBottomDetails"; +import { ChatRepliedQuote } from "../components/ChatRepliedQuote"; +import { useCamera } from "../hooks/useCamera"; +import { useGallery } from "../hooks/useGallery"; + +const Chat = () => { + + const params = useParams(); + + // Global State + const chat = ChatStore.useState(getChat(params.contact_id)); + const chats = ChatStore.useState(getChats); + const contact = ContactStore.useState(getContact(params.contact_id)); + const notificationCount = getNotificationCount(chats); + + const { takePhoto } = useCamera(); + const { prompt } = useGallery(); + + // Local state + const [ message, setMessage ] = useState(""); + const [ showSendButton, setShowSendButton ] = useState(false); + const [ replyToMessage, setReplyToMessage ] = useState(false); + const [ messageSent, setMessageSent ] = useState(false); + + const [ showActionSheet, setShowActionSheet ] = useState(false); + const [ actionMessage, setActionMessage ] = useState(false); + + const [ showToast, setShowToast ] = useState(false); + const [ toastMessage, setToastMessage ] = useState(""); + + // Refs + const contentRef = useRef(); + const swiperRefs = useRef([]); + const textareaRef = useRef(); + const sideRef = useRef(); + const sendRef = useRef(); + const replyToAnimationRef = useRef(); + + const actionSheetButtons = [ + + { + text: (actionMessage && actionMessage.starred) ? "Unstar Message" : "Star Message", + icon: starOutline, + handler: () => starChatMessage(params.contact_id, actionMessage.id) + }, + actionMessage && actionMessage.received ? + { + text: "Reply To Message", + icon: shareOutline, + handler: () => showReplyToMessage(actionMessage) + } + : + { + text: "Unsend Message", + icon: alertOutline, + handler: () => toaster("I haven't implemented unsend :) Simple store update though") + }, + { + text: "Delete Message", + icon: trashOutline, + handler: () => toaster("I haven't implemented delete :) Simple store update though"), + role: "destructive" + } + ]; + + useEffect(() => { + + !showActionSheet && setActionMessage(false); + }, [ showActionSheet ]); + + // Scroll to end of content + // Mark all chats as read if we come into a chat + // Set up our swipe events for animations and gestures + useIonViewWillEnter(() => { + + scrollToBottom(); + setupObserver(); + markAllAsRead(params.contact_id); + setSwipeEvents(); + }); + + // For displaying toast messages + const toaster = message => { + + setToastMessage(message); + setShowToast(true); + } + + // Scroll to end of content + const scrollToBottom = async () => { + + contentRef.current.scrollToBottom(); + } + + // Watch for DOM changes + // Then scroll to bottom + // This ensures that the new chat message has *actually* been rendered + // Check this: + // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver + const setupObserver = () => { + + // Mutation Observers watch for DOM changes + // This will ensure that we scroll to bottom AFTER the new chat has rendered + const observer = new MutationObserver(() => { + + scrollToBottom(); + }); + + // We observe the ion-content (or containing element of chats) + observer.observe(contentRef.current, { + + childList: true + }); + } + + // Long press callback + const onLongPress = (e) => { + + const elementID = e.target.id; + const chatMessageID = elementID.includes("chatText") ? parseInt(elementID.replace("chatText_", "")) : elementID.includes("chatTime") ? parseInt(elementID.replace("chatTime_", "")) : parseInt(elementID.replace("chatBubble_", "")); + + const chatMessage = chat.filter(message => parseInt(message.id) === parseInt(chatMessageID))[0]; + + setActionMessage(chatMessage); + setShowActionSheet(true); + }; + + const longPressEvent = useLongPress(onLongPress, { + + isPreventDefault: true, + delay: 2000, + }); + + const showReplyToMessage = async message => { + + // Activate reply-to functionality + setReplyToMessage(message); + await replyToAnimationRef.current.animation.play(); + contentRef.current.scrollToBottom(300); + } + + const checkBubble = async (bubble, message, event) => { + + if (event.deltaX >= 120) { + + // Activate reply-to functionality + bubble.style.transform = "none"; + showReplyToMessage(message); + } else { + + // Put chat bubble back to original position + bubble.style.transform = "none"; + } + } + + // Function to move a bubble with the deltaX swipe + const moveBubble = (bubble, event) => { + + if (event.velocityX > 0) { + + bubble.style.transform = `translateX(${ event.deltaX }px)`; + } + } + + const setSwipeEvents = () => { + + chat.forEach((message, index) => { + + if (!message.sent) { + + const chatBubble = swiperRefs.current[index]; + + const swipeGesture = createGesture({ + el: chatBubble, + onEnd: e => checkBubble(chatBubble, message, e), + onMove: e => moveBubble(chatBubble, e) + }); + + swipeGesture.enable(); + } + }); + } + + const widthAnimation = { + + property: "width", + fromValue: "110%", + toValue: "100%" + }; + + const fadeAnimation = { + + property: "opacity", + fromValue: "100%", + toValue: "0%" + }; + + const sideButtonsAnimation = { + + duration: 200, + direction: showSendButton ? "normal" : "reverse", + iterations: "1", + fromTo: [ fadeAnimation ], + easing: "ease-in-out" + }; + + const sendButtonAnimation = { + + duration: showSendButton ? 300 : 100, + direction: !showSendButton ? "normal" : "reverse", + iterations: "1", + fromTo: [ fadeAnimation ], + easing: "ease-in-out" + }; + + const textareaAnimation = { + + duration: 200, + direction: !showSendButton ? "normal" : "reverse", + iterations: "1", + fromTo: [ widthAnimation ], + easing: "ease-in-out" + }; + + // Set the state value when message val changes + useEffect(() => { + + setShowSendButton(message !== ""); + }, [ message ]); + + // Play the animations when the state value changes + useEffect(() => { + + textareaRef.current.animation.play(); + sideRef.current.animation.play(); + sendRef.current.animation.play(); + }, [ showSendButton ]); + + const sendMessage = (image = false, imagePath = false) => { + + if (message !== "" || image === true) { + + sendChatMessage(params.contact_id, message, replyToMessage, replyToMessage ? replyToMessage.id : false, image, imagePath); + setMessage(""); + + setMessageSent(true); + setTimeout(() => setMessageSent(false), 10); + image && setTimeout(() => scrollToBottom(), 100); + } + } + + const handlePhoto = async () => { + + const returnedFilePath = await takePhoto(); + sendMessage(true, returnedFilePath); + } + + const handlePrompt = async () => { + + const returnedFilePath = await prompt(); + sendMessage(true, returnedFilePath); + } + + const replyToProps = { + + replyToAnimationRef, + replyToMessage, + setReplyToMessage, + contact: contact.name, + messageSent + }; + + return ( + + + + + 0) ? notificationCount : "" } /> + + +
+ avatar +
+

{ contact.name }

+ last seen today at 22:10 +
+
+
+ + + toaster("As this is a UI only, video calling wouldn't work here.")}> + + + + toaster("As this is a UI only, calling wouldn't work here.")}> + + + +
+
+ + + + { chat.map((message, index) => { + + const repliedMessage = chat.filter(subMessage => parseInt(subMessage.id) === parseInt(message.replyID))[0]; + + return ( +
swiperRefs.current[index] = ref } id={ `chatBubble_${ message.id }`} key={ index } className={ `chat-bubble ${ message.sent ? "bubble-sent" : "bubble-received" }` } { ...longPressEvent }> +
+ + + + { message.preview } + { message.image && message.imagePath && chat message } + +
+ +
+
+ ); + })} + + setShowActionSheet(false) } buttons={ actionSheetButtons } /> + + setShowToast(false) } message={ toastMessage } position="bottom" duration="3000" /> +
+ + { replyToMessage && } + + + + + + + + +
+ + setMessage(e.target.value) } /> + +
+ + + + + + + + + + + + + + + + +
+
+
+
+ ); +} + +export default Chat; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Chats.css b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Chats.css new file mode 100644 index 0000000..c1c674d --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Chats.css @@ -0,0 +1,112 @@ +.chat-row { + + display: flex; + flex-direction: row; + /* justify-content: space-between; */ + align-items: center; + /* align-content: center; */ + padding-left: 1rem; +} + +.chat-row ion-item { + + width: 100%; +} + +.chat-row img { + + height: 3rem; + width: 3rem; + border-radius: 500px; + background-color: inherit; +} + +.chat-content { + + padding-bottom: 1rem; + padding-top: 1rem; + width: 100%; +} + +.chat-content h2 { + + font-size: 1rem; + font-weight: 600; +} + +.chat-content p, +.chat-content h2 { + + margin: 0; + padding: 0; +} + +.chat-content p { + + font-size: 1rem; + margin-top: 0.2rem; + color: rgb(153, 153, 153); +} + +.chat-content p ion-icon { + + margin-right: 0.4rem; +} + +.chat-name-date { + + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.chat-details .chat-date { + + color: rgb(153, 153, 153); + font-size: 0.8rem; + padding-left: 0.5rem;; +} + +.chat-details .chat-unread { + + color: var(--ion-color-primary); +} + +.chat-notification-count { + + margin: 0; + padding: 0; +} + +.chat-details { + + display: flex; + flex-direction: column; + align-content: flex-end; + justify-content: flex-end; + align-content: flex-end; + align-items: flex-end; +} + +.chat-notification { + + font-size: 0.7rem; + padding: 0.2rem; + background-color: var(--ion-color-primary); + border-radius: 500px; + display: flex; + flex-direction: column; + align-content: center; + justify-content: center; + align-content: center; + align-items: center; + height: 1rem; + width: 1rem; +} + +.chat-content-container { + + display: flex; + flex-direction: row; + justify-content: space-between; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Chats.jsx b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Chats.jsx new file mode 100644 index 0000000..e2e454f --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Chats.jsx @@ -0,0 +1,79 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar, IonSearchbar, IonButtons, IonButton, IonIcon, IonItem, IonModal } from '@ionic/react'; +import { checkmarkDone, createOutline } from 'ionicons/icons'; +import './Chats.css'; + +import { ChatStore, ContactStore } from '../store'; +import { getContacts, getChats } from '../store/Selectors'; +import { useEffect, useState } from 'react'; +import ChatItem from '../components/ChatItem'; +import { useRef } from 'react'; +import ContactModal from '../components/ContactModal'; + +const Chats = () => { + + const pageRef = useRef(); + const contacts = ContactStore.useState(getContacts); + const latestChats = ChatStore.useState(getChats); + + const [ results, setResults ] = useState(latestChats); + const [ showContactModal, setShowContactModal ] = useState(false); + + useEffect(() => { + + setResults(latestChats); + }, [ latestChats ]); + + const search = e => { + + const searchTerm = e.target.value; + + if (searchTerm !== "") { + + const searchTermLower = searchTerm.toLowerCase(); + + const newResults = latestChats.filter(chat => contacts.filter(c => c.id === chat.contact_id)[0].name.toLowerCase().includes(searchTermLower)); + setResults(newResults); + } else { + + setResults(latestChats); + } + } + + return ( + + + + + Edit + + + setShowContactModal(true) }> + + + + Chats + + + + + + Chats + + search(e) } /> + + + + { results.map((chat, index) => { + + return ; + })} + + setShowContactModal(false) }> + setShowContactModal(false) } /> + + + + ); +}; + +export default Chats; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Settings.jsx b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Settings.jsx new file mode 100644 index 0000000..79873c4 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Settings.jsx @@ -0,0 +1,120 @@ +import { IonCardSubtitle, IonCol, IonContent, IonHeader, IonIcon, IonItem, IonList, IonPage, IonRow, IonText, IonTitle, IonToolbar } from '@ionic/react'; +import { camera, cloudUpload, cloudUploadOutline, heart, helpOutline, informationOutline, key, laptop, laptopOutline, logoWhatsapp, mailUnreadOutline, notificationsOutline, pencil, qrCodeOutline, star } from 'ionicons/icons'; +import styles from './Settings.module.scss'; + +const Settings = () => { + + const settings = [ + + [ + { + title: "Starred Messages", + url: "/starred-messages", + icon: star, + color: "rgb(255, 208, 0)" + }, + { + title: "WhatsApp Web/Desktop", + icon: laptopOutline, + color: "rgb(33, 165, 114)" + } + ], + [ + { + title: "Account", + icon: key, + color: "rgb(0, 81, 255)" + }, + { + title: "Chats", + icon: logoWhatsapp, + color: "rgb(79, 182, 96)" + }, + { + title: "Notifications", + icon: mailUnreadOutline, + color: "rgb(233, 46, 46)" + }, + { + title: "Storage and Data", + icon: cloudUploadOutline, + color: "rgb(79, 182, 96)" + } + ], + [ + { + title: "Help", + icon: informationOutline, + color: "rgb(0, 81, 255)" + }, + { + title: "Tell a Friend", + icon: heart, + color: "rgb(228, 70, 70)" + } + ] + ]; + + return ( + + + + Settings + + + + + + Settings + + + + + avatar + + + Alan Montgomery + +
+ This is my status! +
+ + + + + + +
+ + { settings.map((setting, index) => { + + return ( + + + + { setting.map((option, index) => { + + var itemStyle = { "--setting-item-color": option.color }; + + return ( + + + +

{ option.title }

+
+ ); + })} +
+ ); + })} + +
+ from + IONIC React HUB +
+
+
+ ); +}; + +export default Settings; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Settings.module.scss b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Settings.module.scss new file mode 100644 index 0000000..7388538 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Settings.module.scss @@ -0,0 +1,69 @@ +.settingsPage { + + ion-item { + + --background: rgb(27, 27, 27); + background: rgb(27, 27, 27); + border-top: 1px solid rgb(41, 41, 41); + border-bottom: 1px solid rgb(41, 41, 41); + padding: 0.5rem; + } +} + +.settingsList { + + ion-item { + + --background: rgb(27, 27, 27); + background: rgb(27, 27, 27); + // border: none !important; + border-top: 1px solid rgb(34, 34, 34); + border-bottom: 1px solid rgb(36, 36, 36); + padding: 0; + + p { + margin: 0; + } + + ion-icon { + + border-radius: 5px; + padding: 0.2rem; + font-size: 1.4rem; + margin-right: 1.2rem; + --setting-item-color: white; + background-color: var(--setting-item-color); + color: rgb(233, 46, 46); + } + } +} + +.smallText { + + font-size: 0.9rem; +} + +.statusAvatar { + + padding: 0.5rem; +} + +.statusAvatar { + + img { + + height: 3.5rem; + width: 3.5rem; + border-radius: 500px; + } +} + +.statusActions { + + ion-icon { + + padding: 0.5rem; + background-color: rgb(56, 56, 56); + border-radius: 500px; + } +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Starred.jsx b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Starred.jsx new file mode 100644 index 0000000..7512537 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Starred.jsx @@ -0,0 +1,87 @@ +import { IonBackButton, IonContent, IonHeader, IonIcon, IonPage, IonTitle, IonToolbar, useIonViewWillEnter } from '@ionic/react'; +import { chevronForward } from 'ionicons/icons'; +import { useState } from 'react'; +import { ChatStore, ContactStore } from '../store'; +import { getChats, getContacts } from '../store/Selectors'; + +import "./Starred.scss"; + +const Starred = () => { + + const contacts = ContactStore.useState(getContacts); + const chats = ChatStore.useState(getChats); + + const [ starredMessages, setStarredMessages ] = useState(false); + + useIonViewWillEnter(() => { + + var tempChats = [ ...chats ]; + var starred = []; + + tempChats.forEach(tempChat => { + + tempChat.chats.forEach(chat => { + + if (chat.starred) { + + starred.push({ + + contact_id: tempChat.contact_id, + ...chat + }); + } + }); + }); + + setStarredMessages(starred); + }); + + return ( + + + + + Starred Messages + + + + + { starredMessages && starredMessages.map(starredMessage => { + + const { id, contact_id, date, preview, received } = starredMessage; + const contact = contacts.filter(c => c.id === contact_id)[0]; + + return ( +
+
+ +
+ starred avatar +

{ contact.name }

+
+ +

{ date }

+ +
+
+

{ preview }

+ +
+
+ ); + })} + + { starredMessages.length < 1 && + +
+ no starred +

No Starred Messages

+

Tap and hold on any message to star it, so you can easily find it later.

+
+ } +
+
+ ); +}; + +export default Starred; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Starred.scss b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Starred.scss new file mode 100644 index 0000000..8bf228a --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Starred.scss @@ -0,0 +1,117 @@ +.starred-message { + + display: flex; + flex-direction: column; + margin-top: 1rem; +} + +.starred-header { + + display: flex; + flex-direction: row; + justify-content: space-between; +} + +.starred-contact { + + display: flex; + flex-direction: row; + align-content: center; + align-items: center; + margin-left: 1rem; + + img { + + height: 2rem; + width: 2rem; + border-radius: 500px; + } + + p { + + margin-left: 1rem; + font-size: 0.9rem; + font-weight: 500; + } +} + +.starred-date { + + margin-right: 1.5rem; + color: rgb(138, 138, 138); + font-size: 0.8rem; +} + +.starred-content { + + display: flex; + flex-direction: row; + align-content: center; + align-items: center; + justify-content: space-between; + margin-right: 1.5rem; + margin-left: 3.2rem; + + ion-icon { + + color: rgb(138, 138, 138); + font-size: 1rem; + } + + p { + + border-radius: 10px; + max-width: 75%; + padding: 0.5rem; + margin: 0; + margin-bottom: 1rem; + } +} + +.received-starred-content { + + p { + + background-color: var(--chat-bubble-received-color); + } +} + +.sent-starred-content { + + p { + + background-color: var(--chat-bubble-sent-color); + } +} + +.starred-content:not(:first-child) { + + border-bottom: 2px solid rgb(24, 24, 24); +} + +.no-starred { + + padding: 3rem; + margin: 0 auto; + text-align: center; + margin-top: 3rem; + + img { + + border-radius: 500px; + width: 10rem; + height: 10rem; + } + + h1 { + + color: rgb(165, 165, 165); + font-size: 1.1rem; + } + + p { + + color: rgb(165, 165, 165); + font-size: 0.9rem; + } +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Status.jsx b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Status.jsx new file mode 100644 index 0000000..3b40f12 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Status.jsx @@ -0,0 +1,53 @@ +import { IonContent, IonCardTitle, IonIcon, IonCol, IonItem, IonHeader, IonPage, IonTitle, IonToolbar, IonButtons, IonButton, IonText, IonRow } from '@ionic/react'; +import { add, camera, pencil } from 'ionicons/icons'; +import styles from './Status.module.scss'; + +const Status = () => { + return ( + + + + + Privacy + + Status + + + + + + Status + + + + + avatar +
+ +
+ + + My Status + +
+ Add to my status +
+ + + + + + + + + + +
+ +

No recent updates to show right now.

+
+
+ ); +}; + +export default Status; diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Status.module.scss b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Status.module.scss new file mode 100644 index 0000000..5962b55 --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/pages/Status.module.scss @@ -0,0 +1,73 @@ +.statusPage { + + ion-item { + + --background: rgb(27, 27, 27); + background: rgb(27, 27, 27); + border-top: 1px solid rgb(41, 41, 41); + border-bottom: 1px solid rgb(41, 41, 41); + padding: 0.5rem; + } +} + +.updates { + + margin-top: 2rem; + text-align: center; + background: rgb(27, 27, 27); + border-top: 1px solid rgb(41, 41, 41); + border-bottom: 1px solid rgb(41, 41, 41); + padding: 1rem; + color: rgb(144, 144, 144); + + ion-text { + + text-align: center; + } +} + +.smallText { + + font-size: 0.9rem; +} + +.statusAvatar { + + padding: 0.5rem; +} + +.statusAvatar { + + img { + + height: 3.5rem; + width: 3.5rem; + border-radius: 500px; + } +} + +.statusActions { + + ion-icon { + + padding: 0.5rem; + background-color: rgb(56, 56, 56); + border-radius: 500px; + } +} + +.imageUpload { + + display: flex; + flex-direction: row; + justify-content: center; + align-content: center; + align-items: center; + position: absolute; + background-color: var(--ion-color-primary); + border-radius: 500px; + height: 1.2rem; + width: 1.2rem; + margin-left: 2.5rem; + margin-top: 1rem; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoReactWhatsAppClone/style.scss b/03_source/mobile/src/pages/DemoReactWhatsAppClone/style.scss new file mode 100644 index 0000000..37c1e1a --- /dev/null +++ b/03_source/mobile/src/pages/DemoReactWhatsAppClone/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/DemoRestaurantFinder/AppPages/Tab1.jsx b/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/Tab1.jsx new file mode 100644 index 0000000..47c63ee --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/Tab1.jsx @@ -0,0 +1,192 @@ +import { IonContent, IonFab, IonFabButton, IonHeader, IonIcon, IonModal, IonPage, IonSearchbar, IonToolbar, isPlatform, useIonViewWillEnter } from '@ionic/react'; +import { useEffect, useState } from 'react'; +import { getRecords } from '../main/yelp'; + +import styles from "../styles/Map.module.scss"; + +import { Map, Marker, Overlay } from "pigeon-maps"; +import { maptiler } from 'pigeon-maps/providers'; + +import { MapOverlay } from "../components/MapOverlay"; +import { CurrentPointOverlay } from "../components/CurrentPointOverlay"; +import { flashOffOutline, flashOutline, list } from 'ionicons/icons'; + +import RecordsStore from '../store/RecordsStore'; +import { fetchRecords } from '../store/Selectors'; +import { getLocation } from '../main/utils'; +import { ListModal } from '../components/ListModal'; + +const maptilerProvider = maptiler('d5JQJPLLuap8TkJJlTdJ', 'streets'); + +const Tab1 = () => { + + const web = isPlatform("web" || "pwa" || "mobileweb" || ""); + + // UNCOMMENT THESE TO USE CURRENT LOCATION. + + // const [ currentPoint, setCurrentPoint ] = useState(false); + + // useEffect(() => { + + // const getCurrentLocation = async () => { + + // const fetchedLocation = await getLocation(); + // setCurrentPoint(fetchedLocation.currentLocation); + // } + + // getCurrentLocation(); + // }, []); + + useIonViewWillEnter(() => { + + getRecords(currentPoint); + }); + + const [ currentPoint, setCurrentPoint ] = useState({ latitude: 40.8264691, longitude: -73.9549618 }); + + const [ showCurrentPointInfo, setShowCurrentPointInfo ] = useState(false); + + const records = RecordsStore.useState(fetchRecords); + const center = RecordsStore.useState(s => s.center); + + const [ results, setResults ] = useState(false); + const [ zoom, setZoom ] = useState(14); + + const [ searchTerm, setSearchTerm ] = useState(""); + const [ moveMode, setMoveMode ] = useState(false); + + const [ showListModal, setShowListModal ] = useState(false); + + useEffect(() => { + + const getData = async () => { + + await getRecords(currentPoint); + } + + getData(); + }, [ currentPoint ]); + + useEffect(() => { + + setResults([...records]); + }, [ records ]); + + useEffect(() => { + + const search = searchTerm.toLowerCase(); + var searchResults = []; + + if (searchTerm !== "") { + + records.forEach(record => { + + if (record.name.toLowerCase().includes(search)) { + + searchResults.push(record); + } + }); + + setResults(searchResults); + } else { + + setResults([...records]); + } + + }, [ searchTerm ]); + + const showMarkerInfo = (e, index) => { + + const tempRecords = JSON.parse(JSON.stringify(results)); + + // Hide all current marker infos + setShowCurrentPointInfo(false); + !tempRecords[index].showInfo && tempRecords.forEach(tempRecord => tempRecord.showInfo = false); + tempRecords[index].showInfo = !tempRecords[index].showInfo; + + setResults(tempRecords); + } + + const hideMarkers = () => { + + const tempRecords = JSON.parse(JSON.stringify(results)); + tempRecords.forEach(tempRecord => tempRecord.showInfo = false); + setResults(tempRecords); + setShowCurrentPointInfo(false); + } + + const handleMapClick = e => { + + const clickedPoint = e.latLng; + setCurrentPoint({ latitude: clickedPoint[0], longitude: clickedPoint[1] }); + setMoveMode(false); + } + + const handleShowCurrentPointInfo = () => { + + hideMarkers(); + setShowCurrentPointInfo(!showCurrentPointInfo); + } + + return ( + + + { (center && center.latitude && center.longitude) && results && + <> + +
+ setSearchTerm(e.target.value) } /> +
+ + moveMode ? handleMapClick(e) : hideMarkers(e) } defaultCenter={ [center.latitude, center.longitude] } defaultZoom={ zoom } provider={ maptilerProvider } touchEvents={ true }> + + + + { results.map((record, index) => { + + return showMarkerInfo(e, index) } key={ index } color="#3578e5" width={ 50 } anchor={ [ record.latitude, record.longitude ] } /> + })} + + { results.map((record, index) => { + + if (record.showInfo) { + + return ( + + + + ); + } + })} + + { showCurrentPointInfo && + + + + + } + + + setMoveMode(!moveMode) }> + + + + + + setShowListModal(!showListModal) }> + + + + + + setShowListModal(false) } swipeToClose={ true } initialBreakpoint={ 0.6 } breakpoints={ [0, 0.6, 1] } backdropBreakpoint={ 0.6 }> + setShowListModal(false) } searchTerm={ searchTerm } search={ setSearchTerm } records={ results } /> + + + } +
+
+ ); +}; + +export default Tab1; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/Tab2.css b/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/Tab2.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/Tab2.jsx b/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/Tab2.jsx new file mode 100644 index 0000000..3e1cf97 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/Tab2.jsx @@ -0,0 +1,66 @@ +import { IonButton, IonCard, IonCardHeader, IonCardSubtitle, IonContent, IonHeader, IonIcon, IonNote, IonPage, IonRow, IonText, IonTitle, IonToolbar } from '@ionic/react'; +import { arrowForward, navigateOutline } from 'ionicons/icons'; +import { RatingStar } from '../components/RatingStar'; +import RecordsStore from '../store/RecordsStore'; +import { fetchRecords } from '../store/Selectors'; + +import styles from "../styles/ViewAll.module.scss"; + +const Tab2 = () => { + + const records = RecordsStore.useState(fetchRecords); + + return ( + + + + All places in your location + + + + + + Feeling hungry? + + + + { records.map((record, index) => { + + const imageURL = record.imageURL ? record.imageURL : "/placeholder.jpeg"; + const rating = Math.floor(record.rating).toFixed(0); + + return ( + +
+ + + { Array.apply(null, { length: 5 }).map((e, i) => ( + + i } /> + ))} + + { record.name } + { record.displayAddress } + + + +

+ + { record.distance } miles away +

+
+ + + + +
+
+ + ); + })} + + + ); +}; + +export default Tab2; diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/Tab3.css b/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/Tab3.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/Tab3.jsx b/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/Tab3.jsx new file mode 100644 index 0000000..903b1a0 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/Tab3.jsx @@ -0,0 +1,25 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab3.css'; + +const Tab3 = () => { + return ( + + + + Tab 3 + + + + + + Tab 3 + + + + + + ); +}; + +export default Tab3; diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/ViewPlace.jsx b/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/ViewPlace.jsx new file mode 100644 index 0000000..1662914 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/AppPages/ViewPlace.jsx @@ -0,0 +1,232 @@ +import { + IonAvatar, + IonBackButton, + IonBadge, + IonButton, + IonButtons, + IonCardSubtitle, + IonCol, + IonContent, + IonGrid, + IonHeader, + IonIcon, + IonItem, + IonLabel, + IonNote, + IonPage, + IonRow, + IonTitle, + IonToolbar, + useIonLoading, + useIonModal, + useIonViewWillEnter, +} from '@ionic/react'; +import { callOutline } from 'ionicons/icons'; +import { Map, Marker } from 'pigeon-maps'; +import { useState } from 'react'; +import { useParams } from 'react-router'; +import { RatingStar } from '../components/RatingStar'; +import { getRecord } from '../main/yelp'; +import { RecordsStore } from '../store'; +import { fetchRecord } from '../store/Selectors'; + +import styles from '../styles/ViewPlace.module.scss'; + +import { maptiler } from 'pigeon-maps/providers'; +import { useRef } from 'react'; +const maptilerProvider = maptiler('d5JQJPLLuap8TkJJlTdJ', 'streets'); + +const ViewPlace = ({}) => { + const pageRef = useRef(); + const [present, dismiss] = useIonLoading(); + const { id } = useParams(); + const record = RecordsStore.useState(fetchRecord(id)); + const [extendedRecord, setExtendedRecord] = useState(false); + + const MapView = () => ( + + + + + Close + + Map View + + + + + + + + ); + + useIonViewWillEnter(() => { + const getData = async () => { + const extendedData = await getRecord(id); + setExtendedRecord(extendedData); + dismiss(); + }; + + present('Fetching extended details...'); + getData(); + }); + + const imageURL = record.imageURL ? record.imageURL : '/placeholder.jpeg'; + const rating = Math.floor(record.rating).toFixed(0); + + const [presentModal, dismissModal] = useIonModal(MapView); + + return ( + + + + + + + {record.distance} miles away + + + + + +
+ + + + {record.name} + +
+ {Array.apply(null, { length: 5 }).map((e, i) => ( + i} small={true} /> + ))} +
+ + {record.distance} miles away +
+
+
+
+
+
+ + + + + {extendedRecord.categories && + extendedRecord.categories.length > 0 && + extendedRecord.categories.map((category, index) => { + return ( + + + {category.title} + + + ); + })} + + + + + View on Yelp → + + + + + + + + + + + + + + + + presentModal({ + swipetoClose: true, + presentingElement: pageRef.current, + }) + } + > + View on map + + + + + {extendedRecord.photos && extendedRecord.photos.length > 0 && ( + + + Photos ({extendedRecord.photos.length}) + + + )} + + + {extendedRecord.photos && + extendedRecord.photos.length > 0 && + extendedRecord.photos.map((photo, index) => { + if (index < 3) { + return ( + +
+ + ); + } + })} + + + {extendedRecord.reviews && extendedRecord.reviews.length > 0 && ( + + + Reviews ({extendedRecord.reviews.length}) + + + )} + + + {extendedRecord.reviews && + extendedRecord.reviews.length > 0 && + extendedRecord.reviews.map((review, index) => { + return ( + + + + + + + + {review.user.name} + + Full review on Yelp → + +

{review.text}

+
+
+
+ ); + })} +
+ + + + ); +}; + +export default ViewPlace; diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/NOTES.md b/03_source/mobile/src/pages/DemoRestaurantFinder/NOTES.md new file mode 100644 index 0000000..4bd1eca --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/NOTES.md @@ -0,0 +1,5 @@ +# notes + +## TODO + +need server for map showing diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/components/CurrentPointOverlay.jsx b/03_source/mobile/src/pages/DemoRestaurantFinder/components/CurrentPointOverlay.jsx new file mode 100644 index 0000000..3dba7a2 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/components/CurrentPointOverlay.jsx @@ -0,0 +1,11 @@ +import { IonCardSubtitle, IonNote } from "@ionic/react"; +import styles from "../styles/MapOverlay.module.scss"; + +export const CurrentPointOverlay = () => ( + +
+ + Current Location + Click on the lightning button then choose a new point on the map to view places around that point. +
+) \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/components/ExploreContainer.css b/03_source/mobile/src/pages/DemoRestaurantFinder/components/ExploreContainer.css new file mode 100644 index 0000000..e99f514 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/components/ExploreContainer.css @@ -0,0 +1,24 @@ +.container { + text-align: center; + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +.container strong { + font-size: 20px; + line-height: 26px; +} + +.container p { + font-size: 16px; + line-height: 22px; + color: #8c8c8c; + margin: 0; +} + +.container a { + text-decoration: none; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/components/ExploreContainer.jsx b/03_source/mobile/src/pages/DemoRestaurantFinder/components/ExploreContainer.jsx new file mode 100644 index 0000000..093861b --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/components/ExploreContainer.jsx @@ -0,0 +1,12 @@ +import './ExploreContainer.css'; + +const ExploreContainer = ({ name }) => { + return ( +
+ {name} +

Explore UI Components

+
+ ); +}; + +export default ExploreContainer; diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/components/ListModal.jsx b/03_source/mobile/src/pages/DemoRestaurantFinder/components/ListModal.jsx new file mode 100644 index 0000000..a2c32ff --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/components/ListModal.jsx @@ -0,0 +1,57 @@ +import { IonButton, IonCard, IonCardHeader, IonCardSubtitle, IonCol, IonGrid, IonIcon, IonNote, IonRow, IonSearchbar, IonText } from "@ionic/react"; +import { RatingStar } from "./RatingStar"; + +import styles from "../styles/ViewAll.module.scss"; +import { arrowForward, navigateOutline } from "ionicons/icons"; + +export const ListModal = ({ records, searchTerm, search, hideModal }) => { + + return ( + + + + + + search(e.target.value) } /> + + + + +
+ { records.map((record, index) => { + + const rating = Math.floor(record.rating).toFixed(0); + + return ( + + + + { Array.apply(null, { length: 5 }).map((e, i) => ( + + i } /> + ))} + + { record.name } + { record.displayAddress } + + + +

+ + { record.distance } miles away +

+
+ + + + +
+
+
+ ); + })} +
+
+
+ ); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/components/MapOverlay.jsx b/03_source/mobile/src/pages/DemoRestaurantFinder/components/MapOverlay.jsx new file mode 100644 index 0000000..90590b2 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/components/MapOverlay.jsx @@ -0,0 +1,50 @@ +import { IonBadge, IonButton, IonCardSubtitle, IonCol, IonIcon, IonNote, IonRow } from "@ionic/react"; +import { arrowForward, call, callOutline, navigateOutline, phoneLandscapeOutline, phonePortraitOutline, pricetag, pricetags, pricetagsOutline } from "ionicons/icons"; +import styles from "../styles/MapOverlay.module.scss"; + +export const MapOverlay = ({ record }) => ( + +
+ + { record.name } + { record.displayAddress } + { record.rating } star rating + +

+ +  { record.distance } miles away +

+ + { record.phone && +

+ +  { record.phone } +

+ } + + + + + View → + + + + + + + { record.phone && + + + + + + } + + + + + + + +
+) \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/components/RatingStar.jsx b/03_source/mobile/src/pages/DemoRestaurantFinder/components/RatingStar.jsx new file mode 100644 index 0000000..ec213be --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/components/RatingStar.jsx @@ -0,0 +1,8 @@ +import { IonIcon } from "@ionic/react"; +import { star, starOutline } from "ionicons/icons"; +import styles from "../styles/RatingStar.module.scss"; + +export const RatingStar = ({ rated = false, small = false }) => ( + + +); \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/index.tsx b/03_source/mobile/src/pages/DemoRestaurantFinder/index.tsx new file mode 100644 index 0000000..0be5edd --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/index.tsx @@ -0,0 +1,45 @@ +import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react'; + +import { cloudOutline, listOutline, mapOutline, searchOutline } from 'ionicons/icons'; +import { Route, Redirect } from 'react-router'; + +import Tab1 from './AppPages/Tab1'; +import Tab2 from './AppPages/Tab2'; +import ViewPlace from './AppPages/ViewPlace'; + +import './style.scss'; + +function DemoRestaurantFinder() { + return ( + + + + + + + + + + + + + + + + + {/* */} + + + + + + + + + + + + ); +} + +export default DemoRestaurantFinder; diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/main/utils.js b/03_source/mobile/src/pages/DemoRestaurantFinder/main/utils.js new file mode 100644 index 0000000..6d4f5b8 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/main/utils.js @@ -0,0 +1,40 @@ +import { Geolocation } from '@capacitor/geolocation'; +import { Capacitor } from '@capacitor/core'; +const platform = Capacitor.getPlatform(); + +export const getLocation = async () => { + + const permission = await Geolocation.checkPermissions(); + var coordinates; + + if (permission.location === "granted") { + + var options = { + + enableHighAccuracy: true, + timeout: 10000, + maximumAge: Infinity + }; + + coordinates = await Geolocation.getCurrentPosition(options); + } else { + + if (platform === "web") { + + console.log("Permission Denied."); + } else { + + await Geolocation.requestPermissions(); + coordinates = await Geolocation.getCurrentPosition(options); + } + } + + return { + + currentLocation: { + + latitude: coordinates.coords.latitude, + longitude: coordinates.coords.longitude + } + } +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/main/yelp.js b/03_source/mobile/src/pages/DemoRestaurantFinder/main/yelp.js new file mode 100644 index 0000000..44a8196 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/main/yelp.js @@ -0,0 +1,32 @@ +import { setStore } from "../store/RecordsStore"; + +export const getRecords = async (currentPoint) => { + + + // Replace lat/long with values from get current location. + // Allow choosing of radius? + // Offset could = amount loaded in an infinite scroll? + var latitude = currentPoint.latitude, longitude = currentPoint.longitude, radius = 1000, offset = 0; + const response = await fetch(`http://localhost:4000/get-records?latitude=${ latitude }&longitude=${ longitude }&radius=${ radius }&offset=${ offset }`); + const data = await response.json(); + setStore(data); +} + +export const getRecord = async recordId => { + + const response = await fetch(`http://localhost:4000/get-record?id=${ recordId }`); + const data = await response.json(); + + const response2 = await fetch(`http://localhost:4000/get-reviews?id=${ recordId }`); + const data2 = await response2.json(); + + data.reviews = data2.reviews; + return data; +} + +export const getCategories = async () => { + + const response = await fetch(`http://localhost:4000/get-categories`); + const data = await response.json(); + return data; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/server.js b/03_source/mobile/src/pages/DemoRestaurantFinder/server.js new file mode 100644 index 0000000..2e0d4ff --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/server.js @@ -0,0 +1,152 @@ +const axios = require('axios'); +const express = require('express'); +const app = express(); +var session = require('express-session'); +var cors = require('cors'); + +// Use session +app.use( + session({ + secret: 'Ionic Rocks!', + cookie: { maxAge: 86400000 }, + resave: true, + saveUninitialized: true, + }) +); +app.use(cors({ origin: '*' })); + +app.listen(process.env.PORT || 4000, function () { + console.log('server is running...'); +}); + +// DON'T LEAVE THIS API KEY IN YOUR PRODUCTION APPS +// This is a test account of mine, so i've left this in for demo purposes. +// Secure your nodejs server and API key when building real things! +let API_KEY = + 'd02MG5N6GCJ0Y6GN5OHYCIW7XBHCbuu0O0w6sxtZmHMuhn-tgvOK1NaFIgST-4r8E3CQp6APMNMjKs0sZV3UHtQO-e32ysCBY-3nGqxJGsvjTCZ_eEM5jE14H-XuYHYx'; + +// REST API for Yelp +let yelpAPI = axios.create({ + baseURL: 'https://api.yelp.com/v3/', + headers: { + Authorization: `Bearer ${API_KEY}`, + 'Content-type': 'application/json', + }, +}); + +app.get('/get-record', function (req, res) { + const { id } = req.query; + + yelpAPI(`/businesses/${id}`).then(({ data }) => { + res.send(JSON.stringify(data)); + }); +}); + +app.get('/get-reviews', function (req, res) { + const { id } = req.query; + + yelpAPI(`/businesses/${id}/reviews`).then(({ data }) => { + res.send(JSON.stringify(data)); + }); +}); + +app.get('/get-categories', function (req, res) { + yelpAPI('/categories').then(({ data }) => { + res.send(JSON.stringify(data)); + }); +}); + +app.get('/get-records', function (req, res) { + const { latitude, longitude, radius } = req.query; + const categories = 'restaurant,takeaway'; + + const params = { + latitude, + longitude, + radius, + categories, + }; + + yelpAPI('/businesses/search', { params: params }).then(({ data }) => { + const allRecords = parseDetails(data); + res.send(JSON.stringify({ allRecords, center: data.region.center })); + }); +}); + +const parseDetails = (info) => { + console.log('Parsing details...'); + var records = []; + + var parsedInfo = info; + var businesses = parsedInfo.businesses; + var total = parsedInfo.total; + + var distance = 0; + var distanceMiles = 0; + + for (var i = 0; i < businesses.length; i++) { + var id = businesses[i].id; + var url = businesses[i].url; + var imageURL = businesses[i].image_url; + var name = businesses[i].name; + var alias = businesses[i].alias; + var phone = businesses[i].display_phone; + var price = businesses[i].price; + var rating = businesses[i].rating; + + var isClosed = businesses[i].is_closed; + var isOpen = isClosed == true ? false : true; + + var coordinates = businesses[i].coordinates; + var latitude = coordinates.latitude; + var longitude = coordinates.longitude; + + var displayAddress = ''; + + if (businesses[i].location) { + var addressDetails = businesses[i].location; + + if (addressDetails.display_address) { + var displayAddressArr = addressDetails.display_address; + + if (Array.isArray(displayAddressArr)) { + for (var j = 0; j < displayAddressArr.length; j++) { + var displayAddressPart = displayAddressArr[j]; + displayAddress = displayAddress + displayAddressPart; + + if (j != displayAddressArr.length - 1) { + displayAddress = displayAddress + ', '; + } + } + } else { + displayAddress = displayAddressArr; + } + } + } + + if (businesses[i].distance) { + var distance = businesses[i].distance; + var distanceMiles = (distance * 0.000621371192).toFixed(2); + } + + if (isClosed != true) { + records.push({ + id, + alias, + url, + imageURL, + name, + phone, + price, + rating, + latitude, + longitude, + displayAddress, + isOpen, + distance: distanceMiles, + }); + } + } + + return records; +}; diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/store/RecordsStore.js b/03_source/mobile/src/pages/DemoRestaurantFinder/store/RecordsStore.js new file mode 100644 index 0000000..e6ba71e --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/store/RecordsStore.js @@ -0,0 +1,15 @@ +import { Store } from 'pullstate'; + +const RecordsStore = new Store({ + + records: [], + center : [] +}); + +export default RecordsStore; + +export const setStore = records => { + + RecordsStore.update(state => { state.records = records.allRecords }); + RecordsStore.update(state => { state.center = records.center }); +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/store/Selectors.js b/03_source/mobile/src/pages/DemoRestaurantFinder/store/Selectors.js new file mode 100644 index 0000000..c83e9f6 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/store/Selectors.js @@ -0,0 +1,15 @@ +import { createSelector } from 'reselect'; + +const getState = state => state; + +// General getters +export const fetchRecords = createSelector(getState, state => state.records); + +// More specific getters +export const fetchRecord = recordId => createSelector(getState, state => { + + return state.records.filter(record => record.id === recordId)[0]; +}); +// export const getPoll = pollId => createSelector(getState, state => state.polls.filter(poll => poll.id === parseInt(pollId))[0]); +// export const getChat = contactId => createSelector(getState, state => state.chats.filter(c => parseInt(c.contact_id) === parseInt(contactId))[0].chats); +// export const getContact = contactId => createSelector(getState, state => state.contacts.filter(c => parseInt(c.id) === parseInt(contactId))[0]); diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/store/index.js b/03_source/mobile/src/pages/DemoRestaurantFinder/store/index.js new file mode 100644 index 0000000..aaaef11 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/store/index.js @@ -0,0 +1 @@ +export { default as RecordsStore } from "./RecordsStore"; \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/style.scss b/03_source/mobile/src/pages/DemoRestaurantFinder/style.scss new file mode 100644 index 0000000..37c1e1a --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/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/DemoRestaurantFinder/styles/Map.module.scss b/03_source/mobile/src/pages/DemoRestaurantFinder/styles/Map.module.scss new file mode 100644 index 0000000..0e5583f --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/styles/Map.module.scss @@ -0,0 +1,19 @@ +.overlaySearch { + + position: absolute; + z-index: 2; + background-color: white; + width: 70%; + height: 3rem; + margin: 0 auto; + margin-left: 14%; + + border-radius: 10px; + box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; + + display: flex; + flex-direction: row; + justify-content: center; + align-content: center; + align-items: center; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/styles/MapOverlay.module.scss b/03_source/mobile/src/pages/DemoRestaurantFinder/styles/MapOverlay.module.scss new file mode 100644 index 0000000..5baa178 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/styles/MapOverlay.module.scss @@ -0,0 +1,60 @@ +.overlayContainer { + + display: flex; + flex-direction: column; + // align-items: center; + // align-content: center; + + padding: 1rem; + + width: 12rem; + height: fit-content; + background-color: white; + + border-radius: 5px; + + box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px; + + ion-card-subtitle { + + font-size: 0.7rem; + color: black; + } + + ion-note { + + font-size: 0.6rem; + margin-bottom: 0.5rem; + } + + ion-badge { + + margin-bottom: 1.5rem; + } + + p { + + padding: 0; + margin: 0; + margin-bottom: 0.3rem; + font-size: 0.6rem; + display: flex; + flex-direction: row; + align-items: center; + align-content: center; + } +} + +.overlayContainer:after { + + content:''; + position: absolute; + top: 100%; + left: 70%; + margin-left: -50px; + width: 0; + height: 0; + border-top: solid 10px white; + border-left: solid 10px transparent; + border-right: solid 10px transparent; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/styles/RatingStar.module.scss b/03_source/mobile/src/pages/DemoRestaurantFinder/styles/RatingStar.module.scss new file mode 100644 index 0000000..bd1a303 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/styles/RatingStar.module.scss @@ -0,0 +1,17 @@ +.star { + + background-color: var(--ion-color-primary); + padding: 0.2rem; + margin-right: 0.1rem; + border-radius: 4px; + color: white; +} + +.outlineStar { + + background-color: rgb(216, 216, 216); + color: white; + padding: 0.2rem; + margin-right: 0.1rem; + border-radius: 4px; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/styles/ViewAll.module.scss b/03_source/mobile/src/pages/DemoRestaurantFinder/styles/ViewAll.module.scss new file mode 100644 index 0000000..be2fb02 --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/styles/ViewAll.module.scss @@ -0,0 +1,54 @@ +.viewCard { + + p { + + display: flex; + flex-direction: row; + align-content: center; + align-items: center; + + ion-icon { + + margin-right: 0.5rem; + } + } + + .cardImage { + + height: 10rem; + width: 100%; + background-position: top center; + background-size: cover; + } +} + +.viewCardModal { + + p { + + display: flex; + flex-direction: row; + align-content: center; + align-items: center; + + ion-icon { + + margin-right: 0.5rem; + } + } + + .cardImage { + + height: 10rem; + width: 100%; + background-position: top center; + background-size: cover; + } +} + +.viewCardContainerModal { + + height: 90vh; + width: 100%; + overflow: scroll; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoRestaurantFinder/styles/ViewPlace.module.scss b/03_source/mobile/src/pages/DemoRestaurantFinder/styles/ViewPlace.module.scss new file mode 100644 index 0000000..144b72c --- /dev/null +++ b/03_source/mobile/src/pages/DemoRestaurantFinder/styles/ViewPlace.module.scss @@ -0,0 +1,81 @@ +.page { + + ion-content { + + ion-toolbar, + ion-header { + + --border-style: none; + --border-color: none; + } + } + + .cardImage { + + height: 10rem; + width: 100%; + background-position: top center; + background-size: cover; + border-radius: 5px; + + ion-row { + + --color: white; + background-color: rgba(0, 0, 0, 0.4); + height: 100%; + display: flex; + flex-direction: row; + align-content: center; + align-items: center; + justify-content: center; + + ion-card-subtitle { + + --color: white; + background-color: rgba(0, 0, 0, 0.6); + padding: 1rem; + text-align: center; + border-radius: 5px; + font-size: 1.2rem; + + div { + + margin-bottom: -0.6rem; + } + + ion-note { + + font-size: 0.6rem; + padding: 0; + margin: 0; + --color: white; + text-transform: lowercase; + font-weight: 600; + } + } + } + } + + a { + + text-decoration: none; + font-size: 0.9rem; + } + + .categoryContainer { + + padding-top: 1rem; + padding-bottom: 1rem; + border-bottom: 2px solid rgb(241, 241, 241); + } + + .placePhoto { + + height: 4rem; + } + + .reviewUser { + + + } +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoShopAppUi/index.tsx b/03_source/mobile/src/pages/DemoShopAppUi/index.tsx index 7de2cf8..619dcbc 100644 --- a/03_source/mobile/src/pages/DemoShopAppUi/index.tsx +++ b/03_source/mobile/src/pages/DemoShopAppUi/index.tsx @@ -39,7 +39,7 @@ import './style.scss'; import { useEffect } from 'react'; // -function DemoWeatherApp() { +function DemoShopAppUi() { useEffect(() => { fetchCategories(); fetchProducts(); @@ -82,4 +82,4 @@ function DemoWeatherApp() { ); } -export default DemoWeatherApp; +export default DemoShopAppUi; diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Home.jsx b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Home.jsx new file mode 100644 index 0000000..5f2bc27 --- /dev/null +++ b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Home.jsx @@ -0,0 +1,70 @@ +import { + IonButton, + IonButtons, + IonContent, + IonHeader, + IonIcon, + IonItem, + IonLabel, + IonList, + IonPage, + IonTitle, + IonToolbar, + useIonRouter, +} from '@ionic/react'; +import { people } from '../data'; +import styles from './Home.module.scss'; +import { chevronBackOutline } from 'ionicons/icons'; + +const Home = () => { + const router = useIonRouter(); + + function handleBackClick() { + router.goBack(); + } + return ( + + + + Profiles + + + handleBackClick()}> + + + + + + + + + + Profiles + + + + + {people.map((person, index) => { + return ( + + list avatar + +

{person.name}

+

{person.location}

+
+
+ ); + })} +
+
+
+ ); +}; + +export default Home; diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Home.module.scss b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Home.module.scss new file mode 100644 index 0000000..9db1def --- /dev/null +++ b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Home.module.scss @@ -0,0 +1,17 @@ +.listAvatar { + + height: 3rem; + width: 3rem; + border-radius: 500px; + margin-right: 1.5rem; +} + +.listItem { + + padding: 0.3rem; + + ion-label { + + padding-bottom: 1rem; + } +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Profile.jsx b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Profile.jsx new file mode 100644 index 0000000..5311050 --- /dev/null +++ b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Profile.jsx @@ -0,0 +1,155 @@ +import { + IonCard, + IonCardContent, + IonCardSubtitle, + IonCardTitle, + IonCol, + IonContent, + IonGrid, + IonHeader, + IonIcon, + IonNote, + IonPage, + IonRow, + useIonRouter, + useIonViewWillEnter, +} from '@ionic/react'; +import { chevronBack } from 'ionicons/icons'; +import { useEffect } from 'react'; +import { useRef, useState } from 'react'; +import { useParams } from 'react-router'; + +import { Swiper, SwiperSlide } from 'swiper/react'; +// import 'swiper/swiper.scss'; +import 'swiper/css'; + +import { collections, people, tags } from '../data'; + +import styles from './Profile.module.scss'; + +const Profile = () => { + const { id } = useParams(); + const router = useIonRouter(); + const headingRef = useRef(); + const [slideSpace, setSlideSpace] = useState(0); + const [profile, setProfile] = useState([]); + + useIonViewWillEnter(() => { + setSlideSpace(5); + }); + + useEffect(() => { + headingRef.current.classList.add('animate__slideInLeft'); + headingRef.current.style.display = ''; + }, []); + + useEffect(() => { + const getProfile = async () => { + const person = await people.filter((person) => parseInt(person.id) === parseInt(id))[0]; + setProfile(person); + }; + + getProfile(); + }, [id]); + + return ( + + +
+ header + +
+
router.goBack()}> + +
+
+ +
+ + + + avatar + + + {profile.name} +

{profile.location}

+ + + + Purchased + {profile.purchased} + + + + Wished + {profile.wished} + + + + Likes + {profile.likes} + + +
+
+
+
+
+
+
+
+ + + + + Collection + + + + +
+ + {collections.map((collection, index) => { + return ( + + + collection + +
+ {collection.name} + {collection.no} wardrobes +
+
+
+ ); + })} +
+
+ + + + + Tags + + + + + {tags.map((tag, index) => { + return ( + + {tag} + + ); + })} + + +
+
+ ); +}; + +export default Profile; diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Profile.module.scss b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Profile.module.scss new file mode 100644 index 0000000..27ddf76 --- /dev/null +++ b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Profile.module.scss @@ -0,0 +1,152 @@ +.customHeader { + + background-color: white; +} + +.avatar { + + border-radius: 10px !important; + height: 7.5rem; + width: 7.5rem; + z-index: 10; + box-shadow: rgba(50, 50, 93, 0.25) 0p\x 13px 27px -5px, rgba(0, 0, 0, 0.3) 0px 8px 16px -8px; +} + +.customBackButton { + + position: absolute; + top: 2rem; + left: 1rem; + font-size: 1.75rem; + display: flex; + flex-direction: row; + align-content: center; + align-items: center; + justify-content: center; + + svg { + + color: rgb(255, 255, 255); + height: 1.75rem; + width: 1.75rem; + } +} + +.mainContent { + + margin-top: -12.2rem !important; + z-index: 99; +} + +.profileHeaderContainer { + + display: flex; + flex-direction: column; + align-items: center; + align-content: center; + justify-content: center; + width: 100%; +} + +.profileHeader { + + $grad_color: #f8f8f8; + $main_color: white; + margin-top: -4rem; + z-index: -1; + width: 100%; + opacity: 90%; + + // background: + // linear-gradient(135deg, $grad_color 25%, transparent 25%) -50px 0, + // linear-gradient(225deg, $grad_color 25%, transparent 25%) -50px 0, + // linear-gradient(315deg, $grad_color 25%, transparent 25%), + // linear-gradient(45deg, $grad_color 25%, transparent 25%); + // background-size: 100px 100px; + // background-color: $main_color; +} + +.profileDetails { + + margin-top: 3rem; + text-align: center; + + ion-card-title { + + font-size: 1.2rem; + margin-top: 1rem; + color: rgb(85, 85, 85); + } + + p { + + font-size: 0.75rem; + color: #94c8fe; + margin-top: 0.2rem; + } +} + +.profileStats { + + margin-top: 2rem; +} + +.title { + + margin-top: -0.8rem; + font-size: 1.2rem; + color: rgb(85, 85, 85); +} + +.collections { + + margin-top: -1.2rem; +} + +.collectionCard { + + width: 8rem; + height: 10rem; + + img { + + height: 100%; + width: 100%; + } + + .collectionDetails { + + position: relative; + bottom: 4rem; + margin-left: 1rem; + background-color:rgba(36, 36, 36, 0.6); + padding: 0.5rem; + border-top-left-radius: 10px; + border-bottom-left-radius: 10px; + } + + ion-card-title { + + color: white; + font-size: 0.9rem; + } + + ion-note { + + color: white; + font-size: 0.6rem; + } +} + +.tag { + + border-radius: 30px; + font-size: 0.8rem; + padding: 0.5rem; + font-weight: 600; + color: white; + text-align: center; + background: rgb(111,130,224); + background: linear-gradient(0deg, rgba(111,130,224,1) 26%, rgba(70,161,231,1) 82%); + border: 5px solid white; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab1.css b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab1.css new file mode 100644 index 0000000..37c5c75 --- /dev/null +++ b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab1.css @@ -0,0 +1,8 @@ + +.list-avatar { + + height: 3rem; + width: 3rem; + border-radius: 500px; + margin-right: 1.5rem; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab2.css b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab2.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab2.jsx b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab2.jsx new file mode 100644 index 0000000..f09edf4 --- /dev/null +++ b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab2.jsx @@ -0,0 +1,25 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab2.css'; + +const Tab2 = () => { + return ( + + + + Tab 2 + + + + + + Tab 2 + + + + + + ); +}; + +export default Tab2; diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab3.css b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab3.css new file mode 100644 index 0000000..e69de29 diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab3.jsx b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab3.jsx new file mode 100644 index 0000000..903b1a0 --- /dev/null +++ b/03_source/mobile/src/pages/DemoSlidingProfile/AppPages/Tab3.jsx @@ -0,0 +1,25 @@ +import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react'; +import ExploreContainer from '../components/ExploreContainer'; +import './Tab3.css'; + +const Tab3 = () => { + return ( + + + + Tab 3 + + + + + + Tab 3 + + + + + + ); +}; + +export default Tab3; diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/components/ExploreContainer.css b/03_source/mobile/src/pages/DemoSlidingProfile/components/ExploreContainer.css new file mode 100644 index 0000000..e99f514 --- /dev/null +++ b/03_source/mobile/src/pages/DemoSlidingProfile/components/ExploreContainer.css @@ -0,0 +1,24 @@ +.container { + text-align: center; + position: absolute; + left: 0; + right: 0; + top: 50%; + transform: translateY(-50%); +} + +.container strong { + font-size: 20px; + line-height: 26px; +} + +.container p { + font-size: 16px; + line-height: 22px; + color: #8c8c8c; + margin: 0; +} + +.container a { + text-decoration: none; +} \ No newline at end of file diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/components/ExploreContainer.jsx b/03_source/mobile/src/pages/DemoSlidingProfile/components/ExploreContainer.jsx new file mode 100644 index 0000000..093861b --- /dev/null +++ b/03_source/mobile/src/pages/DemoSlidingProfile/components/ExploreContainer.jsx @@ -0,0 +1,12 @@ +import './ExploreContainer.css'; + +const ExploreContainer = ({ name }) => { + return ( +
+ {name} +

Explore UI Components

+
+ ); +}; + +export default ExploreContainer; diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/data/index.js b/03_source/mobile/src/pages/DemoSlidingProfile/data/index.js new file mode 100644 index 0000000..5f50286 --- /dev/null +++ b/03_source/mobile/src/pages/DemoSlidingProfile/data/index.js @@ -0,0 +1,87 @@ +export const people = [ + { + id: 1, + name: 'Jane Doe', + image: '/assets/DemoSlidingProfile/avatar1.png', + cover: '/assets/DemoSlidingProfile/flower.jpeg', + location: 'Belfast, Northern Ireland', + purchased: 120, + wished: 271, + likes: '12K', + }, + { + id: 2, + name: 'Joe Bloggs', + image: '/assets/DemoSlidingProfile/avatar2.png', + cover: '/assets/DemoSlidingProfile/cover4.jpeg', + location: 'New York City, America', + purchased: 91, + wished: 811, + likes: '102K', + }, + { + id: 3, + name: 'Antonio Romero', + image: '/assets/DemoSlidingProfile/avatar4.png', + cover: '/assets/DemoSlidingProfile/cover1.jpeg', + location: 'Cambodia, Asia', + purchased: 91, + wished: 811, + likes: '102K', + }, + { + id: 4, + name: 'Aditi Conner', + image: '/assets/DemoSlidingProfile/avatar3.png', + cover: '/assets/DemoSlidingProfile/cover5.jpeg', + location: 'Antartica', + purchased: 91, + wished: 811, + likes: '102K', + }, + { + id: 5, + name: 'Martina Salas', + image: '/assets/DemoSlidingProfile/avatar5.png', + cover: '/assets/DemoSlidingProfile/cover2.jpeg', + location: 'South Africa', + purchased: 91, + wished: 811, + likes: '102K', + }, + { + id: 6, + name: 'Dayna Grimes', + image: '/assets/DemoSlidingProfile/avatar6.png', + cover: '/assets/DemoSlidingProfile/cover6.jpeg', + location: 'China', + purchased: 91, + wished: 811, + likes: '102K', + }, +]; + +export const collections = [ + { + name: 'Winter', + image: '/assets/DemoSlidingProfile/winter.png', + no: 95, + }, + { + name: 'Summer', + image: '/assets/DemoSlidingProfile/summer.png', + no: 73, + }, + { + name: 'Autumn', + image: '/assets/DemoSlidingProfile/autumn.png', + no: 32, + }, + { + name: 'Spring', + image: '/assets/DemoSlidingProfile/spring.png', + no: 18, + }, +]; + +export const tags = ['Jackets', 'Shirts', 'Jeans', 'Sweaters', 'Coats']; diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/index.tsx b/03_source/mobile/src/pages/DemoSlidingProfile/index.tsx new file mode 100644 index 0000000..228f5b8 --- /dev/null +++ b/03_source/mobile/src/pages/DemoSlidingProfile/index.tsx @@ -0,0 +1,62 @@ +import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react'; + +import { cloudOutline, home, menu, notifications, search, searchOutline } from 'ionicons/icons'; +import { Route, Redirect } from 'react-router'; + +import Tab2 from './AppPages/Tab2'; +import Home from './AppPages/Home'; +import Profile from './AppPages/Profile'; + +function DemoSlidingProfile() { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + avatar + + + + + + + + + + + + + ); +} + +export default DemoSlidingProfile; diff --git a/03_source/mobile/src/pages/DemoSlidingProfile/style.scss b/03_source/mobile/src/pages/DemoSlidingProfile/style.scss new file mode 100644 index 0000000..4a24588 --- /dev/null +++ b/03_source/mobile/src/pages/DemoSlidingProfile/style.scss @@ -0,0 +1,108 @@ +/* +Ionic Variables and Theming. For more info, please see: +http://ionicframework.com/docs/theming/ +*/ + +/** Ionic CSS Variables **/ +.demo-sliding-profile { + * { + /** 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; + } + + .nav-avatar { + border-radius: 500px; + height: 2.5rem; + width: 2.5rem; + box-shadow: rgba(99, 99, 99, 0.2) 0px 2px 8px 0px; + } + + ion-tab-button { + height: 1.4rem; + } + + .avatar-tab-button { + height: 2.5rem; + width: 2.5rem; + } + + ion-header, + ion-toolbar { + border: none; + --border-style: transparent; + --border-width: 0px; + } + + * { + font-family: 'Nunito', sans-serif !important; + } +} diff --git a/03_source/mobile/src/pages/DemoWeatherApp/AppPages/Tab1.jsx b/03_source/mobile/src/pages/DemoWeatherApp/AppPages/Tab1.jsx index a6f4101..54ab2b9 100644 --- a/03_source/mobile/src/pages/DemoWeatherApp/AppPages/Tab1.jsx +++ b/03_source/mobile/src/pages/DemoWeatherApp/AppPages/Tab1.jsx @@ -44,6 +44,7 @@ function Tab1() { setCurrentWeather(data); }; + // const router = useIonRouter(); function handleBackClick() { router.goBack(); } diff --git a/03_source/mobile/src/pages/DemoWeatherApp/index.tsx b/03_source/mobile/src/pages/DemoWeatherApp/index.tsx index 0cc5228..a114547 100644 --- a/03_source/mobile/src/pages/DemoWeatherApp/index.tsx +++ b/03_source/mobile/src/pages/DemoWeatherApp/index.tsx @@ -6,6 +6,8 @@ import { Route, Redirect } from 'react-router'; import Tab1 from './AppPages/Tab1'; import Tab2 from './AppPages/Tab2'; +import './style.scss'; + function DemoWeatherApp() { return ( @@ -16,10 +18,10 @@ function DemoWeatherApp() { - - - + + + {/* */} diff --git a/03_source/mobile/src/paths.ts b/03_source/mobile/src/paths.ts index 5cec391..b1beba5 100644 --- a/03_source/mobile/src/paths.ts +++ b/03_source/mobile/src/paths.ts @@ -22,15 +22,34 @@ const paths = { // SIGN_IN: '/mylogin', // - DEMO_PAGE: '/tabs/demo-list', + DEMO_WEATHER_APP: '/demo-weather-app', - DEMO_REACT_SHOP: '/demo-react-shop', + // + // + // + // + // + DEMO_REACT_WHATSAPP_CLONE: '/demo-react-whatsapp-clone', + + DEMO_REACT_POLL_APP: '/demo-react-poll-app', + + DEMO_BLOG_POST_UI: '/demo-blog-post-ui', DEMO_CLUB_HOUSE: '/demo-club-house', - DEMO_SCORE_BOARD: '/demo-score-board', - DEMO_QUOTE_APP: '/demo-quote-app', - DEMO_QR_SCANNER: '/demo-qr-scanner', - DEMO_SHOP_APP_UI: '/demo-shop-app-ui', DEMO_DICTIONARY_APP: '/demo-dictionary-app', + DEMO_PAGE: '/tabs/demo-list', + DEMO_PINTEREST_FLOATING_TAB_BAR: '/demo-pinterest-floating-tab-bar', + DEMO_QR_SCANNER: '/demo-qr-scanner', + DEMO_QUIZ_APP: '/demo-quiz-app', + DEMO_QUOTE_APP: '/demo-quote-app', + DEMO_REACT_OVERLAY_HOOKS: '/demo-react-overlay-hooks', + DEMO_REACT_POLL_APP: '/demo-react-poll-app', + DEMO_REACT_SHOP: '/demo-react-shop', + DEMO_REACT_SWITCH_TABS: '/demo-react-switch-tabs', + DEMO_REACT_TRAVEL_APP: '/demo-react-travel-app', DEMO_RECIPE_APP: '/demo-recipe-app', + DEMO_RESTAURANT_FINDER: '/demo-restaurant-finder', + DEMO_SCORE_BOARD: '/demo-score-board', + DEMO_SHOP_APP_UI: '/demo-shop-app-ui', + DEMO_SLIDING_PROFILE: '/demo-sliding-profile', }; export default paths; diff --git a/03_source/mobile/yarn.lock b/03_source/mobile/yarn.lock index 91d02e4..35b66f8 100644 --- a/03_source/mobile/yarn.lock +++ b/03_source/mobile/yarn.lock @@ -239,6 +239,11 @@ resolved "https://registry.npmjs.org/@capacitor/preferences/-/preferences-7.0.1.tgz" integrity sha512-XF9jOHzvoIBZLwZr/EX6aVaUO1d8Mx7TwBLQS33pYHOliCW5knT5KUkFOXNNYxh9qqODYesee9xuQIKNJpQBag== +"@capacitor/share@^7.0.1": + version "7.0.1" + resolved "https://registry.yarnpkg.com/@capacitor/share/-/share-7.0.1.tgz#1d1e6d312504703c1f34fa0de94bed14af4bcbf8" + integrity sha512-7GAtWrb2inEWohC8E7mx38qAX6D9yqPDDnUtJaZ8JRpo15jjFRS40Cx388M8h4NlBWjV5NU3qf1sHXnyOBSJ5g== + "@capacitor/synapse@^1.0.1": version "1.0.2" resolved "https://registry.npmjs.org/@capacitor/synapse/-/synapse-1.0.2.tgz" @@ -376,6 +381,11 @@ dependencies: "@standard-schema/utils" "^0.3.0" +"@icons/material@^0.2.4": + version "0.2.4" + resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" + integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw== + "@ionic/cli-framework-output@^2.2.8": version "2.2.8" resolved "https://registry.npmjs.org/@ionic/cli-framework-output/-/cli-framework-output-2.2.8.tgz" @@ -2079,6 +2089,16 @@ listr2@6.6.1: rfdc "^1.3.0" wrap-ansi "^8.1.0" +lodash-es@^4.17.15: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" + integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== + +lodash@^4.0.1, lodash@^4.17.15: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + log-update@^5.0.1: version "5.0.1" resolved "https://registry.npmjs.org/log-update/-/log-update-5.0.1.tgz" @@ -2114,6 +2134,11 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +material-colors@^1.2.1: + version "1.2.6" + resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" + integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg== + math-intrinsics@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz" @@ -2710,7 +2735,7 @@ prompts@^2.4.2: kleur "^3.0.3" sisteransi "^1.0.5" -prop-types@^15.6.2, prop-types@^15.8.1: +prop-types@^15.5.10, prop-types@^15.6.2, prop-types@^15.8.1: version "15.8.1" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== @@ -2742,6 +2767,19 @@ qr.js@0.0.0: resolved "https://registry.yarnpkg.com/qr.js/-/qr.js-0.0.0.tgz#cace86386f59a0db8050fa90d9b6b0e88a1e364f" integrity sha512-c4iYnWb+k2E+vYpRimHqSu575b1/wKl4XFeJGpFmrJQz5I88v9aY2czh7s0w36srfCM1sXgC/xpoJz5dJfq+OQ== +react-color@^2.19.3: + version "2.19.3" + resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d" + integrity sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA== + dependencies: + "@icons/material" "^0.2.4" + lodash "^4.17.15" + lodash-es "^4.17.15" + material-colors "^1.2.1" + prop-types "^15.5.10" + reactcss "^1.2.0" + tinycolor2 "^1.4.1" + react-confetti@^6.4.0: version "6.4.0" resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.4.0.tgz#e9416b5b3c8baf6f0bb1c5a8e1e3c89babd2c837" @@ -2883,6 +2921,13 @@ react@19.0.0: resolved "https://registry.npmjs.org/react/-/react-19.0.0.tgz" integrity sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ== +reactcss@^1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd" + integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A== + dependencies: + lodash "^4.0.1" + readable-stream@3: version "3.6.2" resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz" @@ -3286,6 +3331,11 @@ tiny-warning@^1.0.0: resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tinycolor2@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" + integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== + tinyglobby@^0.2.13: version "0.2.13" resolved "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz"