├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── License.txt ├── babel.config.js ├── buymecoffee.png ├── capacitor.config.json ├── cypress.json ├── design.png ├── ionic.config.json ├── ionic.starter.json ├── jest.config.js ├── package-lock.json ├── package.json ├── public ├── _redirects ├── assets │ ├── icon │ │ ├── favicon.png │ │ └── favicon.svg │ ├── img │ │ ├── logo.png │ │ ├── logo1.png │ │ ├── logo2.png │ │ ├── no-events.png │ │ ├── screenshot1.png │ │ └── screenshot2.png │ └── shapes.svg ├── google60dcc85674ba26b1.html ├── index.html ├── manifest.json └── robots.txt ├── readme.md ├── src ├── App.vue ├── components │ ├── ConfList.vue │ ├── EditEventModal.vue │ ├── Fab.vue │ ├── Header.vue │ ├── LoginModal.vue │ ├── NewEventModal.vue │ ├── NoEvents.vue │ ├── SearchFilters.vue │ ├── SkeletonText.vue │ ├── SpeakerList.vue │ ├── Stats.vue │ ├── Subscription.vue │ ├── Tabs.vue │ ├── UserSignUpModal.vue │ └── VenueList.vue ├── firebase.js ├── main.js ├── router │ └── index.js ├── shims-vue.d.ts ├── store │ ├── index.js │ └── modules │ │ ├── auth.js │ │ ├── events.js │ │ ├── speakers.js │ │ ├── userProfile.js │ │ └── venues.js ├── theme │ ├── media-queries.scss │ └── variables.scss └── views │ ├── CreateEvent.vue │ ├── CreateVenue.vue │ ├── Dashboard.vue │ ├── Home.vue │ ├── Login.vue │ ├── Profile.vue │ ├── Register.vue │ ├── Speakers.vue │ └── Venues.vue ├── tests ├── e2e │ ├── .eslintrc.js │ ├── plugins │ │ └── index.js │ ├── specs │ │ └── test.js │ └── support │ │ ├── commands.js │ │ └── index.js └── unit │ └── example.spec.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlodeprojuicer/ionic-vue-mobile-template-05/5f50083e3cc7a4e529e8d1a6676a8ce6ea13715a/.eslintignore -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 2020 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'vue/no-deprecated-slot-attribute': 'off', 17 | 'vue/custom-event-name-casing': 'off' 18 | }, 19 | overrides: [ 20 | { 21 | files: [ 22 | '**/__tests__/*.{j,t}s?(x)', 23 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 24 | ], 25 | env: { 26 | jest: true 27 | } 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.DS_Store 3 | node_modules 4 | android 5 | ios 6 | dist 7 | .gradle 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | # Editor directories and files 19 | .idea 20 | .vscode 21 | *.suo 22 | *.ntvs* 23 | *.njsproj 24 | *.sln 25 | *.sw? 26 | 27 | api_key 28 | config.json 29 | build.json 30 | 31 | 32 | # Cordova 33 | /src-cordova/platforms 34 | /src-cordova/plugins 35 | /public/cordova.js 36 | -------------------------------------------------------------------------------- /License.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SIMO MAFUXWANA 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /buymecoffee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlodeprojuicer/ionic-vue-mobile-template-05/5f50083e3cc7a4e529e8d1a6676a8ce6ea13715a/buymecoffee.png -------------------------------------------------------------------------------- /capacitor.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "io.ionic.starter", 3 | "appName": "my-app", 4 | "bundledWebRuntime": false, 5 | "npmClient": "npm", 6 | "webDir": "dist", 7 | "plugins": { 8 | "SplashScreen": { 9 | "launchShowDuration": 0 10 | } 11 | }, 12 | "cordova": {} 13 | } 14 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlodeprojuicer/ionic-vue-mobile-template-05/5f50083e3cc7a4e529e8d1a6676a8ce6ea13715a/design.png -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "integrations": { 4 | "capacitor": {} 5 | }, 6 | "type": "vue" 7 | } 8 | -------------------------------------------------------------------------------- /ionic.starter.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tabs Starter", 3 | "baseref": "vue-starters", 4 | "tarignore": [ 5 | "node_modules", 6 | "package-lock.json", 7 | "www" 8 | ], 9 | "scripts": { 10 | "test": "npm run build" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript-and-babel', 3 | transform: { 4 | '^.+\\.vue$': 'vue-jest' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-vue-mobile-template-01", 3 | "description": "Hybrid app template built with vue, ionic and capacitor", 4 | "author": "Simo Mafuxwana - @dlodeprojuicer", 5 | "version": "0.2.0", 6 | "private": true, 7 | "scripts": { 8 | "serve": "vue-cli-service serve", 9 | "build": "vue-cli-service build", 10 | "test:unit": "vue-cli-service test:unit", 11 | "test:e2e": "vue-cli-service test:e2e", 12 | "lint": "vue-cli-service lint" 13 | }, 14 | "dependencies": { 15 | "@capacitor/core": "^2.4.1", 16 | "@capacitor/ios": "^2.4.1", 17 | "@ionic/vue": "^5.4.1", 18 | "@ionic/vue-router": "^5.4.1", 19 | "@johmun/vue-tags-input": "^2.1.0", 20 | "@mailchimp/mailchimp_marketing": "^3.0.22", 21 | "chart.js": "^2.9.3", 22 | "core-js": "^3.6.5", 23 | "firebase": "^7.22.0", 24 | "ionic-vue-form": "^1.2.2", 25 | "moment": "^2.29.0", 26 | "node-sass": "^4.14.1", 27 | "sass-loader": "^10.0.2", 28 | "vee-validate": "^4.0.0-beta.1", 29 | "vue": "3.0.0", 30 | "vue-chartjs": "^3.5.1", 31 | "vue-router": "^4.0.0-beta.9", 32 | "vuex": "^4.0.0-beta.4" 33 | }, 34 | "devDependencies": { 35 | "@capacitor/cli": "^2.4.1", 36 | "@vue/cli-plugin-babel": "^4.5.4", 37 | "@vue/cli-plugin-eslint": "^4.5.4", 38 | "@vue/cli-plugin-router": "^4.5.4", 39 | "@vue/cli-plugin-unit-jest": "^4.5.4", 40 | "@vue/cli-service": "^4.5.4", 41 | "@vue/compiler-sfc": "^3.0.0-rc.10", 42 | "@vue/eslint-config-typescript": "^5.1.0", 43 | "@vue/test-utils": "^2.0.0-beta.3", 44 | "eslint": "^6.8.0", 45 | "eslint-plugin-vue": "^7.0.0-beta.3", 46 | "vue-jest": "^5.0.0-alpha.3" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 2 | https://techconf-db.netlify.app https://techconf-db.com -------------------------------------------------------------------------------- /public/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlodeprojuicer/ionic-vue-mobile-template-05/5f50083e3cc7a4e529e8d1a6676a8ce6ea13715a/public/assets/icon/favicon.png -------------------------------------------------------------------------------- /public/assets/icon/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlodeprojuicer/ionic-vue-mobile-template-05/5f50083e3cc7a4e529e8d1a6676a8ce6ea13715a/public/assets/img/logo.png -------------------------------------------------------------------------------- /public/assets/img/logo1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlodeprojuicer/ionic-vue-mobile-template-05/5f50083e3cc7a4e529e8d1a6676a8ce6ea13715a/public/assets/img/logo1.png -------------------------------------------------------------------------------- /public/assets/img/logo2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlodeprojuicer/ionic-vue-mobile-template-05/5f50083e3cc7a4e529e8d1a6676a8ce6ea13715a/public/assets/img/logo2.png -------------------------------------------------------------------------------- /public/assets/img/no-events.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlodeprojuicer/ionic-vue-mobile-template-05/5f50083e3cc7a4e529e8d1a6676a8ce6ea13715a/public/assets/img/no-events.png -------------------------------------------------------------------------------- /public/assets/img/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlodeprojuicer/ionic-vue-mobile-template-05/5f50083e3cc7a4e529e8d1a6676a8ce6ea13715a/public/assets/img/screenshot1.png -------------------------------------------------------------------------------- /public/assets/img/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dlodeprojuicer/ionic-vue-mobile-template-05/5f50083e3cc7a4e529e8d1a6676a8ce6ea13715a/public/assets/img/screenshot2.png -------------------------------------------------------------------------------- /public/assets/shapes.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/google60dcc85674ba26b1.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google60dcc85674ba26b1.html -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | TechConf-db | A concise list of tech conferences in South Africa 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "TechConf-db", 3 | "name": "TechConf-db", 4 | "icons": [ 5 | { 6 | "src": "assets/icon/favicon.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "assets/icon/icon.png", 12 | "type": "image/png", 13 | "sizes": "512x512", 14 | "purpose": "maskable" 15 | } 16 | ], 17 | "start_url": ".", 18 | "display": "standalone", 19 | "theme_color": "#ffffff", 20 | "background_color": "#ffffff" 21 | } 22 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ## Ionic Vue Mobile Template 05 2 | [![Netlify Status](https://api.netlify.com/api/v1/badges/2fbd5ca0-34f6-4f79-903c-74e40d8d8892/deploy-status)](https://app.netlify.com/sites/ionic-vue-mobile-template-05/deploys) 3 | 4 | Hybrid mobile template built with Ionic Vue using capacitor for native builds. 5 | 6 | Template is based on [TechConf-db.com](https://techconf-db.com), a side project of mine. Unlike other templetes, this one is not meant to be a mobile app, atleast not yet, but it is responsive and can be used as a mobile app. 7 | 8 | [Demo](https://ionic-vue-mobile-template-05.netlify.app) 9 | 10 | ## Features 11 | - Vuex 12 | - Firebase 13 | - Mailchimp 14 | - Search Filter 15 | - Responsive 16 | 17 | ## Configs 18 | For this template, I created static data. Techconf-db is using a Firebase backend. Replace Firebase config in `src/firebase.js` or remove Firebase completely to use your prefer your a different backend. 19 | 20 | Also, make sure you replace Mailchimp config URL on line 123 of `src/components/Subscription.vue` 21 | 22 | ## Project setup 23 | ``` 24 | npm install 25 | ``` 26 | 27 | ### Run on the browser - development 28 | ``` 29 | npm run serve 30 | ``` 31 | 32 | ## Design 33 | ![Techconf-db screenshot](/design.png "Techconf-db Ionic Vue Template") 34 | 35 | ## Native 36 | 37 | Using [Capacitor](https://capacitorjs.com/docs/getting-started) for native builds 38 | 39 | ## Prepare native builds 40 | 41 | ### iOS testing and distribution 42 | 1. Download the latest Xcode 43 | 2. `npm run build` 44 | 3. `npx cap add ios` 45 | 3. `npx cap copy` 46 | 4. `npx cap open ios` Xcode takes a few seconds to index the files; keep an eye at the top of Xcode's window for progress. 47 | 48 | [Not compulsory] For sanity check click on the play button in the top left. This will prepare and run the app in a simulator, if all goes well you should be able to run the app and click around. If not, create an issue 🤷 and I will have a look. 49 | 50 | ### Android testing and distribution 51 | 1. Download the latest Android Studio 52 | 2. `npm run build` 53 | 3. `npx cap add android` 54 | 3. `npx cap copy` 55 | 4. `npx cap open android` Android Studio takes a few seconds to index the files, keep an eye at the bottom of Android Studio for progress. 56 | 5. Testing - When indexing is complete, look for a green play button. Click the play button and it will launch the app in an emulator ([See here to setup Emulator](https://developer.android.com/studio/run/managing-avds)) or on the phone, if a phone is connected via USB. 57 | 58 | ## Official Docs 59 | - [Getting started](https://ionicframework.com/vue) 60 | 61 | ## Resources 62 | - [Newsletter](https://mailchi.mp/b9133e120ccf/sqan8ggx22) - Signup to my Ionic Vue newsletter to get templates and other Ionic Vue updates in your inbox! 63 | - [YouTube Channel](https://www.youtube.com/channel/UC5jZ6srZuLwt3O3ZtuM1Dsg) - Subscribe to my YouTube channel. 64 | - [Ionic Vue Tempalates](https://tinyurl.com/y2gl39dk) - Free Ionic Vue Templates. 65 | - [Ionic Vue VSCode Snippets](https://marketplace.visualstudio.com/items?itemName=dlodeprojuicer.ionicvuesnippets) - This extension adds ionic-vue snippets. Quickly add ionic-vue component code by simply typing iv. The iv prefix will show a range of snippets to choose from. 66 | 67 | ## Affiliates 68 | I want to keep doing these templates for free for as long as possible. I have joined a few affiliate programs to help take care of the costs. 69 | - [Pixeltrue](https://www.pixeltrue.com/?via=simo) - High-quality illustrations that will help you build breath-taking websites. 70 | - [Getrewardful](https://www.getrewardful.com/?via=simo) - Create your own affiliate program. 71 | 72 | Alternatively, you can buy me a coffee Buy Me A Coffee 73 | 74 | ## Credits 75 | - [manuelroviradesign](https://www.instagram.com/manuelroviradesign/) via [We Love Web Design](https://www.instagram.com/p/CC1GFMrBB6T/) - App design inspiration 76 | - [Tami Maiwashe](https://www.linkedin.com/in/tami-maiwashe-32824a19a/) - Documentation 77 | - [おかきょー](https://twitter.com/31415O_Kyo) - [Japanese doc translation](https://github.com/dlodeprojuicer/ionic-vue-mobile-template-01/blob/master/readme-ja.md) 78 | 79 | ## Contact 80 | - [@dlodeprojuicer](https://twitter.com/dlodeprojuicer) on Twitter 81 | 82 | ## Contact 83 | - [@dlodeprojuicer](https://twitter.com/dlodeprojuicer) on Twitter 84 | - [@IonicSA](https://twitter.com/ionicsa) - S.A ionic user group page 85 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/ConfList.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 196 | 197 | 216 | -------------------------------------------------------------------------------- /src/components/EditEventModal.vue: -------------------------------------------------------------------------------- 1 | 52 | 53 | 116 | 117 | -------------------------------------------------------------------------------- /src/components/Fab.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 122 | 123 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 141 | 142 | 179 | -------------------------------------------------------------------------------- /src/components/LoginModal.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 114 | 115 | -------------------------------------------------------------------------------- /src/components/NewEventModal.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 192 | 193 | -------------------------------------------------------------------------------- /src/components/NoEvents.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/SearchFilters.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 116 | -------------------------------------------------------------------------------- /src/components/SkeletonText.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 82 | 83 | 88 | -------------------------------------------------------------------------------- /src/components/SpeakerList.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 137 | 138 | 186 | -------------------------------------------------------------------------------- /src/components/Stats.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/Subscription.vue: -------------------------------------------------------------------------------- 1 | 102 | 103 | 153 | 154 | -------------------------------------------------------------------------------- /src/components/Tabs.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 27 | 28 | 40 | -------------------------------------------------------------------------------- /src/components/UserSignUpModal.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 193 | 194 | -------------------------------------------------------------------------------- /src/components/VenueList.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | 167 | 168 | 228 | -------------------------------------------------------------------------------- /src/firebase.js: -------------------------------------------------------------------------------- 1 | // Firebase App (the core Firebase SDK) is always required and 2 | // must be listed before other Firebase SDKs 3 | import * as firebase from "firebase/app"; 4 | 5 | // Add the Firebase services that you want to use 6 | import "firebase/auth"; 7 | import "firebase/storage"; 8 | import "firebase/firestore"; 9 | import "firebase/database"; 10 | import "firebase/analytics"; 11 | 12 | 13 | // replace this config with your own. 14 | const firebaseConfig = { 15 | apiKey: "AIzaSyAzJdnBy6MXtYlpGsmm70FJA-v9aiBPUdw", 16 | authDomain: "techconf-db-template.firebaseapp.com", 17 | databaseURL: "https://techconf-db-template.firebaseio.com", 18 | projectId: "techconf-db-template", 19 | storageBucket: "techconf-db-template.appspot.com", 20 | messagingSenderId: "862672279483", 21 | appId: "1:862672279483:web:f2e02196bafe0a217d969e", 22 | measurementId: "G-ZTFP3035QL" 23 | }; 24 | // Initialize Firebase 25 | firebase.initializeApp(firebaseConfig); 26 | firebase.analytics(); 27 | 28 | export default firebase; 29 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | // import { createStore } from 'vuex'; 3 | import App from './App.vue'; 4 | import router from './router'; 5 | import store from './store'; 6 | 7 | import { IonicVue } from '@ionic/vue'; 8 | 9 | /* Core CSS required for Ionic components to work properly */ 10 | import '@ionic/vue/css/core.css'; 11 | 12 | /* Basic CSS for apps built with Ionic */ 13 | import '@ionic/vue/css/normalize.css'; 14 | import '@ionic/vue/css/structure.css'; 15 | import '@ionic/vue/css/typography.css'; 16 | 17 | /* Optional CSS utils that can be commented out */ 18 | import '@ionic/vue/css/padding.css'; 19 | import '@ionic/vue/css/float-elements.css'; 20 | import '@ionic/vue/css/text-alignment.css'; 21 | import '@ionic/vue/css/text-transformation.css'; 22 | import '@ionic/vue/css/flex-utils.css'; 23 | import '@ionic/vue/css/display.css'; 24 | 25 | /* Theme variables */ 26 | import './theme/variables.scss'; 27 | 28 | // import auth from "./store/modules/auth"; 29 | // const store = createStore({ 30 | // modules: { 31 | // auth, 32 | // // events, 33 | // // userProfile, 34 | // }, 35 | // state: { 36 | // httpLoader: false, 37 | // }, 38 | // getters: { 39 | // httpLoader({ httpLoader }) { 40 | // return httpLoader; 41 | // } 42 | // }, 43 | // mutations: { 44 | // }, 45 | // actions: { 46 | // } 47 | // }); 48 | 49 | 50 | const app = createApp(App) 51 | .use(IonicVue) 52 | .use(router) 53 | .use(store) 54 | 55 | router.beforeEach((to, from, next) => { 56 | store.dispatch("loginStatus"); 57 | 58 | if (to.meta.requiresAuth && !store.getters.loginToken) { 59 | next({ name:"conferences" }); 60 | } else { 61 | next(); 62 | } 63 | 64 | // firebase.auth().onAuthStateChanged(function(user) { 65 | // if (user) { 66 | // // User is signed in. 67 | // } else { 68 | // // No user is signed in. 69 | // } 70 | // }); 71 | }) 72 | router.isReady().then(() => { 73 | app.mount('#app'); 74 | }); -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from '@ionic/vue-router'; 2 | import Tabs from '../components/Tabs.vue' 3 | 4 | const routes = [ 5 | // { 6 | // path: '/', 7 | // component: () => import('@/views/Home.vue') 8 | // }, 9 | // { 10 | // path: '/profile', 11 | // component: () => import('@/views/Profile.vue') 12 | // }, 13 | // { 14 | // path: '/list', 15 | // component: () => import('@/views/List.vue') 16 | // }, 17 | 18 | // add /privacy-policy route for google calendar api 19 | // add /terms-of-service route for google calendar api 20 | { 21 | path: '/', 22 | name: "conferences", 23 | component: Tabs, 24 | children: [ 25 | { 26 | path: '/', 27 | name: "conferences", 28 | component: () => import('@/views/Home.vue'), 29 | meta: { 30 | requiresAuth: false, 31 | } 32 | }, 33 | { 34 | path: '/venues', 35 | name: "venues", 36 | component: () => import('@/views/Venues.vue'), 37 | meta: { 38 | requiresAuth: false, 39 | } 40 | }, 41 | { 42 | path: '/speakers', 43 | name: "speakers", 44 | component: () => import('@/views/Speakers.vue'), 45 | meta: { 46 | requiresAuth: false, 47 | } 48 | }, 49 | 50 | // Maybe this must be its own top-level route? 51 | { 52 | path: '/login', 53 | component: () => import('@/views/Login.vue'), 54 | meta: { 55 | requiresAuth: false, 56 | } 57 | }, 58 | 59 | // Maybe this must be its own top-level route? 60 | { 61 | path: '/register', 62 | component: () => import('@/views/Register.vue'), 63 | meta: { 64 | requiresAuth: false, 65 | } 66 | }, 67 | 68 | // Maybe this must be its own top-level route? 69 | { 70 | path: '/create-venue', 71 | component: () => import('@/views/CreateVenue.vue'), 72 | meta: { 73 | requiresAuth: true, 74 | } 75 | }, 76 | 77 | // Maybe this must be its own top-level route? 78 | { 79 | path: '/profile', 80 | component: () => import('@/views/Profile.vue'), 81 | meta: { 82 | requiresAuth: true, 83 | } 84 | }, 85 | 86 | // Maybe this must be its own top-level route? 87 | { 88 | path: '/create-event', 89 | component: () => import('@/views/CreateEvent.vue'), 90 | meta: { 91 | requiresAuth: true, 92 | } 93 | } 94 | ] 95 | } 96 | ] 97 | 98 | const router = createRouter({ 99 | history: createWebHistory(process.env.BASE_URL), 100 | routes 101 | }) 102 | 103 | export default router 104 | -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { defineComponent } from 'vue' 3 | const component: ReturnType 4 | export default component 5 | } 6 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex'; 2 | import auth from "./modules/auth"; 3 | import events from "./modules/events"; 4 | import venues from "./modules/venues"; 5 | import speakers from "./modules/speakers"; 6 | // import userProfile from "./modules/userProfile"; 7 | import firebase from "../firebase"; 8 | 9 | const store = createStore({ 10 | modules: { 11 | auth, 12 | events, 13 | venues, 14 | speakers, 15 | // userProfile 16 | }, 17 | state: { 18 | httpLoader: false, 19 | searchString: null, 20 | userProfile: {}, 21 | }, 22 | getters: { 23 | httpLoader({ httpLoader }) { 24 | return httpLoader; 25 | }, 26 | searchString({ searchString }) { 27 | return searchString; 28 | }, 29 | userProfile({ userProfile }) { 30 | return userProfile || JSON.parse(localStorage.getItem("tcdbUserProfile")); 31 | }, 32 | }, 33 | mutations: { 34 | userProfile(state, data) { 35 | state.events = data; 36 | localStorage.setItem("tcdbUserProfile", JSON.stringify(data)); 37 | }, 38 | // updateSearch(state, data) { 39 | // console.log("M", data); 40 | // state[data.stateObject] = data; 41 | // } 42 | }, 43 | actions: { 44 | createUser(context, request) { 45 | return new Promise((resolve, reject) => { 46 | firebase.firestore().collection("users") 47 | .doc(request.uid) 48 | .set({...request}) 49 | .then(() => { 50 | resolve(); 51 | }) 52 | .catch(err => { 53 | reject(err); 54 | }); 55 | }) 56 | }, 57 | getUserProfile(context, request) { 58 | return new Promise((resolve, reject) => { 59 | firebase.firestore().collection("users") 60 | .doc(request) 61 | .get() 62 | .then(doc => { 63 | context.commit("userProfile", doc.data()); 64 | resolve(doc.data()); 65 | }).catch(error => { 66 | reject(error); 67 | }); 68 | }) 69 | }, 70 | getUsers() { 71 | return new Promise((resolve) => { 72 | firebase.firestore().collection("users") 73 | .get() 74 | .then(({ docs }) => { 75 | resolve(docs.map(a => a.data())); 76 | }); 77 | }) 78 | }, 79 | updateUser(context, request) { 80 | request.updatedBy = context.getters.loginToken; 81 | return new Promise((resolve) => { 82 | firebase.firestore().collection("users") 83 | .doc(request.uid) 84 | .update(request) 85 | .then(() => { 86 | resolve(); 87 | }); 88 | }) 89 | }, 90 | refTracker(context, request) { 91 | return new Promise((resolve, reject) => { 92 | firebase.database().ref("refTracker") 93 | .once("value") 94 | .then((snapshot) => { 95 | let tracker = snapshot.val(); 96 | context.dispatch("refTrackerUpdate", {[request]: ++tracker[request]}) 97 | .then(res => { 98 | resolve("test", res); 99 | }) 100 | .catch(err => { 101 | reject(err); 102 | }); 103 | }).catch(error => { 104 | reject(error); 105 | }); 106 | }) 107 | }, 108 | refTrackerUpdate(context, request) { 109 | return new Promise((resolve, reject) => { 110 | firebase.database().ref("refTracker").update(request) 111 | .then(data => { 112 | resolve(data); 113 | }) 114 | .catch(err => { 115 | reject(err); 116 | }) 117 | }) 118 | } 119 | } 120 | }); 121 | 122 | export default store; 123 | -------------------------------------------------------------------------------- /src/store/modules/auth.js: -------------------------------------------------------------------------------- 1 | import firebase from "./../../firebase"; 2 | 3 | const addToSpeakers = (speaker) => { 4 | let speakerObj = { 5 | name: speaker.name, 6 | lastname: speaker.lastname, 7 | position: speaker.role, 8 | contact: speaker.contactInfoConsent ? speaker.contact : "", 9 | image: "", 10 | social: [ 11 | { 12 | link: speaker.twitter, 13 | label: "Twitter" 14 | }, 15 | { 16 | link: speaker.linkedin, 17 | label: "LinkedIn" 18 | }, 19 | { 20 | link: speaker.website, 21 | label: "Website" 22 | } 23 | ], 24 | highlights: [ 25 | { 26 | name: speaker.highlight1name, 27 | year: speaker.highlight1year 28 | }, 29 | { 30 | name: speaker.highlight2name, 31 | year: speaker.highlight2year 32 | }, 33 | { 34 | name: speaker.highlight3name, 35 | year: speaker.highlight3year 36 | }, 37 | ], 38 | }; 39 | return speakerObj; 40 | }; 41 | 42 | const state = { 43 | loginToken: false, 44 | } 45 | 46 | const getters = { 47 | loginToken({ loginToken }) { 48 | return loginToken || localStorage.getItem("tcdbLoginToken"); 49 | }, 50 | authError(state) { 51 | return state.authError; 52 | }, 53 | } 54 | 55 | const mutations = { 56 | loginToken(state, token) { 57 | if(token) { 58 | state.loginToken = token; 59 | localStorage.setItem("tcdbLoginToken", token); 60 | } else { 61 | state.loginToken = null; 62 | localStorage.clear(); 63 | } 64 | }, 65 | authError(state, data) { 66 | state.authError = data; 67 | } 68 | } 69 | 70 | const actions = { 71 | login(context, request) { 72 | return new Promise((resolve, reject) => { 73 | firebase.auth().signInWithEmailAndPassword(request.email, request.password) 74 | .then(({ user }) => { 75 | context.dispatch("getUserProfile", user.uid).then(() => { 76 | context.commit("loginToken", user.uid); 77 | resolve(user.uid); 78 | }); 79 | }).catch(function(error) { 80 | reject(error) 81 | }); 82 | }) 83 | }, 84 | signUp(context, request) { 85 | return new Promise((resolve, reject) => { 86 | firebase.auth().createUserWithEmailAndPassword(request.email, request.password) 87 | .then(({ user }) => { 88 | context.commit("loginToken", user.uid); 89 | request.uid = user.uid; 90 | request.verified = true; 91 | delete request.password; 92 | context.dispatch("createUser", request).then(() => { 93 | if (request.isSpeaker) { 94 | let obj = { 95 | ...request.speaker, 96 | name: request.name, 97 | lastname: request.lastname, 98 | contact: request.email 99 | } 100 | context.dispatch("createSpeaker", addToSpeakers(obj)).then(() => { 101 | resolve(user.uid); 102 | }).catch(e => { 103 | reject(e); 104 | }); 105 | } else { 106 | resolve(user.uid); 107 | } 108 | }).catch(err => { 109 | reject(err); 110 | }); 111 | }).catch(error => { 112 | reject(error) 113 | }); 114 | }) 115 | }, 116 | logout(context) { 117 | return new Promise((resolve, reject) => { 118 | firebase.auth().signOut() 119 | .then(() => { 120 | context.commit("loginToken", false); 121 | resolve(); 122 | }).catch(error => { 123 | reject(error) 124 | }); 125 | }) 126 | }, 127 | loginStatus(context) { 128 | return new Promise(() => { 129 | firebase.auth().onAuthStateChanged(user => { 130 | if (user) { 131 | // User is signed in. 132 | context.commit("loginToken", user.uid); 133 | } else { 134 | context.commit("loginToken", false); 135 | } 136 | }); 137 | }); 138 | } 139 | // subscribe(context, request) { 140 | // return new Promise((resolve, reject) => { 141 | // mailchimp.post(`lists/c72f027b89/members`, { 142 | // ...request, 143 | // status: "subscribed" 144 | // }) 145 | // .then(() => resolve()) 146 | // .catch(err => reject(err)) 147 | // }) 148 | // }, 149 | // async mailchimpTest() { 150 | // const response = await mailchimp.ping.get(); 151 | // console.log(response); 152 | // } 153 | } 154 | 155 | export default { state, getters, mutations, actions } 156 | -------------------------------------------------------------------------------- /src/store/modules/events.js: -------------------------------------------------------------------------------- 1 | import firebase from "../../firebase"; 2 | import moment from "moment"; 3 | 4 | const state = { 5 | events: [ 6 | { 7 | "id": "oeHnqwOO6hvJdJxhmUgO", 8 | "venue": "Virtual Conference", 9 | "start": "2020-11-10T09:30:40.637+02:00", 10 | "eventName": ".NET Conf", 11 | "createdBy": "JE3Bh37hpOch095fAEAcNbwrQWI3", 12 | "verified": true, 13 | "price": "", 14 | "contactPerson": "", 15 | "website": "https://www.dotnetconf.net/", 16 | "province": "", 17 | "end": "2020-11-12T09:30:40.640+02:00", 18 | "startFormatted": "10/11/2020", 19 | "endFormatted": "12/11/2020" 20 | }, 21 | { 22 | "id": "bWxcaUQHMSdQs6UORnoB", 23 | "venue": "Pearson Institute of Higher Learning", 24 | "eventName": "0111 CTO Conf", 25 | "address": {}, 26 | "province": "Gauteng", 27 | "town": "Johannesburg", 28 | "verified": true, 29 | "price": "", 30 | "createdBy": "JE3Bh37hpOch095fAEAcNbwrQWI3", 31 | "area": "Midrand", 32 | "website": "https://www.0111conf.co.za/johannesburg", 33 | "contactPerson": "", 34 | "startFormatted": null, 35 | "endFormatted": null 36 | }, 37 | { 38 | "id": "cPspaROtoQZdzLrdAHx1", 39 | "eventName": "0111 CTO Conf", 40 | "website": "https://www.0111conf.co.za/cape-town", 41 | "venue": "University of Stellenbosch", 42 | "contactPerson": "", 43 | "createdBy": "JE3Bh37hpOch095fAEAcNbwrQWI3", 44 | "province": "Western Cape", 45 | "town": "Stellenbosch", 46 | "verified": true, 47 | "price": "", 48 | "startFormatted": null, 49 | "endFormatted": null 50 | } 51 | ], 52 | filteredEvents: [], 53 | updateEventSearchObject: {}, 54 | monthEventCount: 0, 55 | } 56 | 57 | const getters = { 58 | events({ events = [] }) { 59 | return events || JSON.parse(localStorage.getItem("tcdbEvents")); 60 | }, 61 | filteredEvents({ events = [], updateEventSearchObject }) { 62 | if (!updateEventSearchObject?.field || updateEventSearchObject?.field === "") { 63 | return events; 64 | } else { 65 | return events.filter(event => event[updateEventSearchObject.field].toLowerCase().includes(updateEventSearchObject.value.toLowerCase())); 66 | } 67 | }, 68 | userEvents({ userEvents }) { 69 | return userEvents || JSON.parse(localStorage.getItem("tcdbUserEvents")); 70 | }, 71 | monthEventCount({ events = [] }) { 72 | const date = new Date(); 73 | const month = date.getMonth(); 74 | const monthPlus = month + 1; 75 | return events.filter(event => { 76 | if(event.start) { 77 | return event.start.split("/")[1] == monthPlus 78 | } 79 | }); 80 | }, 81 | } 82 | 83 | const mutations = { 84 | events(state, data) { 85 | state.events = data; 86 | localStorage.setItem("tcdbEvents", JSON.stringify(data)); 87 | }, 88 | userEvents(state, data) { 89 | state.events = data; 90 | localStorage.setItem("tcdbUserEvents", JSON.stringify(data)); 91 | }, 92 | updateSearch(state, data) { 93 | state[data.stateObject] = data; 94 | } 95 | } 96 | 97 | const actions = { 98 | // Events 99 | getEvents(context) { 100 | return new Promise((resolve, reject) => { 101 | firebase.firestore().collection("events") 102 | .orderBy("eventName") 103 | .where("verified", "==", true) 104 | .get() 105 | .then(({ docs }) => { 106 | // const events = eventFormater(docs); 107 | const eventData = []; 108 | for (let x =0; docs.length > x; x++) { 109 | const docData = docs[x].data(); 110 | eventData.push({ 111 | id: docs[x].id, 112 | ...docData, 113 | startFormatted: docData.start ? moment(docData.start).format("DD/MM/YYYY") : null, 114 | endFormatted: docData.end ? moment(docData.end).format("DD/MM/YYYY") : null, 115 | }); 116 | } 117 | context.commit("events", eventData); 118 | resolve(eventData); 119 | }).catch( error => { 120 | reject(error) 121 | }); 122 | }) 123 | }, 124 | getUserEvents(context) { 125 | return new Promise((resolve) => { 126 | firebase.firestore().collection("events") 127 | .orderBy("eventName") 128 | .where("createdBy", "==", context.getters.loginToken) 129 | .get() 130 | .then(({ docs }) => { 131 | // const events = eventFormater(docs); 132 | const eventData = []; 133 | for (let x =0; docs.length > x; x++) { 134 | const docData = docs[x].data(); 135 | eventData.push({ 136 | id: docs[x].id, 137 | ...docData, 138 | startFormatted: docData.start ? moment(docData.start).format("DD/MM/YYYY") : null, 139 | endFormatted: docData.end ? moment(docData.end).format("DD/MM/YYYY") : null, 140 | }); 141 | } 142 | context.commit("userEvents", eventData); 143 | resolve(eventData); 144 | }); 145 | }) 146 | }, 147 | createEvent(context, request) { 148 | request.createdBy = context.getters.loginToken; 149 | return new Promise((resolve, reject) => { 150 | const createEventFn = r => { 151 | firebase.firestore().collection("events") 152 | .add(r) 153 | .then(() => { 154 | context.dispatch("getEvents").then(events => { 155 | context.commit("events", events); 156 | resolve(events) 157 | }) 158 | .catch(error => { 159 | reject(error); 160 | }); 161 | }); 162 | } 163 | 164 | firebase.firestore().collection("users") 165 | .doc(context.getters.loginToken) 166 | .get() 167 | .then(user => { 168 | if (user.data().verified) { 169 | request.verified = true; 170 | createEventFn(request); 171 | } else { 172 | createEventFn(request) 173 | } 174 | }); 175 | }); 176 | }, 177 | updateEvent(context, request) { 178 | request.updatedBy = context.getters.loginToken; 179 | return new Promise((resolve) => { 180 | firebase.firestore().collection("events") 181 | .doc(request.id) 182 | .update(request) 183 | .then(() => { 184 | context.dispatch("getEvents"); 185 | resolve(); 186 | }); 187 | }) 188 | }, 189 | deleteEvent(context, request) { 190 | return new Promise(() => { 191 | firebase.firestore().collection("events") 192 | .doc(request) 193 | .delete(); 194 | }) 195 | }, 196 | // Will be used once I figure out why gapi is undefined when used in this module 197 | // gcEvent(context,request) { 198 | // gapi.load("client:auth2", () => { 199 | // gapi.client.init({ 200 | // apiKey: API_KEY, 201 | // clientId: CLIENT_ID, 202 | // discoveryDocs: DISCOVERY_DOCS, 203 | // scope: SCOPES 204 | // }).then(() => { 205 | // if (gapi.auth2.getAuthInstance().isSignedIn.get()) { 206 | // context.dispatch("gcCreateEvent", request); 207 | // } else { 208 | // gapi.auth2.getAuthInstance().signIn().then(() => { 209 | // context.dispatch("gcCreateEvent", request); 210 | // }) 211 | // .catch(() => { 212 | // alert(`You need to signin to your Google account before you can add event to your calendar`); 213 | // }); 214 | // } 215 | // }) 216 | // .catch(err => { 217 | // alert(err.details); 218 | // }) 219 | // }) 220 | // }, 221 | // gcCreateEvent(event) { 222 | // const gcEvent = { 223 | // "summary": event.eventName, 224 | // "location": event.venue, 225 | // "start": { 226 | // "dateTime": moment(event.start).utc().format("YYYY-MM-DDTHH:mm:ss.SSS[Z]"), 227 | // "timeZone": "Africa/Johannesburg" 228 | // }, 229 | // "end": { 230 | // "dateTime": moment(event.end).utc().format("YYYY-MM-DDTHH:mm:ss.SSS[Z]"), 231 | // "timeZone": "Africa/Johannesburg" 232 | // }, 233 | // "reminders": { 234 | // "useDefault": false, 235 | // "overrides": [ 236 | // {"method": "email", "minutes": 24 * 60}, 237 | // {"method": "popup", "minutes": 10} 238 | // ] 239 | // } 240 | // }; 241 | 242 | // var request = gapi.client.calendar.events.insert({ 243 | // 'calendarId': 'primary', 244 | // 'resource': gcEvent, 245 | // }); 246 | 247 | // const rootWindow = window; 248 | 249 | // request.execute(gcEvent => { 250 | // rootWindow.open(gcEvent.htmlLink); 251 | // }) 252 | // } 253 | } 254 | 255 | export default { state, getters, mutations, actions } 256 | -------------------------------------------------------------------------------- /src/store/modules/speakers.js: -------------------------------------------------------------------------------- 1 | import firebase from "../../firebase"; 2 | 3 | const state = { 4 | speakers: [ 5 | { 6 | name: "Makazole", 7 | lastname: "Mapimpi", 8 | position: "S.A rugby wing", 9 | contact: "makazoli@fakeemail.com", 10 | image: "https://ionic-vue-mobile-template-01.netlify.app/assets/img/makazoli.png", 11 | social: [ 12 | { 13 | link: "https://www.linkedin.com/in/simomafuxwana", 14 | label: "LinkedIn" 15 | }, 16 | { 17 | link: "https://simomafuxwana.netlify.com", 18 | label: "Website" 19 | }, 20 | { 21 | link: "https://twitter.com/dlodeprojuicer", 22 | label: "Twitter" 23 | } 24 | ], 25 | highlights: [ 26 | { 27 | name: "Microsoft Ignite", 28 | year: "2020" 29 | }, 30 | { 31 | name: "Microsoft Insiders Dev", 32 | year: "2019" 33 | }, 34 | { 35 | name: "Global DevOps Bootcamp", 36 | year: "2019" 37 | }, 38 | ], 39 | }, 40 | { 41 | name: "Max", 42 | lastname: "Verstapan", 43 | position: "RedBull F1 Driver", 44 | contact: "supermax@fakeemail.com", 45 | image: "https://ionic-vue-mobile-template-01.netlify.app/assets/img/max.png", 46 | social: [ 47 | { 48 | link: "https://www.linkedin.com/in/simomafuxwana", 49 | label: "LinkedIn" 50 | } 51 | ], 52 | highlights: [ 53 | { 54 | name: "Microsoft Ignite", 55 | year: "2020" 56 | }, 57 | { 58 | name: "Microsoft Insiders Dev", 59 | year: "2019" 60 | }, 61 | { 62 | name: "Global DevOps Bootcamp", 63 | year: "2019" 64 | }, 65 | ], 66 | } 67 | ], 68 | } 69 | 70 | const getters = { 71 | speakers({ speakers = [] }) { 72 | return speakers || JSON.parse(localStorage.getItem("tcdbSpeakers")); 73 | }, 74 | } 75 | 76 | const mutations = { 77 | speakers(state, data) { 78 | state.speakers = data; 79 | localStorage.setItem("tcdbSpeakers", JSON.stringify(data)); 80 | }, 81 | } 82 | 83 | const actions = { 84 | createSpeaker(context, request) { 85 | request.createdBy = context.getters.loginToken; 86 | return new Promise((resolve, reject) => { 87 | const createVenueFn = r => { 88 | firebase.firestore().collection("speakers") 89 | .add(r) 90 | .then(() => { 91 | context.dispatch("getSpeakers").then(speakers => { 92 | context.commit("speakers", speakers); 93 | resolve(speakers) 94 | }) 95 | .catch(error => { 96 | reject(error); 97 | }); 98 | }); 99 | } 100 | 101 | firebase.firestore().collection("users") 102 | .doc(context.getters.loginToken) 103 | .get() 104 | .then(user => { 105 | if (user.data().verified) { 106 | request.verified = true; 107 | createVenueFn(request); 108 | } else { 109 | createVenueFn(request) 110 | } 111 | }).catch(err => { 112 | reject(err); 113 | }); 114 | }); 115 | }, 116 | getSpeakers(context) { 117 | return new Promise((resolve, reject) => { 118 | firebase.firestore().collection("speakers") 119 | .orderBy("name") 120 | .where("verified", "==", true) 121 | .get() 122 | .then(({ docs }) => { 123 | context.commit("speakers", docs.map(a => a.data())); 124 | resolve(docs.map(a => a.data())); 125 | }).catch( error => { 126 | reject(error) 127 | }); 128 | }) 129 | }, 130 | } 131 | 132 | export default { state, getters, mutations, actions } 133 | -------------------------------------------------------------------------------- /src/store/modules/userProfile.js: -------------------------------------------------------------------------------- 1 | import firebase from "../../firebase"; 2 | 3 | const state = { 4 | userProfile: {}, 5 | } 6 | 7 | const getters = { 8 | userProfile({ userProfile }) { 9 | return userProfile || JSON.parse(localStorage.getItem("tcdbUserProfile")); 10 | }, 11 | } 12 | 13 | const mutations = { 14 | userProfile(state, data) { 15 | state.events = data; 16 | localStorage.setItem("tcdbUserProfile", JSON.stringify(data)); 17 | }, 18 | } 19 | 20 | const actions = { 21 | createUser(request) { 22 | return new Promise((resolve) => { 23 | firebase.firestore().collection("users") 24 | .doc(request.uid) 25 | .set({...request}) 26 | .then(() => { 27 | resolve(); 28 | }); 29 | }) 30 | }, 31 | getUserProfile(context, request) { 32 | return new Promise((resolve, reject) => { 33 | firebase.firestore().collection("users") 34 | .doc(request) 35 | .get() 36 | .then(doc => { 37 | context.commit("userProfile", doc.data()); 38 | resolve(doc.data()); 39 | }).catch(error => { 40 | reject(error); 41 | }); 42 | }) 43 | }, 44 | getUsers() { 45 | return new Promise((resolve) => { 46 | firebase.firestore().collection("users") 47 | .get() 48 | .then(({ docs }) => { 49 | resolve(docs.map(a => a.data())); 50 | }); 51 | }) 52 | }, 53 | updateUser(context, request) { 54 | request.updatedBy = context.getters.loginToken; 55 | return new Promise((resolve) => { 56 | firebase.firestore().collection("users") 57 | .doc(request.uid) 58 | .update(request) 59 | .then(() => { 60 | resolve(); 61 | }); 62 | }) 63 | } 64 | } 65 | 66 | export { state, getters, mutations, actions } 67 | -------------------------------------------------------------------------------- /src/store/modules/venues.js: -------------------------------------------------------------------------------- 1 | import firebase from "../../firebase"; 2 | 3 | const state = { 4 | venues: [ 5 | { 6 | "area": "Cape Town", 7 | "phone": "021 412 9999", 8 | "venueName": "The Wistin", 9 | "width": 80.6, 10 | "length": 50.2, 11 | "height": 5.63, 12 | "equipment": [ 13 | "Overhead Projector", 14 | "PA System" 15 | ], 16 | "aircon": true, 17 | "createdBy": "JE3Bh37hpOch095fAEAcNbwrQWI3", 18 | "email": "gallagher@gallagher.co.za", 19 | "squareMeter": 27000, 20 | "wheelchairFriendly": true, 21 | "website": "https://gallagher.co.za/hall2", 22 | "capacityMax": 2500, 23 | "capacityMin": 20, 24 | "wifi": true, 25 | "verified": true 26 | }, 27 | { 28 | "area": "Midrand, Johannesburg", 29 | "phone": "011 266 3000", 30 | "venueName": "Hall 2 - Gallagher Converntion Center", 31 | "width": 107.9, 32 | "length": 61.5, 33 | "height": 9.83, 34 | "equipment": [ 35 | "Overhead Projector", 36 | "PA System" 37 | ], 38 | "aircon": true, 39 | "createdBy": "JE3Bh37hpOch095fAEAcNbwrQWI3", 40 | "email": "gallagher@gallagher.co.za", 41 | "squareMeter": 27000, 42 | "wheelchairFriendly": true, 43 | "website": "https://gallagher.co.za/hall2", 44 | "capacityMax": 7000, 45 | "capacityMin": 20, 46 | "wifi": true, 47 | "verified": true 48 | } 49 | ], 50 | filteredVenues: [], 51 | updateVenueSearchObject: {} 52 | } 53 | 54 | const getters = { 55 | venues({ venues = [] }) { 56 | return venues || JSON.parse(localStorage.getItem("tcdbVenues")); 57 | }, 58 | filteredVenues({ venues = [], updateVenueSearchObject }) { 59 | if (!updateVenueSearchObject.field || updateVenueSearchObject.field === "") { 60 | return venues; 61 | } else { 62 | return venues.filter(venue => venue[updateVenueSearchObject.field].toLowerCase().includes(updateVenueSearchObject.value.toLowerCase())); 63 | } 64 | }, 65 | } 66 | 67 | const mutations = { 68 | venues(state, data) { 69 | state.venues = data; 70 | localStorage.setItem("tcdbVenues", JSON.stringify(data)); 71 | }, 72 | updateSearch(state, data) { 73 | state[data.stateObject] = data; 74 | } 75 | } 76 | 77 | const actions = { 78 | createVenue(context, request) { 79 | request.createdBy = context.getters.loginToken; 80 | return new Promise((resolve, reject) => { 81 | const createVenueFn = r => { 82 | firebase.firestore().collection("venues") 83 | .add(r) 84 | .then(() => { 85 | context.dispatch("getVenues").then(venues => { 86 | context.commit("venues", venues); 87 | resolve(venues) 88 | }) 89 | .catch(error => { 90 | reject(error); 91 | }); 92 | }); 93 | } 94 | 95 | firebase.firestore().collection("users") 96 | .doc(context.getters.loginToken) 97 | .get() 98 | .then(user => { 99 | if (user.data().verified) { 100 | request.verified = true; 101 | createVenueFn(request); 102 | } else { 103 | createVenueFn(request) 104 | } 105 | }); 106 | }); 107 | }, 108 | getVenues(context) { 109 | return new Promise((resolve, reject) => { 110 | firebase.firestore().collection("venues") 111 | .orderBy("venueName") 112 | .where("verified", "==", true) 113 | .get() 114 | .then(({ docs }) => { 115 | context.commit("venues", docs.map(a => a.data())); 116 | resolve(docs.map(a => a.data())); 117 | }).catch( error => { 118 | reject(error) 119 | }); 120 | }) 121 | }, 122 | } 123 | 124 | export default { state, getters, mutations, actions } 125 | -------------------------------------------------------------------------------- /src/theme/media-queries.scss: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | ##Device = Desktops 4 | ##Screen = 1281px to higher resolution desktops 5 | */ 6 | 7 | @media (min-width: 1281px) { 8 | .mobile-nav { 9 | display: none; 10 | } 11 | 12 | .home-content { 13 | display: flex; 14 | } 15 | 16 | .desktop-only { 17 | display: inherit; 18 | } 19 | 20 | .mobile-only { 21 | display: none; 22 | } 23 | 24 | .lg-content-center { 25 | margin: auto; 26 | width: 60%; 27 | } 28 | 29 | ion-col.speaker-column { 30 | width: 33.33% !important; 31 | } 32 | 33 | .not-mobile { 34 | display: block; 35 | width: 50%; 36 | text-align: center; 37 | margin: 0 auto; 38 | z-index: 9999999999; 39 | background: #fff; 40 | color: #154d75; 41 | } 42 | 43 | 44 | .no-events-img { 45 | width: 25%; 46 | margin: 5% 38%; 47 | text-align: center; 48 | img { 49 | filter: grayscale(20%); 50 | opacity: 0.7; 51 | } 52 | ion-button { 53 | opacity: 0.9; 54 | } 55 | } 56 | 57 | ion-title.logo2 { 58 | display: none; 59 | } 60 | 61 | } 62 | 63 | /* 64 | ##Device = Laptops, Desktops 65 | ##Screen = B/w 1025px to 1280px 66 | */ 67 | 68 | @media (min-width: 1025px) and (max-width: 1280px) { 69 | .mobile-nav { 70 | display: none; 71 | } 72 | 73 | .current-month { 74 | left: 89% !important; 75 | } 76 | 77 | .lg-content-center { 78 | width: 80%; 79 | margin: auto; 80 | } 81 | 82 | .mobile-only { 83 | display: none; 84 | } 85 | 86 | .desktop-only { 87 | display: inherit; 88 | } 89 | 90 | ion-col.speaker-column { 91 | width: 33.33% !important; 92 | } 93 | 94 | .not-mobile { 95 | display: block; 96 | width: 50%; 97 | margin: 5% 38%; 98 | text-align: center; 99 | margin: 0 auto; 100 | z-index: 9999999999; 101 | background: #fff; 102 | color: #154d75; 103 | } 104 | 105 | 106 | .no-events-img { 107 | width: 25%; 108 | margin: 8% 38%; 109 | text-align: center; 110 | img { 111 | filter: grayscale(20%); 112 | opacity: 0.7; 113 | } 114 | ion-button { 115 | opacity: 0.9; 116 | } 117 | } 118 | 119 | ion-title.logo2 { 120 | display: none; 121 | } 122 | 123 | } 124 | 125 | /* 126 | ##Device = Tablets, Ipads (portrait) 127 | ##Screen = B/w 801px to 1024px 128 | */ 129 | 130 | @media (min-width: 768px) and (max-width: 1024px) and (orientation: portrait) { 131 | .lg-content-center { 132 | width: 60%; 133 | margin: auto; 134 | background: greenyellow; 135 | } 136 | 137 | .mobile-only { 138 | display: inherit; 139 | } 140 | 141 | .desktop-only { 142 | display: none; 143 | } 144 | 145 | ion-col.speaker-column { 146 | width: 33.33% !important; 147 | } 148 | 149 | .desktop-nav { 150 | display: none; 151 | } 152 | 153 | .mobile-nav { 154 | display: inherit; 155 | } 156 | 157 | .current-month { 158 | display: none; 159 | } 160 | 161 | .not-mobile { 162 | display: none; 163 | } 164 | 165 | .no-events-img { 166 | width: 35%; 167 | margin: 8% 30%; 168 | text-align: center; 169 | img { 170 | filter: grayscale(20%); 171 | opacity: 0.7; 172 | } 173 | ion-button { 174 | opacity: 0.9; 175 | } 176 | } 177 | 178 | ion-title.logo2 { 179 | display: none; 180 | } 181 | } 182 | 183 | /* 184 | ##Device = Tablets, Ipads (landscape) 185 | ##Screen = B/w 767px to 1024px 186 | */ 187 | 188 | @media (min-width: 768px) and (max-width: 1024px) and (orientation: landscape) { 189 | .lg-content-center { 190 | width: 90%; 191 | margin: auto; 192 | } 193 | 194 | ion-col.speaker-column { 195 | width: 33.33% !important; 196 | } 197 | 198 | .mobile-only { 199 | display: none; 200 | } 201 | 202 | .desktop-only { 203 | display: inherit; 204 | } 205 | 206 | .mobile-nav { 207 | display: none; 208 | } 209 | 210 | .current-month { 211 | display: none; 212 | } 213 | 214 | .not-mobile { 215 | display: none; 216 | } 217 | 218 | .no-events-img { 219 | width: 35%; 220 | margin: 8% 30%; 221 | text-align: center; 222 | img { 223 | filter: grayscale(20%); 224 | opacity: 0.7; 225 | } 226 | ion-button { 227 | opacity: 0.9; 228 | } 229 | } 230 | 231 | ion-title.logo2 { 232 | display: none; 233 | } 234 | } 235 | 236 | /* 237 | ##Device = Low Resolution Tablets, Mobiles (Landscape) 238 | ##Screen = B/w 481px to 767px 239 | */ 240 | 241 | @media (min-width: 481px) and (max-width: 767px) { 242 | .lg-content-center { 243 | width: 90%; 244 | margin: auto; 245 | } 246 | 247 | ion-col.speaker-column { 248 | width: 50% !important; 249 | } 250 | 251 | .mobile-only { 252 | display: inherit; 253 | } 254 | 255 | .desktop-only { 256 | display: none; 257 | } 258 | 259 | .mobile-nav { 260 | display: none; 261 | } 262 | 263 | .current-month { 264 | display: none; 265 | } 266 | 267 | .not-mobile { 268 | display: none; 269 | } 270 | 271 | .no-events-img { 272 | width: 45%; 273 | margin: 6% 30%; 274 | text-align: center; 275 | img { 276 | filter: grayscale(20%); 277 | opacity: 0.7; 278 | } 279 | ion-button { 280 | opacity: 0.9; 281 | } 282 | } 283 | 284 | ion-title.logo { 285 | display: none; 286 | } 287 | 288 | 289 | ion-title.logo2 { 290 | // display: inherit; 291 | font-weight: 900; 292 | } 293 | } 294 | 295 | /* 296 | ##Device = Most of the Smartphones Mobiles (Portrait) 297 | ##Screen = B/w 320px to 479px 298 | */ 299 | 300 | @media (min-width: 320px) and (max-width: 480px) { 301 | // .lg-content-center { 302 | // background: rgb(5, 140, 178); 303 | // width: 0%; 304 | // } 305 | 306 | ion-col.speaker-column { 307 | width: 100% !important; 308 | } 309 | 310 | .desktop-only { 311 | display: none; 312 | } 313 | 314 | .mobile-only { 315 | display: inherit; 316 | } 317 | 318 | .desktop-nav { 319 | display: none; 320 | } 321 | 322 | .mobile-nav { 323 | display: inherit; 324 | } 325 | 326 | .current-month { 327 | display: none; 328 | } 329 | 330 | .not-mobile { 331 | display: none; 332 | } 333 | 334 | .no-events-img { 335 | width: 60%; 336 | margin: 20% 20%; 337 | text-align: center; 338 | img { 339 | filter: grayscale(20%); 340 | opacity: 0.7; 341 | } 342 | ion-button { 343 | opacity: 0.9; 344 | } 345 | h2 { 346 | font-size: 20px; 347 | } 348 | } 349 | 350 | ion-title.logo { 351 | display: none; 352 | } 353 | 354 | ion-title.logo2 { 355 | font-weight: 900; 356 | margin-left: 10px; 357 | } 358 | 359 | } -------------------------------------------------------------------------------- /src/theme/variables.scss: -------------------------------------------------------------------------------- 1 | /* Ionic Variables and Theming. For more info, please see: 2 | http://ionicframework.com/docs/theming/ */ 3 | 4 | @import "./media-queries.scss"; 5 | 6 | $baseColor: #181819; 7 | 8 | // $colors: (success, #000); 9 | 10 | :root { 11 | --ion-color-primary: #355724; 12 | --ion-color-primary-tint: #19ce4f; 13 | --ion-color-primary-shade: #19ce4f; 14 | 15 | --ion-color-light: #ffffff; 16 | --ion-color-medium: #e6e5e5; 17 | --ion-color-medium-tint: #777777; 18 | --ion-color-medium-shade: #777777; 19 | --ion-color-medium-contrast:#777777; 20 | --ion-background-color: #e2e9ea; 21 | color: #000; 22 | 23 | } 24 | 25 | h1 , h2 , h3 , h4 , p , a , ul li , strong , label { 26 | color: #777777; 27 | } 28 | 29 | ion-toolbar.modal-header { 30 | --background: #144d75; 31 | 32 | ion-title { 33 | color: #fff; 34 | } 35 | } 36 | 37 | .form-buttons { 38 | margin: 50px auto; 39 | } 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/views/CreateEvent.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 178 | 179 | -------------------------------------------------------------------------------- /src/views/CreateVenue.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 204 | 205 | -------------------------------------------------------------------------------- /src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 70 | 71 | 76 | -------------------------------------------------------------------------------- /src/views/Home.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 70 | 71 | -------------------------------------------------------------------------------- /src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 121 | 122 | -------------------------------------------------------------------------------- /src/views/Profile.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 91 | 92 | -------------------------------------------------------------------------------- /src/views/Register.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 366 | 367 | -------------------------------------------------------------------------------- /src/views/Speakers.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 66 | 67 | -------------------------------------------------------------------------------- /src/views/Venues.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /tests/e2e/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'cypress' 4 | ], 5 | env: { 6 | mocha: true, 7 | 'cypress/globals': true 8 | }, 9 | rules: { 10 | strict: 'off' 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | // https://docs.cypress.io/guides/guides/plugins-guide.html 3 | 4 | // if you need a custom webpack configuration you can uncomment the following import 5 | // and then use the `file:preprocessor` event 6 | // as explained in the cypress docs 7 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples 8 | 9 | // /* eslint-disable import/no-extraneous-dependencies, global-require */ 10 | // const webpack = require('@cypress/webpack-preprocessor') 11 | 12 | module.exports = (on, config) => { 13 | // on('file:preprocessor', webpack({ 14 | // webpackOptions: require('@vue/cli-service/webpack.config'), 15 | // watchOptions: {} 16 | // })) 17 | 18 | return Object.assign({}, config, { 19 | fixturesFolder: 'tests/e2e/fixtures', 20 | integrationFolder: 'tests/e2e/specs', 21 | screenshotsFolder: 'tests/e2e/screenshots', 22 | videosFolder: 'tests/e2e/videos', 23 | supportFile: 'tests/e2e/support/index.js' 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe('My First Test', () => { 4 | it('Visits the app root url', () => { 5 | cy.visit('/') 6 | cy.contains('h1', 'Welcome to Your Vue.js + TypeScript App') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import HelloWorld from '@/components/HelloWorld.vue' 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('renders props.msg when passed', () => { 6 | const msg = 'new message' 7 | const wrapper = shallowMount(HelloWorld, { 8 | props: { msg } 9 | }) 10 | expect(wrapper.text()).toMatch(msg) 11 | }) 12 | }) 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "skipLibCheck": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env", 16 | "jest" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx", "src/router/index.js", "src/shims-vue.d.js", "src/main.js" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | --------------------------------------------------------------------------------