├── .browserslistrc ├── .eslintrc.js ├── .github └── workflows │ └── tests.yml ├── .gitignore ├── README.md ├── babel.config.js ├── capacitor.config.json ├── cypress.json ├── ionic.config.json ├── jest.config.js ├── masthead.png ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── assets │ ├── icon │ │ ├── favicon.png │ │ └── icon.png │ └── shapes.svg └── index.html ├── src ├── App.vue ├── data │ └── emails.ts ├── main.ts ├── router │ └── index.ts ├── shims-vue.d.ts ├── theme │ └── variables.css └── views │ ├── Compose.vue │ ├── Email.vue │ └── Inbox.vue ├── tailwind.config.js ├── tests ├── e2e │ ├── .eslintrc.js │ ├── plugins │ │ └── index.js │ ├── specs │ │ └── test.js │ └── support │ │ ├── commands.js │ │ └── index.js └── unit │ └── example.spec.ts ├── tsconfig.json └── vue.config.js /.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /.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 | '@vue/typescript/recommended' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 2020 13 | }, 14 | rules: { 15 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 17 | 'vue/no-deprecated-slot-attribute': 'off', 18 | '@typescript-eslint/no-explicit-any': 'off', 19 | }, 20 | overrides: [ 21 | { 22 | files: [ 23 | '**/__tests__/*.{j,t}s?(x)', 24 | '**/tests/unit/**/*.spec.{j,t}s?(x)' 25 | ], 26 | env: { 27 | jest: true 28 | } 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: End-to-end tests 2 | on: [push] 3 | jobs: 4 | cypress-run: 5 | runs-on: ubuntu-16.04 6 | steps: 7 | - name: Checkout 8 | uses: actions/checkout@v2 9 | # Install NPM dependencies, cache them correctly 10 | # and run all Cypress tests 11 | - name: Cypress run 12 | uses: cypress-io/github-action@v2 13 | with: 14 | start: npm run serve 15 | # quote the url to be safe against YML parsing surprises 16 | wait-on: 'http://localhost:8100' 17 | - uses: actions/upload-artifact@v1 18 | if: failure() 19 | with: 20 | name: cypress-screenshots 21 | path: tests/e2e/screenshots 22 | # Test run video was always captured, so this action uses "always()" condition 23 | - uses: actions/upload-artifact@v1 24 | if: always() 25 | with: 26 | name: cypress-videos 27 | path: tests/e2e/videos -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Specifies intentionally untracked files to ignore when using Git 2 | # http://git-scm.com/docs/gitignore 3 | 4 | *~ 5 | *.sw[mnpcod] 6 | .tmp 7 | *.tmp 8 | *.tmp.* 9 | *.sublime-project 10 | *.sublime-workspace 11 | .DS_Store 12 | Thumbs.db 13 | UserInterfaceState.xcuserstate 14 | $RECYCLE.BIN/ 15 | 16 | *.log 17 | log.txt 18 | npm-debug.log* 19 | 20 | /.idea 21 | /.ionic 22 | /.sass-cache 23 | /.sourcemaps 24 | /.versions 25 | /.vscode 26 | /coverage 27 | /dist 28 | /node_modules 29 | /platforms 30 | /plugins 31 | /www 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # ionic-vue-tailwind-gmail-ui-clone GitHub Workflow Status 4 | 5 | An clone of the Gmail App built with Ionic, Vue and Tailwind. 6 | 7 | ### Included in this Ionic Vue Example 8 | * Custom swipe gestures 9 | * Custom scroll listeners 10 | * Inbox view 11 | * Email view 12 | * Compose view 13 | * Simple search 14 | * Side menu 15 | * Fully tested with Cypress 16 | 17 | ### To run 18 | 19 | ```javascript 20 | npm install 21 | ionic serve 22 | ``` 23 | 24 | Alternatively, you can add the iOS, Android platform and run natively. 25 | 26 | ### Are you on Twitter? Follow me [@jaybird1979](https://twitter.com/jaybird1979) 27 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /capacitor.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "appId": "io.ionic.starter", 3 | "appName": "ionic-vue-tailwind-gmail-ui-clone", 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 | "baseUrl": "http://localhost:8100", 3 | "pluginsFile": "tests/e2e/plugins/index.js", 4 | "scrollBehavior": "center" 5 | } -------------------------------------------------------------------------------- /ionic.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-vue-tailwind-gmail-ui-clone", 3 | "integrations": { 4 | "capacitor": {} 5 | }, 6 | "type": "vue" 7 | } 8 | -------------------------------------------------------------------------------- /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 | transformIgnorePatterns: ['/node_modules/(?!@ionic/vue|@ionic/vue-router)'] 7 | } 8 | -------------------------------------------------------------------------------- /masthead.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayBizzle/ionic-vue-tailwind-gmail-ui-clone/2d7505a8a63a7e4f20661cb9663fd6ebbc964617/masthead.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ionic-vue-tailwind-gmail-ui-clone", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "test:unit": "vue-cli-service test:unit", 9 | "test:e2e": "vue-cli-service test:e2e", 10 | "lint": "vue-cli-service lint" 11 | }, 12 | "dependencies": { 13 | "@capacitor/android": "^2.4.7", 14 | "@capacitor/core": "2.4.7", 15 | "@fontsource/roboto": "^4.2.3", 16 | "@ionic/vue": "^5.4.0", 17 | "@ionic/vue-router": "^5.4.0", 18 | "@tailwindcss/line-clamp": "^0.2.0", 19 | "core-js": "^3.6.5", 20 | "vue": "^3.0.0-0", 21 | "vue-router": "^4.0.0-0" 22 | }, 23 | "devDependencies": { 24 | "@capacitor/cli": "2.4.7", 25 | "@tailwindcss/postcss7-compat": "^2.1.0", 26 | "@types/jest": "^24.0.19", 27 | "@typescript-eslint/eslint-plugin": "^2.33.0", 28 | "@typescript-eslint/parser": "^2.33.0", 29 | "@vue/cli-plugin-babel": "~4.5.0", 30 | "@vue/cli-plugin-e2e-cypress": "~4.5.0", 31 | "@vue/cli-plugin-eslint": "~4.5.0", 32 | "@vue/cli-plugin-router": "~4.5.0", 33 | "@vue/cli-plugin-typescript": "~4.5.0", 34 | "@vue/cli-plugin-unit-jest": "~4.5.0", 35 | "@vue/cli-service": "~4.5.0", 36 | "@vue/compiler-sfc": "^3.0.0-0", 37 | "@vue/eslint-config-typescript": "^5.0.2", 38 | "@vue/test-utils": "^2.0.0-0", 39 | "autoprefixer": "^9.8.6", 40 | "cy-mobile-commands": "^0.2.1", 41 | "cypress": "^6.8.0", 42 | "eslint": "^6.7.2", 43 | "eslint-plugin-vue": "^7.0.0-0", 44 | "npm": "^7.10.0", 45 | "postcss": "^7.0.35", 46 | "postcss-cli": "^7.1.2", 47 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.1.0", 48 | "typescript": "~3.9.3", 49 | "vue-jest": "^5.0.0-0" 50 | }, 51 | "description": "An Ionic project" 52 | } 53 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | } 6 | } -------------------------------------------------------------------------------- /public/assets/icon/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayBizzle/ionic-vue-tailwind-gmail-ui-clone/2d7505a8a63a7e4f20661cb9663fd6ebbc964617/public/assets/icon/favicon.png -------------------------------------------------------------------------------- /public/assets/icon/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JayBizzle/ionic-vue-tailwind-gmail-ui-clone/2d7505a8a63a7e4f20661cb9663fd6ebbc964617/public/assets/icon/icon.png -------------------------------------------------------------------------------- /public/assets/shapes.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Ionic App 6 | 7 | 8 | 9 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 179 | 180 | -------------------------------------------------------------------------------- /src/data/emails.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | 3 | const data = ref([ 4 | { 5 | id: 1, 6 | date: "17:45", 7 | from: "Sky", 8 | subject: "Emily, get the Samsung Galaxy S21+ 5G from £43 a month", 9 | preview: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 10 | isRead: false, 11 | avatar: null, 12 | isStarred: false, 13 | color: "bg-yellow-500", 14 | visible: true 15 | }, 16 | { 17 | id: 2, 18 | date: "16:34", 19 | from: "Amazon", 20 | subject: "Emily, get the Samsung Galaxy S21+ 5G from £43 a month", 21 | preview: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 22 | isRead: false, 23 | avatar: null, 24 | isStarred: false, 25 | color: "bg-green-500", 26 | visible: true 27 | }, 28 | { 29 | id: 3, 30 | date: "16:20", 31 | from: "Stackoverflow", 32 | subject: "The Overflow #69: When internal devs are your customers", 33 | preview: "Welcome to ISSUE #69 of the Overflow! This newsletter is by developers, for developers, written and curated by the Stack Overflow team and Cassidy Williams at Netlify. Our menu this week: branch out with Git, fade to black in old video games, and prevent code injection in JavaScript and Node.", 34 | isRead: false, 35 | avatar: null, 36 | isStarred: false, 37 | color: "bg-blue-500", 38 | visible: true 39 | }, 40 | { 41 | id: 4, 42 | date: "12:42", 43 | from: "Adam Wathan", 44 | subject: "Tailwind UI Update: React + Vue support is here!", 45 | preview: "Hey folks, two big updates for you today! 🥳 First of all, React + Vue support for Tailwind UI is now available! Every single component can be copied as a fully functional, fully accessible React or Vue example, and easily dropped directly into your project.", 46 | isRead: false, 47 | avatar: null, 48 | isStarred: true, 49 | color: "bg-pink-500", 50 | visible: true 51 | }, 52 | { 53 | id: 5, 54 | date: "09:00", 55 | from: "Visa", 56 | subject: "Coming soon: an easier way to verify you on Click to Pay with Visa", 57 | preview: "We take the security of your Visa card information very seriously. That's why we're rolling out an easier way to verify it's you. When you make a payment with Click to Pay, we currently send you a verification code via email. To make this even more accessible and convenient we may now also be sending this code by SMS. Message and data rates may apply.", 58 | isRead: false, 59 | avatar: null, 60 | isStarred: false, 61 | color: "bg-red-500", 62 | visible: true 63 | }, 64 | { 65 | id: 6, 66 | date: "08:11", 67 | from: "Monzo", 68 | subject: "Vote for Monzo as Best British Bank! ❤️", 69 | preview: "Thanks to votes from our customers, we’re finalists in the British Bank Awards for Best British Bank", 70 | isRead: false, 71 | avatar: null, 72 | isStarred: false, 73 | color: "bg-gray-500", 74 | visible: true 75 | }, 76 | { 77 | id: 7, 78 | date: "17:45", 79 | from: "Sky", 80 | subject: "Emily, get the Samsung Galaxy S21+ 5G from £43 a month", 81 | preview: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 82 | isRead: false, 83 | avatar: null, 84 | isStarred: false, 85 | color: "bg-yellow-500", 86 | visible: true 87 | }, 88 | { 89 | id: 8, 90 | date: "16 Apr", 91 | from: "Amazon", 92 | subject: "Emily, get the Samsung Galaxy S21+ 5G from £43 a month", 93 | preview: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 94 | isRead: false, 95 | avatar: null, 96 | isStarred: false, 97 | color: "bg-green-500", 98 | visible: true 99 | }, 100 | { 101 | id: 9, 102 | date: "16 Apr", 103 | from: "Stackoverflow", 104 | subject: "The Overflow #69: When internal devs are your customers", 105 | preview: "Welcome to ISSUE #69 of the Overflow! This newsletter is by developers, for developers, written and curated by the Stack Overflow team and Cassidy Williams at Netlify. Our menu this week: branch out with Git, fade to black in old video games, and prevent code injection in JavaScript and Node.", 106 | isRead: false, 107 | avatar: null, 108 | isStarred: false, 109 | color: "bg-blue-500", 110 | visible: true 111 | }, 112 | { 113 | id: 10, 114 | date: "16 Apr", 115 | from: "Adam Wathan", 116 | subject: "Tailwind UI Update: React + Vue support is here!", 117 | preview: "Hey folks, two big updates for you today! 🥳 First of all, React + Vue support for Tailwind UI is now available! Every single component can be copied as a fully functional, fully accessible React or Vue example, and easily dropped directly into your project.", 118 | isRead: false, 119 | avatar: null, 120 | isStarred: false, 121 | color: "bg-pink-500", 122 | visible: true 123 | }, 124 | { 125 | id: 11, 126 | date: "16 Apr", 127 | from: "Visa", 128 | subject: "Coming soon: an easier way to verify you on Click to Pay with Visa", 129 | preview: "We take the security of your Visa card information very seriously. That's why we're rolling out an easier way to verify it's you. When you make a payment with Click to Pay, we currently send you a verification code via email. To make this even more accessible and convenient we may now also be sending this code by SMS. Message and data rates may apply.", 130 | isRead: false, 131 | avatar: null, 132 | isStarred: false, 133 | color: "bg-red-500", 134 | visible: true 135 | }, 136 | { 137 | id: 12, 138 | date: "16 Apr", 139 | from: "Monzo", 140 | subject: "Vote for Monzo as Best British Bank! ❤️", 141 | preview: "Thanks to votes from our customers, we’re finalists in the British Bank Awards for Best British Bank", 142 | isRead: false, 143 | avatar: null, 144 | isStarred: false, 145 | color: "bg-gray-500", 146 | visible: true 147 | }, 148 | { 149 | id: 13, 150 | date: "12 Apr", 151 | from: "Monzo", 152 | subject: "Vote for Monzo as Best British Bank! ❤️", 153 | preview: "Thanks to votes from our customers, we’re finalists in the British Bank Awards for Best British Bank", 154 | isRead: false, 155 | avatar: null, 156 | isStarred: false, 157 | color: "bg-gray-500", 158 | visible: true 159 | }, 160 | { 161 | id: 14, 162 | date: "12 Apr", 163 | from: "Sky", 164 | subject: "Emily, get the Samsung Galaxy S21+ 5G from £43 a month", 165 | preview: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 166 | isRead: false, 167 | avatar: null, 168 | isStarred: false, 169 | color: "bg-yellow-500", 170 | visible: true 171 | }, 172 | { 173 | id: 15, 174 | date: "1 Mar", 175 | from: "Amazon", 176 | subject: "Emily, get the Samsung Galaxy S21+ 5G from £43 a month", 177 | preview: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", 178 | isRead: false, 179 | avatar: null, 180 | isStarred: false, 181 | color: "bg-green-500", 182 | visible: true 183 | }, 184 | { 185 | id: 16, 186 | date: "22 Feb", 187 | from: "Stackoverflow", 188 | subject: "The Overflow #69: When internal devs are your customers", 189 | preview: "Welcome to ISSUE #69 of the Overflow! This newsletter is by developers, for developers, written and curated by the Stack Overflow team and Cassidy Williams at Netlify. Our menu this week: branch out with Git, fade to black in old video games, and prevent code injection in JavaScript and Node.", 190 | isRead: false, 191 | avatar: null, 192 | isStarred: false, 193 | color: "bg-blue-500", 194 | visible: true 195 | }, 196 | { 197 | id: 17, 198 | date: "21 Feb", 199 | from: "Adam Wathan", 200 | subject: "Tailwind UI Update: React + Vue support is here!", 201 | preview: "Hey folks, two big updates for you today! 🥳 First of all, React + Vue support for Tailwind UI is now available! Every single component can be copied as a fully functional, fully accessible React or Vue example, and easily dropped directly into your project.", 202 | isRead: false, 203 | avatar: null, 204 | isStarred: false, 205 | color: "bg-pink-500", 206 | visible: true 207 | }, 208 | { 209 | id: 18, 210 | date: "18 Feb", 211 | from: "Visa", 212 | subject: "Coming soon: an easier way to verify you on Click to Pay with Visa", 213 | preview: "We take the security of your Visa card information very seriously. That's why we're rolling out an easier way to verify it's you. When you make a payment with Click to Pay, we currently send you a verification code via email. To make this even more accessible and convenient we may now also be sending this code by SMS. Message and data rates may apply.", 214 | isRead: false, 215 | avatar: null, 216 | isStarred: false, 217 | color: "bg-red-500", 218 | visible: true 219 | }, 220 | { 221 | id: 19, 222 | date: "16 Feb", 223 | from: "Monzo", 224 | subject: "Vote for Monzo as Best British Bank! ❤️", 225 | preview: "Thanks to votes from our customers, we’re finalists in the British Bank Awards for Best British Bank", 226 | isRead: false, 227 | avatar: null, 228 | isStarred: false, 229 | color: "bg-gray-500", 230 | visible: true 231 | } 232 | ]) 233 | 234 | export default { data } -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router'; 4 | 5 | import { IonicVue } from '@ionic/vue'; 6 | 7 | import "@fontsource/roboto"; 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.css'; 27 | 28 | const app = createApp(App) 29 | .use(IonicVue) 30 | .use(router); 31 | 32 | router.isReady().then(() => { 33 | app.mount('#app'); 34 | }); -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from '@ionic/vue-router'; 2 | import { RouteRecordRaw } from 'vue-router'; 3 | import Inbox from '../views/Inbox.vue' 4 | import Email from '../views/Email.vue' 5 | import Compose from '../views/Compose.vue' 6 | 7 | const routes: Array = [ 8 | { 9 | path: '/', 10 | redirect: '/inbox' 11 | }, 12 | { 13 | path: '/inbox', 14 | name: 'Inbox', 15 | component: Inbox 16 | }, 17 | { 18 | path: '/email/:id', 19 | name: 'Email', 20 | component: Email 21 | }, 22 | { 23 | path: '/compose', 24 | name: 'Compose', 25 | component: Compose 26 | } 27 | ] 28 | 29 | const router = createRouter({ 30 | history: createWebHistory(process.env.BASE_URL), 31 | routes 32 | }) 33 | 34 | export default router 35 | -------------------------------------------------------------------------------- /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/theme/variables.css: -------------------------------------------------------------------------------- 1 | /* Ionic Variables and Theming. For more info, please see: 2 | http://ionicframework.com/docs/theming/ */ 3 | 4 | /** Ionic CSS Variables **/ 5 | :root { 6 | /** primary **/ 7 | --ion-color-primary: #3880ff; 8 | --ion-color-primary-rgb: 56, 128, 255; 9 | --ion-color-primary-contrast: #ffffff; 10 | --ion-color-primary-contrast-rgb: 255, 255, 255; 11 | --ion-color-primary-shade: #3171e0; 12 | --ion-color-primary-tint: #4c8dff; 13 | 14 | /** secondary **/ 15 | --ion-color-secondary: #3dc2ff; 16 | --ion-color-secondary-rgb: 61, 194, 255; 17 | --ion-color-secondary-contrast: #ffffff; 18 | --ion-color-secondary-contrast-rgb: 255, 255, 255; 19 | --ion-color-secondary-shade: #36abe0; 20 | --ion-color-secondary-tint: #50c8ff; 21 | 22 | /** tertiary **/ 23 | --ion-color-tertiary: #5260ff; 24 | --ion-color-tertiary-rgb: 82, 96, 255; 25 | --ion-color-tertiary-contrast: #ffffff; 26 | --ion-color-tertiary-contrast-rgb: 255, 255, 255; 27 | --ion-color-tertiary-shade: #4854e0; 28 | --ion-color-tertiary-tint: #6370ff; 29 | 30 | /** success **/ 31 | --ion-color-success: #2dd36f; 32 | --ion-color-success-rgb: 45, 211, 111; 33 | --ion-color-success-contrast: #ffffff; 34 | --ion-color-success-contrast-rgb: 255, 255, 255; 35 | --ion-color-success-shade: #28ba62; 36 | --ion-color-success-tint: #42d77d; 37 | 38 | /** warning **/ 39 | --ion-color-warning: #ffc409; 40 | --ion-color-warning-rgb: 255, 196, 9; 41 | --ion-color-warning-contrast: #000000; 42 | --ion-color-warning-contrast-rgb: 0, 0, 0; 43 | --ion-color-warning-shade: #e0ac08; 44 | --ion-color-warning-tint: #ffca22; 45 | 46 | /** danger **/ 47 | --ion-color-danger: #eb445a; 48 | --ion-color-danger-rgb: 235, 68, 90; 49 | --ion-color-danger-contrast: #ffffff; 50 | --ion-color-danger-contrast-rgb: 255, 255, 255; 51 | --ion-color-danger-shade: #cf3c4f; 52 | --ion-color-danger-tint: #ed576b; 53 | 54 | /** dark **/ 55 | --ion-color-dark: #222428; 56 | --ion-color-dark-rgb: 34, 36, 40; 57 | --ion-color-dark-contrast: #ffffff; 58 | --ion-color-dark-contrast-rgb: 255, 255, 255; 59 | --ion-color-dark-shade: #1e2023; 60 | --ion-color-dark-tint: #383a3e; 61 | 62 | /** medium **/ 63 | --ion-color-medium: #92949c; 64 | --ion-color-medium-rgb: 146, 148, 156; 65 | --ion-color-medium-contrast: #ffffff; 66 | --ion-color-medium-contrast-rgb: 255, 255, 255; 67 | --ion-color-medium-shade: #808289; 68 | --ion-color-medium-tint: #9d9fa6; 69 | 70 | /** light **/ 71 | --ion-color-light: #f4f5f8; 72 | --ion-color-light-rgb: 244, 245, 248; 73 | --ion-color-light-contrast: #000000; 74 | --ion-color-light-contrast-rgb: 0, 0, 0; 75 | --ion-color-light-shade: #d7d8da; 76 | --ion-color-light-tint: #f5f6f9; 77 | } 78 | 79 | @media (prefers-color-scheme: dark) { 80 | /* 81 | * Dark Colors 82 | * ------------------------------------------- 83 | */ 84 | 85 | body { 86 | --ion-color-primary: #428cff; 87 | --ion-color-primary-rgb: 66,140,255; 88 | --ion-color-primary-contrast: #ffffff; 89 | --ion-color-primary-contrast-rgb: 255,255,255; 90 | --ion-color-primary-shade: #3a7be0; 91 | --ion-color-primary-tint: #5598ff; 92 | 93 | --ion-color-secondary: #50c8ff; 94 | --ion-color-secondary-rgb: 80,200,255; 95 | --ion-color-secondary-contrast: #ffffff; 96 | --ion-color-secondary-contrast-rgb: 255,255,255; 97 | --ion-color-secondary-shade: #46b0e0; 98 | --ion-color-secondary-tint: #62ceff; 99 | 100 | --ion-color-tertiary: #6a64ff; 101 | --ion-color-tertiary-rgb: 106,100,255; 102 | --ion-color-tertiary-contrast: #ffffff; 103 | --ion-color-tertiary-contrast-rgb: 255,255,255; 104 | --ion-color-tertiary-shade: #5d58e0; 105 | --ion-color-tertiary-tint: #7974ff; 106 | 107 | --ion-color-success: #2fdf75; 108 | --ion-color-success-rgb: 47,223,117; 109 | --ion-color-success-contrast: #000000; 110 | --ion-color-success-contrast-rgb: 0,0,0; 111 | --ion-color-success-shade: #29c467; 112 | --ion-color-success-tint: #44e283; 113 | 114 | --ion-color-warning: #ffd534; 115 | --ion-color-warning-rgb: 255,213,52; 116 | --ion-color-warning-contrast: #000000; 117 | --ion-color-warning-contrast-rgb: 0,0,0; 118 | --ion-color-warning-shade: #e0bb2e; 119 | --ion-color-warning-tint: #ffd948; 120 | 121 | --ion-color-danger: #ff4961; 122 | --ion-color-danger-rgb: 255,73,97; 123 | --ion-color-danger-contrast: #ffffff; 124 | --ion-color-danger-contrast-rgb: 255,255,255; 125 | --ion-color-danger-shade: #e04055; 126 | --ion-color-danger-tint: #ff5b71; 127 | 128 | --ion-color-dark: #f4f5f8; 129 | --ion-color-dark-rgb: 244,245,248; 130 | --ion-color-dark-contrast: #000000; 131 | --ion-color-dark-contrast-rgb: 0,0,0; 132 | --ion-color-dark-shade: #d7d8da; 133 | --ion-color-dark-tint: #f5f6f9; 134 | 135 | --ion-color-medium: #989aa2; 136 | --ion-color-medium-rgb: 152,154,162; 137 | --ion-color-medium-contrast: #000000; 138 | --ion-color-medium-contrast-rgb: 0,0,0; 139 | --ion-color-medium-shade: #86888f; 140 | --ion-color-medium-tint: #a2a4ab; 141 | 142 | --ion-color-light: #222428; 143 | --ion-color-light-rgb: 34,36,40; 144 | --ion-color-light-contrast: #ffffff; 145 | --ion-color-light-contrast-rgb: 255,255,255; 146 | --ion-color-light-shade: #1e2023; 147 | --ion-color-light-tint: #383a3e; 148 | } 149 | 150 | /* 151 | * iOS Dark Theme 152 | * ------------------------------------------- 153 | */ 154 | 155 | .ios body { 156 | --ion-background-color: #000000; 157 | --ion-background-color-rgb: 0,0,0; 158 | 159 | --ion-text-color: #ffffff; 160 | --ion-text-color-rgb: 255,255,255; 161 | 162 | --ion-color-step-50: #0d0d0d; 163 | --ion-color-step-100: #1a1a1a; 164 | --ion-color-step-150: #262626; 165 | --ion-color-step-200: #333333; 166 | --ion-color-step-250: #404040; 167 | --ion-color-step-300: #4d4d4d; 168 | --ion-color-step-350: #595959; 169 | --ion-color-step-400: #666666; 170 | --ion-color-step-450: #737373; 171 | --ion-color-step-500: #808080; 172 | --ion-color-step-550: #8c8c8c; 173 | --ion-color-step-600: #999999; 174 | --ion-color-step-650: #a6a6a6; 175 | --ion-color-step-700: #b3b3b3; 176 | --ion-color-step-750: #bfbfbf; 177 | --ion-color-step-800: #cccccc; 178 | --ion-color-step-850: #d9d9d9; 179 | --ion-color-step-900: #e6e6e6; 180 | --ion-color-step-950: #f2f2f2; 181 | 182 | --ion-item-background: #000000; 183 | 184 | --ion-card-background: #1c1c1d; 185 | } 186 | 187 | .ios ion-modal { 188 | --ion-background-color: var(--ion-color-step-100); 189 | --ion-toolbar-background: var(--ion-color-step-150); 190 | --ion-toolbar-border-color: var(--ion-color-step-250); 191 | } 192 | 193 | 194 | /* 195 | * Material Design Dark Theme 196 | * ------------------------------------------- 197 | */ 198 | 199 | .md body { 200 | --ion-background-color: #121212; 201 | --ion-background-color-rgb: 18,18,18; 202 | 203 | --ion-text-color: #ffffff; 204 | --ion-text-color-rgb: 255,255,255; 205 | 206 | --ion-border-color: #222222; 207 | 208 | --ion-color-step-50: #1e1e1e; 209 | --ion-color-step-100: #2a2a2a; 210 | --ion-color-step-150: #363636; 211 | --ion-color-step-200: #414141; 212 | --ion-color-step-250: #4d4d4d; 213 | --ion-color-step-300: #595959; 214 | --ion-color-step-350: #656565; 215 | --ion-color-step-400: #717171; 216 | --ion-color-step-450: #7d7d7d; 217 | --ion-color-step-500: #898989; 218 | --ion-color-step-550: #949494; 219 | --ion-color-step-600: #a0a0a0; 220 | --ion-color-step-650: #acacac; 221 | --ion-color-step-700: #b8b8b8; 222 | --ion-color-step-750: #c4c4c4; 223 | --ion-color-step-800: #d0d0d0; 224 | --ion-color-step-850: #dbdbdb; 225 | --ion-color-step-900: #e7e7e7; 226 | --ion-color-step-950: #f3f3f3; 227 | 228 | --ion-item-background: #1e1e1e; 229 | 230 | --ion-toolbar-background: #1f1f1f; 231 | 232 | --ion-tab-bar-background: #1f1f1f; 233 | 234 | --ion-card-background: #1e1e1e; 235 | } 236 | } 237 | 238 | @import "tailwindcss/base"; 239 | @import "tailwindcss/components"; 240 | @import "tailwindcss/utilities"; -------------------------------------------------------------------------------- /src/views/Compose.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | -------------------------------------------------------------------------------- /src/views/Email.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | -------------------------------------------------------------------------------- /src/views/Inbox.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: 'jit', 3 | purge: ['./src/**/*.vue'], 4 | safelist: ['bg-yellow-500', 'bg-green-500', 'bg-blue-500', 'bg-pink-500', 'bg-red-500', 'bg-gray-500'], 5 | darkMode: false, // or 'media' or 'class' 6 | theme: { 7 | extend: { 8 | fontFamily: { 9 | logo: ["Roboto", "sans-serif"] 10 | } 11 | }, 12 | }, 13 | variants: { 14 | extend: {}, 15 | }, 16 | plugins: [ 17 | require('@tailwindcss/line-clamp'), 18 | ], 19 | } 20 | -------------------------------------------------------------------------------- /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 | import Emails from '../../../src/data/emails' 2 | 3 | describe('UI tests', () => { 4 | it('can display emails', () => { 5 | cy.viewport('iphone-x') 6 | cy.visit('/') 7 | cy.getByData('email').should('have.length', Emails.data.value.length) 8 | }) 9 | 10 | it('can swipe to delete', () => { 11 | cy.viewport('iphone-x') 12 | cy.visit('/') 13 | 14 | cy.getByData('email').swipe({delay: 100}, [340, 0], [100, 0]) 15 | cy.getByData('email').should('have.length', Emails.data.value.length -1) 16 | 17 | cy.getByData('email').swipe({delay: 100}, [100, 0], [304, 0]) 18 | cy.getByData('email').should('have.length', Emails.data.value.length -1) 19 | }) 20 | 21 | it('can search emails', () => { 22 | cy.viewport('iphone-x') 23 | cy.visit('/') 24 | 25 | cy.getByData('search').type('Amazon') 26 | cy.getByData('email').should('have.length', 3) 27 | 28 | cy.getByData('search').clear('Amazon') 29 | cy.getByData('email').should('have.length', Emails.data.value.length) 30 | }) 31 | 32 | it('can star an email', () => { 33 | cy.viewport('iphone-x') 34 | cy.visit('/') 35 | 36 | const starred = Emails.data.value.filter(e => e.isStarred).length 37 | 38 | cy.getByData('star').first().click() 39 | cy.getByData('star').find('svg.text-yellow-500').should('have.length', starred + 1) 40 | 41 | cy.getByData('star').first().click() 42 | cy.getByData('star').find('svg.text-yellow-500').should('have.length', starred) 43 | 44 | }) 45 | 46 | it('can open an email', () => { 47 | cy.viewport('iphone-x') 48 | cy.visit('/') 49 | 50 | cy.getByData('email').first().click() 51 | cy.getByData('read-view').should('contain', Emails.data.value[0].subject) 52 | cy.getByData('read-view').should('contain', Emails.data.value[0].from) 53 | cy.getByData('read-view').should('contain', Emails.data.value[0].date) 54 | }) 55 | 56 | it('can delete an email from read view', () => { 57 | cy.viewport('iphone-x') 58 | cy.visit('/') 59 | 60 | cy.getByData('email').first().click() 61 | cy.url().should('contain', '/email/1') 62 | cy.getByData('delete').click() 63 | cy.getByData('email').should('have.length', Emails.data.value.length - 1) 64 | cy.url().should('contain', '/inbox') 65 | }) 66 | 67 | it('can star an email from read view', () => { 68 | cy.viewport('iphone-x') 69 | cy.visit('/') 70 | 71 | const starred = Emails.data.value.filter(e => e.isStarred).length 72 | 73 | cy.getByData('email').first().click() 74 | cy.url().should('contain', '/email/1') 75 | cy.getByData('read-view--star').click().should('have.class', 'text-yellow-500') 76 | cy.getByData('read-view--back').click() 77 | cy.url().should('contain', '/inbox') 78 | 79 | cy.getByData('star').find('svg.text-yellow-500').should('have.length', starred + 1) 80 | }) 81 | 82 | it('can hide/show the search bar on scroll', () => { 83 | cy.viewport('iphone-x') 84 | cy.visitMobile('/') 85 | 86 | cy.getByData('inbox').shadow().find('.inner-scroll').scrollTo('bottom', { duration: 500 }) 87 | cy.getByData('search-container').should('not.be.visible') 88 | cy.getByData('inbox').shadow().find('.inner-scroll').scrollTo('top', { duration: 500 }) 89 | cy.getByData('search-container').should('be.visible') 90 | }) 91 | }) 92 | -------------------------------------------------------------------------------- /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 | import 'cy-mobile-commands' 27 | 28 | Cypress.Commands.add('getByData', (selector, ...args) => { 29 | return cy.get(`[data-cy=${selector}]`, ...args) 30 | }) -------------------------------------------------------------------------------- /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 { mount } from '@vue/test-utils' 2 | import Home from '@/views/Home.vue' 3 | 4 | describe('Home.vue', () => { 5 | it('renders home vue', () => { 6 | const wrapper = mount(Home) 7 | expect(wrapper.text()).toMatch('Ready to create an app?') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /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" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | devServer: { 3 | progress: false, 4 | port: 8100 5 | } 6 | } --------------------------------------------------------------------------------